Node.js 爬虫

你可能会把 NodeJS 用作网络服务器,但你知道它还可以用来做爬虫吗? 本教程中会介绍如何爬取静态网页——还有那些烦人的动态网页——使用 NodeJS 和几个有帮助的 NPM 模块。

网络爬虫的一点知识

网络爬虫在网络编程世界中总是被鄙视——说的也很有道理。在现代编程中,API 用于大多数流行的服务,应该用它们来获取数据,而不是用爬虫。爬虫有一个固有问题,就是它依赖于被爬取页面的可视化结构。一旦 HTML 改变了——不管改变多么微小——都有可能完全破坏之前的代码。

忽略这些瑕疵,学习一点关于网络爬虫的知识会很有帮助,一些工具可以帮我们完成这个任务。当一个网站没有给 API 或任何聚合订阅(RSS/Atom等)时,获取内容只剩唯一的选项……爬虫。

注意:如果无法通过 API 或订阅获得想要的信息,这很有可能表示拥有者不希望那些信息是可访问的。但是,还有一些例外。

为什么用 NodeJS?

用所有语言都可以写爬虫,真的。我喜欢用 Node 的原因是因为它的异步特性,表示在进程中我的代码任何时候都不会被阻塞。还有一个额外的优势,就是我很熟悉 JavaScript。最后,有一些为 NodeJS 写的新模块可以帮助轻松爬取网页,用一种可靠的方式(好吧,其实就是爬虫的可靠性极限!)。开始吧。

用 YQL 实现简单爬虫

从简单的使用场景开始:静态网页。这些是标准的工场网页。对于这些,Yahoo! Query Language(YQL)可以很好的完成。对于不熟悉 YQL 的人,它就是一个类似 SQL 的语法,可以用来以一致的方式使用不同的API。

YQL 有一些很棒的表来帮助开发者获取网页的 HTML。我想强调的是:

挨个看一下,看如何用 NodeJS 实现。

html/ table

html 表是从 URL 爬取 HTML 最基本的方式。用这个表实现的常规查询如下:

select * from html where url="http://finance.yahoo.com/q?s=yhoo" and xpath='//div[@id="yfi_headlines"]/div[2]/ul/li/a'

这个查询由两个参数组成:“url” 和 “xpath”。网址大家都知道。XPath 包含一个 XPath 字符串,告诉 YQL 应该返回 HTML 的哪一部分。在这里查询一下试试。

还有一些可用的参数包括 browser (布尔型),charset(字符串)和 compat(字符串)。我没有使用这些参数,但如果你有特别需要的话可以参考文档。

XPath 感觉不舒服?

很不幸,XPath 不是一个获取 HTML 属性结构的常用方式。对于新手读和写都可能很复杂。
看看下一个表,可以完成同样的事,但使用 CSS 做替代

data.html.cssselect

data.html.cssselect 表是我推荐的爬取页面 HTML 方式。和 html 表用相同的方式工作,但可以用 CSS 替代 XPath。实际上,这个表默默把 CSS 转换为 XPath,然后调用 html 表,所以会有一点慢。对于爬取网页来说,区别可以忽略不计。

使用这个表的通常方式是:

select * from data.html.cssselect where url="www.yahoo.com" and css="#news a"

可以看到,整洁许多。我建议在尝试用 YQL 爬取网页的时候优先尝试这个方法。 在这里查询一下试试。

* htmlstring* 表

htmlstring 表在尝试从网页爬取大量格式化文本的时候用。
用这个表可以用一个单独的字符串抓取网页的全部 HTML 内容,而不是基于 DOM 结构切分的 JSON。
例如,一个爬取 <a> 标签的常规 JSON 返回:

"results": {
   "a": {
     "href": "...",
     "target": "_blank",
     "content": "Apple Chief Executive Cook To Climb on a New Stage"
    }
 }

看到 attribute 如何定义为 property 了吧?相反,htmlstring 表的返回看起来会像这样:

"results": {
  "result": {
    "<a href=\"…\" target="_blank">Apple Chief Executive Cook To Climb on a New Stage</a>
   }
}

所以,为什么要这么用呢?从我的经验来看,尝试爬取大量格式化文本的时候会相当有用。例如下面的片段:

<p>Lorem ipsum <strong>dolor sit amet</strong>, consectetur adipiscing elit.</p>
<p>Proin nec diam magna. Sed non lorem a nisi porttitor pharetra et non arcu.</p>

使用 htmlstring 表,可以把这个 HTML 获取为字符串,然后用正则移除 HTML 标签,留下的就只有文本了。这比 JSON 根据页面的 DOM 结构分为属性和子对象的迭代更容易。

在 NodeJS 里用 YQL

现在我们了解了一些 YQL 中可用的表,让我们用 YQL 和 NodeJS 实现一个网络爬虫。幸运的是,相当简单,感谢 Derek Gathright 写的 node-yql 模块。

可以用 npm 安装它:

npm install yql

这个模块极为简单,只包括一个方法:YQL.exec() 方法。定义如下:

function exec (string query [, function callback] [, object params] [, object httpOptions])

