阅读 3808

[译] JavaScript 自动化爬虫入门指北(Chrome + Puppeteer + Node JS):和 Headless Chrome 一起装逼一起飞

JavaScript 自动化爬虫入门指北(Chrome + Puppeteer + Node JS)

和 Headless Chrome 一起装逼一起飞

Udemy Black Friday Sale — Thousands of Web Development & Software Development courses are on sale for only $10 for a limited time! Full details and course recommendations can be found here.

内容简介

本文将会教你如何用 JavaScript 自动化 web 爬虫,技术上用到了 Google 团队开发的 Puppeteer。 Puppeteer 运行在 Node 环境,可以用来操作 headless Chrome。何谓 Headless Chrome?通俗来讲就是在不打开 Chrome 浏览器的情况下使用提供的 API 模拟用户的浏览行为。

如果你还是不理解,你可以想象成使用 JavaScript 全自动化操作 Chrome 浏览器。

前言

先确保你已经安装了 Node 8 及以上的版本,没有的话,可以先到 官网 里下载安装。注意,一定要选“Current”处显示的版本号大于 8 的。

如果你是第一次接触 Node,最好先看一下入门教程:Learn Node JS — The 3 Best Online Node JS Courses.

安装好 Node 之后,创建一个项目文件夹,然后安装 Puppeteer。安装 Puppeteer 的过程中会附带下载匹配版本的 Chromium(译者注:国内网络环境可能会出现安装失败的问题,可以设置环境变量 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 1 跳过下载,副作用是每次使用 launch 方法时,需要手动指定浏览器的执行路径):

npm install --save puppeteer
复制代码

例 1 —— 网页截图

Puppeteer 安装好之后,我们就可以开始写一个简单的例子。这个例子直接照搬自官方文档,它可以对给定的网站进行截图。

首先创建一个 js 文件,名字随便起,这里我们用 test.js 作为示例,输入以下代码:

const puppeteer = require('puppeteer');

async function getPic() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://google.com');
  await page.screenshot({path: 'google.png'});

  await browser.close();
}

getPic();
复制代码

下面我们来逐行分析上面的代码。

  • 第 1 行: 引入依赖。
  • 第 3–10 行: 核心代码,自动化过程在这里完成。
  • 第 12 行: 执行 getPic() 方法。

细心的读者会发现,getPic() 前面有个 async 前缀,它表示 getPic() 方法是个异步方法。asyncawait 成对出现,属于 ES 2017 新特性。介于它是个异步方法,所以调用之后返回的是 Promise 对象。当 async 方法返回值时,对应的 Promise 对象会将这个值传递给 resolve(如果抛出异常,那么会将错误信息传递给 Reject)。

async 方法中,可以使用 await 表达式暂停方法的执行,直到表达式里的 Promise 对象完全解析之后再继续向下执行。看不懂没关系,后面我再详细讲解,到时候你就明白了。

接下来,我们将会深入分析 getPic() 方法:

  • 第 4 行:
const browser = await puppeteer.launch();
复制代码

这段代码用于启动 puppeteer,实质上打开了一个 Chrome 的实例,然后将这个实例对象赋给变量 browser。因为使用了 await 关键字,代码运行到这里会阻塞(暂停),直到 Promise 解析完毕(无论执行结果是否成功)

  • 第 5 行:
const page = await browser.newPage();
复制代码

接下来,在上文获取到的浏览器实例中新建一个页面,等到其返回之后将新建的页面对象赋给变量 page

  • 第 6 行:
await page.goto('https://google.com');
复制代码

使用上文获取到的 page 对象,用它来加载我们给的 URL 地址,随后代码暂停执行,等待页面加载完毕。

  • 第 7 行:
await page.screenshot({path: 'google.png'});
复制代码

等到页面加载完成之后,就可以对页面进行截图了。screenshot() 方法接受一个对象参数,可以用来配置截图保存的路径。注意,不要忘了加上 await 关键字。

  • 第 9 行:
await browser.close();
复制代码

最后,关闭浏览器。

运行示例

在命令行输入以下命令执行示例代码:

node test.js
复制代码

以下是示例里的截图结果:

是不是很厉害?这只是热身,下面教你怎么在非 headless 环境下运行代码。

非 headless?百闻不如一见,自己先动手试一下吧,把第 4 行的代码:

