一个js爬虫
- 1. 第一个demo
- 2. configs详解——之成员
- 3. configs详解——之field
- 4. configs详解——之site, page和console
- 5. configs详解——之回调函数
- 6. 爬虫进阶开发——之内置函数
- 7. 爬虫进阶开发——之模板化
- 8. 爬虫进阶开发——之图片云托管
- 9. 爬虫进阶开发——之自动IP代理
- 10. 爬虫进阶开发——之验证码识别
- 11. 爬虫进阶开发——之自动JS渲染
- 12. 爬虫进阶开发——之技巧篇
- 13. 两个完整demo
- 14. 开发神箭手爬虫的常用工具
- 15. 常见问题
第一个demo
爬虫采用JavaScript编写, 下面以糗事百科为例, 来看一下我们的爬虫长什么样子:
var configs = {
// configs对象的成员domains, scanUrls, contentUrlRegexes和fields
domains: ["www.qiushibaike.com"],
scanUrls: ["http://www.qiushibaike.com/"],
contentUrlRegexes: ["http://www\\.qiushibaike\\.com/article/\\d+"],
fields: [
{
name: "content", // fields成员中第一个field对象的name
selector: "//*[@id='single-next-link']", // fields成员中第一个field对象的selector
required: true // fields成员中第一个field对象的required
},
{
name: "author", // fields成员中第二个field对象的name
selector: "//div[contains(@class,'author')]//h2" // fields成员中第二个field对象的selector
}
]
};
// 向爬虫任务中添加configs配置,并启动爬虫
var crawler = new Crawler(configs);
crawler.start();
爬虫的整体框架就是这样, 首先定义了一个configs
对象, 里面设置了待爬网站的一些信息, 然后通过调用var crawler = new Crawler(configs);
和crawler.start();
来配置并启动爬虫.
configs
对象如何定义, 后面会作详细介绍.
configs详解——之成员
爬虫的整体框架是这样:首先定义了一个
configs
对象, 里面设置了待爬网站的一些信息, 然后通过调用var crawler = new Crawler(configs);
和crawler.start();
来配置并启动爬虫.
configs对象中可以定义下面这些成员
domains
定义爬虫爬取哪些域名下的网页, 非域名下的url会被忽略以提高爬取速度
数组类型 不能为空
举个栗子:
domains: ["wallstreetcn.com"],
domains: ["zhihu.sogou.com", "zhihu.com"],
scanUrls
定义爬虫的入口链接, 爬虫从这些链接开始爬取,
同时这些链接也是监控爬虫所要监控的链接
数组类型 不能为空
举个栗子:
scanUrls: ["http://wallstreetcn.com/news"],
scanUrls: ["http://club2011.auto.163.com/board/biaozhi308/r0a0t0g0bpeo0-n1.html", "http://club2011.auto.163.com/board/fengshen/r0a0t0g0bpeo0-n1.html"],
contentUrlRegexes
定义内容页url的规则
内容页是指包含要爬取内容的网页 比如http://www.qiushibaike.com/article/115878724
就是糗事百科的一个内容页
数组类型 正则表达式 最好填写以提高爬取效率
举个栗子:
contentUrlRegexes: ["http://wallstreetcn\\.com/node/\\d+"],
contentUrlRegexes: ["http://club2011\\.auto\\.163\\.com/post/\\d+\\.html.*"],
特别需要注意的是,正则表达式中.
和?
都是需要转义的。
helperUrlRegexes
定义列表页url的规则
对于有列表页的网站, 使用此配置可以大幅提高爬虫的爬取速率
列表页是指包含内容页列表的网页 比如http://www.qiushibaike.com/8hr/page/2/?s=4867046
就是糗事百科的一个列表页
数组类型 正则表达式
举个栗子:
helperUrlRegexes: ["http://wallstreetcn\\.com/news(\\?/page=\\d+)?"],
helperUrlRegexes: ["http://club2011\\.auto\\.163\\.com/board/biaozhi308/r0a0t0g0bpeo0\\-n\\d+\\.html.*", "http://club2011\\.auto\\.163\\.com/board/fengshen/r0a0t0g0bpeo0\\-n\\d+\\.html.*"],
fields
定义内容页的抽取规则
规则由一个个field
组成, 一个field
代表一个数据抽取项
数组类型 不能为空
举个栗子:
fields: [
{
name: "content",
selector: "//*[@id='single-next-link']",
required: true
},
{
name: "author",
selector: "//div[contains(@class,'author')]//h2"
}
]
上面的例子从网页中抽取内容和作者, 抽取规则是针对糗事百科的内容页写的
field
在configs详解——之field
中作详细介绍。
enableProxy
是否使用代理
如果爬取的网站根据IP做了反爬虫, 可以设置此项为true
布尔类型 可选设置
举个栗子:
enableProxy: true,
configs详解——之field
field
定义一个抽取项, 一个field
可以定义下面这些东西
name
给此项数据起个变量名
变量名中不能包含.
如果抓取到的数据想要以文章或者问答的形式发布到网站(WeCenter, WordPress, Discuz!等),field
的命名请参考两个完整demo中的命名, 否则无法发布成功
String类型 不能为空
举个栗子:
给field
起了个名字叫question
{
name: "question",
selector: "//*[@id='zh-question-title']/h2"
}
selector
定义抽取规则, 默认使用XPath
如果使用其他类型的, 需要指定selectorType
String类型 不能为空
举个栗子:
使用XPath来抽取知乎问答网页的问题,selector
的值就是问题的XPath
{
name: "question",
selector: "//*[@id='zh-question-title']/h2"
}
selectorType
抽取规则的类型
目前可用SelectorType.XPath, SelectorType.JsonPath, SelectorType.Regex
默认SelectorType.XPath
枚举类型
栗子1:selector
默认使用XPath
{
name: "question",
selector: "//*[@id='zh-question-title']/h2" // XPath抽取规则
}
栗子2:selector
使用JsonPath,如果内容是Json数据格式,则使用JsonPath抽取数据比较方便
{
name: "question_answer_content",
selectorType: SelectorType.JsonPath,
selector: "$.comment.content", // JsonPath抽取规则
required: true
}
栗子3:
除了XPath和JsonPath之外,神箭手还支持使用正则表达式来抽取数据
{
name: "title",
selectorType: SelectorType.Regex,
selector: '<div\\sid=\\"page\\"><h1>[^\\/]+<\\/h1>' // Regex抽取规则
}
required
定义该
field
的值是否必须, 默认false
赋值为true
的话, 如果该field
没有抽取到内容, 该field
对应的整条数据都将被丢弃
布尔类型
举个栗子:
{
name: "article_title",
selector: "//div[contains(@class,'location')]/text()[3]",
required: true
}
repeated
定义该
field
抽取到的内容是否是有多项, 默认false
赋值为true
的话, 无论该field
是否真的是有多项, 抽取到的结果都是数组结构
布尔类型
举个栗子:
爬取的网页中包含多条评论,所以抽取评论的时候要将repeated
赋值为true
{
name: "comments",
selector: "//*[@id='zh-single-question-page']//a[contains(@class,'zm-item-tag')]",
repeated: true
}
children
为此
field
定义子项
子项的定义仍然是一个fields
结构, 即一个field
对象的数组
没错, 这是一个树形结构
数组类型
举个栗子:
抓取知乎问答网页的回答,每个回答爬取了内容,作者,赞同数
{
name: "answers",
selector: "//*[@id=\"zh-question-answer-wrap\"]/div",
repeated: true,
children: [
{
name: "content",
selector: "//div[contains(@class,\"zm-editable-content\")]",
required: true
},
{
name: "author",
selector: "//a[@class=\"author-link\"]"
},
{
name: "agree_count",
selector: "//button[@class=\"up\"]/span[@class=\"count\"]"
}
]
}
sourceType
该
field
的数据源, 默认从当前的网页中抽取数据
选择SourceType.AttachedUrl
可以发起一个新的请求, 然后从请求返回的数据中抽取
选择SourceType.UrlContext
可以从当前网页的url附加数据中抽取
url附加数据后面会作介绍
枚举类型
attachedUrl
当
sourceType
设置为SourceType.AttachedUrl
时, 定义新请求的url
String类型
举个栗子:
当爬取的网页中某些内容需要异步加载请求时,就需要使用attachedUrl
,比如,抓取知乎回答中的评论部分,就是通过AJAX异步请求的数据
{
name: "comment_id",
selector: "//div/@data-aid",
},
{
name: "comments",
sourceType: SourceType.AttachedUrl,
// "comments"是从发送"attachedUrl"这个异步请求返回的数据中抽取的
// "attachedUrl"支持引用上下文中的抓取到的"field", 这里就引用了上面抓取的"comment_id"
attachedUrl: "https://www.zhihu.com/r/answers/{comment_id}/comments",
selectorType: SelectorType.JsonPath,
selector: "$.data",
repeated: true,
children: [
...
]
}
UrlContext
当
sourceType
赋值为SourceType.UrlContext
时, 表示从内容页中的附加数据(是开发者自定义的一段代码,例如,html代码)中抽取数据
String类型
举个栗子:
将自定义数据附加到内容页中,然后再提取到field
var configs = {
// configs中的其他成员
...
fields: [
{
name: "extra_data",
// 这里是从开发者附加的一段html代码中抽取的数据
sourceType: SourceType.UrlContext,
selector: "//span[contains(@class,'shenjianshou')]",
}
]
};
configs.onProcessHelperPage = function(page, content, site) {
// 定义附加数据
var extraData = '<div><span class="shenjianshou">100</span></div>';
// 将extraData附加到contentUrl对应的网页中,将contentUrl添加到待爬队列中
site.addUrl(contentUrl, "get", null, extraData);
...
return false;
}
configs详解——之site
, page
和console
site
site
表示当前正在爬取的网站的对象,下面介绍了可以调用的函数
site.addHeader(key, value)
一般在beforeCrawl
回调函数(在configs详解——之回调函数中会详细描述)中调用, 用来添加一些HTTP请求的Header
@param
key
Header的key, 如User-Agent,Referer等@param
value
Header的值
举个栗子:Referer
是HTTP请求Header的一个属性,http://buluo.qq.com/p/index.html
是Referer
的值
configs.beforeCrawl = function(site) {
site.addHeader("Referer", "http://buluo.qq.com/p/index.html");
}
site.addCookie(key, value)
一般在beforeCrawl
回调函数(在configs详解——之回调函数中会详细描述)中调用, 用来添加一些HTTP请求的Cookie
@param
key
Cookie的key@param
value
Cookie的值
举个栗子:
cookie是由键-值对组成的,BAIDUID
是cookie的key,FEE96299191CB0F11954F3A0060FB470:FG=1
则是cookie的值
configs.beforeCrawl = function(site) {
site.addCookie("BAIDUID", "FEE96299191CB0F11954F3A0060FB470:FG=1");
}
site.addCookies(cookies)
一般在beforeCrawl
回调函数(在configs详解——之回调函数中会详细描述)中调用, 用来添加一些HTTP请求的Cookie
@param
cookies
多个Cookie组成的字符串
举个栗子:
cookies是多个cookie的键-值对组成的字符串,用;
分隔。BAIDUID
和BIDUPSID
是cookie的key,FEE96299191CB0F11954F3A0060FB470:FG=1
和FEE96299191CB0F11954F3A0060FB470
是cookie的值,键-值对用=
相连
configs.beforeCrawl = function(site) {
site.addCookies("BAIDUID=FEE96299191CB0F11954F3A0060FB470:FG=1; BIDUPSID=FEE96299191CB0F11954F3A0060FB470;");
}
site.addUrl(url, method, data, contextData)
一般在onProcessScanPage
和onProcessHelperPage
回调函数(在configs详解——之回调函数中会详细描述)中调用, 用来往待爬队列中添加url
@param
url
待添加的url@param
method
默认为GET
请求,也支持POST
请求@param
data
发送请求时需添加的参数,可以为空@param
contextData
此url附加的数据, 可以为空
栗子1:
configs.onProcessHelperPage = function(page, content, site) {
var regex = /https?:\\\/\\\/www\.jiemian\.com\\\/article\\\/\d+\.html/g;
var urls = [];
urls = content.match(regex);
if (urls != "") {
for (var i = 0, n = urls.length; i < n; i++) {
urls[i] = urls[i].replace(/\\/g, "");
// get请求,不需要添加参数,也不需要附加数据
site.addUrl(urls[i]);
}
}
...
return false;
}
栗子2:
configs.onProcessHelperPage = function(page, content, site) {
...
var nextUrl = page.url.replace("page="+currentPage, "page="+page);
// 定义了发送POST请求所需参数
var param = new Object();
param.page = page + "";
param.size = "18";
// 标明该请求是POST请求,并添加了所需参数
site.addUrl(nextUrl, "POST", param);
return false;
}
site.requestUrl(url, method, data)
一般在beforeCrawl
, afterDownloadPage
, onProcessScanPage
和onProcessHelperPage
回调函数(在configs详解——之回调函数中会详细描述)中调用, 下载网页, 得到网页内容
@param
url
待下载的url@param
method
默认为GET
请求,也支持POST
请求@param
data
发送请求时需添加的参数,可以为空
举个栗子:
configs.afterDownloadPage = function(page, site) {
// 定义了发送POST请求所需参数
var data = new Object();
data.sno = 'FK1QPNCEGRYD';
data.CSRFToken = result[1];
var url = "https://checkcoverage.apple.com/cn/zh/?sn=FK1QPNCEGRYD";
// 通过发送带参数的POST请求,下载网页,并将网页内容赋值给result
var result = site.requestUrl(url, "PSOT", data);
...
return page;
}
page
page
表示当前正在爬取的网页页面的对象,下面介绍了可以调用的变量和函数
page.url
当前正在爬取的网页页面的url
举个栗子:
在afterExtractPage
回调函数(在configs详解——之回调函数中会详细描述)中,将url值赋给名叫article_content
的field
configs.afterExtractPage = function(page, data) {
data.article_content = page.url + "";
return data;
}
page.raw
当前网页原始内容
举个栗子:
在onProcessScanPage
回调函数(在configs详解——之回调函数中会详细描述)中,通过page.raw
得到网页原始内容,然后转换成Json对象
configs.onProcessScanPage = function(page, content, site) {
var jsonObj = JSON.parse(page.raw);
...
return false;
}
page.skip(fieldName)
一般在afterExtractField
回调函数(在configs详解——之回调函数中会详细描述)中使用, 用来忽略当前网页的抽取结果或当前上下文的一条结果
@param
fieldName
根据fieldName
来确定忽略范围,可以为空。当为空时,忽略当前网页的抽取结果
栗子1:
例如爬取知乎问答的时候, 想只爬取话题中包含经济学的问答, 可以这样过滤:
configs.afterExtractField = function(fieldName, data) {
if (fieldName == "topics") { // fieldName是topics
if (data.indexOf("经济学") == -1) { // data中不包含"经济学"
page.skip(); // 跳过当前爬取的网页
}
}
return data;
}
栗子2:
例如爬取知乎问答的时候, 想要只爬取赞同数大于10的回答, 可以这样过滤:
configs = {
// configs中的其他成员
...
fields: [
{
name: "answers",
selector: "//*[@id=\"zh-question-answer-wrap\"]/div",
repeated: true,
children: [
{
name: "agree_count",
selector: "//button[@class=\"up\"]/span[@class=\"count\"]"
},
...
]
}
]
};
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "answers.agree_count") { // fieldName是answers数组中的agree_count元素名
if (parseInt(data) < 10) { // 回答的点赞数小于10
page.skip("answers"); // 跳过这个回答
}
}
return data;
}
console
console
对象提供不同级别的日志打印
console.log(message)
打印日志, 调试时使用
举个栗子:
console.log("测试log日志");
console.debug(message)
打印出错级别日志, 调试时使用
举个栗子:
console.debug("正在提取文章标题数据");
console.info(message)
打印普通日志
举个栗子:
console.info("成功处理一个页面");
console.warn(message)
打印警告情况的日志
举个栗子:
console.warn("XX文件解析失败");
console.error(message)
打印错误情况的日志
举个栗子:
console.error("XPath错误");
configs详解——之回调函数
回调函数是在爬虫爬取并处理网页的过程中设置的一些系统钩子, 通过这些钩子可以完成一些特殊的处理逻辑.
回调函数需要设置到
configs
参数里面才起作用
下图是神箭手云爬虫爬取并处理网页的流程图,矩形方框中标识了爬虫运行过程中所使用的重要回调函数:
beforeCrawl(site)
爬虫初始化时调用, 用来指定一些爬取前的操作
@param
site
当前正在爬取的网站的对象
在函数中可以调用site.addHeader(key, value)
, site.addCookie(key, value)
,site.addCookies(cookies)
等
举个栗子:
在爬虫开始爬取之前给所有待爬网页添加一个Header
configs.beforeCrawl = function(site) {
site.addHeader("Referer", "http://buluo.qq.com/p/index.html");
}
isAntiSpider(url, content)
判断当前网页是否被反爬虫了, 需要开发者实现
@param
url
当前网页的url@param
content
当前网页内容@return
如果被反爬虫了, 返回true
, 否则返回false
举个栗子:
configs.isAntiSpider = function(url, content) {
if (content.indexOf("404页面不存在") > -1) { // content中包含"404页面不存在"字符串
return true; // 返回当前网页被反爬虫了
}
return false;
}
afterDownloadPage(page, site)
在一个网页下载完成之后调用. 主要用来对下载的网页进行处理.
@param
page
当前下载的网页页面的对象,调用page.url
可获取当前网页的url,调用page.raw
可获取当前网页内容@param
site
当前正在爬取的网站的对象@return
返回处理后的网页
举个栗子:
比如下载了某个网页,希望向网页的body中添加html标签,处理过程如下:
configs.afterDownloadPage = function(page, site) {
var pageHtml = "<div id=\"comment-pages\"><span>5</span></div>";
var index = page.raw.indexOf("</body>");
page.raw = page.raw.substring(0, index) + pageHtml + page.raw.substring(index);
return page;
}
onProcessScanPage(page, content, site)
在爬取到入口url的内容之后, 添加新的url到待爬队列之前调用. 主要用来发现新的待爬url, 并且能给新发现的url附加数据.
@param
page
当前正在爬取的网页页面的对象,调用page.url
可获取当前网页的url,调用page.raw
可获取当前网页内容,调用page.skip()
便不再爬取当前网页@param
content
当前网页内容@param
site
当前正在爬取的网站的对象@return
返回false表示不需要再从此网页中发现待爬url
此函数中通过调用site.addUrl(url)
来添加新的url到待爬队列。
栗子1:
实现这个回调函数并返回false
,表示爬虫在处理这个scanUrl的时候,不会从中提取待爬url
configs.onProcessScanPage = function(page, content, site) {
return false;
}
栗子2:
生成一个新的url添加到待爬队列中,并通知爬虫不再从当前网页中发现待爬url
configs.onProcessScanPage = function(page, content, site) {
var jsonObj = JSON.parse(page.raw);
for (var i = 0, n = jsonObj.data.length; i < n; i++) {
var item = jsonObj.data[i];
var lastid = item._id;
// 生成一个新的url
var url = page.url + lastid;
// 将新的url插入待爬队列中
site.addUrl(url);
}
// 通知爬虫不再从当前网页中发现待爬url
return false;
}
onProcessHelperPage(page, content, site)
在爬取到列表页url的内容之后, 添加新的url到待爬队列之前调用. 主要用来发现新的待爬url, 并且能给新发现的url附加数据.
@param
page
当前正在爬取的网页页面的对象,调用page.url
可获取当前网页的url,调用page.raw
可获取当前网页内容,调用page.skip()
便不再爬取当前网页@param
content
当前网页内容@param
site
当前正在爬取的网站的对象@return
返回false表示不需要再从此网页中发现待爬url
此函数中通过调用site.addUrl(url)
来添加新的url到待爬队列
栗子1:
实现这个回调函数并返回false
,表示爬虫在处理这个helperUrl的时候,不会从中提取待爬url
configs.onProcessHelperPage = function(page, content, site) {
return false;
}
栗子2:
在onProcessHelperPage
回调函数中,生成新的contentUrl
并添加到待爬队列中,并通知爬虫不再从当前网页中发现待爬url
configs.onProcessHelperPage = function(page, content, site) {
var jsonObj = JSON.parse(content);
var id = 0;
for (var i = 0, n = jsonObj.data.length; i < n; i++) {
var item = jsonObj.data[i];
id = item._id;
// 将新的url插入待爬队列中
site.addUrl("http://service.chanjet.com/api/v1/message/"+id);
}
// 通知爬虫不再从当前网页中发现待爬url
return false;
}
beforeHandleImg(fieldName, img)
在抽取到field
内容之后调用, 对其中包含的img
标签进行回调处理
@param
fieldName
当前field
的name
. 注意: 子field
的name
会带着父field
的name
, 通过.
连接.@param
img
整个img
标签的内容@return
返回处理后的img
标签的内容
很多网站对图片作了延迟加载, 这时候就需要在这个函数里面来处理
举个栗子:
汽车之家论坛帖子的图片大部分是延迟加载的,默认会使用http://x.autoimg.cn/club/lazyload.png
图片url,我们需要找到真实的图片url并替换,具体实现如下:
configs.beforeHandleImg = function(fieldName, img) {
// 通过正则判断img中的src属性是不是真实图片url,如果是,则直接返回img,如果不是,继续执行
var regex = /src="(https?:\/\/.*?)"/;
var rs = regex.exec(img);
if (!rs) return img;
var url = rs[1];
if (url == "http://x.autoimg.cn/club/lazyload.png") {
var regex2 = /src9="(https?:\/\/.*?)"/;
rs = regex2.exec(img);
// 替换成真实图片url
if (rs) {
var newUrl = rs[1];
img = img.replace(url, newUrl);
}
}
return img;
}
beforeCacheImg(fieldName, url)
由于很多网站都有图片防盗链限制, 所以神箭手会缓存爬到的图片, 在缓存之前, 可以对图片作处理
@param
fieldName
当前field
的name
. 注意: 子field
的name
会带着父field
的name
, 通过.
连接.@param
url
图片的url@return
处理后的图片url
举个栗子:
知乎问答页面, 用户的头像链接是这样的: https://pic3.zhimg.com/xxxxxx_s.jpg
研究一下可以发现, 大一点的头像是这样的: https://pic3.zhimg.com/xxxxxx_l.jpg
configs.beforeCacheImage = function(fieldName, url) {
if (fieldName == "answers.avatar") {
return url.replace("_s.jpg", "_l.jpg"); // 对url进行字符串替换,得到较大图片的url
}
return url; // 返回图片url
}
afterExtractField(fieldName, data, page)
当一个field
的内容被抽取到后进行的回调, 在此回调中可以对网页中抽取的内容作进一步处理
@param
fieldName
当前field
的name
. 注意: 子field
的name
会带着父field
的name
, 通过.
连接.@param
data
当前field
抽取到的数据. 如果该field
是repeated
,data
为数组类型, 否则是String
@param
page
当前正在爬取的网页页面的对象,调用page.url
可获取当前网页的url,调用page.skip()
便不再爬取当前网页@return
返回处理后的数据, 注意数据类型需要跟传进来的data
类型匹配
举个栗子:
比如爬取知乎用户的性别信息, 相关网页源码如下:
<span class="item gender" ><i class="icon icon-profile-male"></i></span>
那么可以这样写:
configs = {
// configs的其他成员
...
fields: [
{
name: "gender",
selector: "//span[contains(@class, 'gender')]/i/@class"
}
]
...
};
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "gender") {
if (data.indexOf("icon-profile-male") > -1) { // data中包含"icon-profile-male",说明当前知乎用户是男性
return "男";
}
else if (data.indexOf("icon-profile-female") > -1) { // data中包含"icon-profile-female",说明当前知乎用户是女性
return "女";
}
else { // data未匹配到上面的两个字符串,无法判断用户性别
return "未知";
}
}
return data;
}
afterExtractPage(page, data)
在一个网页的所有field
抽取完成之后, 可能需要对field
进一步处理, 以发布到自己的网站
@param
page
当前正在爬取的网页页面的对象,调用page.url
可获取当前网页的url,调用page.skip()
便不再爬取当前网页@param
data
当前网页抽取出来的所有field
的数据, JavaScript对象@return
返回处理后的数据, 注意数据类型不能变
举个栗子:
比如从某网页中得到time
和title
两个field抽取项, 现在希望把time
的值添加中括号后拼凑到title
中,处理过程如下:
configs.afterExtractPage = function(page, data) {
var title = "[" + data.time + "] " + data.title;
data.title = title;
return data;
}
爬虫进阶开发——之内置函数
本节介绍常用的内置js函数
extract(data, xpath)
使用xpath
从data
中抽取单条数据
@param
data
html代码片段@param
xpath
XPath表达式@return
返回符合XPath表达式的第一条html元素
举个栗子:
假设data
是:
<span class="location item" title="加州"><a href="/topic/19581783" title="加州" class="topic-link" data-token="19581783" data-topicid="10505">加州</a></span>
<span class="business item" title="互联网"><a href="/topic/19550517" title="互联网" class="topic-link" data-token="19550517" data-topicid="99">互联网</a></span>
<span class="item gender"><i class="icon icon-profile-male"></i></span>
执行以下代码:
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "content") {
data = extract(data, "//a");
}
return data;
}
返回的data
结果是:
<a href="/topic/19581783" title="加州" class="topic-link" data-token="19581783" data-topicid="10505">加州</a>
extractList(data, xpath)
使用xpath
从data
中抽取多条数据
@param
data
html代码片段@param
xpath
XPath表达式@return
返回符合XPath表达式的html元素数组
举个栗子:
仍然使用上例中data
,执行以下代码:
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "content") {
var contents = [];
contents = extractList(data, "//a");
}
...
}
contents
数组的内容是:
['<a href="/topic/19581783" title="加州" class="topic-link" data-token="19581783" data-topicid="10505">加州</a>',
'<a href="/topic/19550517" title="互联网" class="topic-link" data-token="19550517" data-topicid="99">互联网</a>']
exclude(data, xpath)
从data
中去除符合xpath
的html元素
@param
data
html代码片段@param
xpath
要去除的元素的XPath表达式@return
返回去除之后的html代码片段
举个栗子:
仍然使用上例中data
,执行以下代码:
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "content") {
data = exclude(data, "//a");
}
return data;
}
data
的结果是:
<span class="location item" title="加州"></span>
<span class="business item" title="互联网"></span>
<span class="item gender"><i class="icon icon-profile-male"></i></span>
encodeURI
和encodeURIComponent
js自带的这两个函数在处理汉字时有问题, 系统重新定义了这两个函数
getCaptcha(type, url)
验证码识别的函数,通过验证码图片获取识别的验证码
@param
type
验证码类型,详情见爬虫进阶开发——之验证码识别中的验证码价目表@param
url
验证码图片url@return
返回Json格式的验证码数据,格式为:{"result":"UNEL","ret":0}(result是识别的验证码结果;ret是返回码,识别成功时值为0,失败时值为非零负数)
举个栗子:
要处理某网站登陆界面的验证码,需调用getCaptcha
函数,执行以下代码:
configs.afterDownloadPage = function(page, site) {
// 验证码图片url
var url = "https://account.guokr.com/captcha/117654328/";
// type是52(1-4位不定长数字英文混合),将得到的验证码Json数据赋值给"imgCaptchaData"
var imgCaptchaData = getCaptcha(52, url);
...
return page;
};
getCaptchaFromBase64(type, imgBase64)
验证码识别的函数,通过Base64编码的验证码图片获取识别的验证码
@param
type
验证码类型,详情见爬虫进阶开发——之验证码识别中的验证码识别价目表@param
imgBase64
验证码图片的Base64编码,不包含图片Base64的头:" data:image/jpeg;base64,"@return
返回Json格式的验证码数据,格式为:{"result":"UNEL","ret":0}(result是识别的验证码结果;ret是返回码,识别成功时值为0,失败时值为非零负数)
举个栗子:
要处理某网站登陆界面Base64编码的验证码图片,需调用getCaptchaFromBase64
函数,执行以下代码:
configs.afterDownloadPage = function(page, site) {
// 验证码图片的Base64编码
var imgBase64 = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQ...";
// type是53(1-5位不定长数字英文混合),将得到的验证码Json数据赋值给"imgCaptchaData"
var imgCaptchaData = getCaptchaFromBase64(53, imgBase64);
...
return page;
};
爬虫进阶开发——之模板化
添加“输入框”
格式如下:
var keywords = "苹果产品,Apple Pay";//@input(keywords, 搜索关键字, 只采集此关键字在zhihu.sogou.com中的搜索结果,多个关键字用逗号分隔)
效果图如下:
向输入框
中输入的信息,可作为变量的值,比如,根据关键字爬取知乎,可在输入框
中输入需爬取的关键字,在爬虫代码中对数据加以处理,即可爬到相应的数据。
添加“选择框”
格式如下:
var filter = true;//@input(filter, 结果过滤, 标题或主题中必须包含搜索关键字)
效果图如下:
选择框
的信息,可作为判断条件,对数据进行操作,比如,现在要爬取某文章类网站,并添加是否过滤文章图片的功能,就需要将这个变量作为判断依据,并在爬虫代码中对不同情况进行相应处理。
爬虫进阶开发——之图片云托管
当我们需要爬取一个多图网站的时候,最直接的方法是直接爬取网站中的img标签,这种方式常常会导致采集到的图片无法正常显示或显示防盗链图。这种情况,选择图片云托管是最明智的。
那如何使用图片云托管呢?
最简单的方法,在神箭手后台配置爬虫任务的时候,勾选“自动缓存图片”,在任务运行时,就会帮你将采集到的所有图片托管到神箭手云服务器上。但这种方式只能帮你托管后缀名是jpg
、jpeg
、png
和gif
的图片。
这样做当然无法满足开发者的需求,为此,我们提供了cacheImg
内置函数,目前cacheImg
内置函数只能在afterExtractField
回调函数中使用,可以处理用户头像、文章封面图和内容中的图片。
举个栗子:
在afterExtractField
回调函数中,通过cacheImg
函数将非标准的图片url托管到神箭手云服务器上
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "article_cover_img") {
return cacheImg(data); // data是图片url,返回可被神箭手云服务器处理的url
}
return data;
}
爬虫进阶开发——之自动IP代理
当爬取大众点评时会出现爬取不到数据的情况,这是怎么回事呢?其实,是我们的爬虫IP被网站封掉了,这个问题,选择我们神箭手提供的自动IP代理就能解决。
举个栗子:
在configs对象中添加成员enableProxy
并设置为true
,就可使用自动IP代理
var configs = {
// configs的其他成员
...
enableProxy: true, // 开启自动IP代理
...
};
爬虫进阶开发——之验证码识别(内测版)
当需要爬取的网页需要需要输入验证码才能访问的时候,该怎么办呢?这个问题,使用神箭手提供的验证码识别服务就能解决。
验证码识别是付费功能,在使用前请先确保你的账户有足够的余额,否则会影响使用。
验证码识别的调用函数请参考内置函数getCaptcha和getCaptchaFromBase64。
举个栗子:
当输入Base64编码的验证码才能爬取到某网页的数据时,先调用getCaptchaFromBase64
函数得到验证码数据,再通过发送POST
请求得到网页数据,执行以下代码:
configs.afterDownloadPage = function(page, site) {
var imgBase64 = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQ...";
// 得到Base64编码的验证码数据,数据是Json格式,验证码类型是53(1-5位不定长数字英文混合)
var imgCaptchaData = getCaptchaFromBase64(53, imgBase64);
var imgCaptcha = JSON.parse(imgCaptchaData);
if (imgCaptcha == null) {
return page;
}
// 判断是否成功得到验证码数据
if (imgCaptcha.ret < 0) {
return page;
}
var reg = /csrfToken: \"([0-9a-zA-Z-]+)"/;
var result = reg.exec(page.raw);
var data = new Object();
data.sno = 'FK1QPNCEGRYD';
// 将验证码赋值给"data.ans"
data.ans = imgCaptcha.result;
data.captchaMode = "image";
data.CSRFToken = result[1];
page.url = "https://checkcoverage.apple.com/cn/zh/?sn=FK1QPNCEGRYD";
// 发送带参数的"POSt"请求得到网页数据
page.raw = site.requestUrl('https://checkcoverage.apple.com/cn/zh/?sn=FK1QPNCEGRYD', 'post', data);
return page;
}
验证码识别价目表(成功识别一次的价格)
验证码类型(Type) | 价格(分) | 类型描述 |
---|---|---|
1 | 1 | 1位数字 |
2 | 1 | 2位数字 |
3 | 1 | 3位数字 |
4 | 1 | 4位数字 |
5 | 1 | 5位数字 |
6 | 1 | 6位数字 |
7 | 2 | 7位数字 |
8 | 2 | 8位数字 |
9 | 3 | 9位数字 |
10 | 4 | 10位数字 |
11 | 1 | 1-2位不定长数字 |
12 | 1 | 1-3位不定长数字 |
13 | 1 | 1-4位不定长数字 |
14 | 1 | 1-5位不定长数字 |
15 | 1 | 1-6位不定长数字 |
16 | 2 | 1-7位不定长数字 |
17 | 2 | 1-8位不定长数字 |
18 | 3 | 1-9位不定长数字 |
19 | 4 | 1-10位不定长数字 |
20 | 1 | 1位英文字符 |
21 | 1 | 2位英文字符 |
22 | 1 | 3位英文字符 |
23 | 1 | 4位英文字符 |
24 | 1 | 5位英文字符 |
25 | 1 | 6位英文字符 |
26 | 2 | 7位英文字符 |
27 | 2 | 8位英文字符 |
28 | 2 | 9位英文字符 |
29 | 2 | 10位英文字符 |
121 | 2 | 11位英文字符 |
122 | 2 | 12位英文字符 |
123 | 2 | 13位英文字符 |
124 | 2 | 14位英文字符 |
125 | 2 | 15位英文字符 |
126 | 2 | 16位英文字符 |
127 | 2 | 17位英文字符 |
128 | 2 | 18位英文字符 |
129 | 2 | 19位英文字符 |
130 | 2 | 20位英文字符 |
30 | 1 | 1-2位不定长字符 |
31 | 1 | 1-3位不定长字符 |
32 | 1 | 1-4位不定长字符 |
33 | 1 | 1-5位不定长字符 |
34 | 1 | 1-6位不定长字符 |
35 | 2 | 1-7位不定长字符 |
36 | 2 | 1-8位不定长字符 |
37 | 3 | 1-9位不定长字符 |
38 | 4 | 1-10位不定长字符 |
111 | 4 | 1-11位不定长字符 |
112 | 4 | 1-12位不定长字符 |
113 | 5 | 1-13位不定长字符 |
114 | 5 | 1-14位不定长字符 |
115 | 5 | 1-15位不定长字符 |
116 | 5 | 1-16位不定长字符 |
117 | 5 | 1-17位不定长字符 |
118 | 6 | 1-18位不定长字符 |
119 | 6 | 1-19位不定长字符 |
120 | 6 | 1-20位不定长字符 |
281 | 6 | 1-20位不定长字符,输入斜体部分单词 |
30 | 1 | 1-2位不定长字符 |
31 | 1 | 1-3位不定长字符 |
32 | 1 | 1-4位不定长字符 |
33 | 1 | 1-5位不定长字符 |
34 | 1 | 1-6位不定长字符 |
35 | 2 | 1-7位不定长字符 |
36 | 2 | 1-8位不定长字符 |
37 | 3 | 1-9位不定长字符 |
38 | 4 | 1-10位不定长字符 |
111 | 4 | 1-11位不定长字符 |
112 | 4 | 1-12位不定长字符 |
113 | 5 | 1-13位不定长字符 |
114 | 5 | 1-14位不定长字符 |
115 | 5 | 1-15位不定长字符 |
116 | 5 | 1-16位不定长字符 |
117 | 6 | 1-17位不定长字符 |
118 | 6 | 1-18位不定长字符 |
119 | 6 | 1-19位不定长字符 |
120 | 6 | 1-20位不定长字符 |
281 | 6 | 1-20位不定长字符,输入斜体部分单词 |
50 | 1 | 1-2位不定长数字英文混合 |
51 | 1 | 1-3位不定长数字英文混合 |
52 | 1 | 1-4位不定长数字英文混合 |
53 | 1 | 1-5位不定长数字英文混合 |
54 | 1 | 1-6位不定长数字英文混合 |
55 | 2 | 1-7位不定长数字英文混合 |
56 | 2 | 1-8位不定长数字英文混合 |
57 | 3 | 1-9位不定长数字英文混合 |
58 | 4 | 1-10位不定长数字英文混合 |
131 | 5 | 1-11位不定长数字英文混合 |
132 | 5 | 1-12位不定长数字英文混合 |
133 | 5 | 1-13位不定长数字英文混合 |
134 | 5 | 1-14位不定长数字英文混合 |
135 | 5 | 1-15位不定长数字英文混合 |
136 | 6 | 1-16位不定长数字英文混合 |
137 | 6 | 1-17位不定长数字英文混合 |
138 | 6 | 1-18位不定长数字英文混合 |
139 | 6 | 1-19位不定长数字英文混合 |
59 | 1 | 1位中文 |
60 | 2 | 2位中文 |
61 | 3 | 3位中文 |
62 | 4 | 4位中文 |
63 | 5 | 5位中文 |
64 | 5 | 6位中文 |
65 | 6 | 7位中文 |
66 | 7 | 8位中文 |
67 | 8 | 9位中文 |
68 | 9 | 10位中文 |
259 | 10 | 11位中文 |
260 | 11 | 12位中文 |
261 | 12 | 13位中文 |
262 | 13 | 14位中文 |
263 | 14 | 15位中文 |
264 | 15 | 16位中文 |
265 | 16 | 17位中文 |
266 | 17 | 18位中文 |
267 | 18 | 19位中文 |
268 | 19 | 20位中文 |
69 | 2 | 1-2位不定长中文 |
70 | 3 | 1-3位不定长中文 |
71 | 4 | 1-4位不定长中文 |
72 | 5 | 1-5位不定长中文 |
73 | 5 | 1-6位不定长中文 |
74 | 6 | 1-7位不定长中文 |
75 | 7 | 1-8位不定长中文 |
76 | 8 | 1-9位不定长中文 |
77 | 9 | 1-10位不定长中文 |
269 | 10 | 1-11位不定长中文 |
270 | 11 | 1-12位不定长中文 |
271 | 12 | 1-13位不定长中文 |
272 | 13 | 1-14位不定长中文 |
273 | 14 | 1-15位不定长中文 |
274 | 15 | 1-16位不定长中文 |
275 | 16 | 1-17位不定长中文 |
276 | 17 | 1-18位不定长中文 |
277 | 18 | 1-19位不定长中文 |
278 | 19 | 1-20位不定长中文 |
79 | 1 | 2位纯英文或纯数字 |
80 | 1 | 3位纯英文或纯数字 |
81 | 1 | 4位纯英文或纯数字 |
82 | 1 | 5位纯英文或纯数字 |
83 | 2 | 6位纯英文或纯数字 |
84 | 2 | 7位纯英文或纯数字 |
85 | 2 | 8位纯英文或纯数字 |
86 | 3 | 9位纯英文或纯数字 |
87 | 4 | 10位纯英文或纯数字 |
88 | 1 | 1-2位不定长纯英文或纯数字 |
89 | 1 | 1-3位不定长纯英文或纯数字 |
90 | 1 | 1-4位不定长纯英文或纯数字 |
91 | 1 | 1-5位不定长纯英文或纯数字 |
92 | 1 | 1-6位不定长纯英文或纯数字 |
93 | 2 | 1-7位不定长纯英文或纯数字 |
94 | 2 | 1-8位不定长纯英文或纯数字 |
95 | 3 | 1-9位不定长纯英文或纯数字 |
96 | 4 | 1-10位不定长纯英文或纯数字 |
140 | 6 | 1-20位不定长字符 |
爬虫进阶开发——之自动JS渲染
当需要爬取js动态生成的数据时,该怎么处理呢?神箭手为你提供了一个核武器,只需设置一个参数就可让爬虫爬取这类动态网页。
开启自动JS渲染后,爬取速度会变慢,请耐心等待。
举个栗子:
在configs对象中添加成员enableJS
并设置为true
,就开启了自动JS渲染,然后就可以像爬取普通网页一样爬取动态网页了
var configs = {
// configs的其他成员
...
enableJS: true, // 开启JS渲染
...
};
爬虫进阶开发——之技巧篇
本节的八篇文章是开发爬虫模板时需要了解的技巧。包括,在爬取网站过程中经常遇到的问题,回调函数和内置函数的使用技巧等。
如何实现模拟登陆?
通过模拟登陆,可以解决登陆后才能爬取某些网站数据的问题。接下来给你介绍模拟登陆的实现。
举个栗子:
在beforeCrawl回调函数(在configs详解——之回调函数中会详细描述)中,调用site.requestURl
函数发送带参数的POST
请求,就可以实现模拟登陆:
configs.beforeCrawl = function(site) {
// 登陆页url
var loginUrl = "http://www.waduanzi.com/login?url=http%3A%2F%2Fwww.waduanzi.com%2F";
// 提交的参数
var urlParam = {"LoginForm[returnUrl]":"http%3A%2F%2Fwww.waduanzi.com%2F", "LoginForm[username]":"用户名", "LoginForm[password]":"密码", "yt0":"登录"};
// 发送登陆请求
site.requestUrl(loginUrl, "post", urlParam);
}
figs的其他成员
...
scanUrls: ["http://wallstreetcn.com/news?status=published&type=news&order=-created_at&limit=30&page=1"],
helperUrlRegexes: ["http://wallstreetcn\\.com/news(\\?[^\\s]+&page=\\d+)?"],
...
};
var currentPage = 1;
configs.onProcessHelperPage = function(page, content, site) {
// 得到当前列表页的页码
currentPage = parseInt(page.url.substring(page.url.indexOf("&page=") + 6));
// 定义下一个列表页的页码
var pageNum = currentPage + 1;
// 通过对当前列表页url得到下一个列表页url
var nextUrl = page.url.replace("&page=" + currentPage, "&page=" + pageNum);
// 将下一个列表页url插入待爬队列
site.addUrl(nextUrl);
return false;
}
如何爬取AJAX动态生成的数据?
比如,百度百家文章的列表页是通过AJAX动态生成的,再比如,兴趣部落的评论也是通过AJAX动态生成的,对于这两种情况,我们都提供了相应接口来处理。
以爬取百度百家的文章为栗:
因为百度百家文章的列表页是通过AJAX动态生成的, 所以解决方案是在onProcessHelperPage回调函数中,根据当前的列表页生成新的列表页url并添加到待爬队列中,具体实现如下:
configs.onProcessHelperPage = function(page, content, site) {
var i = page.url.indexOf("page=");
if (i < 0) return false;
// 得到当前列表页页码
var currentPage = parseInt(page.url.substring(i + 5));
var pageNum = currentPage + 1;
// 通过当前列表页页码,生成新的列表页url
var nextUrl = page.url.replace("page=" + currentPage, "page=" + pageNum);
// 将生成的列表页url添加到待爬队列中
site.addUrl(nextUrl);
return false;
}
以爬取兴趣部落的问答为栗:
这种情况的解决方案是使用attachedUrl
发送请求:
{
name: "question_answer_bid",
selector: "//a[contains(@class,'bid')]"
},
{
name: "question_answer_pid",
selector: "//a[contains(@class,'pid')]"
},
{
name: "question_answer",
sourceType: SourceType.AttachedUrl,
// "question_answer"是从发送"attachedUrl"这个异步请求返回的数据中抽取的
// "attachedUrl"支持引用上下文中的抓取到的"field", 这里就引用了上面抓取的"question_answer_bid"和"question_answer_pid"
attachedUrl: "http://buluo.qq.com/cgi-bin/bar/post/get_comment_by_page_v2?bid={$.question_answer_bid}&pid={$.question_answer_pid}&num=20&start=0&barlevel=1&r=" + Math.random() + "&bkn=",
selectorType: SelectorType.JsonPath,
selector: "$.result.comments",
repeated: true,
children: [
...
]
}
通过Post或Get请求获取网页数据
一般的网站是通过Get请求来得到网页数据,但有些网站比较特殊,需要使用Post请求或在Headers中添加参数才可得到网页数据,这类情况需要调用
addUrlPost
、addHeader
等函数来处理。
举个HTTP POST
请求的栗子:
configs.onProcessHelperPage = function(page, content, site) {
// 获取下一个列表页url并插入待爬队列
var currentPage = parseInt(page.url.substring(page.url.indexOf("page=") + 5, page.url.indexOf("&size=")));
var pageNum = currentPage + 1;
var nextUrl = page.url.replace("page=" + currentPage, "page=" + pageNum);
var param = new Object();
param.page = page + "";
param.size = "18";
site.addUrl(nextUrl, "Post", param);
return false;
}
举个HTTP GET
请求的栗子:
在Get请求的Headers
中添加Referer
属性
configs.beforeCrawl = function(site) {
// Referer是Headers的属性,"http://buluo.qq.com/p/index.html"是Referer对应的值
site.addHeader("Referer", "http://buluo.qq.com/p/index.html");
}
如何爬取列表页中的数据?
一般情况下,我们只需爬取内容页的数据即可,不过有时候列表网页中也会有需要爬取的数据,那想要爬取这部分数据,就要用到
addUrl
函数。
举个栗子:
在爬取爱游网的时候,除了基本的内容页信息外,还需要爬取浏览次数(或阅读量),但是这些数据是在列表页中,这就需要在onProcessHelperPage
回调函数中做处理
var configs = {
// configs的其他成员
...
fields: [
{
name: "question_view_count",
// 从内容页的数据中提取浏览次数(或阅读量)
sourceType: SourceType.UrlContext,
selector: "//a[contains(@class,'shenjianshou')]"
}
]
};
configs.onProcessHelperPage = function(page, content, site) {
// 从列表页中找到浏览次数(或阅读量)
var pageView = '<div><a class="shenjianshou">5000</a></div>';
// 将找到的浏览次数(或阅读量)附加到内容页的数据中
site.addUrl(contentUrl, "get", null, pageView);
...
return false;
}
如何去掉网页中的广告?
当成功爬取到的网页数据中有很多不相干的html广告标签时,你是否会感到无可奈何,有时候即使将XPath的效果发挥到极致,也无法去掉顽固的html广告标签,咋整呢?
本节给你介绍通过exclude
和extract
内置函数去除html广告标签的方法,可提取有用数据或清理无用数据。
举个栗子:
在爬取某论坛问答帖时,发现有很多html广告标签以及一些无用数据,就需要在afterExtractField
回调函数中调用exclude
内置函数了
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "question_detail") {
// 将data中符合XPath:"//div[contains(@class,'a_pr')]"的数据去掉
data = exclude(data, "//div[contains(@class,'a_pr')]");
return data;
}
return data;
}
有时,如果无用数据太多,最好调用extract
内置函数直接将有用的数据提取出来,这么做会比调用exclude
内置函数方便。
如果内容页有分页,该如何爬取到完整数据?
如果要爬取的某个内容页中有多个分页,该如何爬取这个内容页的完整数据呢?这里就无法使用
onProcessHelperPage
回调函数了,而需要使用field
中的attachedUrl
来请求其他分页的数据。
举个栗子:
爬取某网站文章时,发现有些文章有多个内容页面,处理过程如下:
var configs = {
// configs的其他成员
...
fields: [
{
name: "contents",
selector: "//ul[contains(@class,'pagination')]//@href",
repeated: true,
children: [
{
// 抽取出其他分页的url待用
name: "content_page_url",
selector: "//text()"
},
{
// 抽取其他分页的内容
name: "page_content",
sourceType: SourceType.AttachedUrl,
// 发送"attachedUrl"请求获取其他的分页数据
// "attachedUrl"使用了上面抓取的"content_page_url"
attachedUrl: "{content_page_url}",
selector: "//*[@id='article_page']"
}
]
}
]
};
在爬取到所有的分页数据之后,可以在afterExtractPage
回调函数中将这些数据组合成完整的数据
configs.afterExtractPage = function(page, data) {
var contents = data.contents;
if (contents != null) {
data.article_content = "";
for (var i = 0, n = contents.length; i < n; i++) {
data.article_content += contents[i].page_content;
}
}
data.article_content = data.content_cover_img + data.article_content;
return data;
}
文章和问答demo
本节给出两个完整的抓取爬虫, 一个是文章类型, 一个是问答类型
如果抓取到的数据想要以文章或者问答的形式发布到网站(WeCenter, WordPress, Discuz!等),
field
的命名请参考这两个demo的命名, 否则无法发布成功
文章采集爬虫demo
如果爬取的内容想要以文章形式通过我们的发布器发布到您的网站, 那么
field
的name
请与下面demo的命名一致
爬取华尔街见闻的全球资讯, 爬虫如下:
var configs = {
domains: ["wallstreetcn.com"],
scanUrls: ["http://wallstreetcn.com/news"],
contentUrlRegex: "http://wallstreetcn\\.com/node/\\d+",
helperUrlRegexes: ["http://wallstreetcn\\.com/news(\\?[^\\s]+&page=\\d+)?"],
fields: [
{
// 文章标题
name: "article_title",
selector: "//*[@id='main']/article/h1",
required: true
},
{
// 文章内容
name: "article_content",
selector: "//*[@id='main']/article/div[contains(@class,'article-content')]",
required: true
},
{
// 文章作者
name: "article_author",
selector: "//*[@id='main']/article/div[contains(@class,'meta')]//a",
},
{
// 文章发布时间
name: "article_publish_time",
selector: "//*[@id='main']/article/div[contains(@class,'meta')]/span[contains(@class,'time')]",
}
]
};
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "article_publish_time") {
var timestamp = Date.parse(data.replace("年","-").replace("月","-").replace("日",""));
return isNaN(timestamp) ? "0" : timestamp/1000 + "";
}
return data;
};
var crawler = new Crawler(configs);
crawler.start();
r: "//*[@id='bbs_show_top']//h3",
required: true
},
{
// 问题作者
name: "question_author",
selector: "//*[@id='f1']/a[2]"
},
{
// 问题作者头像
name: "question_author_avator",
selector: "//*[@id='f1']/a[1]/img"
},
{
// 问题详情
name: "question_detail",
selector: "//*[@id='mainPost']//div[contains(@class,'bbs_show_main')]/div[contains(@class,'bbs_show_content')]"
},
{
// 问题提问时间
name: "question_publish_time",
selector: "//*[@id='mainPost']//small",
required: true
},
{
// 问题答案
name: "question_answer",
selector: "//*[@id='uc_wrap']/div[5]//div[contains(@class,'bbs_show_item')]",
repeated: true,
children: [
{
// 问题答案内容
name: "question_answer_content",
selector: "//div[contains(@class,'bbs_show_main')]/div[contains(@class,'bbs_show_content')]",
required: true
},
{
// 问题答案作者
name: "question_answer_author",
selector: "//div[contains(@class,'bbs_user_head')]//a[2]"
},
{
// 问题答案作者头像
name: "question_answer_author_avatar",
selector: "//div[contains(@class,'bbs_user_head')]/a[1]/img"
},
{
// 问题答案回答时间
name: "question_answer_publish_time",
selector: "//small"
}
]
}
]
};
configs.afterExtractField = function(fieldName, data, page) {
if (fieldName == "question_publish_time" || fieldName == "question_answer.question_answer_publish_time") {
data = data.substring(5);
var timestamp = Date.parse(data);
return isNaN(timestamp) ? "0" : timestamp/1000 + "";
}
else if (fieldName == "question_detail" || fieldName == "question_answer.question_answer_content") {
data = exclude(data, "//div[contains(@class,'image_note')]");
return data;
}
return data;
}
var crawler = new Crawler(configs);
crawler.start();
开发神箭手爬虫的常用工具
“工欲善其事,必先利其器”,开发神箭手爬虫,起码得有几件顺手的工具才行吧,接下来给你逐个介绍。
谷歌Chrome浏览器
说起谷歌的Chrome浏览器(以下简称Chrome),相信大家都耳熟能详了吧,不仅使用流畅,而且功能强大,对开发神箭手爬虫非常有帮助。
我们主要使用的是Chrome的开发者工具,如下图所示:
或者可以直接在网页上点击鼠标右键,选择“检查”,也可打开开发者工具。
开发者工具顶部有Elements
、Console
、Network
等八个栏目。常用的有三个:Elements
,用来查看需爬取字段的HTML标签信息;Console
,可以检测你的JS代码;Network
,用来分析HTTP请求。
XPath Helper
XPath Helper是Chrome浏览器的插件,可以在Chrome应用商店安装下载,主要用来分析当前网页信息的XPath,并将其精简化。具体操作步骤如下:
1、在Chrome浏览器上,选择抽取的html字段并右击,点击“检查”,即可弹出开发者工具;右击已选字段,点击Copy XPath
即可将该字段的XPath
保存到浏览器剪贴板上,如下图所示:
2、打开XPath Helper
插件,将得到的XPath
复制进去,最好进行简化修改后再使用,如下图所示:
3、在XPath中,如果使用class
属性来定位元素,最好使用contains
函数,因为元素可能含有多个class
:
{
name: "article_publish_time",
selector: "//span[contains(@class,'date')]/span"
},
DHC REST
DHC REST也是Chrome浏览器的插件,可以在Chrome应用商店安装下载,主要用来模拟HTTP客户端发送测试数据到服务器。HTTP Get
请求在开发中比较常用。
正则表达式测试工具
推荐使用站长工具中的正则表达式测试工具,链接如下: http://tool.chinaz.com/regex/
常见问题
Q:为什么要在configs
成员中添加domains
?
A:domains
定义爬虫爬取指定域名下的网页,非域名下的url会被忽略,可提高爬取速度。
值得注意,如果scanUrls
,contentUrls
和helperUrls
不在domains
包含的域名范围内,那么爬虫将无法爬取到数据。
Q:为什么contentUrlRegexes
和helperUrlRegexes
正则表达式中的转义字符要加双反斜杠\\
?
A:因为contentUrlRegexes
和helperUrlRegexes
的每个正则表达式都是字符串类型,如下所示:
contentUrlRegexes: ["http://wallstreetcn\\.com/node/\\d+"],
helperUrlRegexes: ["http://wallstreetcn\\.com/news(\\?/page=\\d+)?"],
在JS语法中规定若正则表达式以字符串形式表示,其中的转义字符需加双反斜杠。
Q:为什么我的爬虫爬取到的图片都是裂图或网站默认图?
A:首先,在神箭手后台检查你的任务配置中是否勾选了“自动缓存图片”,我们建议勾选上,因为这样做,无论你要爬取的网站图片是否加了“防盗链”,我们的系统都会帮你把图片缓存到神箭手云服务器上;
然后,判断需要爬取的图片url的后缀名是不是jpg
,jpeg
,png
或gif
。如果不是,请加神箭手开发者QQ群咨询,群号:342953471;如果是,请耐心等待几分钟,因为我们的图片缓存服务器还在努力下载图片中。
Q:为什么我一定要将爬取到的文章发布时间转换成Unix时间戳?
A:当发布文章或问答到网站时,需要通过我们提供的发布插件进行发布,而我们的发布插件对发布时间有特殊要求,需要Unix时间戳。
值得注意,如果爬取的数据不需要发布到网站,则不用对时间做处理。
Q:我想按网站分类爬取文章,为什么爬到的数据中总是包含其他分类的文章?
A:爬取到的文章不够精准,说明你的contentUrlRegexes
或helperUrlRegexes
中的正则表达式有问题,匹配了其他分类的contentUrl
(内容页url)或helperUrl
(列表页url),请仔细检查并进行修改。
Q:为什么我爬取的网页数据中有很多无用html标签,怎么处理?
A:爬取的数据中包含无用html标签,原因可能是,你写的XPath不够精确,抽取的内容中包含了很多无用html标签。如果无法通过XPath来解决,请考如何去掉网页中的广告?。
Q:在神箭手上写的爬虫容易调试吗?
A:很容易,我们向开发者提供了用于打印日志信息的console
对象,可以调用console.log(message)
,console.debug(message)
等方法来分级打印你需要的日志。详情见configs详解——之site, page和console。
Q:如何爬一个有反爬虫的网站?
A:在configs
对象中添加enableProxy: true
,就能使用我们平台提供的IP代理服务了。
Q:为什么我写的爬虫可以爬取到数据却提示发布失败?
A:遇到这种情况,最好先检查一下你写的爬虫代码,确保每个field
对象的name
与两个完整demo中的命名一致。如果不一致,请参照demo将命名改掉;如果一致,可能是发布的问题,请直接联系客服,客服QQ:2879835984。
Q:神箭手云爬虫是不是可以爬所有网站?
A:理论上是可以的,为了兼容更多用户的爬取需求,我们的平台也在不断完善中。
一个js爬虫的更多相关文章
- 【重学Node.js 第4篇】实现一个简易爬虫&启动定时任务
实现一个简易爬虫&启动定时任务 课程介绍看这里:https://www.cnblogs.com/zhangran/p/11963616.html 项目github地址:https://gith ...
- webmagic的设计机制及原理-如何开发一个Java爬虫
之前就有网友在博客里留言,觉得webmagic的实现比较有意思,想要借此研究一下爬虫.最近终于集中精力,花了三天时间,终于写完了这篇文章.之前垂直爬虫写了一年多,webmagic框架写了一个多月,这方 ...
- Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs)
Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs) Cola:一个分布式爬虫框架 发布时间:2013-06-17 14:58:27, 关注:+2034, 赞美: ...
- Node.js爬虫-爬取慕课网课程信息
第一次学习Node.js爬虫,所以这时一个简单的爬虫,Node.js的好处就是可以并发的执行 这个爬虫主要就是获取慕课网的课程信息,并把获得的信息存储到一个文件中,其中要用到cheerio库,它可以让 ...
- node.js爬虫
这是一个简单的node.js爬虫项目,麻雀虽小五脏俱全. 本项目主要包含一下技术: 发送http抓取页面(http).分析页面(cheerio).中文乱码处理(bufferhelper).异步并发流程 ...
- webmagic的设计机制及原理-如何开发一个Java爬虫 转
此文章是webmagic 0.1.0版的设计手册,后续版本的入门及用户手册请看这里:https://github.com/code4craft/webmagic/blob/master/user-ma ...
- Node JS爬虫:爬取瀑布流网页高清图
原文链接:Node JS爬虫:爬取瀑布流网页高清图 静态为主的网页往往用get方法就能获取页面所有内容.动态网页即异步请求数据的网页则需要用浏览器加载完成后再进行抓取.本文介绍了如何连续爬取瀑布流网页 ...
- [Java]使用HttpClient实现一个简单爬虫,抓取煎蛋妹子图
第一篇文章,就从一个简单爬虫开始吧. 这只虫子的功能很简单,抓取到”煎蛋网xxoo”网页(http://jandan.net/ooxx/page-1537),解析出其中的妹子图,保存至本地. 先放结果 ...
- 自己封装的一个JS分享组件
因为工作的需求之前也封装过一个JS分享插件,集成了我们公司常用的几个分享平台. 但是总感觉之前的结构上很不理想,样式,行为揉成一起,心里想的做的完美,实际上总是很多的偏差,所以这次我对其进行了改版. ...
随机推荐
- php 支持递归函数.递归函数就是调用函数本身.
例子 将一个字符进行颠倒 function reverse_r($str){ if(strlen($str)){ reverse_r(substr($str,1));// } echo substr( ...
- python时间函数
import time print time.time() print time.localtime() fm = '%Y-%m-%d %H:%M:%S' YMD = time.strftime(fm ...
- 【ecos学习3】redboot on vmware 网络配置
需要图形化编译的时候必须 Build->Packages 加入:Common ethernet support 才可以有网络功能 导入vmare需要修改网络连接模式: 1- 设置redbootI ...
- Raknet实现的简单服务器与客户端的交互
1. 首先下载Raknet的源代码,我用的是4.0的,不是最新的,解压后编译DLL工程,编译完成后进入解压的根目录下,进入Lib文件夹下找到RakNet_DLL_Debug_Win32.dll, R ...
- Razor学习(二)@Html标签
原文链接:http://blog.csdn.net/pasic/article/details/7093802 只是因为原文作者说的东西,还有很多作为基础知识的东西,我都没有掌握,所以总结在这里,蓝字 ...
- linux命令之mv
linux下的mv即move的意思 该命令的一般形式: mv [选项] 参数1 参数2 选项: -b 如果已存在相同文件名,则覆盖前进行备份 -f ...
- react-native 自己搭建热更新服务器
使用到的框架是 react-native-update react-native-update-cli 这个应该执行热更新的时候的终端命令. 通过这个,自己搭建一个热更新的服务器.
- 两年前实习时的文档——Platform学习总结
1 概述 驱动程序实际上是硬件与应用程序之间的中间层.在Linux操作系统中,设备驱动程序对各种不同的设备提供了一致的訪问接口,把设备映射成一个特殊的设备文件,用户程序能够像其它文件一样对设备文件进 ...
- ISG2015
一天的成果. Re300 是男人就下一百层 一个64位的程序,放到IDA里的话,IDA就会分析不动,这样就把人给下着了.objdump –d re300 > output,这样拿到汇编代码,大概 ...
- Reverse Key Indexes反向索引
Reverse Key Indexes反向索引A reverse key index is a type of B-tree index that physically reverses the by ...