爬虫知识:补环境相关知识

发布于:2024-07-02 ⋅ 阅读:(179) ⋅ 点赞:(0)

学习目标:知道为什么要补环境,知道要补什么环境(使用Proxy检测)。没有讲解怎么补

本章没有动手去实操,只是纯理论知识

补环境介绍

DOM与BOM

DOM主要关注文档内容和结构,而BOM关注浏览器窗口和功能。在浏览器中,window对象既是BOM的核心,也是全局对象,而document对象(DOM的核心)是window对象的一个属性。

浏览器环境:指JS代码在浏览器中的运行时环境

  1. V8引擎自动构建的对象(即ECMAScript规范的内容,如Date、Array)
  2. 浏览器(内置)提供给V8引擎用于操作DOM和BOM的对象(如document、navigator)

Node环境:基于V8引擎的JavaScript运行时环境

  1. V8引擎提供的核心JavaScript功能
  2. Node.js自身的API,如fs(文件系统), http(网络请求), path(路径处理)等

环境对比

浏览器环境 Node环境
JavaScript引擎 V8(Chrome) V8
全局对象 window global
DOM API 无(可通过第三方库模拟)
BOM API 无(可通过第三方库模拟)
文件系统访问 文件系统访问 完全访问(fs模块)
网络功能 XMLHttpRequest, Fetch http, https模块
定时器 setTimeout, setInterval setTimeout, setInterval
console 浏览器控制台 终端输出
模块系统 ES Modules, CommonJS CommonJS, ES Modules
典型用途 前端Web开发 服务器端开发,工具脚本

"补浏览器环境" :实际上是指补充浏览器环境中存在而Node环境中缺失的部分,主要是BOM(浏览器对象模型)和DOM(文档对象模型)相关的对象和API。

当我们成功提取出网站中的JavaScript加密算法代码,并确认其在浏览器环境中能够正确执行后,下一步是将其迁移到Node环境中执行。然而,由于Node环境与浏览器环境之间存在差异,可能会导致某些JavaScript代码在这两种环境中的执行结果不一致,这可能会影响我们的逆向工程成果。

window对象

window对象是JavaScript中的全局对象,在浏览器环境中代表了当前打开的浏览器窗口。它是所有全局JavaScript对象、函数和变量的顶层容器。以下是window对象的一些重要特性和用途:

在爬虫和反爬context中,window对象尤为重要:

  • 许多网站使用window对象的属性来检测是否在真实浏览器环境中运行。
  • 一些反爬措施会检查window对象的特定属性或方法是否存在或行为是否正常。
  • 在补环境时,常常需要模拟window对象及其众多属性和方法,以欺骗网站的检测机制。

需要注意的是,在Node.js等非浏览器环境中,默认是没有window对象的。在这些环境中补充window对象是模拟浏览器环境的重要步骤。

常见对象

  1. document对象:代表整个HTML文档
    • 包含了操作DOM的方法和属性
    • 例如:getElementById(), createElement(), querySelector(),cookie等
  2. location对象:包含有关当前URL的信息
    • 如href, protocol, host, pathname, search, hash等
    • 还有方法如assign(), replace(), reload()等
  3. history对象:包含浏览器的历史记录信息
    • 方法如back(), forward(), go()等
  4. navigator对象:包含有关浏览器的信息
    • 如userAgent, platform, language等
  5. screen对象:包含有关用户屏幕的信息
    • 如width, height, availWidth, availHeight等
  6. localStorage对象:提供本地存储功能
  7. sessionStorage对象:提供会话存储功能
  8. console对象:提供控制台调试功能
    • 如log(), error(), warn()等方法
  9. setTimeout和setInterval:用于设置定时器的函数
  10. alert(), confirm(), prompt():用于创建对话框的方法
  11. JSON对象:用于JSON数据的解析和序列化
  12. Math对象:提供数学计算相关的方法和常量
  13. Date对象:用于日期和时间操作
  14. Array, String, Number等基本对象的构造函数
  15. XMLHttpRequest或fetch:用于网络请求
  16. performance对象:用于性能相关的测量
  17. WebSocket:用于全双工通信

