Instantly share code, notes, and snippets.
Created
November 21, 2019 21:00
-
Star
11
(11)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
Save ultrafunkamsterdam/f6d8b02ae29d69dbb2970cda11fe634f to your computer and use it in GitHub Desktop.
Selenium ChromeDriver patch to stay invisible for bot-detection or anti-bot services like distilnetworks.com.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
888 888 d8b | |
888 888 Y8P | |
888 888 | |
.d8888b 88888b. 888d888 .d88b. 88888b.d88b. .d88b. .d88888 888d888 888 888 888 .d88b. 888d888 | |
d88P" 888 "88b 888P" d88""88b 888 "888 "88b d8P Y8b d88" 888 888P" 888 888 888 d8P Y8b 888P" | |
888 888 888 888 888 888 888 888 888 88888888 888 888 888 888 Y88 88P 88888888 888 | |
Y88b. 888 888 888 Y88..88P 888 888 888 Y8b. Y88b 888 888 888 Y8bd8P Y8b. 888 | |
"Y8888P 888 888 888 "Y88P" 888 888 888 "Y8888 "Y88888 888 888 Y88P "Y8888 888 88888888 | |
888 d8b 888 888 | |
888 Y8P 888 888 | |
888 888 888 | |
88888b. 888 .d88888 .d88888 .d88b. 88888b. | |
888 "88b 888 d88" 888 d88" 888 d8P Y8b 888 "88b | |
888 888 888 888 888 888 888 88888888 888 888 | |
888 888 888 Y88b 888 Y88b 888 Y8b. 888 888 | |
888 888 888 "Y88888 "Y88888 "Y8888 888 888 | |
BY ULTRAFUNKAMSTERDAM (https://github.com/ultrafunkamsterdam) | |
################################################################## | |
Optimized Selenium Chromedriver patch which does not trigger anti-bot services like Distill Network. | |
Automatically downloads the driver binary. | |
Not tested on Chrome higher than 78! | |
################################################################# | |
USAGE | |
# by far the easiest | |
from chromedriver_hidden import Chrome | |
driver = Chrome() | |
driver.get('https://distilnetworks.com') | |
# patches current selenium instance (for current session, so not persistent) | |
import chromedriver_hidden | |
chromedriver_hidden.patch_selenium_webdriver() | |
from selenium.webdriver import Chrome | |
driver = Chrome() | |
driver.get('https://distilnetworks.com') | |
OR | |
import chromedriver_hidden | |
chromedriver_hidden.patch_selenium_webdriver() | |
from selenium.webdriver import Chrome, ChromeOptions | |
opts = ChromeOptions() | |
opts.add_argument( ... ) | |
driver = Chrome(options=opts) | |
driver.get('https://distilnetworks.com') | |
OR (careful) | |
import chromedriver_hidden | |
driver_exe = chromedriver_hidden.fetch_and_patch(78) # downloads to current working directory | |
driver = Chrome(executable_path=driver_exe) | |
# this last one does not make chrome fully undetectable. you will need the custom ChromeOptions object or | |
# create a new ChromeOptions instance: | |
# opts = ChromeOptions() | |
# opts.add_experimental_option("excludeSwitches", ["enable-automation"]) | |
# opts.add_experimental_option("useAutomationExtension", False) | |
# driver = Chrome(options=opts) | |
OR | |
a combination of function(s) from this module :) | |
""" | |
import io | |
import os | |
import zipfile | |
from urllib.request import urlopen, urlretrieve | |
import logging | |
from selenium.webdriver import Chrome as _Chrome | |
from selenium.webdriver import ChromeOptions as _ChromeOptions | |
_DL_BASE = "https://chromedriver.storage.googleapis.com/" | |
__is_patched__ = 0 | |
class Chrome: | |
def __new__(cls, *args, **kwargs): | |
if not __is_patched__: | |
patch_selenium_webdriver() | |
return _Chrome() | |
class ChromeOptions: | |
def __new__(cls, *args, **kwargs): | |
if not __is_patched__: | |
patch_selenium_webdriver() | |
return _ChromeOptions() | |
def patch_selenium_webdriver(executable_path=None, platform="win32", version_int=78): | |
""" | |
Patches existing webdriver path on <executable_path> OR if executable_path is None, will download | |
and patch a new webdriver binary for chrome <version_int> (automatically finds latest release of main version) | |
:param str executable_path: path to existing chromedriver executable. | |
:param str platform: win32, mac64, linux64 | |
:param int version_int: chrome version main version. default 78 | |
:return: | |
""" | |
import selenium.webdriver.chrome.service | |
import selenium.webdriver.chrome | |
import selenium.webdriver | |
if executable_path: | |
patch(executable_path) | |
else: | |
cur_path = os.getcwd() | |
executable = fetch_and_patch(platform=platform, version_int=version_int) | |
executable_path = os.path.join(cur_path, executable) | |
# Monkeypatching ChromeDriver Service | |
Service__init__ = selenium.webdriver.chrome.service.Service.__init__ | |
def patched_Service__init__(self, *a, **k): | |
logging.warning("Using patched ChromeDriver Service class") | |
Service__init__(self, executable_path, **k) | |
selenium.webdriver.chrome.service.Service.__init__ = patched_Service__init__ | |
# monkeypatching ChromeOptions | |
ChromeOptions__init__ = selenium.webdriver.ChromeOptions.__init__ | |
def patched_ChromeOptions__init__(self): | |
logging.warning("Using patched ChromeOptions class") | |
ChromeOptions__init__(self) | |
self.add_argument("start-maximized") | |
self.add_experimental_option("excludeSwitches", ["enable-automation"]) | |
self.add_experimental_option("useAutomationExtension", False) | |
selenium.webdriver.ChromeOptions.__init__ = patched_ChromeOptions__init__ | |
logging.warning("Now it is safe to import Chrome and ChromeOptions from selenium") | |
def fetch_and_patch(platform="win32", version_int=78): | |
""" | |
Convenience function for downloading, unpacking and patching chromedriver from source | |
:return: binary name on success, else False | |
""" | |
return patch(fetch_chromedriver(platform="win32", main_version=version_int)) | |
def get_latest_release_version_number(main_version=None): | |
""" | |
Gets the latest version number | |
:param main_version: | |
if specified, get the latest subversion of <main_version> | |
this could be higher than the version returned without specifying the main_version, since | |
pre-release and beta's are also considered versions. | |
:return: version string | |
""" | |
path = "LATEST_RELEASE" if not main_version else f"LATEST_RELEASE_{main_version}" | |
return urlopen(_DL_BASE + path).read().decode() | |
def fetch_chromedriver(platform="win32", main_version=None): | |
""" | |
Downloads ChromeDriver from source and unpacks the executable | |
:param platform: win32, mac64, linux64 | |
:param version_int: chrome version main version integer. 77, 78, 79, ... | |
:return: on success, name of the unpacked executable | |
""" | |
base_ = "chromedriver{}" | |
exe_name = base_.format(".exe") | |
zip_name = base_.format(".zip") | |
latest = get_latest_release_version_number(main_version) | |
urlretrieve( | |
f"{_DL_BASE}{latest}/{base_.format(f'_{platform}')}.zip", filename=zip_name | |
) | |
with zipfile.ZipFile(zip_name) as zf: | |
zf.extract(exe_name) | |
os.remove(zip_name) | |
return exe_name | |
def patch(binary): | |
""" | |
Patches the ChromeDriver binary | |
:param binary: path or open file object to the chromedriver binary | |
:return: False on failure, binary name on success | |
""" | |
global __is_patched__ | |
if not hasattr(binary, "read"): | |
binary = open(binary, "rb") | |
if binary.mode not in ("r+b", "a+b"): | |
binary.close() | |
binary = io.open(binary.name, "r+b") | |
for line in iter(lambda: binary.readline(), b""): | |
if b"cdc_" in line: | |
binary.seek(-len(line), 1) | |
line = b" var key = '$azc_abcdefghijklmnopQRstuv_';\n" | |
binary.write(line) | |
break | |
else: | |
return False | |
__is_patched__ = 1 | |
return binary.name |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment