看到很多人使用Python来进行爬虫操作,一时技痒,也用Nodejs实现了一个简单的爬虫。
概念和要件
先来简单分析一下,实现一个爬虫,应该具备的要件和需要注意的问题:
- HTTP请求
首先当然是需要一个http客户端程序,来模拟浏览器访问网页,获取网页的内容。在其他的系统中,通常使用一个第三方库,但这个要求在Nodejs中非常简单,内置就有http和https的支持。调用和操作也很方便,我们这里也就没有做更进一步的封装。
- 页面分析
获取一个页面的内容非常简单,但要从页面里,提取出需要的信息,就需要开发者非常熟悉和了解HTML、CSS和JS相关的内容。一般通过分析页面代码结构来完成。由于不同的网站和系统,它们的页面结构可能差异很大,所以很难编写一个通用性的分析程序,而需要程序员在检查爬虫目标页面后,在修改和编写适合于这个页面的程序,虽然不难,因为很难自动化,可能是个体力活。
比如本文中,我们以"经典"的豆瓣电影TOP250来说明。我们访问以下地址,可以获得一个电影列表的页面:
要分析这个页面,通常使用浏览器自带的检查器(F12,然后选择elements),它应该可以显示这个页面的DOM结构。
比如我们要获取标题、海报图片地址、星级、引语等内容,就需要找到这些内容和对应的结构位置,以及所使用的CSS类名等等。
比如这个星级的信息,相关CSS类名链差不多就是:
article-item-info-bd-star-ratingnumber
规划好这些信息以后,就可以开始编写程序来进行处理了。
- 页面分析程序
我们找到一个cheerio库,来通过HTML内容对页面的结构和内容进行分析。主要原因是这个库的功能强大,使用简单,风格和使用方式有点类似于以前的jquery,非常容易上手。它的一般使用方法是,通过页面DOM结构中,找到所需要访问信息所在的类或者封装对象,然后通过DOM路径来访问其中的内容。
比如,我们要访问标题、星级等内容,通过cheerio,可以使用以下的方式:
const title = $('.title', this).text();
const star = $('.info .bd .rating_num', this).text();
const pic = $('.pic img', this).attr('src');
const quote = $('.info .bd .quote .inq', this).text();
要获取更多信息和详细的使用方式,可以访问它的项目页面:
The fast, flexible, and elegant library for parsing and manipulating HTML and XML
- 其他工程化的考虑
在真实的爬虫执行过程中,会遇到各种各样的情况,让你不能得到想要的结果。遇到这些情况,就需要一个个的对实际遇到的问题进行分析,然后进行针对性的解决,这同样很难有一种通用的处理方式,都是具体问题具体分析具体解决。
比如,我们测试和验证的对象是豆瓣的TOP250电影页面。但打开一看,只显示了25条啊,这就是这种信息列表常用的分页模式。我们继续浏览其他页面,可以发现它是通过在URL地址中加入一个开始参数,来进行分页显示的(第二页地址):
看到这种情况,我们就知道,需要稍微修改一下代码和流程,如需要构造新的URL地址,循环迭代的访问多个页面,然后分别提取并合并数据就可以了(代码后祥)。
其实,这是相当简单的情况,其他的情况,比如如果是通过API获取呢,或者中间出现错误,超时等等,一个成熟的爬虫程序都应该能进行处理,来增强程序的可用性和可靠性。
代码实现
根据以上构思,简单的实现的代码如下(这里有一个nodejs依赖库,不能用代码片段来直接执行,需要读者在自己的环境中执行):
const
https = require('https'),
cheerio = require('cheerio'),
URL_FETCH = 'https://movie.douban.com/top250';
const
TOTAL = 30,
MOVIES = [];
const doGet = (start = 0)=>{
https.get(URL_FETCH + "?start=" + start,(res) => {
let html = ''
res
.on('data', (chunk)=> html += chunk)
.on("end", ()=>handlResult(html));
});
}
const handlResult =(content)=>{
const $ = cheerio.load(content);
let no= MOVIES.length + 1;
$('li .item').each(function () {
const title = $('.title', this).text();
const star = $('.info .bd .rating_num', this).text();
const pic = $('.pic img', this).attr('src');
const quote = $('.info .bd .quote .inq', this).text();
MOVIES.push({ No: no++, title, star, quote, pic });
})
if (MOVIES.length < TOTAL) {
doGet(MOVIES.length);
} else {
console.log("fetch:\n", MOVIES);
}
}
doGet();
// 执行效果如下
fetch:
[
{
No: 1,
title: '肖申克的救赎 / The Shawshank Redemption',
star: '9.7',
quote: '希望让人自由。',
pic: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.jpg'
},
{
No: 2,
title: '霸王别姬',
star: '9.6',
quote: '风华绝代。',
pic: 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2561716440.jpg'
},
{
No: 3,
title: '阿甘正传 / Forrest Gump',
star: '9.5',
quote: '一部美国近现代史。',
pic: 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2372307693.jpg'
},
{
...
获取图片
这里再附加一个基于爬虫数据中图片地址,来进一步爬取图片的程序,可以独立或者结合使用。
使用前,需要先在程序所在文件夹创建一个images文件夹来保存爬取的图片文件。然后将爬取图片地址,加入到程序中就可以了,程序会自动启动获取过程,而且会队列化操作不至于被误解成网络攻击。
// 在获取数据完成后,调用队列化获取图片的方法
// MOVIES.map(v=> imageFetcher.add(v.pic));
const fs = require('fs');
const imageFetcher = {
IMG_LIST: [],
isFetching: false,
add : (img) =>{
imageFetcher.IMG_LIST.push(img);
setTimeout(imageFetcher.fetchImg,1000);
},
fetchImg : ()=>{
if (imageFetcher.isFetching) return;
if (imageFetcher.IMG_LIST.length == 0) return setTimeout(imageFetcher.fetchImg,2000);
let imageUrl = imageFetcher.IMG_LIST.shift();
imageFetcher.isFetching = true;
// image file
let fimg = __dirname + "/images/"+ imageUrl.split("/").slice(-1)[0];
// 发起 HTTP GET 请求获取图片数据
https.get(imageUrl, (res) => {
const fstream = fs.createWriteStream(fimg); // writable stream
res
.on('data', (chunk) => fstream.write(chunk))
.on('end', () => {
fstream.end();
imageFetcher.isFetching = false;
setTimeout(imageFetcher.fetchImg,1000);
});
}).on('error', (error) => {
imageFetcher.isFetching = false;
setTimeout(imageFetcher.fetchImg,1000);
console.error('图片下载失败:', error);
});
}
}
最后的图片文件爬取效果如下: