Skip to content

Commit ef215b6

Browse files
committed
Make multiple improvements to UC Mode
1 parent 3bb813f commit ef215b6

File tree

11 files changed

+467
-147
lines changed

11 files changed

+467
-147
lines changed

help_docs/method_summary.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ driver.uc_open_with_tab(url) # (New tab with default reconnect_time)
10461046

10471047
driver.uc_open_with_reconnect(url, reconnect_time=None) # (New tab)
10481048

1049-
driver.uc_open_with_disconnect(url) # Open in new tab + disconnect()
1049+
driver.uc_open_with_disconnect(url, timeout=None) # New tab + sleep()
10501050

10511051
driver.reconnect(timeout) # disconnect() + sleep(timeout) + connect()
10521052

@@ -1056,7 +1056,15 @@ driver.connect() # Starts the webdriver service to allow actions again
10561056

10571057
driver.uc_click(selector) # A stealthy click for evading bot-detection
10581058

1059-
driver.uc_switch_to_frame(frame) # switch_to_frame() in a stealthy way
1059+
driver.uc_gui_press_key(key) # Use PyAutoGUI to press the keyboard key
1060+
1061+
driver.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys
1062+
1063+
driver.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster
1064+
1065+
driver.uc_gui_handle_cf(frame="iframe") # PyAutoGUI click CF Turnstile
1066+
1067+
driver.uc_switch_to_frame(frame="iframe") # Stealthy switch_to_frame()
10601068
```
10611069

10621070
--------

help_docs/uc_mode.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ driver.uc_open_with_tab(url)
159159

160160
driver.uc_open_with_reconnect(url, reconnect_time=None)
161161

162-
driver.uc_open_with_disconnect(url)
162+
driver.uc_open_with_disconnect(url, timeout=None)
163163

164164
driver.reconnect(timeout)
165165

@@ -171,6 +171,14 @@ driver.uc_click(
171171
selector, by="css selector",
172172
timeout=settings.SMALL_TIMEOUT, reconnect_time=None)
173173

174+
driver.uc_gui_press_key(key)
175+
176+
driver.uc_gui_press_keys(keys)
177+
178+
driver.uc_gui_write(text)
179+
180+
driver.uc_gui_handle_cf(frame="iframe")
181+
174182
driver.uc_switch_to_frame(frame, reconnect_time=None)
175183
```
176184

@@ -211,6 +219,8 @@ driver.reconnect("breakpoint")
211219
<li>Timing. (<b translate="no">UC Mode</b> methods let you customize default values that aren't good enough for your environment.)</li>
212220
<li>Not using <b><code translate="no">driver.uc_click(selector)</code></b> when you need to remain undetected while clicking something.</li>
213221

222+
👤 On Linux, you may need to use `driver.uc_gui_handle_cf()` to successfully bypass a Cloudflare CAPTCHA. If there's more than one iframe on that website (and Cloudflare isn't the first one) then put the CSS Selector of that iframe as the first arg to `driver.uc_gui_handle_cf()`. This method uses `pyautogui`. In order for `pyautogui` to focus on the correct element, use `xvfb=True` / `--xvfb` to activate a special virtual display on Linux.
223+
214224
👤 To find out if <b translate="no">UC Mode</b> will work at all on a specific site (before adjusting for timing), load your site with the following script:
215225

