源网站数据采集方案之解析DOM(五)

写在开篇

看过上一节的朋友是不是很期待这一小节?嘿嘿,那现在就开讲吧。

首先,我们来认识一下cheerio库。

cheerio 库是一个基于 Node.js 的轻量级 HTML/XML 解析工具,专门用于在服务端高效处理网页内容,其设计灵感来源于前端 JavaScript 的jQuery库,语法和操作方式高度相似,因此学习成本极低。

上面是豆包关于cheerio库的介绍,简单来说,cheerio库就是可以像jquery一样操作html中的dom节点的一个JS库,两者区别就是cheerio可以将html字符串转换成dom节点后再进行操作,jquery库是直接操作dom节点。

技术方案设计

知道了cheerio库的作用后,"chrome.scripting + cheerio"这个方案的流程理解起来就真的很容易了。

浏览器插件使用scripting函数解析DOM数据

结合上图,流程说明如下:

  1. service Worker打开TAB;
  2. 由Service Worker使用chrome.scripting函数获取新TAB页body下的所有dom节点的字符串;
  3. 利用cheerio进行dom节点的解析获取到数据;
  4. 将数据抛给第三方存储系统或向发起请求的TAB页抛出反馈信息。
    1. 数据的抛出可以单个单个向存储系统/请求侧抛出,也可以整体抛出,在这个方案中不会有多大影响(我们这里使用单个单个抛给服务端的方式,减少各个采集过程的相互影响);

特别需要提醒的是,在这个方案中,新打开的TAB页不过就是展示出数据等待Service Worker去取而已,并不会涉及到新开tab页中的浏览器API的调用。

另外,最重要的一点是,由以上流程图可以清晰的看出 单个链接的采集流程是同步执行的、两个链接采集之间也是同步执行的,所以,无论是流程理解上还是代码编写上,对于技术人员来说简直就是顺手拧开瓶般轻松;

方案落地与Demo

讲完理论的东西,我们当然要落到代码上啦~

首先,还是在发起的TAB页中插入可执行按钮。

js 复制代码
// content.js

const _container = document.getElementById('ce-test-id');

const _btnDom = document.createElement('button');

_btnDom.innerHTML = '点击 BY SCRIPT';

_container.appendChild(_btnDom);


_btnDom.addEventListener('click', async function () {

    contentToServiceWorkerByLongConnection({

        command: 'batchCollectDataByScript',

        payload: {

            batchId: uuidV4(), // 一般这个批次ID由服务端生成,这里只是为了演示方便,所以直接由前端产生

            urlList: ['https://www.baidu.com/', 'https://www.baidu.com/'], // 这里为了测试,所以放了两个一样的地址,只是为了有更直挂的感受

        },

    });

});

接着就是Service Worker的事情了,很简单的同步代码。

js 复制代码
// service.worker.js

import { v4 as uuidV4 } from 'uuid';

