第一个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"
    }
]

上面的例子从网页中抽取内容和作者, 抽取规则是针对糗事百科的内容页写的

fieldconfigs详解——之field中作详细介绍。

enableProxy

是否使用代理
如果爬取的网站根据IP做了反爬虫, 可以设置此项为true

布尔类型 可选设置

举个栗子:

enableProxy: true,

configs详解——之field

field定义一个抽取项, 一个field可以定义下面这些东西

name

给此项数据起个变量名
变量名中不能包含.
如果抓取到的数据想要以文章或者问答的形式发布到网站(WeCenterWordPressDiscuz!等),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.XPathSelectorType.JsonPathSelectorType.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详解——之sitepageconsole

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.htmlReferer的值

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的键-值对组成的字符串,用;分隔。BAIDUIDBIDUPSID是cookie的key,FEE96299191CB0F11954F3A0060FB470:FG=1FEE96299191CB0F11954F3A0060FB470是cookie的值,键-值对用=相连

configs.beforeCrawl = function(site) {
    site.addCookies("BAIDUID=FEE96299191CB0F11954F3A0060FB470:FG=1; BIDUPSID=FEE96299191CB0F11954F3A0060FB470;");
}

site.addUrl(url, method, data, contextData)

一般在onProcessScanPageonProcessHelperPage回调函数(在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)

一般在beforeCrawlafterDownloadPageonProcessScanPageonProcessHelperPage回调函数(在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_contentfield

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 当前fieldname. 注意: 子fieldname会带着父fieldname, 通过.连接.
@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 当前fieldname. 注意: 子fieldname会带着父fieldname, 通过.连接.
@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 当前fieldname. 注意: 子fieldname会带着父fieldname, 通过.连接.
@param data 当前field抽取到的数据. 如果该fieldrepeateddata为数组类型, 否则是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 返回处理后的数据, 注意数据类型不能变

举个栗子:
比如从某网页中得到timetitle两个field抽取项, 现在希望把time的值添加中括号后拼凑到title中,处理过程如下:

configs.afterExtractPage = function(page, data) {
    var title = "[" + data.time + "] " + data.title;
    data.title = title;
    return data;
}

爬虫进阶开发——之内置函数

本节介绍常用的内置js函数

extract(data, xpath)

使用xpathdata中抽取单条数据

@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)

使用xpathdata中抽取多条数据

@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>

encodeURIencodeURIComponent

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标签,这种方式常常会导致采集到的图片无法正常显示或显示防盗链图。这种情况,选择图片云托管是最明智的。

那如何使用图片云托管呢?

最简单的方法,在神箭手后台配置爬虫任务的时候,勾选“自动缓存图片”,在任务运行时,就会帮你将采集到的所有图片托管到神箭手云服务器上。但这种方式只能帮你托管后缀名是jpgjpegpnggif的图片。

这样做当然无法满足开发者的需求,为此,我们提供了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代理
    ...
};

爬虫进阶开发——之验证码识别(内测版)

当需要爬取的网页需要需要输入验证码才能访问的时候,该怎么办呢?这个问题,使用神箭手提供的验证码识别服务就能解决。

验证码识别是付费功能,在使用前请先确保你的账户有足够的余额,否则会影响使用。

验证码识别的调用函数请参考内置函数getCaptchagetCaptchaFromBase64

举个栗子:
当输入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中添加参数才可得到网页数据,这类情况需要调用addUrlPostaddHeader等函数来处理。

举个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广告标签,咋整呢?

本节给你介绍通过excludeextract内置函数去除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

本节给出两个完整的抓取爬虫, 一个是文章类型, 一个是问答类型

如果抓取到的数据想要以文章或者问答的形式发布到网站(WeCenterWordPressDiscuz!等),field的命名请参考这两个demo的命名, 否则无法发布成功

文章采集爬虫demo

如果爬取的内容想要以文章形式通过我们的发布器发布到您的网站, 那么fieldname请与下面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的开发者工具,如下图所示:

或者可以直接在网页上点击鼠标右键,选择“检查”,也可打开开发者工具。

开发者工具顶部有ElementsConsoleNetwork等八个栏目。常用的有三个: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会被忽略,可提高爬取速度。

值得注意,如果scanUrlscontentUrlshelperUrls不在domains包含的域名范围内,那么爬虫将无法爬取到数据。

Q:为什么contentUrlRegexeshelperUrlRegexes正则表达式中的转义字符要加双反斜杠\\

A:因为contentUrlRegexeshelperUrlRegexes的每个正则表达式都是字符串类型,如下所示:

contentUrlRegexes: ["http://wallstreetcn\\.com/node/\\d+"],
helperUrlRegexes: ["http://wallstreetcn\\.com/news(\\?/page=\\d+)?"],

在JS语法中规定若正则表达式以字符串形式表示,其中的转义字符需加双反斜杠。

Q:为什么我的爬虫爬取到的图片都是裂图或网站默认图?

A:首先,在神箭手后台检查你的任务配置中是否勾选了“自动缓存图片”,我们建议勾选上,因为这样做,无论你要爬取的网站图片是否加了“防盗链”,我们的系统都会帮你把图片缓存到神箭手云服务器上;

然后,判断需要爬取的图片url的后缀名是不是jpgjpegpnggif。如果不是,请加神箭手开发者QQ群咨询,群号:342953471;如果是,请耐心等待几分钟,因为我们的图片缓存服务器还在努力下载图片中。

