Gran parte del Web utilizza API riposanti per spostare i dati da e verso i server. riposo è un concetto semplice che non ha nulla a che fare con il dormire; riposo significa trasferimento statale rappresentativo , ma questo articolo non riguarda riposo . Questo articolo riguarda un meccanismo maldestro per aggirare la mancanza di un'API di riposo quando hai davvero bisogno di recuperare alcuni dati da un sito.
I siti Web potrebbero non volere che tu lo faccia per vari motivi: la larghezza di banda per i dati costa denaro, la licenza dati costa denaro e anche non guardare gli annunci pubblicitari potrebbe costare denaro. Questo strumento, selenio , è nominalmente per testare un sito web, non per accelerare su un dosso. La maggior parte dei siti ha termini di utilizzo; puoi trovare i termini di JMP in fondo a questa pagina.
Un file JSL completo è allegato. È scritto per Firefox, Windows e il sito web di JMP come appariva l'8 maggio 2022. La parte Firefox può essere cambiata, probabilmente, in molti altri browser. Potrebbe funzionare anche su Mac, non è stato fatto alcun test. Il sito Web di JMP cambierà nel tempo e JSL dovrà essere ottimizzato. Questo è lo svantaggio di non utilizzare un'API ufficiale.
Prima di iniziare, scarica un driver e installa il selenio come mostrato nei commenti qui sotto. Avrai bisogno anche di Firefox o fai qualche ricerca sul driver per il tuo browser preferito.
// 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 ), "" ) ) );
PythonInit() deve essere eseguito solo una volta; collega JMP a Python e richiede alcuni secondi la prima volta. Puoi chiamarlo di nuovo senza penalità. PythonExecute(...) non invia variabili ma ne recupera tre dal codice che esegue. Ci vuole un po' per caricare tutto e avviare il browser.
You are looking at an empty browser controlled by JMPStai guardando un browser vuoto controllato da JMP
Apri la pagina successiva di JMP.COM. Potresti vedere un reindirizzamento che normalmente passa inosservato.
// 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 funzione nav restituisce "ok" o un messaggio di errore. La pagina Web di JMP viene caricata nel browser. Ignora le persone nello screenshot.
The icon means the browser is remote controlled.L'icona indica che il browser è controllato da remoto.
Script per l'accesso al sito JMP. Fai clic con il pulsante destro del mouse sul pulsante Accedi per scoprire il valore ID HTML del pulsante. Ricorda come farlo; Salterò questa spiegazione alla fine...
F12 might bring you to the next screen, but this way the control will already be selected.F12 potrebbe portarti alla schermata successiva, ma in questo modo il controllo sarà già selezionato.
E la console per sviluppatori si apre con l'ID del controllo visualizzato. Più in basso ci saranno controlli che hanno una classe ma non un id. XPATH di Selenium può gestirlo. Il trucco è simile alla navigazione nella casella di visualizzazione: trovare un percorso che non sia troppo fragile e comunque abbastanza specifico.
Use the button id in the JSL that follows.Utilizzare l'id del pulsante nel JSL che segue.
Gli ID sono in genere la scelta migliore quando disponibili perché sono univoci sulla pagina. WaitID attende fino a 10 secondi per la visualizzazione del pulsante di accesso. Potrebbe non essere necessario attendere. Non ci vuole tempo se è già 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)));
Freddo! Il segno sullo schermo si apre. Trova il campo del nome utente successivo.
Now get the id for the user name field by right-click...Ora ottieni l'id per il campo del nome utente facendo clic con il pulsante destro del mouse ...
Ancora una volta, attendi il campo previsto. Ora è necessaria una funzione di sequenza di tasti ...
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.Nome utente falso per password falsa.
Il mio ID utente viene fatto scorrere fuori dallo schermo e il pulsante successivo è visibile...trova il suo nome...
Click the Next button to get the password prompt.Fare clic sul pulsante Avanti per ottenere la richiesta della password.
fai clic su di esso, quindi attendi il campo della password ...
// 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) ) );
quindi inserire la password
// 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)));
e ripeti il processo...trova il pulsante di accesso...
After entering the password, click the sign in button.Dopo aver inserito la password, fare clic sul pulsante di accesso.
cliccalo
// click Sign In
rc = clickID( "okta-signin-submit" );
if( rc != "ok", throw("click signin submit: "||char(rc)));
Abbiamo eseguito l'accesso.
Must be signed in, there is an edit profile choice.Devi essere registrato, c'è una scelta di modifica del profilo.
C'è un campo di ricerca nell'immagine, digita JSL e fai clic sulla lente di ingrandimento. "searchField" è l'id. È possibile fare clic sulla lente d'ingrandimento, ma il selenio ha un meccanismo di invio del modulo che funzionerà al di fuori del searchField, che è un campo di input nel modulo.
The search field needs a bigger window to be visible, here it is.Il campo di ricerca ha bisogno di una finestra più grande per essere visibile, eccolo qui.
C'è del JavaScript asincrono che carica alcune parti della pagina. L'attesa per un campo particolare potrebbe non essere necessaria se il campo viene caricato come parte della pagina.
// 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)));
Ora preparati a sfogliare i risultati. Gli elementi dell'elenco multipagina hanno questo aspetto
The pink outer element holds three inner parts: title, description, link.L'elemento esterno rosa contiene tre parti interne: titolo, descrizione, collegamento.
c'è un elenco degli elementi di dati cerchiati in rosa che si estende su più pagine.
// 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);
);
Sopra: alcune funzioni da utilizzare nel ciclo sottostante. Ci sono pulsanti in fondo alla pagina per andare alla pagina successiva; eseguono alcuni JavaScript che distruggono e ricreano l'elenco di elementi. Le funzioni vengono richiamate nuovamente per recuperare il nuovo elenco. JSL e Python sono abbastanza buoni per questo esempio. Si romperanno se c'è più di un elenco di cui tenere traccia contemporaneamente: vedi Python elenco variabile. Sono abbastanza sicuro che sia necessario attendere il caricamento dei dati dopo ogni pagina successiva...
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>
A questo punto il browser è aperto e questa tabella è sullo schermo.
Today there were 57 entries spanning three pages.Oggi c'erano 57 voci su tre pagine.
È ora di chiudere il browser.
// 5: quit
Python Submit(
"\[
driver.quit() # close the browser
]\" );
Python Term();
Verso la fine c'è un XPATH
rc = clickID("//ul[@id='pager']//a[@class='pager-next']",By_XPATH);
questo significa
// - 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']
Potrebbe non essere necessario che sia così complicato. È un esempio che utilizza un ID univoco per trovare un elemento che potrebbe non essere univoco se fosse considerata solo la classe.