Une grande partie du Web utilise des API reposantes pour déplacer des données vers et depuis des serveurs. Repos est un concept simple qui n'a rien à voir avec le sommeil ; le repos moyens transfert d'état représentatif , mais cet article ne concerne pas le repos . Cet article concerne un mécanisme kludgy pour contourner le manque d'API de repos lorsque vous avez vraiment besoin de récupérer des données à partir d'un site.
Les sites Web peuvent ne pas vouloir que vous le fassiez pour diverses raisons : la bande passante pour les données coûte de l'argent, les licences de données coûtent de l'argent et ne pas regarder les publicités peut également coûter de l'argent. Cet outil, sélénium , est théoriquement destiné à tester un site Web, et non à accélérer sur un ralentisseur. La plupart des sites ont des conditions d'utilisation ; vous pouvez trouver les termes de JMP au bas de cette page.
Un fichier JSL complet est joint. Il est écrit pour Firefox, Windows et le site Web JMP tel qu'il était le 8 mai 2022. La partie Firefox peut être modifiée, probablement, sur de nombreux autres navigateurs. Cela pourrait aussi fonctionner sur Mac, aucun test n'a été effectué. Le site Web de JMP changera au fil du temps et la JSL devra être peaufinée. C'est l'inconvénient de ne pas utiliser d'API officielle.
Avant de commencer, téléchargez un pilote et installez le sélénium comme indiqué dans les commentaires ci-dessous. Vous aurez également besoin de Firefox ou faites des recherches sur le pilote de votre navigateur préféré.
// JMP + Python + Selenium + Firefox (probably works with Chrome or Edge with a few tweaks)
// https://firefox-source-docs.mozilla.org/testing/geckodriver/index.html
// https://selenium-python.readthedocs.io/
// downloaded https://github.com/mozilla/geckodriver/releases -- geckodriver-...-win64.zip (pick latest)
// expand on desktop geckodriver.exe
// installed selenium python -m pip install selenium
// the following code is welded into the JMP.COM's HTML of 8May2022. It could change at anytime.
// the functions are sprinkled through the JSL below, near where I first needed them, reused later.
// you'll need similar functions, probably, and want to consult the Python Selenium bindings to write them.
// I only wrote enough to make this simple example work. There is no actual need to be logged in to JMP.
//
// Using try...except on snips of python code makes debugging much easier. JMP loses the exception message without it.
//
// the JSL functions are thin wrappers around the python calls to selenium which (I believe) is
// a thin wrapper to generate an API call to the gecko (etc) driver. I *think* it might be possible to
// remove the python layer and load/call the driver directly from JSL. But Selenium is documented.
Python Init(); // one-time startup...
// 0: startup
xrc = Python Execute( {}, {By_ID, By_XPATH, rc},
"\[
try:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.firefox.options import Options
#
options = webdriver.FirefoxOptions()
#options.add_argument("--private") # example. you most likely don't want private, some things don't work.
#
service=Service(r'C:\Users\v1\Desktop\geckodriver.exe')
# sometimes people use "browser" rather than "driver". It will be used below.
driver = webdriver.Firefox(service=service,options=options)
# return two magic values. you may need some others, just add them in the same way...
By_ID = By.ID
By_XPATH = By.XPATH
rc = "ok"
except Exception as e:
rc = repr(e)
]\"
);
If( xrc != 0 | rc != "ok", Throw( "start up Selenium failed" || Try( ": " || Char( rc ), "" ) ) );
Le PythonInit() n'a besoin d'être fait qu'une seule fois ; il connecte JMP à Python et prend quelques secondes la première fois. Vous pouvez le rappeler sans pénalité. Le PythonExecute(...) n'envoie aucune variable mais en récupère trois à partir du code qu'il exécute. Il faut un peu de temps pour tout charger et démarrer le navigateur.
You are looking at an empty browser controlled by JMPVous regardez un navigateur vide contrôlé par JMP
Ouvrez ensuite la page JMP.COM. Vous pourriez voir une redirection qui passe normalement inaperçue.
// 1: navigate to jmp.com
nav = Function( {url}, {rc},
Python Execute( {url}, {rc},
"\[
try:
driver.get(url)
rc = "ok"
except Exception as e:
rc = repr(e)
]\" );
return(rc);
);
rc = nav( "https://www.jmp.com/" );
if( rc != "ok", throw("nav: "||char(rc)));
La fonction nav renvoie "ok" ou un message d'erreur. La page Web JMP se charge dans le navigateur. Ignorez les personnes sur la capture d'écran.
The icon means the browser is remote controlled.L'icône signifie que le navigateur est télécommandé.
Créez un script de connexion au site JMP. Cliquez avec le bouton droit sur le bouton Se connecter pour connaître la valeur de l'identifiant HTML du bouton. Rappelez-vous comment faire cela; Je vais sauter cette explication à la fin...
F12 might bring you to the next screen, but this way the control will already be selected.F12 peut vous amener à l'écran suivant, mais de cette façon, le contrôle sera déjà sélectionné.
Et la console développeur s'ouvre avec l'identifiant du contrôle affiché. Plus bas, il y aura des contrôles qui ont une classe mais pas d'identifiant. XPATH de Selenium peut le gérer. L'astuce est similaire à la navigation displaybox - trouver un chemin qui n'est pas trop fragile et toujours assez spécifique.
Use the button id in the JSL that follows.Utilisez l'identifiant du bouton dans le JSL qui suit.
Les identifiants sont généralement le meilleur choix lorsqu'ils sont disponibles, car ils sont uniques sur la page. WaitID attend jusqu'à 10 secondes pour que le bouton de connexion apparaisse. Il n'est peut-être pas nécessaire d'attendre. Cela ne prend pas de temps s'il est déjà là.
// 2: login with userid/password credentials
// wait for an id to be available
waitID = Function( {id, timeout = 10, BYformat=By_ID}, {rc},
Python Execute( {id, timeout, BYformat}, {rc},
"\[
try:
myElem = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((BYformat, id)))
rc = "ok"
except TimeoutException:
rc = "timeout"
except Exception as e:
rc = repr(e)
]\"
);
Return( rc );
);
rc = waitID( "loginButton", 5 );
If( rc != "ok", Throw( "no login button: " || char(rc) ) );
// click a button ID
clickID = Function( {id, BYformat=By_ID}, {rc},
Python Execute( {id, BYformat}, {rc},
"\[
try:
driver.find_element(BYformat, id).click()
rc = "ok"
except Exception as e:
rc = repr(e)
]\"
);
return(rc);
);
rc = clickID( "loginButton" );
if( rc != "ok", throw("login button: "||char(rc)));
Cool! L'écran de connexion apparaît. Recherchez ensuite le champ du nom d'utilisateur.
Now get the id for the user name field by right-click...Obtenez maintenant l'identifiant du champ du nom d'utilisateur en cliquant avec le bouton droit de la souris...
Encore une fois, attendez le champ attendu. Maintenant, une fonction de frappe est nécessaire...
rc = waitID( "idp-discovery-username", 10 );
If( rc != "ok", Throw( "no username field: " || char(rc) ) );
// type a value into a field
keysToID = Function( {id, txt}, {rc},
Python Execute( {id, txt}, {rc},
"\[
try:
driver.find_element(By.ID, id).send_keys(txt)
rc = "ok"
except Exception as e:
rc = repr(e)
]\"
);
return(rc);
);
rc = keysToID( "idp-discovery-username", Include( "$documents/UserID.jsl" ) ); // file contains "AliBaba@1000&OneNights.com", in quotation marks, possibly encrypted
if( rc != "ok", throw("keysToID username: "||char(rc)));
Fake user name for fake password.Faux nom d'utilisateur pour faux mot de passe.
Mon ID utilisateur défile hors de l'écran et le bouton suivant est visible... trouvez son nom...
Click the Next button to get the password prompt.Cliquez sur le bouton Suivant pour obtenir l'invite de mot de passe.
cliquez dessus, puis attendez le champ mot de passe...
// click Next button
rc = clickID( "idp-discovery-submit" );
if( rc != "ok", throw("click submit user name: "||char(rc)));
// wait for password field
rc = waitID( "okta-signin-password", 5 );
If( rc != "ok", Throw( "no password field: " || char(rc) ) );
puis entrez le mot de passe
// enter password
rc = keysToID( "okta-signin-password", Include( "$documents/password.jsl" ) ); // file contains "OpenSesame", in quotation marks, possibly encrypted
if( rc != "ok", throw("keysToID password: "||char(rc)));
et répétez le processus... trouvez le bouton de connexion...
After entering the password, click the sign in button.Après avoir entré le mot de passe, cliquez sur le bouton de connexion.
cliquez dessus
// click Sign In
rc = clickID( "okta-signin-submit" );
if( rc != "ok", throw("click signin submit: "||char(rc)));
Nous sommes connectés.
Must be signed in, there is an edit profile choice.Doit être connecté, il y a un choix de profil d'édition.
Il y a un champ de recherche dans l'image, tapez JSL et cliquez sur la loupe. "searchField" est l'identifiant. La loupe peut être cliquée, mais le sélénium a un mécanisme de soumission de formulaire qui fonctionnera à partir du searchField, qui est un champ de saisie dans le formulaire.
The search field needs a bigger window to be visible, here it is.Le champ de recherche a besoin d'une plus grande fenêtre pour être visible, le voici.
Il existe du JavaScript asynchrone qui charge certaines parties de la page. L'attente d'un champ particulier peut ne pas être nécessaire si le champ est chargé dans le cadre de la page.
// 3: query for articles about JSL
rc = waitID( "searchField" );
If( rc != "ok", Throw( "no search field: " || char(rc) ) );
// enter a search string. "jsl" currently retruns 3 pages
rc = keysToID( "searchField", "jsl" );
if( rc != "ok", throw("keysToID search field: "||char(rc)));
submitForm = Function( {id}, {rc},
Python Execute( {id}, {rc},
"\[
try:
driver.find_element(By.ID, id).submit()
rc = "ok"
except Exception as e:
rc = repr(e)
]\"
);
return(rc);
);
rc = submitForm( "searchField" ); // submit form works OK off of this input field.
if( rc != "ok", throw("submitForm searchField: "||char(rc)));
Préparez-vous maintenant à parcourir les résultats. Les éléments de la liste multi-pages ressemblent à ceci
The pink outer element holds three inner parts: title, description, link.L'élément extérieur rose contient trois parties intérieures : titre, description, lien.
il y a une liste des éléments de données entourés de rose qui s'étend sur plusieurs pages.
// 4: page through the results to capture them
getElements = Function( {id, BYformat=By_ID}, {rc},
Python Execute( {id, BYformat}, {rc},
"\[
try:
list = driver.find_elements(BYformat,id)
rc = "ok"
except Exception as e:
rc = repr(e)
]\"
);
Return( rc );
);
getNElements = function({},{n},
Python Execute( {}, {n},
"\[
try:
n = len(list)
except Exception as e:
print(repr(e))
n = -1
]\"
);
return(n);
);
getElementItext = function({i,id, BYformat=By_ID},{txt},
Python Execute( {i, id, BYformat}, {txt},
"\[
try:
txt = list[int(i)].find_element(BYformat, id).text
except Exception as e:
txt = "Error: getElementItext: " + repr(e)
]\"
);
return(txt);
);
getElementIattribute = function({i,id, BYformat=By_ID, attr},{txt},
Python Execute( {i, id, BYformat, attr}, {txt},
"\[
try:
txt = list[int(i)].find_element(BYformat, id).get_attribute(attr)
except Exception as e:
txt = "Error: getElementIattribute: " + repr(e)
]\"
);
return(txt);
);
Ci-dessus : quelques fonctions à utiliser dans la boucle ci-dessous. Il y a des boutons au bas de la page pour passer à la page suivante ; ils exécutent du JavaScript qui détruit et recrée la liste des éléments. Les fonctions sont appelées à nouveau pour récupérer la nouvelle liste. Le JSL et Python sont assez bons pour cet exemple. Ils tomberont en panne s'il y a plus d'une liste à suivre en même temps - voir le Python liste variable. Je suis à peu près sûr qu'il faut attendre que les données se chargent après chaque page suivante...
dt = New Table( "articles",
New Column( "link", character,
Set Property("Event Handler",
Event Handler(
Click(JSL Quote(Function( {thisTable, thisColumn, iRow}, Web( Char( thisTable:thisColumn[ iRow ] ) ); );)),
Tip(JSL Quote(Function( {thisTable, thisColumn, iRow}, "Open " || Char( thisTable:thisColumn[ iRow ] ) || " in your browser."; );)),
Color(JSL Quote(Function( {thisTable, thisColumn, iRow}, RGBColor("link"); );))
)
)
),
New Column( "title", character ),
New Column( "description", character )
);
// this while loop will grab screens of answers and break() when the NextScreen button goes dim
while(1,
// not sure, yet, what to wait for. maybe the card(s)...
// this html is updated in the background by a json ajax mechanism that
// is hard to see.
rc = waitID("//div[@id='searchresults']//div[@class='result-card']",10,By_XPATH);
If( rc != "ok", Throw( "no search results" ) );
// get the current set
rc = getElements("//div[@id='searchresults']//div[@class='result-card']",By_XPATH);
if( rc != "ok", throw("getElements: "||char(rc)));
n = getNElements();
for(i=0,i<N></N>
À ce stade, le navigateur est ouvert et ce tableau est à l'écran.
Today there were 57 entries spanning three pages.Aujourd'hui, il y avait 57 entrées sur trois pages.
Il est temps de fermer le navigateur.
// 5: quit
Python Submit(
"\[
driver.quit() # close the browser
]\" );
Python Term();
Vers la fin il y a un XPATH
rc = clickID("//ul[@id='pager']//a[@class='pager-next']",By_XPATH);
cela signifie
// - somewhere below the root of the document find a
ul - a <ul> tag (some sort of HTML list)
[@id='pager'] - the list has this id
// - more nested tags, followed by...
a = a <a> tag (link)
[@class='pager-next']
Cela n'a peut-être pas besoin d'être si compliqué. C'est un exemple qui utilise un identifiant unique pour trouver un élément qui pourrait ne pas être unique si seule la classe était prise en compte.