项目中需要从百度图片和谷歌图片批量抓取一系列关键词的图片,而且需要是大图资源,不能是缩略图。
在后端通过http请求直接拉取内容抓取,遇到下面两个问题:

  • 有的大图地址是在前端通过脚本生成的,拉取页面内容之后无法直接得到大图地址
  • 翻页请求并不是简单的pageindex++,拿到下一页内容。抓取第一页后边的内容也需要分析翻页请求链接组装,以及返回的数据如何解析。

这两个问题导致通过后端爬取大图列表十分困难。于是我想起了以前玩过的杂技——浏览器插件。通过javascript控制浏览器打开网页,搜索关键词,页面渲染完毕之后拿到大图地址,第一页拿完之后让页面滚动到底部,继续加载图片,and so on!直到拿到足够数量的图片。
做完这个小工具,想着总结一下经验,加深点印象,免得以后某一天有需要再来做的时候一脸懵逼,于是抽时间慢慢写下这边文章记录一下我对浏览器插件的认识。

什么是chrome浏览器插件


地址栏右侧那些icon就是一个个浏览器插件。点击插件图标可以弹出插件窗口。
我所理解的chrome浏览器插件功能有三大块:

  • 弹出一个窗口,让用户执行操作,或者显示信息
  • 向网页中注入脚本文件,执行某些功能
  • 调用chrome提供的native api,执行浏览器tab页开关、窗口开关、文件下载等操作

如上图腾讯电脑管家的插件,就是用来提供广告过滤功能的。它的工作原理应该就是给需要过滤的网址插入一段脚本,来把页面上的广告标签干掉。

chrome插件开发

文档地址:https://developer.chrome.com/extensions/overview

代码模块

chrome插件完全由javascript、html、css开发,和上面的插件功能相对应,代码也可以分为三大模块:

  • popup 弹出窗口代码集合。弹窗UI通过html+css开发,弹窗中也可以引用js脚本来控制交互操作。每个弹窗都相当于一个独立的tab页,运行在其中的js脚本拥有一个独立的上下文。
  • inject.js。注入网页文件的脚本。需要注意的是,注入的脚本上下文也是独立的,它可以操作目标网页DOM,但是并不在目标网页脚本的上下文中。
  • background.js。插件后台脚本,拥有独立的上下文,且此上下文是唯一的,无论浏览器打开多少个tab页,background.js的上下文都不会变化,除非关闭浏览器。

这三块代码之间的关系我画了个图方便理解:

图中,黑色的部分代表chrome原生部分,其他的每一个方块都拥有一个独立的javascript上下文。

manifest.json

chrome插件有一个比较重要的配置文件,manifest.json,用来指定各个模块的代码文件名、插件权限、插件图标、inject脚本插入时机等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "imagefetcher",
"version": "0.0.1",
"manifest_version": 2,
"description": "抓取图片网站大图文件",
"background": { "scripts": ["dist/background.js"] },
"icons": { "16": "icon.jpg",
"48": "icon.jpg",
"128": "icon.jpg" },

"permissions": [
"tabs","downloads",
"http://*.baidu.com/",
"http://*.google.com.hk/"
],

"browser_action": {
"default_icon": "icon.jpg" ,
"default_title": "抓取图片",
"default_popup": "index.html"
}
,

"content_scripts":[{
"run_at":"document_end",
"matches":["<all_urls>"],
"js":["lib/jquery-2.0.0.min.js", "dist/inject.js"]
}]

}

background指定background代码文件路径;content_scripts指定inject的脚本列表,以及注入的条件、注入时机;browser_action中的default_popup指定popup弹出的html文件路径;permissions指定能访问的网页或chrome提供的一些功能的权限;icons指定插件图标。

模块间通信

按照上面的结构,很容易可以联想到各个模块的分工:
popup模块 的代码负责显示弹窗,让用户输入关键词,下发开始抓取指令;显示抓取进度;下发下载指令。
inject.js负责分析网页的DOM,拿到大图资源链接,并翻页,直到获取足够数量的图片。
background.js负责汇总各个网页抓取的结果,并将结果显示到弹窗中。
由于这些脚本拥有各自的执行上下文,并不能通过直接调用函数的方式来通信,所以我们需要通过chrome提供的方式来进行模块间的通信。

与background的通信

在popup中或者inject中发出消息给background接收。

1
chrome.runtime.sendMessage({action:ACTION.START_FETCH,data:xxxx});

sendMessage函数还可以接受一个回调函数,处理收到消息之后处理的返回结果。 rome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback)

在background.js中监听消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
chrome.runtime.onMessage.addListener(function(request, sender, sendRequest){
var data=request.data;
var fetch;
if(data.tab_id){
fetch=window.FETCH_ITEMS.getByTabId(data.tab_id);
}
//开始抓取消息,读取抓取队列的第一个,开始抓取,抓取完成之后继续读
if(request.action==ACTION.START_FETCH){
__fetch_list.push(data);
readFetchList();
}
else if(request.action==ACTION.FETCH_PROGTRESS){
fetch.urls=data.urls;
}
//抓取完成,修改fetch_item状态,若存在弹窗,通知弹窗刷新视图
else if(request.action==ACTION.FETCH_SUCCESS){
fetch.status=DOWNLOAD_STATUS.SUCCESS;
fetch.urls=data.urls;
}
});

