Skip to content

Instantly share code, notes, and snippets.

@ultrafunkamsterdam
Created November 21, 2019 21:00
Show Gist options
  • Save ultrafunkamsterdam/f6d8b02ae29d69dbb2970cda11fe634f to your computer and use it in GitHub Desktop.
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.
#!/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