[631]一行js代码识别Selenium+Webdriver

本文介绍如何在使用Selenium WebDriver进行自动化测试时,通过修改navigator.webdriver属性和使用ChromeDevtools-Protocol来避免被网站识别,实现更隐蔽的爬虫操作。

一行js代码识别Selenium+Webdriver

有不少朋友在开发爬虫的过程中喜欢使用Selenium + Chromedriver,以为这样就能做到不被网站的反爬虫机制发现。

先不说淘宝这种基于用户行为的反爬虫策略,仅仅是一个普通的小网站,使用一行Javascript代码,就能轻轻松松识别你是否使用了Selenium + Chromedriver模拟浏览器。

我们来看一个例子,使用下面这一段代码启动Chrome窗口:

from selenium.webdriver import Chrome
driver = Chrome()

现在,在这个窗口中打开开发者工具,并定位到Console选项卡,如下图所示。

image

现在,在这个窗口输入如下的js代码并按下回车键:

window.navigator.webdriver

可以看到,开发者工具返回了true。如下图所示。

image

但是,如果你打开一个普通的Chrome窗口,执行相同的命令,可以发现这行代码的返回值为undefined,如下图所示。

image

所以,如果网站通过js代码获取这个参数,返回值为undefined说明是正常的浏览器,返回true说明用的是Selenium模拟浏览器。一抓一个准。这里给出一个检测Selenium的js代码例子:

webdriver = window.navigator.webdriver;
if(webdriver){    
console.log('你这个傻逼你以为使用Selenium模拟浏览器就可以了?')
} else {    
console.log('正常浏览器')
}

网站只要在页面加载的时候运行这个js代码,就可以识别访问者是不是用的Selenium模拟浏览器。如果是,就禁止访问或者触发其他反爬虫的机制。

那么对于这种情况,在爬虫开发的过程中如何防止这个参数告诉网站你在模拟浏览器呢?

可能有一些会js的朋友觉得可以通过覆盖这个参数从而隐藏自己,但实际上这个值是不能被覆盖的:

image

对js更精通的朋友,可能会使用下面这一段代码来实现:

Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});
 js = r"Object.defineProperty(navigator, 'webdriver', {get: () => undefined,});"                           
self.driver.execute_script(js)   

运行效果如下图所示:

image

确实修改成功了。这种写法就万无一失了吗?并不是这样的,如果此时你在模拟浏览器中通过点击链接、输入网址进入另一个页面,或者开启新的窗口,你会发现,window.navigator.webdriver又变成了true。如下图所示。

image

那么是不是可以在每一个页面都打开以后,再次通过webdriver执行上面的js代码,从而实现在每个页面都把window.navigator.webdriver设置为undefined呢?也不行。

因为当你执行:driver.get(网址)的时候,浏览器会打开网站,加载页面并运行网站自带的js代码。所以在你重设window.navigator.webdriver之前,实际上网站早就已经知道你是模拟浏览器了。

接下来,又有朋友提出,可以通过编写Chrome插件来解决这个问题,让插件里面的js代码在网站自带的所有js代码之前执行。

这样做当然可以,不过有更简单的办法,只需要设置Chromedriver的启动参数即可解决问题。

在启动Chromedriver之前,为Chrome开启实验性功能参数excludeSwitches,它的值为['enable-automation'],完整代码如下:

from selenium.webdriver import Chrome
from selenium.webdriver import ChromeOptions 

option = ChromeOptions()
#开发者模式
option.add_experimental_option('excludeSwitches', ['enable-automation'])
driver = Chrome(options=option)

此时启动的Chrome窗口,在右上角会弹出一个提示,不用管它,不要点击停用按钮。

再次在开发者工具的Console选项卡中查询window.navigator.webdriver,可以发现这个值已经自动变成undefined了。并且无论你打开新的网页,开启新的窗口还是点击链接进入其他页面,都不会让它变成true。运行效果如下图所示。

image

截至2019年02月12日20:46分,本文所讲的方法可以用来登录知乎。如果使用 Selenium 直接登录知乎,会弹出验证码;先使用本文的方法再登录知乎,能够成功伪装成真实的浏览器,不会弹出验证码。

实际上,Selenium + Webdriver能被识别的特征不止这一个。