Q:为什么我一定要将爬取到的文章发布时间转换成Unix时间戳?

A:当发布文章或问答到网站时,需要通过我们提供的发布插件进行发布,而我们的发布插件对发布时间有特殊要求,需要Unix时间戳。

值得注意,如果爬取的数据不需要发布到网站,则不用对时间做处理。

Q:我想按网站分类爬取文章,为什么爬到的数据中总是包含其他分类的文章?

A:爬取到的文章不够精准,说明你的contentUrlRegexeshelperUrlRegexes中的正则表达式有问题,匹配了其他分类的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爬虫的更多相关文章

  1. 【重学Node.js 第4篇】实现一个简易爬虫&启动定时任务

    实现一个简易爬虫&启动定时任务 课程介绍看这里:https://www.cnblogs.com/zhangran/p/11963616.html 项目github地址:https://gith ...

  2. webmagic的设计机制及原理-如何开发一个Java爬虫

    之前就有网友在博客里留言,觉得webmagic的实现比较有意思,想要借此研究一下爬虫.最近终于集中精力,花了三天时间,终于写完了这篇文章.之前垂直爬虫写了一年多,webmagic框架写了一个多月,这方 ...

  3. Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs)

    Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs) Cola:一个分布式爬虫框架 发布时间:2013-06-17 14:58:27, 关注:+2034, 赞美: ...

  4. Node.js爬虫-爬取慕课网课程信息

    第一次学习Node.js爬虫,所以这时一个简单的爬虫,Node.js的好处就是可以并发的执行 这个爬虫主要就是获取慕课网的课程信息,并把获得的信息存储到一个文件中,其中要用到cheerio库,它可以让 ...

  5. node.js爬虫

    这是一个简单的node.js爬虫项目,麻雀虽小五脏俱全. 本项目主要包含一下技术: 发送http抓取页面(http).分析页面(cheerio).中文乱码处理(bufferhelper).异步并发流程 ...

  6. webmagic的设计机制及原理-如何开发一个Java爬虫 转

    此文章是webmagic 0.1.0版的设计手册,后续版本的入门及用户手册请看这里:https://github.com/code4craft/webmagic/blob/master/user-ma ...

  7. Node JS爬虫:爬取瀑布流网页高清图

    原文链接:Node JS爬虫:爬取瀑布流网页高清图 静态为主的网页往往用get方法就能获取页面所有内容.动态网页即异步请求数据的网页则需要用浏览器加载完成后再进行抓取.本文介绍了如何连续爬取瀑布流网页 ...

  8. [Java]使用HttpClient实现一个简单爬虫,抓取煎蛋妹子图

    第一篇文章,就从一个简单爬虫开始吧. 这只虫子的功能很简单,抓取到”煎蛋网xxoo”网页(http://jandan.net/ooxx/page-1537),解析出其中的妹子图,保存至本地. 先放结果 ...

  9. 自己封装的一个JS分享组件

    因为工作的需求之前也封装过一个JS分享插件,集成了我们公司常用的几个分享平台. 但是总感觉之前的结构上很不理想,样式,行为揉成一起,心里想的做的完美,实际上总是很多的偏差,所以这次我对其进行了改版. ...

随机推荐

  1. php 支持递归函数.递归函数就是调用函数本身.

    例子 将一个字符进行颠倒 function reverse_r($str){ if(strlen($str)){ reverse_r(substr($str,1));// } echo substr( ...

  2. python时间函数

    import time print time.time() print time.localtime() fm = '%Y-%m-%d %H:%M:%S' YMD = time.strftime(fm ...

  3. 【ecos学习3】redboot on vmware 网络配置

    需要图形化编译的时候必须 Build->Packages 加入:Common ethernet support 才可以有网络功能 导入vmare需要修改网络连接模式: 1- 设置redbootI ...

  4. Raknet实现的简单服务器与客户端的交互

    1. 首先下载Raknet的源代码,我用的是4.0的,不是最新的,解压后编译DLL工程,编译完成后进入解压的根目录下,进入Lib文件夹下找到RakNet_DLL_Debug_Win32.dll,  R ...

  5. Razor学习(二)@Html标签

    原文链接:http://blog.csdn.net/pasic/article/details/7093802 只是因为原文作者说的东西,还有很多作为基础知识的东西,我都没有掌握,所以总结在这里,蓝字 ...

  6. linux命令之mv

    linux下的mv即move的意思 该命令的一般形式: mv [选项] 参数1 参数2 选项: -b                如果已存在相同文件名,则覆盖前进行备份 -f             ...

  7. react-native 自己搭建热更新服务器

    使用到的框架是 react-native-update react-native-update-cli 这个应该执行热更新的时候的终端命令. 通过这个,自己搭建一个热更新的服务器.

  8. 两年前实习时的文档——Platform学习总结

    1  概述 驱动程序实际上是硬件与应用程序之间的中间层.在Linux操作系统中,设备驱动程序对各种不同的设备提供了一致的訪问接口,把设备映射成一个特殊的设备文件,用户程序能够像其它文件一样对设备文件进 ...

  9. ISG2015

    一天的成果. Re300 是男人就下一百层 一个64位的程序,放到IDA里的话,IDA就会分析不动,这样就把人给下着了.objdump –d re300 > output,这样拿到汇编代码,大概 ...

  10. Reverse Key Indexes反向索引

    Reverse Key Indexes反向索引A reverse key index is a type of B-tree index that physically reverses the by ...