爬虫重点关注的对象

  1. document对象
    • 用于解析和操作页面内容
    • 重要方法:querySelector(), getElementById(), getElementsByClassName(),cookie 等
    • 用于提取目标数据
  2. location对象
    • 用于获取和操作URL
    • 重要属性:href, pathname, search, hash
    • 用于页面导航和URL解析
  3. navigator对象
    • 用于伪造浏览器信息
    • 重要属性:userAgent, platform, language
    • 常用于反爬绕过
  4. history对象
    • 模拟浏览历史
    • 方法如back(), forward()
    • 用于模拟真实用户行为
  5. localStorage 和 sessionStorage
    • 用于存储和获取网站可能用于验证的数据
    • 模拟持久化存储
  6. XMLHttpRequest 或 fetch
    • 用于发送网络请求
    • 模拟AJAX调用
  7. setTimeout 和 setInterval
    • 用于处理异步操作和定时任务
    • 模拟页面加载和动态内容
  8. window.crypto
    • 用于处理加密操作
    • 某些网站可能用于生成特定的加密参数
  9. performance对象
    • 用于模拟真实的页面加载性能指标
  10. screen对象
    • 用于模拟屏幕分辨率等信息
  11. console对象
    • 用于调试和日志输出
    • 某些网站可能会检查console的行为

在爬虫中,这些对象主要用于以下目的:

  1. 数据提取:使用document对象解析页面结构,提取所需信息。
  2. 请求伪造:使用navigator和其他对象伪造浏览器特征,绕过反爬检测。
  3. 模拟交互:使用history、setTimeout等模拟用户行为。
  4. 处理动态内容:应对JavaScript渲染的页面,需要模拟完整的浏览器环境。
  5. 绕过反爬措施:某些反爬技术会检查这些对象的存在性和行为,需要精确模拟。

在实际爬虫开发中,根据目标网站的特性和反爬措施,可能需要重点关注和模拟其中的一部分对象。通常,越是复杂的网站,需要模拟的对象和行为就越多。


Proxy对象

从上面的知识点可以看出,我们本地模拟浏览器需要各式各样的对象属性,而怎么确定使用什么对象,就要使用Proxy来进行观察了。

为什么使用Proxy

a) 环境模拟:Node.js环境与浏览器环境不同,缺少许多浏览器特有的对象和方法(如window、document等)。使用Proxy可以模拟这些缺失的对象和方法。

b) 动态监控:Proxy允许我们拦截和自定义对象的基本操作,如属性查找、赋值、枚举等。这使我们能够动态地监控和修改JS代码的行为。

c) 按需补充:我们可以只补充代码实际需要的部分,而不是完整模拟整个浏览器环境,这样更加高效。

d) 灵活性:Proxy提供了一种灵活的方式来处理未知的属性访问,这在处理复杂的反爬逻辑时非常有用。

Proxy如何工作

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义。在补充JS环境时,我们通常会这样使用它:

  • 这段代码创建了一个假的window对象。当代码尝试访问window的属性时,Proxy会:
    • 首先检查自己是否有这个属性
    • 如果没有,检查全局对象是否有这个属性
    • 如果全局对象也没有,返回一个空函数,防止代码报错

优势

a) 精确控制:我们可以精确控制每个属性的行为,包括读取、写入和方法调用。

b) 调试便利:通过Proxy,我们可以轻松记录所有被访问的属性,这对于理解和调试目标JS代码非常有帮助。

c) 性能优化:相比完全模拟整个浏览器环境,使用Proxy按需模拟可以显著提高性能。

d) 适应性强:对于未知或复杂的环境依赖,Proxy提供了一种灵活的处理方式。

实际应用

在爬虫中,我们通常会遇到需要执行目标网站JS代码的情况,特别是在处理加密参数时。使用Proxy可以让我们在Node.js环境中"骗过"这些JS代码,使其以为自己是在浏览器中运行,从而正确执行并得到我们需要的结果。

Proxy对象的基本语法

let proxy = new Proxy(target, handler);

这里有两个主要参数:

  1. target: 这是要代理的原始对象。可以是任何类型的对象,包括数组、函数,甚至另一个代理。
  2. handler: 这是一个对象,其属性是定义代理行为的函数。这些函数被称为"捕获器"(traps)。

handler对象可以定义以下主要的捕获器:

  • get(target, property, receiver): 用于拦截对象属性的读取操作。
  • set(target, property, value, receiver): 用于拦截对象属性的设置操作。
  • has(target, prop): 用于拦截 in 操作符。
  • deleteProperty(target, property): 用于拦截删除操作。
  • apply(target, thisArg, argumentsList): 用于拦截函数调用。
  • construct(target, argumentsList, newTarget): 用于拦截 new 操作符。

代码示例

在这个例子中:

  1. target 是我们要代理的原始对象。
  2. handler 定义了如何拦截对对象的操作。
  3. 我们定义了 get 和 set 捕获器来拦截读取和设置属性的操作。
  4. 当我们通过代理对象访问或修改属性时,相应的捕获器会被调用。
// 目标对象
let target = {
  name: "John",
  age: 30
};

