Python Selenium 爬虫如何隐藏Headles-Chrome不被检测出来 避免检测


Python Selenium 爬虫如何隐藏Headles-Chrome不被检测出来

 

这是上一篇文章的对偶篇, 上一篇文章讲如何检测客户端是否是Chrome Headless, 这篇文章讲那些检测方法是如何失效的!

 

关于黑客新闻的大多数讨论集中在作者有点可疑的断言上,即网络抓取是一种“恶意任务”,与广告欺诈和黑客网站属于同一类别。这总是一个有趣的辩论,但我真正对这篇文章提出的问题是它隐含地提倡了基于浏览器指纹识别来阻止用户的想法。就我而言,这通常是一个糟糕的想法,你更有可能阻止和挫败你的用户,而不是为那些你试图阻止的人提供任何有意义的威慑。

为了说明这一点,我通过了这篇文章中提出的所有测试,毫不奇怪,我的标准日常浏览器未通过某些测试(这说明这些方法很可能阻断正常的用户)。

UserAgent

这几乎是唯一可以合法识别Chrome Headless的测试,但它也是最容易绕过的。在无头模式下运行Chrome时的默认userAgent是这样的:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/60.0.3112.50 Safari/537.36

要更改此设置,我们只需向Chrome提供--user-agent命令行选项即可。如果您直接从命令行运行Chrome,那么除了无头之外,您还需要包含此选项。

如果您使用的是Python+Selenium+ChromeDriver,则可以使用ChromeOptions.add_argument()指定相同的选项。

Python 全选
from selenium import webdriver

user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.50 Safari/537.36'

options = webdriver.ChromeOptions()
# specify headless mode
options.add_argument('headless')
# specify the desired user agent
options.add_argument(f'user-agent={user_agent}')
driver = webdriver.Chrome(chrome_options=options)

# user-agent is now set

如果你想使用Chrome DevTools协议,这种方法会略有不同,但也可以做到。

CDP的nodejs代码:

Python 全选
const CDP = require('chrome-remote-interface');

const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.50 Safari/537.36'

CDP(async function(client) {
  const {Network, Page} = client;
  await Page.enable();
  await Network.enable();
  await Network.setUserAgentOverride({userAgent});

  // user-agent is now set
});

如果你要用python, CDP也被人封装到了python中, 试试pychrome。

所有这些方法都将更改HTTP标头和window.navigator.userAgent中的用户代理。现在进入更具挑战性任务!

Languages and Plugins

建议的两个测试是检查navigator.plugins.length === 0和navigator.languages ==’’。通过JavaScript注入, 将每个页面的window.navigator覆盖,可以绕过这些。让我们现在专注于JavaScript来模拟这些值。一旦我们完成所有测试,我们将回过头来看看如何注入它,因为它们都需要立即注入。

我们基本上只想覆盖navigator的两个属性: plugins和languages。您的第一个想法可能是直接设置属性:

Python 全选
// overwrite the `languages` property to use a custom getter
Object.defineProperty(navigator, 'languages', {
  get: function() {
    return ['en-US', 'en'];
  },
});

// overwrite the `plugins` property to use a custom getter
Object.defineProperty(navigator, 'plugins', {
  get: function() {
    // this just needs to have `length > 0`, but we could mock the plugins too
    return [1, 2, 3, 4, 5];
  },
});

在页面上执行此JavaScript后,navigator.languages将返回[‘en-US’,’en’],navigator.plugins的长度为5. plugins可以做的更真实一点,但这不是必需的, 在这种情况下我们只检查数组的长度。

WebGL Vendor and Renderer

下一个测试是创建一个WebGL上下文,然后检查供应商和渲染器字符串。供应商 Brian Paul或渲染器Mesa OffScreen暗示这可能是Chrome Headless。我无法在我的机器上重现这些字符串,但让我们看看如何在它们存在的情况下模拟这些值。我们只需硬编码WebGLRenderingContext.getParameter(),通过修改WebGLRenderingContext的原型来返回我们想要的供应商和渲染器字符串。修改原型可确保在创建的任何WegGlRenderingContext实例上调用我们的修补版本(例如,通过执行canvas.getContext(’webgl’))。

Python 全选
const getParameter = WebGLRenderingContext.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
  // UNMASKED_VENDOR_WEBGL
  if (parameter === 37445) {
    return 'Intel Open Source Technology Center';
  }
  // UNMASKED_RENDERER_WEBGL
  if (parameter === 37446) {
    return 'Mesa DRI Intel(R) Ivybridge Mobile ';
  }

  return getParameter(parameter);
};