我们 require 它然后调用 YQL.exec() 就可以用了。例如,假设要抓取 Nettuts 主页所有文章的标题:

var YQL = require("yql");
 
new YQL.exec('select * from data.html.cssselect where url="http://net.tutsplus.com/" and css=".post_title a"', function(response) {
 
    //response consists of JSON that you can parse
 
});

YQL 最棒的就是能够实时测试查询然后确定会返回的 JSON。去 console 用一下试试,或者点击这里查看原生 JSON。

paramshttpOptions 对象是可选的。参数可以包括像 env(是否为表使用特定的环境) 和 format (xml 或 json)这样的属性。所有传给 params 的属性都是 URI 编码然后附到查询字符串的尾端。httpOptions 对象被传递到请求头中。例如这里你可以指定是否想启用 SSL。

叫做 yqlServer.js 的 JavaScript 文件,包含使用 YQL 爬取所需的最少代码。可以在终端里用以下命令来运行它:

node yqlServer.js

例外情况和其它知名工具

YQL 是我推荐的爬取静态网页内容的选择,因为读起来简单、用起来也简单。然而,如果网页有 robots.txt 文件来拒绝响应,YQL 就会失败。在这种情况下,可以看看下面提到的工具,或者用下一节会讲的 PhantomJS。

Node.io 是一个实用的 Node 工具,为数据爬取而特别设计。可以创建接受输入,处理并返回某些输出的作业。Node.io 在 GitHub 上关注量很高,有一些实用的例子帮你上手。

JSDOM 是一个很流行的项目,用 JavaScript 实现了 W3C DOM。当提供 HTML 时,它可以构造一个能够与之交互的 DOM。查看文档,了解如何使用 JSDOM 和任意 JS 库(如 jQuery )一起从网页抓取数据。

从页面抓取动态内容

到目前为止,我们已经看过一些工具,可以帮助我们抓取静态内容的网页。有了YQL,相当简单。不幸的是,我们经常看到一些内容是用JavaScript动态加载的页面。在这些情况下,页面最初通常为空,然后随后附加内容。如何处理这个问题呢?

例子

我提供了一个例子;我上传了一个简单的 HTML 文件到我自己的网站,document.ready() 函数被调用后两秒通过 JavaScript 附加了一些内容。可以在这里查看这个页面。源文件如下:

<!DOCTYPE html>
<html>
    <head>
        <title>Test Page with content appended after page load</title>
    </head>
 
    <body>
        Content on this page is appended to the DOM after the page is loaded.
 
        <div id="content">
 
        </div>
 
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script>
        $(document).ready(function() {
 
            setTimeout(function() {
                $('#content').append("<h2>Article 1</h2><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p><h2>Article 2</h2><p>Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.</p><h2>Article 3</h2><p>Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.</p>");
            }, 2000);
 
        });
    </script>
    </body>
</html>

现在尝试用 YQL 从 <div id=“content”> 中抓取文本。

var YQL = require("yql");
 
new YQL.exec('select * from data.html.cssselect where url="http://tilomitra.com/repository/screenscrape/ajax.html" and css="#content"', function(response) {
 
    //This will return undefined! The scraping was unsuccessful!
    console.log(response.results);
 
});

你会发现 YQL 返回了 undefined,因为页面被加载后,<div id=“content”> 是空的。内容还没有被附加上去。可以在这里自己尝试一下。

来看看如何解决这个问题!

PhantomJS

PhantomJS 可以加载网页,并模仿基于 Webkit 的浏览器,然而并没有 GUI。

从这类站点爬取信息我建议的方式是使用 PhantomJS 。PhantomJS 形容自己是“用 JavaScript API 的无用户界面 Webkit。“简单来说,表示 PhantomJS 可以加载网页然后模仿基于 Webkit 的浏览器,然而并没有GUI。作为一个开发者,可以调用 PhantomJS 提供的特定方法在页面上执行代码。由于它的行为像浏览器,网页上的脚本就像在一个普通的浏览器中运行。

为了从我们的页面获取数据,要使用 PhantomJS-Node,这是一个很小的开源项目,它将 PhantomJS 与NodeJS 桥接起来。此模块默默把 PhantomJS 作为一个子进程运行。

安装 PhantomJS
在安装 PhantomJS-Node NPM 模块之前,必须安装 PhantomJS。但安装和构建 PhantomJS 可能有点棘手。

首先,去 PhantomJS.org 并为操作系统下载相应的版本。我是Mac OSX。
下载后,将其解压到某个位置,例如/ Applications /。接下来,您要将其添加到PATH

sudo ln -s /Applications/phantomjs-1.5.0/bin/phantomjs /usr/local/bin/

1.5.0 替换为你下载的 PhantomJS 版本。请注意,并非所有系统都具有/ usr / local / bin /。一些系统将有:/ usr / bin // bin /usr / X11 / bin

对于 Windows 用户,看这里的 短篇 教程。如果你打开终端,输入 phantomjs 并且没有任何错误,就安装完成了。

如果你不想编辑 PATH,记下你解压 PhantomJS 的地方,我会在下一节中展示另一种设置方法,虽然我建议你编辑 PATH

