制作一个谷歌浏览器插件,实现网页数据爬虫

news2024/11/23 12:11:51

一、什么是浏览器插件

浏览器插件,基于浏览器的原有功能,另外增加新功能的工具,是可定制浏览体验的小型软件程序,让用户可以根据个人需要或偏好来定制浏览器。

如拦截网页中的广告、划词翻译、倍速视频等等。

Chrome、edge等浏览器中都有专门的插件下载商店。 受某些原因限制,Chrome服务并不能正常访问

这里提供几个常用的浏览器插件下载地址:

Chrome插件,谷歌浏览器插件下载,chrome谷歌商店插件crx应用推荐与下载-扩展迷

Chrome插件,Chrome商店,谷歌浏览器插件下载,谷歌商店 - Chrome插件网

极简插件_Chrome扩展插件商店_优质crx应用下载

有兴趣的小伙伴可以进入网站看看有没有感兴趣的、满足自己定制化需求的插件。

常见爬虫方法的对比

后面我们会实现一个爬虫功能的插件。 在开始实战之前,我们可以先聊一聊常见爬虫能力的优缺点。

  1. api接口 该方法速度快,容易上手,会任意编程语言都可以实现,且操作用户对此无感知。

但同时也有很大的缺点,这种方法很难同时发起用户行为收集请求,有些产品会通过这些行为收集接口分析用户的操作,如果逻辑变化,需要手动更新代码到客户处。

如果只有数据接口请求,没有统计接口请求,很容易被判定为爬虫,从而产生一系列负面影响。

有些产品还会有加密代码,需要一些逆向工作,这就更进一步提高这种方法爬取数据的难度了。

  1. Selenium 该方法是通过运行测试的开源工具实现的,常见编程语言都有对应的工具,相较于第一种方法有着更广范围的应用场景。

该方法通过启动相关驱动支持的真实的浏览器,尽可能的模拟用户操作,相关行为分析会自动请求,几乎不需要逆向,一定程度上填补了第一种方法的弊端。

但同时该方法也有弊端,需要给客户机安装运行环境和客户的Chrome浏览器升级等问题。

浏览器升级可能导致Selenium驱动版本和浏览器版本不匹配,程序就会运行失败。逻辑变化需要手动更新到客户处。

该方式也会被产品方识别出是程序启动而不是真实用户启动的浏览器,从而产生负面影响。

  1. 浏览器插件 该方法是通过浏览器的开放能力实现的,是用户启动的真实浏览器,进一步填充了前两种方法的弊端,通过各种形式的脚本实现复杂的操作。

可以发布到像app发布到应用商店一样发布到浏览器应用商店,且提供线上更新功能。

该方法必须得会JavaScript脚本语言,同时熟知浏览器的开放能力,增加了学习难度。

该方法仍可以被产品识别出,如使用 MutationObserver 方法检测出dom变化等。

没有完美的方法,只有更适合的方法,不同场景使用不同的技术应对不同的困难即可。

实现一个浏览器爬虫插件

介绍完常见爬虫的区别后,接下来,我们就开始实现一个浏览器爬虫插件。

此处假设小伙伴已经阅读上述推荐的博客并基本熟悉浏览器插件的能力。

需求:爬取10页boss直聘网站上全国范围内Python岗位的招聘信息。

拆解需求:

  1. 目标网站:

boss直聘网站

  1. 筛选条件:

城市:全国 关键词:Python

  1. 数量:

1-10页内的全部数据

url地址:https://www.zhipin.com/web/geek/job?query=Python&city=100010000&page=1

难点分析

  1. 使用什么脚本类型

插件有injected、content、popup、background、devtools 5种类型的脚本,不同类型拥有不同的能力,相互之间的通信方式也不尽相同。

所以首先需要根据需求结合具体类型脚本的能力来确定使用什么脚本。

popup肯定是需要的,给文件指定名称和下达开始爬取的命令时要用到该类型脚本。

此处已确定popup脚本,其他类型待定。

  1. 拦截网络请求