此版本getParameter()返回任何我们想要的数据,同时不影响任何其他参数值的标准实现。这里的整数是唯一标识参数的标准常量。

Broken Image

我们已经提到过,”破碎图像”测试确实没有意义,因为Chrome 60报告的图像大小为0x0,用于无法加载的图像。让我们来看看我们如何做到这一点,只是为了表明它也很容易绕过。这种方法结合了我们已经使用过的几种技术。

我们将再次修改原型,这次是HTMLImageElement,这样我们所做的更改将适用于在DOM中创建的任何图像。宽度和高度都是具有访问器的属性,因此我们还需要使用Object.defineProperty()来覆盖其getter方法。对于损坏的图像,我们将返回20的宽度和高度,否则简单地遵循标准的getter实现。

Python 全选
['height', 'width'].forEach(property => {
  // store the existing descriptor
  const imageDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, property);

  // redefine the property with a patched descriptor
  Object.defineProperty(HTMLImageElement.prototype, property, {
    ...imageDescriptor,
    get: function() {
      // return an arbitrary non-zero dimension if the image failed to load
      if (this.complete && this.naturalHeight == 0) {
        return 20;
      }
      // otherwise, return the actual dimension
      return imageDescriptor.get.apply(this);
    },
  });
});

Retina/HiDPI Hairline Feature

最后提出的方法是使用Modernizr库检测对Hairline的支持。这是另一项测试,并没有真正有意义,因为大多数人没有HiDPI屏幕,大多数用户的浏览器不支持此功能。然而,即使用作测试也是有意义的,绕过它也是很简单的。

我们可以从Modernizr代码看到,测试基本上等于将id为modernizr的div标签与下面的样式表一起插入到页面中。

然后检查div的offsetHeight属性,如果它的值为1,则支持Hairline特征。我们需要做的就是修改HTMLDivElement的原型,使得如果id为modernizr,offsetHeight返回1。这个基本模式现在应该变得非常熟悉……

Python 全选
// store the existing descriptor
const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');

// redefine the property with a patched descriptor
Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
  ...elementDescriptor,
  get: function() {
    if (this.id === 'modernizr') {
        return 1;
    }
    return elementDescriptor.get.apply(this);
  },
});

代码注入

在这一点上,我们有一堆JavaScript代码片段将绕过Chrome Headless测试。我们只需要Chrome在目标网站上的测试代码之前执行它们。使用Selenium通过调用WebDriver.executeScript()将JavaScript注入页面是可能的,但遗憾的是,只有在文档的onload事件被触发后才会调用它。这意味着,如果有一个同步测试阻止页面加载,它在你能够注入js之前运行测试。

如果我们想要可靠地通过测试,那么我们需要找到一种方法来确保我们的代码在页面上的测试代码之前运行。我最喜欢的方法是编写一个小型Chrome扩展程序,将脚本注入任何访问过的页面的头部。不幸的是,Chrome Headless尚不支持扩展,在可预见的未来可能不会。在添加该功能之前,我们需要使用替代方法。

将JavaScript注入页面的更健壮的方法是实际修改所请求的HTML并在浏览器有机会查看原始文件之前注入脚本。这种技术更常用于提供站点自己脚本的修补版本,但它可以以相同的方式应用于此用例。我们将使用mitmproxy,一个非常容易编写脚本的TLS-capabable HTTP代理来注入我们的代码。

首先,我们需要同时安装mitmproxy和BeautifulSoup4。这些可能都是通过您系统的软件包管理器提供的,但如果需要,您也可以使用pip install mitmproxy bs4在virtualenv中安装它们。我们现在将制作一个名为inject.py的简短python脚本,其中包含以下内容。

Python 全选
from bs4 import BeautifulSoup
from mitmproxy import ctx

# load in the javascript to inject
with open('content.js', 'r') as f:
    content_js = f.read()

def response(flow):
    # only process 200 responses of html content
    if flow.response.headers['Content-Type'] != 'text/html':
return
    if not flow.response.status_code == 200:
return

    # inject the script tag
    html = BeautifulSoup(flow.response.text, 'lxml')
    container = html.head or html.body
    if container:
script = html.new_tag('script', type='text/javascript')
script.string = content_js
container.insert(0, script)
flow.response.text = str(html)

ctx.log.info('Successfully injected the content.js script.')

inject.py脚本定义了一个response(flow)函数,该函数将在每个响应被代理之前由mitmproxy调用。如果响应具有200个状态代码和text / html的内容类型,那么我们将脚本注入从content.js加载的内容。这里的content.js只包含我们在前面每个部分中开发的所有JavaScript测试。