216226
```python

seleniumbase/behave/behave_sb.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -862,14 +862,17 @@ def get_configured_sb(context):
862862
and not sb.headless2
863863
and not sb.xvfb
864864
):
865-
print(
866-
'(Linux uses "-D headless" by default. '
867-
'To override, use "-D headed" / "-D gui". '
868-
'For Xvfb mode instead, use "-D xvfb". '
869-
"Or you can hide this info by using"
870-
'"-D headless" / "-D headless2".)'
871-
)
872-
sb.headless = True
865+
if not sb.undetectable:
866+
print(
867+
'(Linux uses "-D headless" by default. '
868+
'To override, use "-D headed" / "-D gui". '
869+
'For Xvfb mode instead, use "-D xvfb". '
870+
"Or you can hide this info by using"
871+
'"-D headless" / "-D headless2" / "-D uc".)'
872+
)
873+
sb.headless = True
874+
else:
875+
sb.xvfb = True
873876
# Recorder Mode can still optimize scripts in --headless2 mode.
874877
if sb.recorder_mode and sb.headless:
875878
sb.headless = False

seleniumbase/core/browser_launcher.py

Lines changed: 191 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import warnings
1212
from selenium import webdriver
1313
from selenium.common.exceptions import ElementClickInterceptedException
14+
from selenium.common.exceptions import InvalidSessionIdException
1415
from selenium.webdriver.chrome.service import Service as ChromeService
1516
from selenium.webdriver.common.options import ArgOptions
1617
from selenium.webdriver.common.service import utils as service_utils
@@ -28,6 +29,7 @@
2829
from seleniumbase.core import sb_driver
2930
from seleniumbase.fixtures import constants
3031
from seleniumbase.fixtures import js_utils
32+
from seleniumbase.fixtures import page_actions
3133
from seleniumbase.fixtures import shared_utils
3234

3335
urllib3.disable_warnings()
@@ -409,7 +411,7 @@ def uc_open(driver, url):
409411
if (url.startswith("http:") or url.startswith("https:")):
410412
with driver:
411413
script = 'window.location.href = "%s";' % url
412-
js_utils.call_me_later(driver, script, 33)
414+
js_utils.call_me_later(driver, script, 5)
413415
else:
414416
driver.default_get(url) # The original one
415417
return None
@@ -440,22 +442,28 @@ def uc_open_with_reconnect(driver, url, reconnect_time=None):
440442
url = "https://" + url
441443
if (url.startswith("http:") or url.startswith("https:")):
442444
script = 'window.open("%s","_blank");' % url
443-
js_utils.call_me_later(driver, script, 3)
444-
time.sleep(0.007)
445+
driver.execute_script(script)
446+
time.sleep(0.05)
445447
driver.close()
446448
if reconnect_time == "disconnect":
447449
driver.disconnect()
448-
time.sleep(0.007)
450+
time.sleep(0.008)
449451
else:
450452
driver.reconnect(reconnect_time)
451-
driver.switch_to.window(driver.window_handles[-1])
453+
time.sleep(0.004)
454+
try:
455+
driver.switch_to.window(driver.window_handles[-1])
456+
except InvalidSessionIdException:
457+
time.sleep(0.05)
458+
driver.switch_to.window(driver.window_handles[-1])
452459
else:
453460
driver.default_get(url) # The original one
454461
return None
455462

456463

457-
def uc_open_with_disconnect(driver, url):
464+
def uc_open_with_disconnect(driver, url, timeout=None):
458465
"""Open a url and disconnect chromedriver.
466+
Then waits for the duration of the timeout.
459467
Note: You can't perform Selenium actions again
460468
until after you've called driver.connect()."""
461469
if url.startswith("//"):
@@ -464,11 +472,16 @@ def uc_open_with_disconnect(driver, url):
464472
url = "https://" + url
465473
if (url.startswith("http:") or url.startswith("https:")):
466474
script = 'window.open("%s","_blank");' % url
467-
js_utils.call_me_later(driver, script, 3)
468-
time.sleep(0.007)
475+
driver.execute_script(script)
476+
time.sleep(0.05)
469477
driver.close()
470478
driver.disconnect()
471-
time.sleep(0.007)
479+
min_timeout = 0.008
480+
if timeout and not str(timeout).replace(".", "", 1).isdigit():
481+
timeout = min_timeout
482+
if not timeout or timeout < min_timeout:
483+
timeout = min_timeout
484+
time.sleep(timeout)
472485
else:
473486
driver.default_get(url) # The original one
474487
return None
@@ -490,7 +503,7 @@ def uc_click(
490503
pass
491504
element = driver.wait_for_selector(selector, by=by, timeout=timeout)
492505
tag_name = element.tag_name
493-
if not tag_name == "span": # Element must be "visible"
506+
if not tag_name == "span" and not tag_name == "input": # Must be "visible"
494507
element = driver.wait_for_element(selector, by=by, timeout=timeout)
495508
try:
496509
element.uc_click(
@@ -509,7 +522,154 @@ def uc_click(
509522
driver.reconnect(reconnect_time)
510523

511524

512-
def uc_switch_to_frame(driver, frame, reconnect_time=None):
525+
def verify_pyautogui_has_a_headed_browser():
526+
"""PyAutoGUI requires a headed browser so that it can
527+
focus on the correct element when performing actions."""
528+
if sb_config.headless or sb_config.headless2:
529+
raise Exception(
530+
"PyAutoGUI can't be used in headless mode!"
531+
)
532+
533+
534+
def install_pyautogui_if_missing():
535+
verify_pyautogui_has_a_headed_browser()
536+
pip_find_lock = fasteners.InterProcessLock(
537+
constants.PipInstall.FINDLOCK
538+
)
539+
with pip_find_lock: # Prevent issues with multiple processes
540+
try:
541+
import pyautogui
542+
try:
543+
use_pyautogui_ver = constants.PyAutoGUI.VER
544+
if pyautogui.__version__ != use_pyautogui_ver:
545+
del pyautogui
546+
shared_utils.pip_install(
547+
"pyautogui", version=use_pyautogui_ver
548+
)
549+
import pyautogui
550+
except Exception:
551+
pass
552+
except Exception:
553+
print("\nPyAutoGUI required! Installing now...")
554+
shared_utils.pip_install(
555+
"pyautogui", version=constants.PyAutoGUI.VER
556+
)
557+
558+
559+
def get_configured_pyautogui(pyautogui_copy):
560+
if (
561+
IS_LINUX
562+
and hasattr(pyautogui_copy, "_pyautogui_x11")
563+
and "DISPLAY" in os.environ.keys()
564+
):
565+
if (
566+
hasattr(sb_config, "_pyautogui_x11_display")
567+
and sb_config._pyautogui_x11_display
568+
and hasattr(pyautogui_copy._pyautogui_x11, "_display")
569+
and (
570+
sb_config._pyautogui_x11_display
571+
== pyautogui_copy._pyautogui_x11._display
572+
)
573+
):
574+
pass
575+
else:
576+
import Xlib.display
577+
pyautogui_copy._pyautogui_x11._display = (
578+
Xlib.display.Display(os.environ['DISPLAY'])
579+
)
580+
sb_config._pyautogui_x11_display = (
581+
pyautogui_copy._pyautogui_x11._display
582+
)
583+
return pyautogui_copy
584+
585+
586+
def uc_gui_press_key(driver, key):
587+
install_pyautogui_if_missing()
588+
import pyautogui
589+
pyautogui = get_configured_pyautogui(pyautogui)
590+
gui_lock = fasteners.InterProcessLock(
591+
constants.MultiBrowser.PYAUTOGUILOCK
592+
)
593+
with gui_lock:
594+
pyautogui.press(key)
595+
596+
597+
def uc_gui_press_keys(driver, keys):
598+
install_pyautogui_if_missing()
599+
import pyautogui
600+
pyautogui = get_configured_pyautogui(pyautogui)
601+
gui_lock = fasteners.InterProcessLock(
602+
constants.MultiBrowser.PYAUTOGUILOCK
603+
)
604+
with gui_lock:
605+
for key in keys:
606+
pyautogui.press(key)
607+
608+
609+
def uc_gui_write(driver, text):
610+
install_pyautogui_if_missing()
611+
import pyautogui
612+
pyautogui = get_configured_pyautogui(pyautogui)
613+
gui_lock = fasteners.InterProcessLock(
614+
constants.MultiBrowser.PYAUTOGUILOCK
615+
)
616+
with gui_lock:
617+
pyautogui.write(text)
618+
619+
620+
def uc_gui_handle_cf(driver, frame="iframe"):
621+
source = driver.get_page_source()
622+
if (
623+
"//challenges.cloudflare.com" not in source
624+
and 'aria-label="Cloudflare"' not in source
625+
):
626+
return
627+
install_pyautogui_if_missing()
628+
import pyautogui
629+
pyautogui = get_configured_pyautogui(pyautogui)
630+
gui_lock = fasteners.InterProcessLock(
631+
constants.MultiBrowser.PYAUTOGUILOCK
632+
)
633+
with gui_lock: # Prevent issues with multiple processes
634+
needs_switch = False
635+
is_in_frame = js_utils.is_in_frame(driver)
636+
if is_in_frame and driver.is_element_present("#challenge-stage"):
637+
driver.switch_to.parent_frame()
638+
needs_switch = True
639+
is_in_frame = js_utils.is_in_frame(driver)
640+
if not is_in_frame:
641+
# Make sure the window is on top
642+
page_actions.switch_to_window(
643+
driver,
644+
driver.current_window_handle,
645+
timeout=settings.SMALL_TIMEOUT,
646+
)
647+
if not is_in_frame or needs_switch:
648+
# Currently not in frame (or nested frame outside CF one)
649+
try:
650+
driver.switch_to_frame(frame)
651+
except Exception:
652+
if driver.is_element_present("iframe"):
653+
driver.switch_to_frame("iframe")
654+
else:
655+
return
656+
try:
657+
driver.execute_script('document.querySelector("input").focus()')
658+
except Exception:
659+
try:
660+
driver.switch_to.default_content()
661+
except Exception:
662+
return
663+
driver.disconnect()
664+
try:
665+
pyautogui.press(" ")
666+
except Exception:
667+
pass
668+
reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.5
669+
driver.reconnect(reconnect_time)
670+
671+
672+
def uc_switch_to_frame(driver, frame="iframe", reconnect_time=None):
513673
from selenium.webdriver.remote.webelement import WebElement
514674
if isinstance(frame, WebElement):
515675
if not reconnect_time:
@@ -3822,6 +3982,26 @@ def get_local_driver(
38223982
driver.uc_click = lambda *args, **kwargs: uc_click(
38233983
driver, *args, **kwargs
38243984
)
3985+
driver.uc_gui_press_key = (
3986+
lambda *args, **kwargs: uc_gui_press_key(
3987+
driver, *args, **kwargs
3988+
)
3989+
)
3990+
driver.uc_gui_press_keys = (
3991+
lambda *args, **kwargs: uc_gui_press_keys(
3992+
driver, *args, **kwargs
3993+
)
3994+
)
3995+
driver.uc_gui_write = (
3996+
lambda *args, **kwargs: uc_gui_write(
3997+
driver, *args, **kwargs
3998+
)
3999+
)
4000+
driver.uc_gui_handle_cf = (
4001+
lambda *args, **kwargs: uc_gui_handle_cf(
4002+
driver, *args, **kwargs
4003+
)
4004+
)
38254005
driver.uc_switch_to_frame = (
38264006
lambda *args, **kwargs: uc_switch_to_frame(
38274007
driver, *args, **kwargs

0 commit comments

Comments
 (0)