经分析,从dom结构中获取数据不靠谱。 如,跳转链接,某些产品的链接并不放在dom中,而是通过点击事件句柄判断按钮的index、id等唯一标识,从js作用域中找到对应的链接进行跳转。

那么就需要考虑怎么能拦截到网络请求了。插件的核心是不同类型js脚本,不同类型的脚本能力不同,需结合实际考虑。

在5种脚本类型对比可知,只有injected、devtools、background可以拦截到网络请求。但background拿不到响应体,故抛弃。

devtools功能很强大,它可以模拟出一个和开发者工具(F12)-网络(network)功能几乎一样的面板,但实现起来会相对复杂。

前端同学常用的React Developer Tools、vue-tools调试面板就是使用该技术开发的。

经过权衡对比,使用更加简单的injected来做网络拦截。

此处已确定popup和injected两种脚本。

  1. 通信

爬取过程很简单,通信是一件复杂的事,详情通信可参考上述文档。

现在的流程是 injected拦截网络请求 -> popup下达开始爬取的指令 -> injected开始执行脚本收集数据 -> injected清洗并导出数据。

现在确定的popup和injected两种类型能满足吗?很遗憾,不能,通过上述博客中总结的通信方式可知,这两种类型的脚本不能直接通信,也就是popup不能告诉injected可以开始收集数据了。

怎么实现呢?需要引入一个“中介”——content,作为popup和injected中间通信的桥梁。

现在的过程就变成了,injected拦截网络请求 -> popup下达开始爬取的指令 -> content转发指令-> injected开始执行脚本收集数据 -> injected清洗并导出数据

这里可以留一个小问题,最后一步可以使用content实现吗,为什么不使用这种方式?

此处已确定popup和injected、content三种脚本。

代码部分

确定脚本选型后就可以创建工程了,新建一个文件夹,创建如下的目录结构:

boss-plugin
├─ html
│  └─ popup
│     ├─ popup.html // 点击浏览器右上角插件,弹出popup,传递用户指令
│     └─ popup.js
├─ js
│  ├─ content // content脚本通过manifest.json配置文件可以直接添加到页面中
│  │  ├─ install.js // injected脚本并不能直接通过配置添加到页面中,需要通过content执行js代码动态插入到dom中
│  │  └─ page.js // “中介角色”,转发指令
│  └─ inject
│     ├─ network.js // 拦截网络请求
│     ├─ page.js // 具体执行收集、清洗、导出数据的逻辑
│     └─ pikazExcel.js  // 导出数据为Excel的js类库
└─ manifest.json // 浏览器识别插件配置的文件,必须

manifest.json

{
  "name": "爬取boss数据",
  "version": "1.0",
  "manifest_version": 2,
  "browser_action": {
    "default_popup": "/html/popup/popup.html"
  },
  "content_scripts": [
    {
      "matches": ["*://www.zhipin.com/*"],
      "js": ["/js/content/page.js", "/js/content/install.js"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": [
    "/js/inject/pikazExcel.js",
    "/js/inject/page.js",
    "/js/inject/network.js"
  ]
}

html/popup/popup.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <style>
      #box {
        align-items: center;
      }
      #input-box {
        display: flex;
        align-items: center;
      }
      .label {
        white-space: nowrap;
      }
      #btn-box {
        padding-left: 50px;
        padding-top: 10px;
      }
    </style>
  </head>
  <body>
    <div id="box">
      <div id="input-box">
        <span class="label">文件名:</span>
        <input id="filename" type="text" placeholder="可选,默认为当前时间" />
      </div>
      <div id="btn-box">
        <button id="export-btn">导出数据</button>
      </div>
    </div>
    <script src="/html/popup/popup.js"></script>
  </body>
</html>

html/popup/popup.js

function onClickExport() {
    document.getElementById('export-btn').disabled = true
    const filename = document.getElementById('filename').value
    const cb = (tab) => {
        chrome.tabs.sendMessage(tab.id, { action: "CHANGE_POPUP_ALLOW_DOWNLOAD", filename });
    }
    chrome.tabs.getSelected(null, cb);
}
document.getElementById('export-btn').onclick = onClickExport

js/content/install.js

setTimeout(() => {
    const pageScript = document.createElement('script');
    pageScript.setAttribute('type', 'text/javascript');
    pageScript.setAttribute('src', chrome.extension.getURL("/js/inject/page.js"));
    document.head.appendChild(pageScript);

    const networkScript = document.createElement('script');
    networkScript.setAttribute('type', 'text/javascript');
    networkScript.setAttribute('src', chrome.extension.getURL('/js/inject/network.js'));
    document.head.appendChild(networkScript);

    const excelScript = document.createElement('script');
    excelScript.setAttribute('type', 'text/javascript');
    excelScript.setAttribute('src', chrome.extension.getURL("/js/inject/pikazExcel.js"));
    document.head.appendChild(excelScript);
});

js/content/page.js

// 转发popup指令  popup => content script => inject script
chrome.extension.onMessage.addListener(
    function (request) {
        if (request.action == "CHANGE_POPUP_ALLOW_DOWNLOAD") {
            // popup 告诉页面可以开始收集并下载数据了
            window.postMessage({ action: 'CHANGE_POPUP_ALLOW_DOWNLOAD', popupAllowDownload: true, filename: request.filename }, '*');
        }
    }
);

js/inject/network.js

此处需要注意浏览器发起请求的两种方式:xhr和fetch,前者使用较多,后者也在开发过程中见到过。

const _requestTools = {
    formatQueryString(queryString = '') {
        const result = {};
        if (queryString.length > 0) {
            queryString = queryString.split('?')[1].split('&');
            for (let kv of queryString) {
                kv = kv.split('=');
                if (kv[0]) result[kv[0]] = decodeURIComponent(kv[1]);
            }
        }
        return result
    }
}

function _initXMLHttpRequest() {
    // 拦截网络请求方法1
    const open = XMLHttpRequest.prototype.open;
    const _targetApiList = [
        'wapi/zpgeek/search/joblist.json'
    ]
    XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener('load', function () {
            // 如果当前url并不包含_targetApiList中任意一个地址,则阻止后续操作
            if (!_targetApiList.find(item => this.responseURL.includes(item))) return

            const result = {
                responseHeaders: {},
                responseData: {},
                request: this,
                status: this.status,
                params: _requestTools.formatQueryString(this.responseURL)
            }
            // 格式化响应头
            this.getAllResponseHeaders().split("\r\n").forEach((item) => {
                const [key, value] = item.split(": ");
                if (key) result.responseHeaders[key] = value;
            });
            if (result.responseHeaders["content-type"].includes("application/json")) {
                // 如果响应头是content-type是json,则格式化响应体
                if (this.response?.length) result.responseData = JSON.parse(this.response);
            }

            _crawler.collectData(result)
        })

        return open.apply(this, args);
    };

    // 拦截网络请求方法2
    // 此处的方法拦截在目标网站中并没有遇到,在其他项目中遇到过,故添加在此做补充知识点。
    const { fetch: originalFetch } = window;
    window.fetch = async (...args) => {
        let [resource, config] = args;

        let response = await originalFetch(resource, config);
        if (response.status === 200) {
            response
                .clone()
                .json()
                .then((data) => {
                    console.log('响应数据:', data)
                });
        }

        return response;
    };

}
_initXMLHttpRequest();

js/inject/page.js

// 因为inject js和页面共享js作用域,为防止污染全局变量,故插件中变量名都以_开头
class _Crawler {
    constructor() {
        this.downloadPageNum = 10  // 允许下载多少页
        this.filename = ''  // 从popup传进来的输入的文件名
        this.allowDownload = false // popup给出指令允许下载
        this.collectionList = [] // 收集每页请求得到的数据
    }

    /**
     * 获取当前年-月-日 时:分:秒
     * @returns string
     */
    getTime() {
        const time = new Date();
        const timeInfo =
            (time.getFullYear() + '-' + (time.getMonth() + 1) + '-' + time.getDate() + ' ' + time.getHours() + ':' + time.getMinutes() + ':' + time.getSeconds())
        return timeInfo
    }

