源网站数据采集方案之解析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层需要等待数据出现的时间;

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

写在最后

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

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

相关推荐
xw524 分钟前
Trae安装指定版本的插件
前端·trae
默默地离开42 分钟前
前端开发中的 Mock 实践与接口联调技巧
前端·后端·设计模式
南岸月明43 分钟前
做副业,稳住心态,不靠鸡汤!我的实操经验之路
前端
嘗_1 小时前
暑期前端训练day7——有关vue-diff算法的思考
前端·vue.js·算法
MediaTea1 小时前
Python 库手册:html.parser HTML 解析模块
开发语言·前端·python·html
杨荧1 小时前
基于爬虫技术的电影数据可视化系统 Python+Django+Vue.js
开发语言·前端·vue.js·后端·爬虫·python·信息可视化
BD_Marathon1 小时前
IDEA中创建Maven Web项目
前端·maven·intellij-idea
waillyer1 小时前
taro跳转路由取值
前端·javascript·taro
凌辰揽月2 小时前
贴吧项目总结二
java·前端·css·css3·web
代码的余温2 小时前
优化 CSS 性能
前端·css