const browser = await puppeteer.launch();
复制代码

换成这句:

const browser = await puppeteer.launch({headless: false});
复制代码

然后再次运行:

node test.js
复制代码

是不是更炫酷了?当配置了 {headless: false} 之后,就可以直观的看到代码是怎么操控 Chrome 浏览器的。

这里还有一个小问题,之前我们的截图有点没截完整的感觉,那是因为 page 对象默认的截屏尺寸有点小的缘故,我们可以通过下面的代码重新设置 page 的视口大小,然后再截取:

await page.setViewport({width: 1000, height: 500})
复制代码

这下就好多了:

最终代码如下:

const puppeteer = require('puppeteer');

async function getPic() {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  await page.goto('https://google.com');
  await page.setViewport({width: 1000, height: 500})
  await page.screenshot({path: 'google.png'});

  await browser.close();
}

getPic();
复制代码

例 2 —— 爬取数据

通过上面的例子,你应该掌握了 Puppeteer 的基本用法,下面再来看一个稍微复杂点的例子。

开始前,不妨先看看 官方文档。你会发现 Puppeteer 能干很多事,像是模拟鼠标的点击、填充表单数据、输入文字、读取页面数据等。

在接下来的教程里,我们将爬一个叫 Books To Scrape 的网站,这个网站是专门用来给开发者做爬虫练习用的。

还是在之前创建的文件夹里,新建一个 js 文件,这里用 scrape.js 作为示例,然后输入以下代码:

const puppeteer = require('puppeteer');

let scrape = async () => {
  // Actual Scraping goes Here...
  
  // Return a value
};

scrape().then((value) => {
    console.log(value); // Success!
});
复制代码

有了上一个例子的经验,这段代码要看懂应该不难。如果你还是看不懂的话......那也没啥问题就是了。

首先,还是引入 puppeteer 依赖,然后定义一个 scrape() 方法,用来写爬虫代码。这个方法返回一个值,到时候我们会处理这个返回值(示例代码是直接打印出这个值)

先在 scrape 方法中添加下面这一行测试一下:

let scrape = async () => {
  return 'test';
};
复制代码

在命令行输入 node scrape.js,不出问题的话,控制台会打印一个 test 字符串。测试通过后,我们来继续完善 scrape 方法。

步骤 1:前期准备

和例 1 一样,先获取浏览器实例,再新建一个页面,然后加载 URL:

let scrape = async () => {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  await page.goto('http://books.toscrape.com/');
  await page.waitFor(1000);
  // Scrape
  browser.close();
  return result;
};
复制代码

再来分析一下上面的代码:

首先,我们创建了一个浏览器实例,将 headless 设置为 false,这样就能直接看到浏览器的操作过程:

const browser = await puppeteer.launch({headless: false});
复制代码

然后创建一个新标签页:

const page = await browser.newPage();
复制代码

访问 books.toscrape.com

await page.goto('http://books.toscrape.com/');
复制代码

下面这一步可选,让代码暂停执行 1 秒,保证页面能完全加载完毕:

await page.waitFor(1000);
复制代码

任务完成之后关闭浏览器,返回执行结果。

browser.close();
return result;
复制代码

步骤 1 结束。

步骤 2: 开爬

打开 Books to Scrape 网站之后,想必你也发现了,这里面有海量的书籍,只是数据都是假的而已。先从简单的开始,我们先抓取页面里第一本书的数据,返回它的标题和价格信息(红色边框选中的那本)。

查一下文档,注意到这个方法能模拟页面点击:

page.click(selector[, options])

  • selector 选择器,定位需要进行点击的元素,如果有多个元素匹配,以第一个为准。

这里可以使用开发者工具查看元素的选择器,在图片上右击选中 inspect:

上面的操作会打开开发者工具栏,之前选中的元素也会被高亮显示,这个时候点击前面的三个小点,选择 copy - copy selector:

有了元素的选择器之后,再加上之前查到的元素点击方法,得到如下代码:

await page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');
复制代码

然后就会观察到浏览器点击了第一本书的图片,页面也会跳转到详情页。

在详情页里,我们只关心书的标题和价格信息 —— 见图中红框标注。

为了获取这些数据,需要用到 page.evaluate() 方法。这个方法可以用来执行浏览器内置 DOM API ,例如 querySelector()

首先创建 page.evaluate() 方法,将其返回值保存在 result 变量中:

