【web补环境篇-0】document.all

开新坑,之前的魔改node大概是有思路了,但是还需要结合实际来不断进行优化。就先拿document.all 试一下水。之前的思路是魔改node。但是在重新整理的过程中,由于编译耗时较久,选择了这个node addon的方式先实现一套轻量版的,等完善后再去移植到node原生进行完整node。通过addon ,可以在任何环境中直接导入 const addon = require('./addon') 即可使用。 这个./addon是编译好的 addon.node扩展。

为什么 document.all 这么难模拟?

document.all 是 IE4 时代的遗留产物。为了兼容旧网页,现代浏览器(Chrome/Firefox)保留了它,但为了不鼓励开发者使用,W3C 和浏览器厂商搞了一个非常反直觉的设计:"Undetectable"特性

在 Chrome 控制台里试一下就知道有多诡异:

javascript 复制代码
// 既存在,又不存在
typeof document.all === 'undefined' // true
document.all === undefined          // false (严格相等)
document.all == undefined           // true  (宽松相等)

// 看起来是 falsy,但能取值
if (document.all) { /* 不会执行 */ }
document.all.length                 // 正常返回数字
document.all[0]                     // 正常返回元素

这就是 Node.js 纯 JS 模拟的死穴。

无论怎么用 Proxy 拦截,或者用 Object.defineProperty,在 JS 层面你永远无法让一个对象的 typeof 变成 'undefined'。JSDOM 至今没有完美支持这一点(它返回的是 'object'),这就是很多反爬脚本检测 JSDOM 的核心依据。

常见的检测逻辑

对方想抓你,只需要一行代码:

javascript 复制代码
// 绝杀检测
if (typeof document.all !== 'undefined' && document.all) {
    console.log("检测到模拟环境,封禁!");
}

或者更恶心一点的:

javascript 复制代码
// 原型链检测
if (Object.prototype.toString.call(document.all) !== "[object HTMLAllCollection]") {
    return false; // 伪造失败
}

解决方案:从 C++ 层面入手

既然 JS 层面无解,我们就下沉到 V8 引擎层面。V8 提供了 MarkAsUndetectable 接口,专门就是为了实现这种怪异行为的。

我们需要写一个简单的 Node.js C++ Addon。

核心 C++ 实现

核心就这几行:

cpp 复制代码
// 1. 创建对象模板
Local<ObjectTemplate> template = ObjectTemplate::New(isolate);

// 2. 注入灵魂:标记为 Undetectable
// 这一步之后,typeof 就会返回 undefined,且在布尔判断中为 false
template->MarkAsUndetectable();

// 3. 拦截函数调用:支持 document.all("id"),document.all(0)
template->SetCallAsFunctionHandler(CallHandler);

// 4. 拦截索引访问:支持 document.all[0]
template->SetHandler(IndexedPropertyHandlerConfiguration(IndexedGetter));

// 5. 实例化并导出
Local<Object> instance = template->NewInstance(context).ToLocalChecked();

通过这个 Addon 生成的对象,在 Node.js 环境里表现得和浏览器一模一样。

对接 JS 层

C++ 只负责提供"虽然存在但 typeof 是 undefined"的容器,具体的 DOM 查询逻辑(比如根据 ID 找元素)还是写在 JS 里比较方便。

我们可以这样把两者结合起来:

javascript 复制代码
const {JSDOM} = require('jsdom');
const addon = require('./addon');

const dom = new JSDOM(`<!DOCTYPE html><p id="app">Hello world</p></html>`);
window = dom.window;

// 保存原始 document 引用
const realDocument = dom.window.document;

function myAllHandler(arg) {
    // 使用原始 document 的方法
    console.log('myAllHandler arg' ,arg)
    if (arg === undefined) {
        return realDocument.getElementsByTagName('*');
    }

    if (typeof arg === 'number') {
        const all = realDocument.getElementsByTagName('*');

        // console.log('myAllHandler get all ',all[0].innerHTML)
        return all[arg] || null;
    }

    if (typeof arg === 'string') {
        const byId = realDocument.getElementById(arg);
        if (byId) return byId;

        const byName = realDocument.getElementsByName(arg);
        if (byName && byName.length > 0) return byName[0];
        return null;
    }

    return null;
}