// 处理器对象
let handler = {
  // 拦截读取属性操作
  get: function(obj, prop) {
    console.log(`正在获取 ${prop} 属性`);
    return prop in obj ? obj[prop] : `${prop} 不存在`;
  },
  
  // 拦截设置属性操作
  set: function(obj, prop, value) {
    console.log(`正在设置 ${prop} 属性为 ${value}`);
    obj[prop] = value;
    return true;
  }
};

// 创建代理
let proxy = new Proxy(target, handler);

// 使用代理
console.log(proxy.name);  // 输出: 正在获取 name 属性 \n John
console.log(proxy.age);   // 输出: 正在获取 age 属性 \n 30
console.log(proxy.job);   // 输出: 正在获取 job 属性 \n job 不存在

proxy.name = "Jane";      // 输出: 正在设置 name 属性为 Jane
console.log(proxy.name);  // 输出: 正在获取 name 属性 \n Jane

 这就是Proxy的基本工作原理。在更复杂的场景中,比如模拟浏览器环境,我们可以使用这种机制来创建看起来像浏览器对象(如window、document等)的对象,并控制对这些对象的访问和修改。

Proxy实验

运行测试

创建demo.js,打开命令行终端,导航到您保存 demo.js 文件的目录,然后运行以下命令:

node demo.js

// 创建一个模拟的window对象
const fakeWindow = new Proxy({}, {
    get: function(target, property) {
      console.log(`Accessing property: ${property}`);
      if (property in target) {
        return target[property];
      } else if (typeof global[property] !== 'undefined') {
        return global[property];
      } else {
        // 模拟缺失的属性或方法
        return () => console.log(`Called method: ${property}`);
      }
    },
    set: function(target, property, value) {
      console.log(`Setting property: ${property} = ${value}`);
      target[property] = value;
      return true;
    }
  });
  
  // 测试访问属性
  console.log(fakeWindow.navigator);
  
  // 测试设置属性
  fakeWindow.location = 'https://example.com';
  
  // 测试调用方法
  fakeWindow.alert('Hello, world!');
  
  // 测试访问真实的全局属性
  console.log(fakeWindow.console === console);  // 应该返回 true
  
  // 测试访问不存在的属性
  fakeWindow.nonExistentMethod();

观察结果

这个输出展示了Proxy如何拦截和处理各种操作:

  • 访问属性(navigator, console)
  • 设置属性(location)
  • 调用方法(alert, nonExistentMethod)
  • 处理存在和不存在的属性
Accessing property: navigator
undefined
Setting property: location = https://example.com
Accessing property: alert
Called method: alert
Accessing property: console
true
Accessing property: nonExistentMethod
Called method: nonExistentMethod

更多的检测功能

代码包含了以下功能:

  1. 模拟window对象
  2. 模拟document对象
  3. 处理cookie的特殊逻辑
  4. 记录所有属性的访问和设置
  5. 模拟不存在的方法
  6. 访问真实的全局属性
// 创建一个模拟的window对象
const fakeWindow = new Proxy({
  document: new Proxy({
    _cookie: '',
  }, {
    get: function(target, property) {
      console.log(`Accessing document property: ${property}`);
      if (property === 'cookie') {
        console.log('Reading cookie');
        return target._cookie;
      }
      return target[property];
    },
    set: function(target, property, value) {
      console.log(`Setting document property: ${property} = ${value}`);
      if (property === 'cookie') {
        console.log(`Setting cookie: ${value}`);
        target._cookie = value;
      } else {
        target[property] = value;
      }
      return true;
    }
  })
}, {
  get: function(target, property) {
    console.log(`Accessing window property: ${property}`);
    if (property in target) {
      return target[property];
    } else if (typeof global[property] !== 'undefined') {
      return global[property];
    } else {
      // 模拟缺失的属性或方法
      return () => console.log(`Called window method: ${property}`);
    }
  },
  set: function(target, property, value) {
    console.log(`Setting window property: ${property} = ${value}`);
    target[property] = value;
    return true;
  }
});

// 测试访问window属性
console.log(fakeWindow.navigator);

// 测试设置window属性
fakeWindow.location = 'https://example.com';

// 测试调用window方法
fakeWindow.alert('Hello, world!');

// 测试访问真实的全局属性
console.log(fakeWindow.console === console);  // 应该返回 true

// 测试访问不存在的window属性
fakeWindow.nonExistentMethod();

// 测试document.cookie
fakeWindow.document.cookie = 'user=john';
console.log(fakeWindow.document.cookie);

// 测试访问其他document属性
fakeWindow.document.title = 'Test Page';
console.log(fakeWindow.document.title);