const result = await page.evaluate(() => {
// return something
});
复制代码

同样,要在方法里选择我们要用到的元素,再次打开开发者工具,选择需要 inspect 的元素:

标题是个简单的 h1 元素,使用下面的代码获取:

let title = document.querySelector('h1');
复制代码

其实我们需要的只是元素里的文字部分,可以在后面加上 .innerText,代码如下:

let title = document.querySelector('h1').innerText;
复制代码

获取价格信息同理:

刚好价格元素上有个 price_color class,可以用这个 class 作为选择器获取到价格对应的元素:

let price = document.querySelector('.price_color').innerText;
复制代码

这样,标题和价格都有了,把它们放到一个对象里返回:

return {
  title,
  price
}
复制代码

回顾刚才的操作,我们获取到了标题和价格信息,将它们保存在一个对象里返回,返回结果赋给 result 变量。所以,现在你的代码应该是这样:

const result = await page.evaluate(() => {
  let title = document.querySelector('h1').innerText;
  let price = document.querySelector('.price_color').innerText;
return {
  title,
  price
}
});
复制代码

然后只需要将 result 返回即可,返回结果会打印到控制台:

return result;
复制代码

最后,综合起来代码如下:

const puppeteer = require('puppeteer');

let scrape = async () => {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();

    await page.goto('http://books.toscrape.com/');
    await page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');
    await page.waitFor(1000);

    const result = await page.evaluate(() => {
        let title = document.querySelector('h1').innerText;
        let price = document.querySelector('.price_color').innerText;

        return {
            title,
            price
        }

    });

    browser.close();
    return result;
};

scrape().then((value) => {
    console.log(value); // Success!
});
复制代码

在控制台运行代码:

node scrape.js
// { title: 'A Light in the Attic', price: '£51.77' }
复制代码

操作正确的话,在控制台会看到正确的输出结果,到此为止,你已经完成了 web 爬虫。

例 3 —— 后期完善

稍加思考一下你会发现,标题和价格信息是直接展示在首页的,所以,完全没必要进入详情页去抓取这些数据。既然这样,不妨再进一步思考,能否抓取所有书的标题和价格信息?

所以,抓取的方式其实有很多,需要你自己去发现。另外,上面提到的直接在主页抓取数据也不一定可行,因为有些标题可能会显示不全。

拔高题

目标 —— 抓取主页所有书籍的标题和价格信息,并且用数组的形式保存返回。正确的输出应该是这样:

开干吧,伙计,其实实现起来和上面的例子相差无几,如果你觉得实在太难,可以参考下面的提示。


提示:

其实最大的区别在于你需要遍历整个结果集,代码的大致结构如下:

const result = await page.evaluate(() => {
  let data = []; // 创建一个空数组
  let elements = document.querySelectorAll('xxx'); // 选择所有相关元素
  // 遍历所有的元素
    // 提取标题信息
    // 提取价格信息
    data.push({title, price}); // 将数据插入到数组中
  return data; // 返回数据集
});
复制代码

如果提示了还是做不出来的话,好吧,以下是参考答案。在以后的教程中,我会在下面这段代码的基础上再做一些拓展,同时也会涉及一些更高级的爬虫技术。你可以在 这里 提交你的邮箱地址进行订阅,有新的内容更新时我们会通知你。

参考答案:

const puppeteer = require('puppeteer');

let scrape = async () => {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();

    await page.goto('http://books.toscrape.com/');

    const result = await page.evaluate(() => {
        let data = []; // 创建一个数组保存结果
        let elements = document.querySelectorAll('.product_pod'); // 选择所有书籍

        for (var element of elements){ // 遍历书籍列表
            let title = element.childNodes[5].innerText; // 提取标题信息
            let price = element.childNodes[7].children[0].innerText; // 提取价格信息

            data.push({title, price}); // 组合数据放入数组
        }

        return data; // 返回数据集
    });

    browser.close();
    return result; // 返回数据
};

scrape().then((value) => {
    console.log(value); // 打印结果
});
复制代码

结语:

谢谢观看!如果你有学习 NodeJS 的意向,可以移步 Learn Node JS — The 3 Best Online Node JS Courses

每周我都会发布 4 篇有关 web 开发的技术文章,欢迎订阅!或者你也可以在 Twitter 上 关注我


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