安装 PhantomJS-Node

设置 PhantomJS-Node 就简单多了。如果已经安装了 NodeJS,可以通过 npm 来安装它:

npm install phantom

如果你在前一节安装 PhantomJS 的时候没有编辑 PATH,可以去 npm pull 下来的 phantom/ 目录,在 phantom.js 里编辑这一行。

ps = child.spawn('phantomjs', args.concat([__dirname + '/shim.js', port]));

把路径改为:

ps = child.spawn('/path/to/phantomjs-1.5.0/bin/phantomjs', args.concat([__dirname + '/shim.js', port]));

完成后,可以运行这段代码进行测试:

var phantom = require('phantom');
phantom.create(function(ph) {
  return ph.createPage(function(page) {
    return page.open("http://www.google.com", function(status) {
      console.log("opened google? ", status);
      return page.evaluate((function() {
        return document.title;
      }), function(result) {
        console.log('Page title is ' + result);
        return ph.exit();
      });
    });
  });
});

在命令行运行它应该会有如下输出:


opened google?  success
Page title is Google

如果正确得到了,就已经设置完成。如果没有,在现在评论一下我会试着帮你解决!

使用 PhantomJS-Node

为了让你更容易,我已经在下载中包含了一个名为 phantomServer.js 的 JS 文件,使用了一些 PhantomJS 的 API 来加载网页。等待 5 秒后执行 JavaScript 来爬取页面。你可以通过导航到该目录并在终端中使用以下命令来运行它:

node phantomServer.js

我将概述一下它在这里是如何工作的。首先,我们需要 PhantomJS:

var phantom = require('phantom’);

接下来,利用 API 实现一些方法。也就是说,我们创建一个实例页面,然后调用open()方法:

phantom.create(function(ph) {
  return ph.createPage(function(page) {
 
    //From here on in, we can use PhantomJS' API methods
    return page.open("http://tilomitra.com/repository/screenscrape/ajax.html",          function(status) {
 
            //The page is now open      
            console.log("opened site? ", status);
 
        });
    });
});

页面打开后,我们可以注入一些 JavaScript 到页面上。通过 page.injectJS() 方法来注入 jQuery:

phantom.create(function(ph) {
  return ph.createPage(function(page) {
    return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) {
      console.log("opened site? ", status);         
 
            page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() {
                //jQuery Loaded
                //We can use things like $("body").html() in here.
 
            });
    });
  });
});

jQuery 现在加载好了,但我们不知道页面上的动态内容是否加载完毕。为了解决这个问题,我通常会把我的爬虫代码放在一个 setTimeout() 函数中,在特定时间间隔后执行。如果你想要一个更灵活的方案,PhantomJS API 允许监听和模仿指定事件。看一下简单的例子:

setTimeout(function() {
    return page.evaluate(function() {
 
        //Get what you want from the page using jQuery. 
        //A good way is to populate an object with all the jQuery commands that you need and then return the object.
 
        var h2Arr = [], //array that holds all html for h2 elements
        pArr = []; //array that holds all html for p elements
 
        //Populate the two arrays
        $('h2').each(function() {
            h2Arr.push($(this).html());
        });
 
        $('p').each(function() {
            pArr.push($(this).html());
        });
 
        //Return this data
        return {
            h2: h2Arr,
            p: pArr
        }
    }, function(result) {
        console.log(result); //Log out the data.
        ph.exit();
    });
}, 5000);

全部放在一起后,我们的 phantomServer.js 看起来会像这样:

var phantom = require('phantom');
phantom.create(function(ph) {
  return ph.createPage(function(page) {
    return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) {
      console.log("opened site? ", status);         
 
            page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() {
                //jQuery Loaded.
                //Wait for a bit for AJAX content to load on the page. Here, we are waiting 5 seconds.
                setTimeout(function() {
                    return page.evaluate(function() {
 
                        //Get what you want from the page using jQuery. A good way is to populate an object with all the jQuery commands that you need and then return the object.
                        var h2Arr = [],
                        pArr = [];
                        $('h2').each(function() {
                            h2Arr.push($(this).html());
                        });
                        $('p').each(function() {
                            pArr.push($(this).html());
                        });
 
                        return {
                            h2: h2Arr,
                            p: pArr
                        };
                    }, function(result) {
                        console.log(result);
                        ph.exit();
                    });
                }, 5000);
 
            });
    });
    });
});

这个实现有一些粗糙、无组织性,但重点找到了。使用 PhantomJS,能够抓取具有动态内容的页面!控制台应输出以下内容:

→ node phantomServer.js
opened site?  success
{ h2: [ 'Article 1', 'Article 2', 'Article 3' ],
  p: 
   [ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
     'Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.',
     'Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.' ] }

总结

在本教程中讲了实现网络爬虫的两种不同方式。抓取静态网页可以用 YQL,很容易设置和使用。另一方面,对于动态站点可以用 PhantomJS。设置起来更麻烦,但提供更多功能。记住:也可以使用PhantomJS 抓取静态网站!

如果你对这个话题有任何疑问,可以在下面随时询问,我会尽我所能帮助你。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容