function internalAllHandler(opOrIndex, maybeArg) {
    if (opOrIndex === 'INVOKE') {
        return myAllHandler(maybeArg);
    }
    return myAllHandler(opOrIndex);
}

addon.SetAllHandler(internalAllHandler);  // document.all 回调
addon.SetTraceLog(true);  // 调用链日志 
addon.SetVerboseLog(false); // 详细日志
// 直接使用 addon.khall,不经过 watch

// 替换 document.all
document = addon.watch(realDocument);

// 然后在 proxy 上直接设置(绕过 watcher)
Object.defineProperty(document, 'all', {
    get: function() {
        return addon.khall;
    },
    configurable: true
});

console.log(Object.prototype.toString.call(document.all));




console.log('\n--- 1. Access & Call ---');
// 索引访问
console.log('document.all[0]:', document.all[0]);
// 字符串键访问
console.log('document.all["app"]:', document.all["app"]);
// 函数式调用 - 数字
console.log('document.all(0):', document.all(0));
// 函数式调用 - 字符串
console.log('document.all("app"):', document.all("app"));
// 长度
console.log('document.all.length:', document.all.length);



// ==========================================
console.log('\n--- 2. Typeof & Undefined Check ---');

// 预期: 'undefined' (尽管它是一个对象/函数)
console.log('typeof document.all:', typeof document.all);

// 预期: true (因为 typeof 是 undefined)
console.log('document.all === undefined:', document.all === undefined);

// 预期: true
console.log('document.all == undefined:', document.all == undefined);

// 预期: true
console.log('document.all == null:', document.all == null);

// 反向验证:它实际上不是 null
console.log('document.all === null:', document.all === null); // 应该是 false

// ==========================================
// 3. 布尔值检测 (Boolean Coercion)
// 只有在 " undetectable " 模式下才会为 false
// ==========================================
console.log('\n--- 3. Boolean Logic ---');

// 强制转换
console.log('Boolean(document.all):', Boolean(document.all)); // 预期: false
console.log('!!document.all:', !!document.all);               // 预期: false
console.log('!document.all:', !document.all);                 // 预期: true

// if 语句行为
if (document.all) {
    console.log('Check: if (document.all) is TRUE [❌ Fail or Old IE]');
} else {
    console.log('Check: if (document.all) is FALSE [✅ Pass - Modern Behavior]');
}

// 逻辑运算
console.log('document.all || "fallback":', document.all || "fallback"); // 预期: "fallback"
console.log('document.all && "hidden":', document.all && "hidden");     // 预期: document.all (因为它是 falsy)

// ==========================================
// 4. 原型与标签 (Prototype & Tag)
// ==========================================
console.log('\n--- 4. Prototype & Object Tag ---');

// 预期: [object HTMLAllCollection]
console.log('Object.prototype.toString.call(document.all):',
    Object.prototype.toString.call(document.all));

// 预期: HTMLAllCollection
if (Symbol.toStringTag in document.all) {
    console.log('Symbol.toStringTag:', document.all[Symbol.toStringTag]);
}

// 检查构造函数
console.log('document.all.constructor.toString:',
    document.all.constructor ? document.all.constructor.toString() : 'undefined');

console.log('document.all.constructor.name:',
    document.all.constructor ? document.all.constructor.name : 'undefined');
// ==========================================
// 5. 属性枚举 (Enumeration)
// 作为一个"类数组"对象,它应该可以被遍历
// ==========================================
console.log('\n--- 5. Enumeration ---');


// 获取所有键 (如果是 Proxy 或正常对象)
// 注意:如果模拟得像浏览器,这通常会列出索引和ID
try {
    console.log('Object.keys(document.all).length:', Object.keys(document.all).length);
} catch (e) {
    console.log('Object.keys failed:', e.message);
}

验证结果

跑一下测试,看看效果:

javascript 复制代码
[object HTMLAllCollection]

--- 1. Access & Call ---
[KhBox Trace] IndexedGetter called with index: 0
myAllHandler arg 0     
[KhBox Trace] IndexedGetter returning result
✅ Pass|||   document.all[0]: HTMLHtmlElement {}
document.all["app"]: undefined
[KhBox Trace] CALL: khall(0)
myAllHandler arg 0
✅ Pass|||   document.all(0): HTMLHtmlElement {}
[KhBox Trace] CALL: khall(app)
myAllHandler arg app
✅ Pass|||   document.all("app"): HTMLParagraphElement {}
document.all.length: 0

--- 2. Typeof & Undefined Check ---
✅ Pass|||   typeof document.all: undefined
✅ Pass|||   document.all === undefined: false
✅ Pass|||   document.all == undefined: true
✅ Pass|||   document.all == null: true
✅ Pass|||   document.all === null: false

--- 3. Boolean Logic ---
Boolean(document.all): false
!!document.all: false
!document.all: true
Check: if (document.all) is FALSE [✅ Pass - Modern Behavior]
document.all || "fallback": fallback
document.all && "hidden": HTMLAllCollection {
  length: 0,
  constructor: [Function: HTMLAllCollection] { toString: [Function (anonymous)] },
  Symbol(Symbol.toStringTag): 'HTMLAllCollection'
}

--- 4. Prototype & Object Tag ---
Object.prototype.toString.call(document.all): [object HTMLAllCollection]
Symbol.toStringTag: HTMLAllCollection
document.all.constructor.toString: function HTMLAllCollection() { [native code] }
document.all.constructor.name: HTMLAllCollection

--- 5. Enumeration ---
Object.keys(document.all).length: 2

新的问题

这个length 目前是0很好改,改成document.getElementsByTagName('*').length的值就可以了,但是这个length方法应该是在原型链上,不是实例对象上。在HTMLAllCollection)里面。

所以,这个addon思路和魔改node的最大缺陷就是 不知道某个方法是在实例,还是在它的哪一层原型上。这个必须要在js层来处理。

不过,如果是和之前的思路结合,应该能省下非常多的代码。比如

javascript 复制代码
const handler = function(obj, prop, args) {
    const className = obj.constructor.name;
    const key = `${className}_${prop}`;
    // 1. 日志
    console.log(`{get: ${key}}`);
    // 2. 如果有自定义实现
    if (khBox.envFuncs[key]) {
        return khBox.envFuncs[key].apply(obj, args);
    }
    // 3. 否则走JSDOM
    return Reflect.get(khBox.memory.jsdom[className], prop);
};

// 转到c层去拦截使用
khboxAddon.setupInterceptor(document, handler);

这样就简单的实现了自己的函数+jsdom结合补全,且不用proxy 来层层追踪代理。因为c层自动加了get,set等的回调,自动输出调用链。相当于是实现了之前js框架里的 proxy(获取调用关系),dispatch(先保护后分发) ,setnative (保护)等的工具函数。

唯一的问题就是找到原型上的所有方法并保存下来,把他写成符合这个思路的模板。

后续会不断更改这个框架,争取早日完善成型。

更多文章,敬请关注gzh:零基础爬虫第一天

相关推荐
interception11 小时前
js逆向之京东原型链补环境h5st
javascript·爬虫·网络爬虫
半路_出家ren12 小时前
17.python爬虫基础,基于正则表达式的爬虫,基于BeautifulSoup的爬虫
网络·爬虫·python·网络协议·正则表达式·网络爬虫·beautifulsoup
我想吃烤肉肉1 天前
Playwright中page.locator和Selenium中find_element区别
爬虫·python·测试工具·自动化
lbb 小魔仙1 天前
【Python】零基础学 Python 爬虫:从原理到反爬,构建企业级爬虫系统
开发语言·爬虫·python
努力变大白1 天前
借助AI零基础快速学会Python爬取网页信息-以天眼查爬虫为例
人工智能·爬虫·python
AC赳赳老秦2 天前
Unity游戏开发实战指南:核心逻辑与场景构建详解
开发语言·spring boot·爬虫·搜索引擎·全文检索·lucene·deepseek
小花皮猪2 天前
LLM驱动智能数据采集:2026年10大AI网络爬虫工具对比评测
爬虫