要使用此脚本启动代理,我们现在可以运行以下代码。

mitmdump -p 8080 -s "inject.py"

启动chrome:

/opt/google/chrome-beta/chrome \
    --headless \
    --proxy-server=localhost:8080 \
    --remote-debugging-port=9222

--headless 选项可预测地告诉Chrome以无头模式运行--remote-debugging-port指定我们将用于与之通信并控制实例的调试接口。您现在可以运行该命令,它只会等待我们在几分钟内连接到调试端口。

我们将使用Chrome远程接口来实际控制实例。这个界面有点类似于Selenium,但它只能使用Chrome,允许对某些事物进行更精细的控制。特别是,它允许我们接受来自mitmproxy的自签名证书。这可以通过ChromeDriver实现,因为acceptInsecureCerts被指定为W3规范的一部分,但这个特殊功能尚未在Chrome Headless中实现。

要安装Chrome远程接口,您可以运行yarn add chrome-remote-interface或与您选择的软件包管理器等效的接口。安装完成后,创建一个名为test-headless.js的文件,其中包含以下内容。

Python 全选
const CDP = require('chrome-remote-interface');
const fs = require('fs');

// global settings
const filename = 'headless-results.png';
const url = 'https://intoli.com/blog/making-chrome-headless-undetectable/chrome-headless-test.html';
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.50 Safari/537.36'