async function _batchCollectDataByScript(payload, port) {

    const { batchId, urlList } = payload;

    for (const _url of urlList) {

        // 一般在这里做一些参数的校验、获取等操作

        console.log('_batchCollectDataByScript===新TAB准备打开', _url);

        const _tab = await chrome.tabs.create({ active: false, url: _url }); // 自动打开页面,active:false 代表的是静默打开tab页

        console.log(`_batchCollectDataByScript===新TAB已打开,TAB ID - ${_tab.id}`);'
        

        let _urlDetail;

        let _checkLoopIndex = 0;

        while (_checkLoopIndex < 20) {

            await sleep(2000); // 每2秒检查一次,最多检查40秒还不出结果就直接判为失败,实际运用中需要根据源网站数据实际情况

            console.log(`_batchCollectDataByScript===第${_checkLoopIndex + 1}次检验`);

            
            ++_checkLoopIndex;

            const _itemId = uuidV4(); // 为本次请求生成一个唯一的ID

            const _itemMessage = {

                url: _url,

                batchId: batchId, // 批次ID

                itemId: _itemId, // 本次请求的唯一ID

                tabId: _tab.id,

                status: 'running',

                startTime: new Date().getTime(),

                loopIndex: _checkLoopIndex,

            };

            
            // 或为了交互、或为了保持content与service长链接通信通道,一般,我们会每次请求阶段给发出采集请求的content层回复一条消息;

            // 这块逻辑和本章的内容没有非常直接的关系,是出于技术方案的完整性考虑

            port.postMessage({

                command: 'resolveBatchCollectDataByDom',

                payload: _itemMessage,

            });


            const _result = await chrome.scripting.executeScript({

                target: { tabId: _tab.id },

                func: () => {

                    return document.body.innerHTML; // 这里之所以直接获取body数据,是因为除了数据节点,很有可能需要处理其他异常场景

                },

            });
            

            if (!_result) {

                continue;

            }
            

            console.log('_batchCollectDataByScript===_result', _result);
            

            let _body;

            for (const _item of _result) {

                if (_item.result) {

                    _body = _item.result;

                    break;

                }

            }
            

            if (!_body) {

                continue;

            }


            const _bodyDom = load(_body);

            _urlDetail = _bodyDom('#su').val(); // 我们这里只是举例获取一个数据,真实业务场景需要自行调研哦~

            if (_urlDetail) {

                break;

            }

        }
        

        chrome.tabs.remove(_tab.id, () => {

            console.log(`选项卡${_tab.id}已经关闭`);

        });
        

        console.log('_batchCollectDataByScript===_urlDetail', _urlDetail);

        if (!_urlDetail) {

            // 这里需要向服务端/用户侧直接发送采集结果;

            continue;

        }
        

        // 接下去就需要根据获取到的_urlDetail判断是否是需要的数据;

        // 比如是否需要让用户登陆、是否被风控了呀,一般都可以在dom节点中判断出来的(就是上一章中content层做的事情);

        // 解析完数据之后,就可以向服务端/用户侧发送请求了;

        // const _result = await fetch('xxxxx',{

        // method: 'POST',

        // body: JSON.stringify({

        // batchId,

        // url: url,

        // status: collectStatus,

        // result: collectResult

        // }),

        // headers: {

        // 'Content-Type': 'application/json',

        // },

        // });

        // 这个30秒的等待时间,一方面是为了不给源网站造成困扰,另一方面是一分钟以上的打开TAB页的时间间隔更加贴近用户操作路径;

        // 如果可以做的更精细一些的话,可以产生一个随机数来做间隔时间,也可以根据不同的站点,缩短间隔时间

        await sleep(10000);
    
    }

    
    // 因为整个过程是同步执行的,所以代码执行到这里就说明所有的链接都已经被采集过,不管成功与否,服务端/用户侧都已经接收到所有链接的结果;

    // 当然,最好再向服务端/用户侧发送请求:告知服务端本次采集批次已经采集完毕;

    // const _result = await fetch('xxxxx',{

    // method: 'POST',

    // body: JSON.stringify({

    // batchId,

    // status: 'finish',

    // }),

    // headers: {

    // 'Content-Type': 'application/json',

    // },

    // });

    
    // 2. 向Content层发送消息告知"本次采集完毕",请自行去服务端获取采集结果;

    port.postMessage({

        command: 'resolveBatchCollectDataByScript',

        payload: {

            batchId,

            status: 'finish',

        },

    });

}


chrome.runtime.onConnect.addListener(function (port) {

    port.onMessage.addListener(function (message) {

        const { command, payload } = message;

        if (command === 'batchCollectDataByScript') {

            _batchCollectDataByScript(payload, port);

        }

    });

});

以上代码实践后,就会有这样的效果: 【👉 由于这里不能上传视频,麻烦移步我的博客去查看测试流程交互视频吧】

与上一节方案对比

看到这里,你是不是也意识到:当数据存在于用户可视页面上时,本文阐述的方案更便捷呢?那接下来,我们就好好整理下两个方案到底有哪些区别吧!

  1. 首当其冲的当然是最大的优势:在足够满足技术方案完整性的情况下,涉及的技术端之间的交互更少;

    这里说的是,虽然方案设计时我们加入了服务端的内容,但事实上,因为本次方案不像上一小节的方案具有并发问题,也就是插件端存储数据的能力就完全可用了,即,服务端的存储可要可不要;

    另一方面就是,虽然本小节的方案依然涉及到了TAB页的操控,但新TAB页中不会涉及到消息通信、浏览器API的调用,相当于出现异常逻辑的可能性更少了;

  2. 第二个就是本文方案是同步执行,在开发方面对技术人员的比较友好(不仅流程理解性高,而且编码量具少);

  3. 兜底策略上,本方案只需注意dom节点的出现时机即可;

  4. 但从执行时间上来讲,其实两个方案的执行时间是差不多的,只不过本方案更方便去调试、缩短执行时间。

    上一节的方案中有几个必要的停留时间:链接与链接之间的间隔时间、Service Worker等待Content层准备好的时间、Content层需要等待数据出现的时间、为避免消息丢失而需要等待的时间、最后判断是否全部链接采集完毕的等待时间;

    本方案的必要停留时间就很少了:链接与链接之间的间隔时间、Content层需要等待数据出现的时间;

    相比较之下,越少的等待场景,调试起来肯定越简单;

写在最后

到此,应对源网站风控的几个方案就讲完了,后续如果有其他方案的,会继续更新,望各位朋友持续关注哈~

今日与君共勉:作为前端工程师,在构思技术方案时,需以全局视野构建兼具完整性与前瞻性的体系,确保方案各环节逻辑自洽、无遗漏死角。同时,需将用户交互体验置于核心位置,从操作流畅度、视觉反馈到场景适配性等维度精细打磨,让技术方案不仅实现功能目标,更能为用户带来自然、愉悦且高效的使用感受。

相关推荐
卸任31 分钟前
Vibe coding工具使用心得
前端·ai编程
brien91 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance2 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M2 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写2 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李2 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易2 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马2 小时前
前沿视界
前端
kyriewen2 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员