    // 生成随机延迟秒数, 默认3-4秒
    getRandomTimeOut(x = 3000, y = 4000) {
        return Math.round(Math.random() * (y - x) + x)
    }

    collectData(result) {
        // 首次进来或搜索条件变化,清空收集结果
        const currentPage = result.params.page * 1;
        if (currentPage * 1 === 1) this.collectionList = []
        if (!this.collectionList.find(el => result.request.responseURL.includes(el.responseURL))) {
            const item = {
                responseURL: result.request.responseURL,
                responseData: result.responseData
            }
            this.collectionList.push(item)
        }

        // 如果没有点击导出按钮,则阻止后续操作
        if (!this.allowDownload) return

        // 结束收集行为的条件,然后进行数据清洗和导出excel
        if (currentPage >= this.downloadPageNum) {
            const sheet = this.clearData()
            this.download(sheet)
        } else {
            // 随机3-4秒后进行点击下一页
            // 这是写爬虫最基本的道德了,尽量在学习技术的同时,不要对目标服务器产生压力和影响其正常运行
            const randomTimeout = this.getRandomTimeOut()
            setTimeout(() => {
                this.handleClickNext()
            }, randomTimeout);
        }
    }

    clearData() {
        const headerAndKeyList = [
            {
                header: '岗位名称',
                key: 'jobName'
            },
            {
                header: '地址',
                key: 'jobAddress'
            },
            {
                header: '薪资',
                key: 'salaryDesc'
            },
            {
                header: '经验',
                key: 'jobExperience'
            },
            {
                header: '学历',
                key: 'jobDegree'
            },
            {
                header: '技术栈',
                key: 'skills'
            },
            {
                header: '公司名称',
                key: 'brandName'
            },
            {
                header: '公司行业',
                key: 'brandIndustry'
            },
            {
                header: '公司融资阶段',
                key: 'brandStageName'
            },
            {
                header: '公司规模',
                key: 'brandScaleName'
            },
            {
                header: '福利待遇',
                key: 'welfareList'
            },
        ]
        const itemTableConfig = {
            tHeader: headerAndKeyList.map(el => el.header),
            keys: headerAndKeyList.map(el => el.key),
            table: []
        }
        this.collectionList.forEach(el1 => {
            el1.responseData.zpData.jobList.forEach(el2 => {
                const { jobName, cityName, areaDistrict, businessDistrict, salaryDesc, jobExperience, jobDegree, skills, brandName, brandIndustry, brandStageName, brandScaleName, welfareList } = el2
                const item = {
                    jobName,
                    jobAddress: `${cityName}·${areaDistrict}·${businessDistrict}`,
                    salaryDesc, jobExperience, jobDegree, skills, brandName, brandIndustry, brandStageName, brandScaleName, welfareList
                }
                itemTableConfig.table.push(item)
            })
        })
        return [itemTableConfig]
    }

    download(sheet) {
        const filename = this.filename || this.getTime()
        window.pikazExcelJs.default.excelExport({
            sheet,
            filename,
            beforeStart: (bookType, filename, sheet) => {
                console.log("开始导出", bookType, sheet, filename);
            },
        }).then(() => {
            this.filename = ''
            this.allowDownload = false
            this.collectionList = []
        });
    }

    handleClickNext() {
        const nextSelector = '.pagination-area .options-pages a:last-child'
        const nextDom = document.querySelector(nextSelector)
        nextDom.click()
        // 如果目标网站有收集用户行为的接口,此处可添加模拟用户操作,如滚动页面、点击某些元素
    }
}
const _crawler = new _Crawler();

// 监听从popup发送的指令 popup => content script => inject script
window.addEventListener("message", function (e) {
    if (e.data.action === 'CHANGE_POPUP_ALLOW_DOWNLOAD') {
        _crawler.filename = e.data.filename
        _crawler.allowDownload = true
        _crawler.handleClickNext()
    }
}, false);

js/inject/pikazExcel.js

文档和下载地址: 
https://www.npmjs.com/package/pikaz-excel-js

最后在Chrome浏览器中打开这个地址 chrome://extensions/

