用JavaScript和Node.js编写爬虫

2021-07-01

Node.js是目前非常流行,非常成熟的JavaScript运行环境。基于谷歌的V8引擎,Node.js的性能也十分优秀。这使得非常适合基于Node.js环境,使用JavaScript语言来编写爬虫程序。

Node.js自诞生以来一直受到广泛的关注,生态也逐渐完善。Node.js的出现使得JavaScript语言不仅仅是Web前端开发的语言,让JavaScript应用于更广泛的领域。现在JavaScript已经是排名前十的编程语言了。

另外JavaScript原生支持JSON数据格式,JSON格式已经成为目前API设计的事实标准。这使得基于JavaScript语言和Node.js环境编写爬虫变得十分合适。

然而,在Node.js生态下,并没有类似Scrapy的成熟完整的爬虫框架。我们需要组合不通的优秀框架或库来帮我们完成爬取数据的不同子任务。

注意:如果你没有JavaScript的任何经验或者完全不理解JavaScript,建议你先看看w3cschool的JavaScript教程

抓取静态页面的爬虫

完成爬虫程序主要需要解决两个任务:

  1. 请求目标网页获取返回的HTML信息。
  2. 从HTML信息中抽取我们需要的数据字段。

这次我们的目标是京东商品的页面,我们希望从页面中获取商品名,品牌和价格信息。

安装Node.js

首先我们需要安装好Node.js环境,直接访问官网。下载对应系统的安装包完成安装。

安装完成后,我们检查一下安装是否成功:

➜  ~ node -v
v14.17.1

然后我们创建爬虫文件夹,我们编写的爬虫程序就放这里了:

➜  ~ mkdir jd
➜  ~ cd jd
➜  jd
使用Axios请求目标页面,获得HTML信息

我们的第一步要做的就是对目标页面发起HTTP请求,把返回的HTML拿到供后续的数据抽取使用。Node.js生态下有很多优秀的HTTP请求库可以选择,我们选择Axios作为我们的请求库。

安装Axios:

npm install axios --save

安装完成后,我们的工作目录下会增加node_modules目录和package-lock.json,我们暂时不去详细解释目录和文件的作用。只要确保能完成安装就行。

我们创建新文件,命名为jd_crawler.js。 先完成请求过程:

const axios = require('axios');
const url = "https://item.jd.com/100013299648.html";

axios.get(url, {
    headers: {
        'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36"
    }
}).then( response => {
    const html = response.data;
    console.log(response.status);
    console.log(html.length);
}).catch( e => console.log(e) );

代码很直接,我们首先通过const axios = require('axios');引用axios库,后续我们就可以使用axios来完成请求。 请求会返回后,我们将HTML信息保存在html中,以备后续使用。如果出错,我们打印出错信息。

注意:我们必须要设置合适的User Agent信息,不然会请求不到页面信息。

运行代码:

➜  jd node jd_crawler.js
200
143697
使用Cheerio抽取数据

完成HTML信息的获取后,我们需要从HTML信息中抽取我们需要的数据。Node.js没有提供原生的HTML处理支持,我们选择用cheerio来帮助处理HTML信息。

安装cheerio:

npm install cheerio --save

我们首先在浏览器上,访问我们的目标页面,然后打开开发者工具,寻找我们需要抽取的数据的特征(主要是DOM的特征):

商品信息

从DOM结构来看,我们可以用div.sku-name的内容抽取到商品名信息。加上抽取数据的代码如下:

axios.get(url, {
    headers: {
        'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36"
    }
}).then( response => {
    const html = response.data;
    const $ = cheerio.load(html);

    const name = $("div.sku-name").text().trim();
    const brand = $("ul#parameter-brand > li > a").text().trim();
    const price = $("span.p-price > span.price").text();

    console.log(name);
    console.log(brand);
    console.log(price);

}).catch( e => console.log(e) );

运行我们的爬虫,等到如下的信息:

➜  jd node jd_crawler.js
索尼(SONY)KD-65X9500H 65英寸 4K超高清 HDR 液晶平板电视 全面屏 X1旗舰版图像芯片
索尼(SONY)

我们成功的抽取到了商品名和品牌信息,但是我们没有抽取到价格信息。从网页的源代码里面观察可以看出来,价格的DOM结构存在但是并没有价格信息,价格信息是HTML加载以后动态插入的。这就需要我们能够渲染JS代码才能抽取到价格信息了。

用Puppeteer抓取动态网页

上面我们发现,价格信息在网页的HTML信息中是不存在的,但是浏览器的页面上却能正确展示出来,这是为什么呢?这是一种常见的保护敏感信息不被轻易抓取方法,把敏感信息用JavaScript动态渲染上去。 如果我们想要抽取这类信息,我们就必须要有渲染JavaScript的能力。一般我们会用Headless浏览器来处理。这里我们使用Puppeteer。

Puppeteer是谷歌推出的基于谷歌浏览器内核的Headless浏览器。广泛的应用于自动化测试等领域。

首先我们安装:

npm install puppeteer --save

Puppeteer会自己处理请求目标网页并渲染的过程,所以我们重新创建一个文件:jd_puppeteer.js

我们引入puppeteer和cheerio,因为puppeteer的接口都是异步的,我们把我们的代码封装在一个async函数内,使得我们可以使用async/await语法写出更加清晰的代码:

const puppeteer=require('puppeteer');
const cheerio=require('cheerio');
const url = "https://item.jd.com/100013299648.html";

(async()=>{
    const browser=await puppeteer.launch();
    const page=await browser.newPage();
    try {
        await page.goto(url, {timeout: 10000});
        const html = await page.content();
    } catch(err) { console.log(err); }
})();;

这时候,我们同样把HTML信息保存起来以备抽取数据用了,但是请注意,这里的HTML信息已经是JS渲染以后的结果了。可以理解为是真实的浏览器在渲染页面。

然后我们还是同样的利用cheerio来抽取对应的数据。完整代码如下:

const puppeteer=require('puppeteer');
const cheerio=require('cheerio');
const url = "https://item.jd.com/100013299648.html";

(async()=>{
    const browser=await puppeteer.launch();
    const page=await browser.newPage();
    try {
        await page.goto(url, {timeout: 10000});
        const html = await page.content();
        const $ = cheerio.load(html);

        const name = $("div.sku-name").text().trim();
        const brand = $("ul#parameter-brand > li > a").text().trim();
        const price = $("span.p-price > span.price").text();

        console.log(name);
        console.log(brand);
        console.log(price);

        await page.close();
        await browser.close();
    } catch(err) { console.log(err); }
})();

运行我们的脚本:

➜  jd node jd_puppeteer.js
索尼(SONY)KD-65X9500H 65英寸 4K超高清 HDR 液晶平板电视 全面屏 X1旗舰版图像芯片
索尼(SONY)
7499.00

Bingo! 我们这次已经能够拿到价格信息了。

注意,使用Headless浏览器可以抓取几乎所有的网页,但是性能的消耗也是巨大的,可以认为系统就是在运行浏览器打开渲染目标网页。 如果不是必要,还是使用静态网页抓取的模式,性能和效率才是最高的。

好了,至此,我们已经有了两个可用的爬虫,一个可以抓取静态信息,另外一个可以抓取动态信息。希望能对你有帮助。

附1:除了axios请求库,还有不少各具特色的请求库可以使用:

附2: 除了puppeteer, 还有如下Headless浏览器备选: