你可能会把 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。
params
和 httpOptions
对象是可选的。参数可以包括像 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 抓取静态网站!
如果你对这个话题有任何疑问,可以在下面随时询问,我会尽我所能帮助你。