【编程】Chrome extension扩展开发实战
怎样从众多网页上快速提取信息?比如说:
- 从数百个宝贝页面提取价格和属性
- 从数千个招聘职位页面中提取招聘信息
你可能需要kSpider这样一个工具。我的开源项目地址 http://10knet.com/zhyuzh/kspider
强烈建议你仔细阅读下面的内容然后再使用。
问题分析
由于现代Web开发方式(React,Vue,AngularJS等)的流行,很多页面都采用先加载页面再用js加载数据的模式,这就导致我们在页面上看到的内容和网页源码不一致,简单说就是Ctrl+S保存下来的页面文件并不包含你想要的信息。——这对于数据采集来说非常不利。
当然,可以使用爬虫技术直接从页面的Request接口中直接获取数据,但很多网站越来越多的使用反爬虫机制来阻止这种方式,爬虫技术和反爬虫技术之争就是魔道之争,总体来说,爬虫技术是很被动的,人家服务端换个花样,就让爬虫工程师折腾几天。
解决思路
对于普通用户来说,终极解决方案是浏览器自动化,模拟人类的浏览行为进行数据获取。换句话说,就是人能看到的数据就能够抓取下来。这样才能让爬虫工程师以不变应反爬虫服务器的万变。
PhantomJS和Selenium等爬虫工具都是类似的思路。但这些编程框架对于普通办公人员来说都太难使用。其实我们日常最多的工作就是抓取数十数百个数据,没必要兴师动众的写程序。用几个小时写爬虫,测爬虫,好容易代码成功运行,又用不了几天就被服务器升级搞得不能用...真不如每天花十几分钟手工保存来的简单,反正每天的新数据也就几十几百条而已。
最直接的简单思路就是:
- 手工打开这些页面(能脚本自动化打开更好);
- 执行一些必要的点击操作(或者根本不需要操作);
- 把看到的页面内容保存下来(注意是页面内容而不是页面源码);
- 用Python读取这些页面,BeautifulSoup解析,提取需要的信息,做数据处理。
解决方案
恐怕没有什么比直接写个Chrome浏览器插件来的更简单。写一个插件,实现保存单个页面内容,批量保存多个页面内容的方法,将会是个非常好的起步。
下面先从几个步骤简单介绍这个kSpider插件的开发思路。
插件本质
Chrome插件就是一个文件夹里面的几个web网页和js代码。和常规网页不同的是这些js代码可以使用Chrome的各种接口,简单说就是可以用js控制Chrome浏览器甚至进一步控制操作系统。比如说用js来打开和关闭tab页面,甚至捕获操作系统桌面。
调试插件
怎么运行下载的项目或者自己编写的项目?
从右上角菜单【更多工具-扩展程序】打开页面。
勾选右上角的【开发者模式】,然后就可以加载你的项目文件夹,修改后只要点这个刷新按钮即可重新加载运行。
manifest.json
项目文件夹必须要有一个manifest.json文件,它设置了使用哪些网页和js代码文件。官方说明看这里:https://developer.chrome.com/extensions/manifest
下面这个是我的配置,仅供参考。
{
"name": "kSpider",
"version": "1.0",
"description": "Data spider!",
"manifest_version": 2,
"icons": {
"16": "public/img/icon.png",
"48": "public/img/icon.png",
"128": "public/img/icon.png"
},
"background": {
"page": "public/index.html",
"_scripts": [
"public/js/background.js"
],
"persistent": true
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"public/js/docstart.js"
],
"run_at": "document_start"
},
{
"matches": [
"<all_urls>"
],
"js": [
"public/js/docend.js"
],
"run_at": "document_end"
}
],
"permissions": [
"activeTab",
"activeTab",
"background",
"downloads",
"declarativeContent",
"history",
"notifications",
"pageCapture",
"tabCapture",
"unlimitedStorage",
"storage",
"webNavigation",
"webRequest",
"<all_urls>",
"alarms"
],
"page_action": {
"default_popup": "public/popup.html",
"default_icon": {
"16": "public/img/icon.png",
"48": "public/img/icon.png",
"128": "public/img/icon.png"
}
},
"options_page": "public/options.html",
"commands": {
"saveThisPage": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "快速保存当前页面内容"
}
}
}
需要特别注意的是下面几点:
- background。必须。至关重要!它不是可有可无的背景,而是指实际运行在后台的js代码!几乎就是各种编程语言里面的
main
函数的意思。它有两种设置方法,html或者js文件,我这里是用的html,index.html是整个插件的入口。_script
去掉下划线后也可以。background指定的脚本可以直接控制浏览器甚至操作系统。加载扩展程序的页面里,点击插件卡片上那个不显眼的【查看视图:背景页】或者【查看视图:public/index.html】打开的就是这个页面的控制台。你也可以使用下面这个代码直接打开它的页面。
chrome.windows.create({
url: chrome.extension.getURL("public/index.html"),
type: "popup",
height: 480,
});
-
content_scripts。可选。注入到每个网页的脚本,这个脚本可以在网页内运行!就相当于网页里面的自己的js代码,它可以访问网页的全部元素!你可以让这个脚本提前页面运行start或者等页面都加载后再运行end。不需要额外设置,只要这里设置之后脚本就会自动随每个页面自动执行。但是这种脚本并不能使用Chrome的API功能,换而言之,它不可以控制浏览器或操作系统。如果你希望它能控制浏览器,那么只能让它通过
chrome.runtime.sendMessage
向background发送message,然后background通过chrome.runtime.onMessage.addListener
接收到message之后替它执行。 -
page_action。必须。插件图标一般都出现在浏览器右上角地址栏右侧,点击图标会弹出一个菜单。这个菜单实际是个网页!就是那个popup.html页面,它也可以自带js代码。这里的脚本和background的脚本一样,可以直接控制浏览器,也可以通过
chrome.extension.getBackgroundPage()
获取background内的函数代码使用。
-
permissions。可选。插件可能要用到的权限。
-
options_page。可选。在那个加载扩展程序的页面,点击那个【详细信息】然后找到【扩展程序选项】,点击右侧箭头,就会打开你这里设置的
options.html
。可以用来为你的插件提供更多设置界面。但实际上藏得这么深根本没啥用。
- commands。可选。这个是注册快捷键。可以在page_action或者background的代码中添加下面的代码来绑定到这个快捷键。
chrome.commands.onCommand.addListener(function (cmd) {
if (cmd == 'saveThisPage') { //ctrl+shif+s 保存当前页面
try {
saveThisPage()
} catch (e) {
console.log('kSpider:saveThisPage failed:', e)
}
}
})
代码实现
下图是我的项目文件目录。
其中真正有用的就是index.html-index.js、popup.html-popup.js、docend.js,其他的都可以忽略。
下面是这几个文件的代码,最核心的部分都在index.js里面,这里并没有用到网上提到较多的message通信方法,而是使用更巧妙的办法实现了页面内容的获取和存储,你可能需要仔细看一下代码注释,这里不啰嗦解释了。
index.js
console.log('kSpider:Hello from kspiders index.js.')
var urlList = [] //所有待处理的地址列表
var pageCode = 'console.log("kSpider:Runcode for ervery page.")' //每个页面要执行的代码
//确保扩展能够对每个页面都有效
chrome.runtime.onInstalled.addListener(function () {
//初始化后背景页面控制台输出
chrome.storage.sync.set({ color: '#3aa757' }, function () {
console.log('Hello from kspiders onInstalled background.js.');
});
//所有页面都激活扩展
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostContains: '' },
})],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
});
let docHtmlStr = 'document.documentElement.innerHTML' //获取当前页面内容的命令
let curActTabInfo = { active: true, currentWindow: true, url: '<all_urls>' } //过滤当前激活窗口的设置
/**
* 保存页面特定内容到html
* 为index.js中的pageRun函数的querystr提供支援
* 存储的文件名,kSpiderSavedPage.html,路径使用用户默认设置,自动避免重名
* 不同于保存源代码,这是将整个DOM实时的内容进行保存,需要关闭浏览器【设置-高级-下载内容-下之前询问...】
* @param {*} content 字符串,文字内容
*/
function saveContent(content) {
if (!content) content = document.documentElement.innerHTML
let blob = new Blob([content], { type: 'text/html' });
let objectURL = URL.createObjectURL(blob);
let filename = 'kSpiderSavedPage.html'
let conf = { url: objectURL, filename: filename, conflictAction: "uniquify" }
chrome.downloads.download(conf, function (downloadId) {
console.log("kSpider:SavePage filename:", filename, downloadId);
});
}
/**
* 在页面上执行动作,可以是tabsinfo过滤到的多个页面
* 只能根据tabinfo对象进行筛选页面,不能直接指定页面
* @param {*} [callback=(r) => { console.log(r) }] 要执行的函数,r参数是querystr返回的结果,比如一个页面元素
* @param {*} querystr 字符串,可以使用JQuery语法,$('.someclass').click()
* @param {boolean} [tabsinfo={ active: true, currentWindow: true }] 选择目标页面,默认是当前页
*/
function pageRun(callback = (r) => { console.log(r) }, querystr = docHtmlStr, tabsinfo = curActTabInfo) {
chrome.tabs.query(tabsinfo, function (tabs) {
for (var i = 0; i < tabs.length; i++) {
console.log(tabs[i].url)
chrome.tabs.executeScript(tabs[i].id, { code: querystr }, function (result) {
callback(result)
})
}
})
}
/**
* 保存当前激活的页面内容
*/
function saveThisPage() {
pageRun(saveContent, 'document.documentElement.innerHTML', { active: true, currentWindow: true, url: '<all_urls>' })
}
/**
* 保存当前窗口所有页面内容
*/
function saveAllPage() {
pageRun(saveContent, 'document.documentElement.innerHTML', { currentWindow: true, url: ['http://*/*', 'https://*/*'] })
}
//快捷键监听
chrome.commands.onCommand.addListener(function (cmd) {
if (cmd == 'saveThisPage') { //ctrl+shif+s 保存当前页面
try {
saveThisPage()
} catch (e) {
console.log('kSpider:saveThisPage failed:', e)
}
}
})
popup.js
console.log('kSpider:Hello from popup.js.')
let kbg = chrome.extension.getBackgroundPage() //调用index的方法
//一键保存当前页面到预先设定的路径
let saveThisBtn = document.getElementById('saveThis');
saveThisBtn.onclick = (e) => {
try {
kbg.saveThisPage()
} catch (e) {
console.log('kSpider:saveThisPage failed:', e)
}
}
//一键保存所有页面到预先设定的路径
let saveAllBtn = document.getElementById('saveAll');
saveAllBtn.onclick = (e) => {
try {
kbg.saveAllPage()
} catch (e) {
console.log('kSpider:saveAllPage failed:', e)
}
}
//打开kSpider控制台
let openConsoleBtn = document.getElementById('openConsole');
openConsoleBtn.onclick = function (element) {
chrome.windows.create({
url: chrome.extension.getURL("public/index.html"),
type: "popup",
height: 480,
});
};
docend.js
console.log('kSpider:Hello from docend.js.')
/**
* 用来载入外部函数库
* 为index.js中的pageRun函数的querystr提供支援
* @param {*} url js文件地址,字符串
* @param {*} onDone 载入后执行的函数onDone()
* @param {*} onError 载入失败执行的函数onError(e)
*/
function loadJS(url, onDone, onError) {
if (!onDone) onDone = function () { };
if (!onError) onError = function () { };
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status == 0) {
try {
eval(xhr.responseText);
} catch (e) {
onError(e);
return;
}
onDone();
} else {
onError(xhr.status);
}
}
}.bind(this);
try {
xhr.open("GET", url, true);
xhr.send();
} catch (e) {
onError(e);
}
}
//载入JQuery
loadJS('https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js', function () {
console.log('juquery ok')
console.log($("p"))
})
index.html,实际上并不需要这么复杂,只要能实现三个按钮就可以了。
<!DOCTYPE html>
<html>
<body>
<script src="js/index.js"></script>
</body>
</html>
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="js/lib/twitter-bootstrap/4.4.1/css/bootstrap.css">
<title>kSpider</title>
</head>
<style>
.mybtn{
border-radius: 0px;
width: 100%;
color: white;
}
.mybtn:hover{
color: white;
}
</style>
<body>
<div class="btn-group-vertical " style="width: 150px;">
<button id="saveThis" type="button" class="btn-sm btn-warning mybtn">Save This Page</button>
<hr style="height: 1px;margin: 0;">
<button id="saveAll" type="button" class="btn-sm btn-warning mybtn">Save All Pages</button>
<hr style="height: 1px;margin: 0;">
<button id="openConsole" type="button" class="btn-sm btn-warning mybtn">About kSpider</button>
</div>
<script src="js/lib/jquery/3.4.1/jquery.js"></script>
<script src="js/lib/twitter-bootstrap/4.4.1/js/bootstrap.js"></script>
<script src="js/popup.js"></script>
</body>
</html>
项目地址
项目已经放在我的网站上开源了,大家可以直接下载到本地,然后浏览器加载这个项目运行使用。
http://10knet.com/zhyuzh/kspider
欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】
每个人的智能新时代
如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~
END