然而时过境迁,随着 Chrome 版本升级,这一方法也宣告失效。

如何正确移除Selenium中的 window.navigator.webdriver(最新版)

我们今天的方法非常简单。就是使用 Google 的Chrome Devtools-Protocol(Chrome 开发工具协议)简称CDP

我们打开 CPD 的官方文档[1],可以看到如下的命令:

image

在每个Frame 刚刚打开,还没有运行 Frame 的脚本前,运行给定的脚本。

通过这个命令,我们可以给定一段 JavaScript 代码,让 Chrome 刚刚打开每一个页面,还没有运行网站自带的 JavaScript 代码时,就先执行我们给定的这段代码。

那么如何在 Selenium 中调用 CDP 的命令呢?实际上非常简单,我们使用driver.execute_cdp_cmd。根据 Selenium 的官方文档[2],传入需要调用的 CDP 命令和参数即可:

image

于是我们可以写出如下代码:

from selenium.webdriver import Chrome

driver = Chrome('./chromedriver')
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})
driver.get('http://exercise.kingname.info')

运行效果如下图所示:
image

完美隐藏window.navigator.webdriver。并且,关键语句:

driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})

只需要执行一次,之后只要你不关闭这个driver开启的窗口,无论你打开多少个网址,他都会自动提前在网站自带的所有 js 之前执行这个语句,隐藏window.navigator.webdriver

如果有人运行上面的代码,出现如下报错:

image

那么请升级你的 ChromeDriver。老版本的 Chrome + ChromeDriver 只能用以前的方法,不能用今天的方法。新版本的 Chrome + ChromeDriver 可以使用今天的方法,但不能用老方法。正应了那句话:

上帝给你关上一扇门的时候,悄悄为你开了一扇窗。

虽然使用以上代码就可以达到目的了,不过为了实现更好的隐藏效果,大家也可以继续加入两个实验选项:

from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(options=options, executable_path='./chromedriver')
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})
driver.get('http://exercise.kingname.info')

附一些网站检测selenium的示例