开启开发者模式 -> 加载已解压的扩展程序 -> 选择刚才新建的文件夹 -> 确认导入

这时候就已经把刚才编写的导入到浏览器中了,打开目标页面

然后点击红框区域,输入文件名(可选),点击导出数据,就可以开始爬取内容了

最终效果:

代码下载地址

链接:https://pan.baidu.com/s/1RHYE-CuZqmBJm7Wj9G4fYQ

提取码:u5cp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/59146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WEB前端网页设计 网页代码参数(背景、图片)类

目录 设置圆角 旋转属性&#xff1a; box-sizing属性&#xff1a; 设置背景图像固定background-attachment 设置多重背景图像 鼠标光标形状&#xff1a;cursor ”图片背景“ background-size 背景图片的大小 背景图像的位置 px 无序列表 &#xff1a; 标签 项目符…

解决cocos2d-x-4.0 Android Demo构建遇到的问题

环境 硬件&#xff1a;macbook pro 四核Intel Core i7系统&#xff1a;macOS Big Sur 11.4.2、 xcode Version 13.1 、cmake 3.20.5软件&#xff1a;iterm2 Build 3.4.8、zsh 5.8、Android Studio Dolphin | 2021.3.1cocos2d-x v4 &#xff1a; 官方下载压缩包 http://cocos2d…

讲点登录业务

1.单点Session 通过判断用户是否有服务器赋予的session_id&#xff0c;点对点服务器的用户信息&#xff0c;确认用户身份 缺点&#xff1a; 单点性能压力大无法扩展&#xff0c;如果是分布式的话&#xff0c;其他的服务怎么进行认证呢&#xff1f; 2.Redis解决共享问题 我们…

JavaWeb(四)

前言 在学习JSP之前&#xff0c;首先咱们要了解的是&#xff0c;学这个语言有什么用&#xff0c;这个语言用在哪里呢&#xff1f; 这就要从咱们的MVC框架开始讲起 MVC模式是一种软件架构模式&#xff0c;对于我这种软件工程专业的人来说&#xff0c;真的是逃离不了学这个东西。…

Java_题目_学生管理系统_注册登录忘记密码

学生管理系统升级版 Java_题目_学生管理系统_业务分析并搭建主菜单_查询添加删除修改 需求&#xff1a; ​ 为学生管理系统书写一个登陆、注册、忘记密码的功能。 ​ 只有用户登录成功之后&#xff0c;才能进入到学生管理系统中进行增删改查操作。 分析&#xff1a; 登录…

微信小程序自动化测试实践(附 Python 源码)| 实战系列

为什么要进行小程序自动化测试 随着微信小程序的功能和生态日益完善&#xff0c;很多公司的产品业务形态逐渐从 App 延升到微信小程序、微信公众号等。小程序项目页面越来越多&#xff0c;业务逻辑也越来越复杂&#xff0c;全手工测试已无法满足快速增长的业务需求。 然而&am…

LL(1)文法的核心原理

来自编译原理课本&#xff0c;课本上讲的非常好&#xff0c;这里用我自己的方法再讲述一下。 讨论范围&#xff1a;2型文法&#xff0c;产生式的左边只有一个非终结符号。&#xff08;这样才能构建树&#xff09; 用语法树去进行巨型分析的时候会遇到的问题&#xff1a;多个候…

web前端期末大作业 html+css+javascript汽车介绍网页设计实例 企业网站制作(带报告3490字)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

WEB前端网页设计 CSS网页代码 基础参数(三)

目录 font-size属性单位; color&#xff1a;文本颜色 间距 text-decoration&#xff1a;文本装饰 text-align&#xff1a;水平对齐方式 white-space&#xff1a;空白符处理 text-overflow&#xff1a;标示对象内溢出文本 盒子模型&#xff1a; 高度坍…

Python课程设计-图书管理系统

Python课程设计-图书管理系统摘要第一章 绪论1.1 开发环境及技术1.2 系统实现功能描述第二章 功能详细设计与实现2.1 系统框架各层次实现2.1.1 可视页面设计2 数据库设计3 逻辑流程设计2.2 主要功能的设计与实现1 功能 1用户登录2 功能 2展示图书3 功能 3添加图书4 功能 4删除图…

