记一次Nodejs爬虫实现

225 阅读4分钟

看到很多人使用Python来进行爬虫操作,一时技痒,也用Nodejs实现了一个简单的爬虫。

概念和要件

先来简单分析一下,实现一个爬虫,应该具备的要件和需要注意的问题:

  • HTTP请求

首先当然是需要一个http客户端程序,来模拟浏览器访问网页,获取网页的内容。在其他的系统中,通常使用一个第三方库,但这个要求在Nodejs中非常简单,内置就有http和https的支持。调用和操作也很方便,我们这里也就没有做更进一步的封装。

  • 页面分析

获取一个页面的内容非常简单,但要从页面里,提取出需要的信息,就需要开发者非常熟悉和了解HTML、CSS和JS相关的内容。一般通过分析页面代码结构来完成。由于不同的网站和系统,它们的页面结构可能差异很大,所以很难编写一个通用性的分析程序,而需要程序员在检查爬虫目标页面后,在修改和编写适合于这个页面的程序,虽然不难,因为很难自动化,可能是个体力活。

比如本文中,我们以"经典"的豆瓣电影TOP250来说明。我们访问以下地址,可以获得一个电影列表的页面:

movie.douban.com/top250

要分析这个页面,通常使用浏览器自带的检查器(F12,然后选择elements),它应该可以显示这个页面的DOM结构。

db250.png

比如我们要获取标题、海报图片地址、星级、引语等内容,就需要找到这些内容和对应的结构位置,以及所使用的CSS类名等等。

db2502.png

比如这个星级的信息,相关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();

要获取更多信息和详细的使用方式,可以访问它的项目页面:

github.com/cheeriojs/c…

The fast, flexible, and elegant library for parsing and manipulating HTML and XML

  • 其他工程化的考虑

在真实的爬虫执行过程中,会遇到各种各样的情况,让你不能得到想要的结果。遇到这些情况,就需要一个个的对实际遇到的问题进行分析,然后进行针对性的解决,这同样很难有一种通用的处理方式,都是具体问题具体分析具体解决。

比如,我们测试和验证的对象是豆瓣的TOP250电影页面。但打开一看,只显示了25条啊,这就是这种信息列表常用的分页模式。我们继续浏览其他页面,可以发现它是通过在URL地址中加入一个开始参数,来进行分页显示的(第二页地址):

movie.douban.com/top250?star…

看到这种情况,我们就知道,需要稍微修改一下代码和流程,如需要构造新的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);
        });
    }
}

最后的图片文件爬取效果如下:

a.png