runBotDetection = function () {
    var documentDetectionKeys = [
        "__webdriver_evaluate",
        "__selenium_evaluate",
        "__webdriver_script_function",
        "__webdriver_script_func",
        "__webdriver_script_fn",
        "__fxdriver_evaluate",
        "__driver_unwrapped",
        "__webdriver_unwrapped",
        "__driver_evaluate",
        "__selenium_unwrapped",
        "__fxdriver_unwrapped",
    ];

    var windowDetectionKeys = [
        "_phantom",
        "__nightmare",
        "_selenium",
        "callPhantom",
        "callSelenium",
        "_Selenium_IDE_Recorder",
    ];

    for (const windowDetectionKey in windowDetectionKeys) {
        const windowDetectionKeyValue = windowDetectionKeys[windowDetectionKey];
        if (window[windowDetectionKeyValue]) {
            return true;
        }
    };
    for (const documentDetectionKey in documentDetectionKeys) {
        const documentDetectionKeyValue = documentDetectionKeys[documentDetectionKey];
        if (window['document'][documentDetectionKeyValue]) {
            return true;
        }
    };

    for (const documentKey in window['document']) {
        if (documentKey.match(/\$[a-z]dc_/) && window['document'][documentKey]['cache_']) {
            return true;
        }
    }

    if (window['external'] && window['external'].toString() && (window['external'].toString()['indexOf']('Sequentum') != -1)) return true;

    if (window['document']['documentElement']['getAttribute']('selenium')) return true;
    if (window['document']['documentElement']['getAttribute']('webdriver')) return true;
    if (window['document']['documentElement']['getAttribute']('driver')) return true;

    return false;
};
try {
 if (window.document.documentElement.getAttribute("webdriver")) return !+[]
} catch (IDLMrxxel) {}
try {
 if ("_Selenium_IDE_Recorder" in window) return !+""
} catch (KknKsUayS) {}
try {
 if ("__webdriver_script_fn" in document) return !+""

改写特征参数的js

// 改写 `languages` 
Object.defineProperty(navigator, "languages", {
  get: function() {
    return ["en", "es"];
  }
});

//改写 `plugins`
Object.defineProperty(navigator, "plugins", {
  get: () => new Array(Math.floor(Math.random() * 6) + 1),
});

// 改写`webdriver`
Object.defineProperty(navigator, "webdriver", {
  get: () => false,
});

driver.execute_script

  • 执行单行javascript代码。例:已知‘id’,向表格里注入用户名
driver.execute_script("document.getElementById('id').value='用户名';"
  • 执行多行javascript代码。例:用javascript注入js文件
javacriptCode = '''
var scriptElt = document.createElement('script');
scriptElt.type = 'text/javascript';
scriptElt.src = jsFile;
document.getElementsByTagName('head')[0].appendChild(scriptElt);
'''
try
    driver.execute_script(javacriptCode)
except:
    pass

为什么使用Try / Except 语句?
执行javascript代码时很多情况python会报错,但往往代码却可以被正确的执行。加上Try / Except来保护程序的执行。

  • 根据坐标,命令selenium点击特定位置。有的时候很难定位一个位置,我们可以用坐标这样的绝对位置来定位
#例如点击屏幕上 (271,100)这个位置
driver.execute_script("document.elementFromPoint(271, 100).click();")

参考资料

[1]CPD 的官方文档: https://chromedevtools.github.io/devtools-protocol/tot/Page#method-addScriptToEvaluateOnNewDocument
[2]官方文档: https://www.selenium.dev/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.webdriver.html#selenium.webdriver.chrome.webdriver.WebDriver.execute_cdp_cmd

参考:https://blog.youkuaiyun.com/weixin_33816821/article/details/88590077
https://blog.youkuaiyun.com/jiduochou963/article/details/88200217
https://blog.youkuaiyun.com/sinat_38682860/article/details/86221844
https://mp.weixin.qq.com/s/U2aAC6K6RuQDRqfb8m35_w

import time import pandas as pd import openpyxl from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.select import Select from selenium.webdriver import ActionChains, Keys from selenium.webdriver.chrome.options import Options chrome_options = Options() chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) chrome_options.add_argument("--disable-blink-features=AutomationControlled") service = Service("D:/Python313/chromedriver.exe") driver = webdriver.Chrome(service=service, options=chrome_options) driver.maximize_window() file_path = 'C:/Users/一羽风/Desktop/机器人python/1.小组件测试文件/Best_Match.xlsx' wb = openpyxl.load_workbook(file_path) sheet1 = wb.worksheets[0] list_text = [] account_list = [] for row in sheet1.iter_rows(min_row=1,values_only=True): account_list.append((row[0])) driver.get("https://www.bilibili.com/") search_box = driver.find_element(By.XPATH, '//*[@id="nav-searchform"]/div[1]/input') for account in account_list[:2]: search_box.send_keys(Keys.CONTROL + 'a') search_box.send_keys(Keys.DELETE) search_box.send_keys(account) search_box.send_keys(Keys.RETURN) time.sleep(2) original_window = driver.current_window_handle new_window = [w for w in driver.window_handles if w != original_window][0] driver.switch_to.window(new_window) for page in range(3): next_page = driver.find_element(By.CLASS_NAME, 'vui_button vui_pagenation--btn') elements = driver.find_elements(By.CLASS_NAME,'bili-video-card__info') for element in elements[:5]: value2 = element.get_attribute("innerText") # print(value2) time.sleep(5) list_text.append(value2) print(list_text) next_page.click() time.sleep(8) driver.close() driver.switch_to.window(original_window) b站搜索内容之后,想点击下一页获取数据,但总是点击不了下一页按钮,总是报错,出错代码为:next_page = driver.find_element(By.CLASS_NAME, 'vui_button vui_pagenation--btn')
03-08
<think>嗯,用户在使用Selenium自动化B站搜索时遇到了点击下一页按钮报错的问题。错误出现在查找下一页按钮的那行代码:`next_page = driver.find_element(By.CLASS_NAME, 'vui_button vui_pagenation--btn')`。我需要仔细分析可能的原因。 首先,检查元素的定位是否正确。用户使用了By.CLASS_NAME,但传入的是两个类名,这其实是不正确的。因为By.CLASS_NAME只能接受单个类名。如果元素同时有多个类名,应该用CSS选择器或者XPath来定位。例如,正确的CSS选择器应该是".vui_button.vui_pagenation--btn",用点号连接类名。或者用户可能误将多个类名当作一个,需要确认页面中实际的类名是什么。 其次,页面加载时间可能不够,导致元素未加载出来。用户虽然用了time.sleep,但隐式等待可能更可靠。建议使用WebDriverWait结合expected_conditions,比如等待元素可点击再进行操作。这能避免因页面加载延迟导致的元素找不到的问题。 另外,窗口切换可能有问题。用户在新窗口操作后关闭,然后切换回原窗口。需要确保切换窗口的逻辑正确,特别是在多次循环中,窗口句柄是否正确获取。可能在切换窗口时,新的标签页没有被正确识别,导致后续操作在错误的窗口中进行。 还有,分页元素的动态变化。有些网站的分页按钮在翻页后可能DOM结构变化,或者有动画效果,需要等待元素重新稳定后再查找。此外,如果页面有iframe,可能需要先切换到iframe内部才能找到元素,但B站一般不太可能用iframe。 用户可能还遇到了其他异常情况,比如元素被遮挡,或者点击后页面未及时响应,导致后续操作失败。需要添加异常处理,比如try-except块,捕获NoSuchElementException等错误,并给出相应的处理措施。 总结下来,可能的问题点包括:类名使用错误、等待时间不足、窗口切换问题、元素定位方式不当。需要逐一排查,优先修正类名的定位方式,改用CSS选择器,并添加显式等待,确保元素加载完成后再操作。</think>这个代码存在多个可能导致分页按钮无法点击的问题,以下是逐步排查和解决方案: $\textcolor{red}{问题定位步骤}$: 1. $\textcolor{blue}{类名定位错误}$(核心问题) - $\textcolor{green}{错误}$:By.CLASS_NAME参数应该接受单个class名称,但代码中同时使用了两个class(vui_button 和 vui_pagenation--btn) - $\textcolor{green}{修正}$:改用CSS选择器组合这两个class ```python next_page = driver.find_element(By.CSS_SELECTOR, '.vui_button.vui_pagenation--btn') ``` 2. $\textcolor{blue}{等待机制缺失}$(常见问题) - 添加显式等待确保元素加载完成: ```python from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC next_page = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, '.vui_button.vui_pagenation--btn')) ) ``` 3. $\textcolor{blue}{窗口焦点问题}$(潜在风险) - 切换窗口后添加等待: ```python driver.switch_to.window(new_window) WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, 'body'))) ``` 4. $\textcolor{blue}{完整修正代码段}$: ```python for account in account_list[:2]: # ...省略前面代码... driver.switch_to.window(new_window) try: for page in range(3): # 添加显式等待 next_page = WebDriverWait(driver, 15).until( EC.element_to_be_clickable((By.CSS_SELECTOR, '.vui_button.vui_pagenation--btn')) ) # 滚动到元素可见位置 driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", next_page) # 使用ActionChains点击更可靠 ActionChains(driver).move_to_element(next_page).click().perform() time.sleep(2) # 适当保留缓冲 except Exception as e: print(f"出现异常:{str(e)}") driver.save_screenshot('error_screenshot.png') ``` $\textcolor{red}{其他注意事项}$: 1. $\textcolor{orange}{页面结构验证}$: 在浏览器控制台验证元素定位: ```javascript document.querySelector('.vui_button.vui_pagenation--btn') // 应返回正确元素 ``` 2. $\textcolor{orange}{反爬机制应对}$: 添加更完整的Chrome选项配置: ```python chrome_options.add_argument("--disable-infobars") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--remote-debugging-port=9222") chrome_options.add_argument(f'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(80,110)}.0.0.0 Safari/537.36') ``` 3. $\textcolor{orange}{日志监控}$: 添加调试日志记录: ```python import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) try: # 操作代码... except NoSuchElementException as e: logger.error(f"元素未找到:{str(e)}") raise ``` $\textcolor{red}{最终验证方法}$: 在点击操作前添加屏幕截图验证: ```python driver.save_screenshot('before_click.png') next_page.click() driver.save_screenshot('after_click.png') ``` 如果仍然报错,建议检查: 1. 页面是否存在验证码或登录拦截 2. 分页按钮是否被动态加载(可能需要触发滚动事件) 3. 网络延迟是否导致元素加载超时(可适当增加等待时间)
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周小董

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值