3dmax 打开查看模型

下载一个3dmax模型如下图&#xff1b;包含一个.max文件&#xff0c;一个文件夹&#xff1b; 从File菜单打开该模型&#xff1b;打开对话框右侧会显示模型的一个缩略图&#xff1b; 有任何情况均忽略&#xff0c;直接打开&#xff0c;出现一个Scene Converter对话框&#xff0c;…

Spring MVC 源码分析

Spring MVC 源码分析1. 回顾Servlet1.1. 什么是Servlet1.2. Servlet工作模式1.3. Servlet的工作原理1.4. 源码分析1.4.1. Servlet接口1.4.2. GenericServlet抽象类1.4.3. HttpServlet抽象类1.5. Servlet的局限性2. Spring MVC简介2.1. 什么是MVC2.2. 什么是Spring MVC&#xff…

【深度学习】详解 BEiT

目录 摘要 一、引言 二、方法 2.1 图像表示 2.1.1 图像 patch 2.1.2 视觉 token 2.2 主干网络&#xff1a;图像 Transformer 2.3 预训练 BEiT&#xff1a;掩码图像建模 2.4 从变分自动编码器的角度来看 2.5 预训练设置 2.6 在下游视觉任务微调 BEiT 三、实验 3.…

谁还说我没表情包用?马上用Python采集上万张个表情包

前言 今天来表演一手 采集全网表情包图片 虽然我现在的wx表情包已经996个了&#xff0c;但是我还在存表情包哈哈&#xff0c;多了就继续删 现在跟人聊天&#xff0c;不发个表情包&#xff0c;我都觉得不对劲&#xff0c;怪难受的 索性今天就来&#xff0c;给你们分享一下&a…

Vue3:分析elementplus表格第一列序号hover变多选框实现思路

灵感来自Vue el-table 表格第一列序号与复选框hover切换 源码是通过Vue2elementui去实现的&#xff0c;本篇是通过Vue3elementplus实现&#xff0c;所以在代码上面有些许不同&#xff0c;但函数名一致 实现思路&#xff1a; ①通过表头是多选框&#xff0c;我们可以判定这一…

9.1、面向对象编程

文章目录面向对象编程简介面向对象编程面向对象编程的三大特性对象和类封装练习继承什么是继承重写父类方法多继承私有属性和私有方法多态项目案例&#xff1a;栈和队列的封装栈的封装队列的封装python是面向对象的编程语言 面向对象编程简介 “面向过程”(Procedure Oriente…

Java并发编程—synchronized

文章目录synchronized 的底层实现原理监视器锁对象的锁的获取过程如下&#xff1a;monitorexit&#xff1a;加synchronized锁前后对比synchronized的作用synchronized的三种主要用法synchronized为什么是 非公平锁&#xff1f;————————————————————————…

大数据项目 --- 电商数仓(一)

这个项目实在数据采集基础使用的,需要提前复习之前学的东西,否则的话就是很难继续学习.详见博客数据项目一 ---数据采集项目.大数据项目 --- 数据采集项目_YllasdW的博客-CSDN博客大数据第一个项目笔记整理https://blog.csdn.net/m0_47489229/article/details/127477626 目录 …

Android 基于物理特性动画 —— 弹簧动画

在安卓开发中我们可以通过动画添加视觉提示&#xff0c;向用户通知应用中的动态。当界面状态发生改变时&#xff08;例如有新内容加载或有新操作可用时&#xff09;&#xff0c;动画尤其有用。动画还为应用增加了优美的外观&#xff0c;使其拥有更高品质的外观和风格。 首先来…

Java并发编程—并发和并行、线程上下文

文章目录并发和并行并发和并行的区别上下文切换相关问题为什么循环次数少的情况下&#xff0c;单线程快&#xff1f;什么时候需要用多线程&#xff1f;线程上下文切换消耗的时长&#xff1f;用什么测试的线程上下文&#xff1f;面试回答下面的工具会加分&#xff1a;如何减少上…