CDP(async function(client) {
  const {Network, Page, Security} = client;
  await Page.enable();
  await Network.enable();
  await Network.setUserAgentOverride({userAgent});

  // ignore all certificate errors to support mitmproxy certificates
  await Security.enable();
  await Security.setOverrideCertificateErrors({override: true});
  Security.certificateError(({eventId}) => {
    Security.handleCertificateError({
        eventId,
        action: 'continue'
    });
  });

  // navigate to the page and wait for it to load
  await Page.navigate({url});
  await Page.loadEventFired();

  setTimeout(async function() {
    // save the screenshot
    const screenshot = await Page.captureScreenshot({format: 'png'});
    const buffer = new Buffer(screenshot.data, 'base64');
    fs.writeFile(filename, buffer, 'base64', function(err) {
      if (err) {
        console.error(`Error saving screenshot: `${err}`);
      } else {
        console.log(`"$`

此脚本将连接到我们当前运行的Headless Chrome实例,访问测试页,1秒后保存屏幕截图,然后退出。 Chrome本身已经在运行,并且已经配置为通过mitmproxy代理所有内容。因此,Chrome呈现的页面应在文档的head标记中包含我们的代码。

所有部分现在都已到位,我们现在可以使用`node test-headless.js`运行脚本(请注意,由于使用了async / await,这将需要至少7.6的节点版本)。当脚本运行时,我们应该在mitmdump终端中看到类似于以下内容的输出。
Loading script: inject.py
Proxy server listening at http://0.0.0.0:8080
127.0.0.1:57132: clientdisconnect
127.0.0.1:59524: clientconnect
127.0.0.1:59524: CONNECT intoli.com:443
<< Cannot establish TLS with client (sni: intoli.com): TlsException(“(104, ‘ECONNRESET’)”,)
127.0.0.1:59526: clientconnect
127.0.0.1:59524: clientdisconnect
Successfully injected the content.js script.
127.0.0.1:59526: GET https://intoli.com/blog/making-chrome-headless-undetectable/chrome-headless-test.html
<< 200 OK 1.12k
127.0.0.1:59528: clientconnect
127.0.0.1:59526: GET https://intoli.com/blog/making-chrome-headless-undetectable/modernizr.js
<< 200 OK 2.43k
127.0.0.1:59528: CONNECT intoli.com:443
<< Cannot establish TLS with client (sni: intoli.com): TlsException(“(-1, ‘Unexpected EOF’)”,)
127.0.0.1:59530: clientconnect
127.0.0.1:59528: clientdisconnect
127.0.0.1:59526: GET https://intoli.com/blog/making-chrome-headless-undetectable/chrome-headless-test.js
<< 200 OK 2.27k
127.0.0.1:59526: GET https://intoli.com/nonexistent-image.png
<< 404 Not Found 189b
`

还有一些错误,但这些都不用担心,因为它们是客户端需要覆盖证书错误的结果。忽略这些,看起来我们看到了预期的请求,并且在返回chrome-headless-test.html响应之前成功注入了脚本标记。

最后,让我们看一下生成的headless-results.png,以验证我们现在是否通过了所有测试。

总结

这无疑是绕过测试的一种错综复杂的方法。我采用这条较长路线的原因是我想要真正强调的是,测试本身并没有检查任何不容易被欺骗的东西。

根本没有办法区分行为良好的机器人和人类用户。你为什么要这么做?如果机器人代表用户访问网站,并以与用户相同的价格浏览,那么真正的区别是什么?我可以完全理解基于滥用访问的阻止用户,但是它倾向于在风车上试图限制任何形式的自动访问。你最终会阻止无辜的用户,智能机器人将无法被发现。

如果你试图通过它的阻止机制来抓一个有点过于热心的网站,那么请随时与我们保持联系。 Intoli的团队成员是编写与人类用户无法区分的表现良好的机器人的专家。我们还可以帮助您提供自定义数据验证和分析工作流程,帮助您从数据中提取价值。

原文链接: https://intoli.com/blog/making-chrome-headless-undetectable/

 

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:Python Selenium 前端如何检测Chrome-Headless不被爬虫虐
下一篇:Python使用SQL Server数据库
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

Python Selenium 爬虫如何隐藏Headles-Chrome检测出来 避免检测
selenium爬虫检测到 该如何
Python Selenium 前端如何检测Chrome-Headless爬虫
Python Selenium ChromeDriver 规避检测
Python使用selenium+chrome配置指南
C#爬虫:使用Selenium,Selenium配置指南
Python使用selenium+chrome进行抓包
带有爬虫检测的网站汇总
Python Selenium使用火狐浏览器驱动
如何在 Entity Framework 中自动截断超长字符串并避免异常
Python Selenium Firefox 代理设置
Python爬虫解析网页的提取html信息的常用方式
使用 mitmproxy + python 做拦截代理
WPF DataGrid 如何选中行带到视野中
在 SA 和 Windows 等账户都禁用的情况下如何登录?
Python退出主进程后子线程会退出的解决方案
python抓包 ChromeDriver下载地址
asp.net mvc Action直接返回图片浏览器缓存
Python使用UUID模块云服务器上获取MAC地址,重启后就一样了
VS2017调试闪退之Chrome

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单 ME2A 供应商确认 采购监控 Flexible Workflow 凭证释放 采购审批 释放策略 实地盘点 物料凭证 货物移动 MIGO 收货 移动类型 已撤回 供应商退货 货物发出 STO 库存转储 转移过账 生产订单 预留 GR/IR MIRO 供应商发票 物流发票校验 OMR2 税码 FI PP SD 实操教程 MRBR OMR6 发票差异 交货成本 后续借记 MI01 实物盘点 盘点差异 公司代码 工厂 组织结构 OMS2 主数据定制 自动科目确定 BP角色 CVI 伙伴确定 编号范围 凭证类型 字段选择 FBN1 OMBT OMC2 会计凭证 OMJJ BOM 委外加工 项目类别L MRKO 供应商寄售 特殊库存K MRKON PIPE Pipeline 特殊库存P ERS MRIS 发票计划 周期性结算 里程碑付款 变更追踪 版本管理 采购凭证 SFTP WebDAV 网盘 飞牛fnOS AMPL HERS MPN 中文教程 库存确定 可用性检查 缺件检查 Output Management 消息确定 输出确定 分割评估 库存计价 评估类别 评估类型 PB00 RM0000 条件技术 采购定价 MM-FI集成 OBYC 库存估价 文本类型 文本采用 EFB EVO MSV SU3 用户参数 发票校验 合同参照 履约保留款 特别总账 预付款 Fiori Launchpad SAP Fiori 应用导航 用户体验 LSMW LTMC Migration Cockpit 数据迁移 BRFplus OPD Output Control My Inbox 审批流程 灵活工作流 SAP PP 外部加工 SAP QM 检验批 质量信息记录 采购收货 SAP PM 维护BOM 维护订单 SAP SD SAP Service 端到端流程 MM模块培训 FI-MM集成 供应商管理 审批配置 FICO入门 SAP FICO 财务配置 供应商税务 预扣税 House Bank 银行对账 客户清账 应收账款 FI控制 验证与替代 印度 GST 税务配置 F110 FBZP EWM入门 SAP EWM 仓库管理 OX14 成本核算 物料评估 后勤配置 物料组 价值更新 数量更新 PP-PI 流程制造 生产计划 容差配置 SAP事务码 SAP基础 TCODE Basis 事务代码 MMNR 编号区间 采购实操 组织架构 OMSF SAP实操 FI配置
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码