与inject的通信

要指挥inject的脚本执行一些操作,必须给inject发消息,而inject是注入网页中的,所以发消息第一步必须先获取tab页的tabid,然后将消息发给特定的tab页。
在background中发出消息:

1
2
3
4
5
chrome.tabs.getSelected(function(tab){
chrome.tabs.sendMessage(tab.id, data, function(response) {
console.log(response);
});
});

inject中接收消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
var data=request.data;
//开始抓取
if(request.action==ACTION.START_FETCH){
var site=SITES.getSite(data.site);
if(!site){
sendResponse({err:1,message:"未实现此网页抓取"});
return;
}
sendResponse({err:0});
fetcher=require('./fetchors/'+data.site);
fetcher(data).then(function(urls){
data.urls=urls;
chrome.runtime.sendMessage({action:ACTION.FETCH_SUCCESS, data:data}); //发送给background
}).done()
}
});

调试

不能调试还写什么代码! chrome插件的三大模块也是可以调试的,只不过都藏在各种犄角旮旯里边,下面扒一扒怎么分别给他们打断点。

background

打开chrome://extensions/

点“检查视图”后边的链接,就可以打开控制台了,在source里边打上断点,调试走起


在插件图标上右键——审查弹出内容,打开控制台,在source里边打上断点,调试走起

inject


按F12打开被注入的页面的控制台,点Sources,点右侧中间的Content Scripts,就可以看到这个页面被那些插件注入了脚本了,根据名称找到自己的脚本,打上断点,调试走起

使用vue开发chrome插件

vue带来的好处

干掉DOM操作

我开发的chrome插件是一个用来完成图片下载任务的插件。抓取过程中,需要显示抓取进度,并可以进行删除下载,其中涉及很多DOM操作。使用vue可以减少大量的dom操作代码,这个不细讲,参见http://vuejs.org/guide/

干掉复杂的通信

这个是我觉得用vue开发chrome插件最有价值的部分了,前边介绍了插件几大模块之间的通信,需要调用chrome提供的接口进行频繁的发送消息和监听处理。
通过

1
chrome.extension.getBackgroundPage()

可以拿到插件background脚本的window对象,注意这个background的执行上下文是只有一个的,所以在插件运行期间我们可以用它来存储各个tab页抓取回来的数据。
在popup的脚本中:

1
2
3
4
5
6
7
8
9
var FETCH_ITEMS=chrome.extension.getBackgroundPage().FETCH_ITEMS;
var vm=new Vue({
el: '#wrapper',
data: {
FETCH_ITEMS: FETCH_ITEMS
},
methods:{
}
});

这样,将从各个tab页抓取回来的数据push到background.js暴露出来的一个对象中,popup弹出的网页中就可以实时显示抓取进度了,不需要在popup和background之间编写大量的通信代码。
至于各个tab页和background之间的通信,可以使用上面chrome提供的通信方式,也可以自己拿到background的window对象暴露出的变量,再进行操作。这里并没有复杂的视图更新和用户操作,所以怎么通信都无所谓了~

代码结构


按照上面理解的结构,每个模块的代码集中到一起。
另外,插件中也允许根据路径直接访问插件中的资源,其路径是“chrome-extension://[extensionId]/[resourceName]” 。
select目录中存放的是一个用来筛选图片的页面代码,在popup页面中直接跳转到/select.html即可打开此页面。

插件开发完成打包之后,把这些资源放到一个文件夹中

然后打开chrome://extensions/

选择上面的目录即可看到地址栏右侧出现插件的图标。

初次在chrome插件开发中使用vue的时候,遇到了这样一个问题:

模型更新了,视图始终不更新,然后打开调试界面后运行vm.$mount("#wrapper");,视图却更新了。
百思不得其解,google之,发现chrome插件中有些javascript代码写法和正常环境中有所不同,幸好vue居然贴心的为这种情况准备了一个特殊的包,不然就前功尽弃了 :(
这里总结了一个经验,用前端框架开发的过程中最好不要使用.min的包,打包好的代码里边会去掉一些警告的逻辑… 把.min包替换成非压缩包之后,看到了这个错误:

根据提示,在vue的项目里边找到了一个叫CSP的分支
https://github.com/vuejs/vue/tree/csp/dist

看文档说明,果然就是为插件开发定制的啊! 没想到那么偏门的场合他们也有关注到!

成品

https://github.com/zouchengzhuo/BigImageFetcher
抓取的资源路径都是源站的,所以并不用担心百度和google会封IP,可以放心抓取~

☞ 参与评论