cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Check out the JMP® Marketplace featured Capability Explorer add-in
Choose Language Hide Translation Bar
Craige_Hales
Super User
Browser Scripting with Python Selenium

Much of the web uses restful APIs to move data to and from servers. Rest is a simple concept that has nothing to do with sleeping; rest means representational state transfer, but this article is not about rest. This article is about a kludgy mechanism for working around the lack of a rest API when you really need to retrieve some data from a site.

 

Web sites might not want you to do this for various reasons: bandwidth for data costs money, data licensing costs money, and not watching the advertisements might cost money too. This tool, selenium, is nominally for testing a web site, not for speeding over a speed bump. Most sites have terms of use; you can find JMP's terms at the bottom of this page.

 

A complete JSL file is attached. It is written for Firefox, Windows, and the JMP web site as it looked on 8May2022. The Firefox part can be changed, probably, to many other browsers. It might work on Mac too, no testing was done. The JMP web site will change over time and the JSL will need tweaking. That's the downside of not using an official API.

 

Before starting, download a driver and install selenium as shown in the comments below. You'll need Firefox too, or do some research on the driver for your preferred browser.

 

// 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 ), "" ) ) );

 

The PythonInit() only needs to be done once; it connects JMP to Python and takes a few seconds the first time. You can call it again with no penalty. The PythonExecute(...) sends no variables in but gets three back from the code it runs. It takes a bit to load everything and start the browser.

 

You are looking at an empty browser controlled by JMPYou are looking at an empty browser controlled by JMP

 

Open the JMP.COM page next. You might see a redirect that normally goes unnoticed.

 

// 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)));

 

The nav function returns "ok" or an error message. The JMP web page loads in the browser. Ignore the people in the screenshot.

 

The icon means the browser is remote controlled.The icon means the browser is remote controlled.

 

Script the sign-in to the JMP site. Right-click the Sign in button to find out the button's HTML id value. Remember how to do this; I'll skip this explanation at the end...

 

F12 might bring you to the next screen, but this way the control will already be selected.F12 might bring you to the next screen, but this way the control will already be selected.

 

And the developer console opens with the control's id showing. Further down there will be controls that have a class but not an id. Selenium's XPATH can handle it. The trick is similar to displaybox navigation--finding a path that is not too brittle and still specific enough.

 

Use the button id in the JSL that follows.Use the button id in the JSL that follows.

 

IDs are usually the best choice when available because they are unique on the page. WaitID waits for up to 10 seconds for the login button to appear. It might not be necessary to wait. It takes no time if it is already there.

 

// 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! The sign on screen pops up. Find the user name field next.

 

Now get the id for the user name field by right-click...Now get the id for the user name field by right-click...

 

Again, wait for the expected field. Now a keystroke function is needed...

 

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.Fake user name for fake password.

 

My userid is scrolled off the screen and the next button is visible...find its name...

 

Click the Next button to get the password prompt.Click the Next button to get the password prompt.

 

click it, then wait for the password field...

 

// 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) ) );

 

then enter the 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)));

and repeat the process...find the sign in button...

 

After entering the password, click the sign in button.After entering the password, click the sign in button.

 

click it

 

// click Sign In
rc = clickID( "okta-signin-submit" );
if( rc != "ok", throw("click signin submit: "||char(rc)));

 

We are signed in.

 

Must be signed in, there is an edit profile choice.Must be signed in, there is an edit profile choice.

 

There is a search field in the picture, Type in JSL and click the magnifier. "searchField" is the id. The magnifier could be clicked, but selenium has a submit form mechanism that will work off the searchField, which is an input field in the form.

 

The search field needs a bigger window to be visible, here it is.The search field needs a bigger window to be visible, here it is.

 

There is some asynchronous JavaScript that loads some parts of the page. Waiting for any particular field might not be necessary if the field is loaded as part of the 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)));

 

Now get ready to page through the results. The multi-page listing elements look like this

 

The pink outer element holds three inner parts: title, description, link.The pink outer element holds three inner parts: title, description, link.

 

there is a list of the pink-circled data items that spans multiple 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);
);

 

Above: some functions to use in the loop below. There are buttons at the bottom of the page to go to the next page; they run some JavaScript that destroys and recreates the list of items. The functions are called again to recapture the new list. The JSL and Python are good enough for this example. They will break down if there is more than one list to keep track of at the same time--see the Python list variable. I'm pretty sure it is necessary to wait for the data to load after each next page...

 

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,i+=1,
        // fetch the elements
        dt<<addrows(1);
        dt:title[nrows(dt)] = getElementItext(i,"a[@class='result-title_txt_all']",By_XPATH);
        dt:description[nrows(dt)] = getElementItext(i,"section[@class='result-description_txt_all']",By_XPATH);
        dt:link[nrows(dt)] = getElementIattribute(i,"a[@class='result-url']",By_XPATH,"href");
    );
     
    // advance to next page via pager-next button within the id=pager
    rc = waitID("//ul[@id='pager']//a[@class='pager-next']",1,By_XPATH);
    if(rc != "ok", // check for end vs error
        rc = waitID("//ul[@id='pager']//span[@class='pager-disabled pager-next']",1,By_XPATH);
        if(rc=="ok", break(/*normal exit with disabled span*/), throw("did not find expected pager button disabled"));
    ,//
        rc = clickID("//ul[@id='pager']//a[@class='pager-next']",By_XPATH);
        if( rc != "ok", throw("click pager next: "||char(rc)));
    );
    
);

 

At this point the browser is open and this table is on the screen.

 

Today there were 57 entries spanning three pages.Today there were 57 entries spanning three pages.

 

Time to shut down the browser.

 

// 5: quit
 
Python Submit( 
"\[
driver.quit() # close the browser
]\" );
Python Term();

 

Towards the end there is an XPATH

 

 

rc = clickID("//ul[@id='pager']//a[@class='pager-next']",By_XPATH);

 

 

that means

// - 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']

It might not need to be that complicated. It is an example that uses a unique id to find an item that might not be unique if only the class was considered.

Last Modified: May 8, 2022 10:01 PM
Comments
lala
Level VIII

Hello, Craige!
I'm curious to see if it's easy to implement this control of Selenium in JMP 18 using python, which is built into JMP.
It would be nice if the experts could provide more concise JMP and python intermodal code.
Many thanks to experts!

lala
Level VIII

I mainly want to automatically get the request data response parameters of the web page through JSL.

Like this:

  • This is done manually in the browser by clicking F12.

Thanks!

2024-08-16_22-38-14.png

MarkovHedgehog9
Level II

OK

Paul_Nelson
Staff

I believe what @Craige_Hales wrote here two years ago should still function 'as is' in JMP 18.  While Python Init() is deprecated, because Python is already enabled, it won't break the script.  You will get a warning that Python Init() is deprecated.  Craige has illustrated one of the powerful techniques when you want to use Python from a JSL script.  Creating user defined JSL functions that are implemented by Python code.  Rewriting as a purely Python script should be possible as well since the selenium package is a Python module.  The biggest challenge to just 'running' this script is that JMP's website has likely diverged quite a bit in the past 2+ years.  But the example remains a good one showcasing the power of JMP and Python.  

lala
Level VIII

Thanks Experts!

Use python directly.

2024-09-11_09-52-00.png