作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
尼克·奇科瓦尼的头像

尼克Chikovani

拥有超过6年的开发和团队领导经验, 尼克酷爱强健的体魄, 可伸缩和干净的代码设计.

工作经验

9

Share

在本文中, 我们将看到,使用一个有点非传统的方法来执行网页抓取(网页自动化)是多么容易 无头的浏览器.

什么是无头浏览器,为什么需要它?

在过去的几年里,我们看到了网络从单纯的HTML和CSS网站发展而来. 现在有更多具有漂亮ui的交互式web应用程序, 它们通常是用Angular或React等框架构建的. 换句话说, 如今,JavaScript统治着网络, 包括你在网站上接触的几乎所有东西.

就我们的目的而言,JavaScript是一种客户端语言. 服务器返回注入到HTML响应中的JavaScript文件或脚本, 浏览器处理它. 现在,这是一个问题,如果我们做一些网络抓取或网络 自动化 因为很多时候, 我们想要看到或抓取的内容实际上是由JavaScript代码呈现的,并且不能从服务器提供的原始HTML响应中访问.

正如我们上面提到的,浏览器 do 知道如何处理JavaScript和渲染漂亮的网页. Now, 如果我们可以利用这个功能来满足我们的抓取需求,并且有一种通过编程来控制浏览器的方法会怎么样呢? 这正是无头浏览器自动化的用武之地!

无头? 对不起? 是的,这只是意味着没有图形用户界面(GUI). 与通常使用的与视觉元素交互的方式(例如与鼠标或触摸设备)不同,您可以使用控件来自动化用例 命令行界面 (CLI).

无头铬和木偶师

有很多网页抓取工具可以用于无头浏览,比如 Zombie.js or 使用Selenium的无头Firefox. 但今天我们将探索无头Chrome通过 傀儡师。,因为它是一款相对较新的播放器,于2018年初发布. 编者注:值得一提 Intoli的远程浏览器但这将是另一篇文章的主题.

到底什么是木偶师? 它是一个节点.js库,它提供了一个高级API来控制无头Chrome或 或与DevTools协议交互. 它由Chrome DevTools团队和一个很棒的开源社区维护.

说得够多了——让我们进入代码并探索如何使用 傀儡师。无头浏览!

准备环境

首先,你需要 Node.js 8+安装在您的机器上. 你可以安装它 here,或者如果你像我一样是CLI爱好者,喜欢在Ubuntu上工作,遵循这些命令:

curl -sL http://deb.nodesource.com/setup_8.. x | sudo - e bash -
install -y nodejs

您还需要一些软件包,这些软件包在您的系统上可能可用,也可能不可用. 为了安全起见,尝试安装这些:

Sudo apt-get install -yq——no-install- recommended libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0 - 0 libglib2.0-0 libgtk-3-0 libspr4 libpango-1.0 - 0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3

设置无头铬和木偶

我建议安装傀儡师。 npm, 因为它还将包含稳定的最新铬版本,保证与库一起工作.

在你的项目根目录下运行这个命令:

NPM I木偶师——保存

注意:这可能需要一段时间,因为傀儡师。需要在后台下载并安装铬.

好了,现在我们都设置好了,让我们开始吧!

使用傀儡师。 API进行自动网页抓取

让我们开始我们的木偶教程与一个基本的例子. 我们将编写一个脚本,使我们的无头浏览器截取我们选择的网站的屏幕截图.

在项目目录中创建一个新文件 截图.js 然后在您喜欢的代码编辑器中打开它.

首先,让我们在脚本中导入傀儡师。库:

Const puppeteer = require('puppeteer');

接下来,让我们从命令行参数中获取URL:

Const url =进程.argv [2];
if (!url) {
    抛出"请提供URL作为第一个参数";
}

Now, 我们需要记住,傀儡师。是一个基于承诺的库:它在底层执行对无头Chrome实例的异步调用. 让我们保持代码整洁 通过使用async/await. 为此,我们需要定义an async 函数,然后把所有的傀儡师。代码放进去:

异步函数run () {
    Const浏览器=等待木偶师.发射();
    Const page = await浏览器.newPage ();
    等待页面.转到(url);
    等待页面.截图({路径:“截图.png的});
    浏览器.close ();
}
run ();

总的来说,最终的代码看起来像这样:

Const puppeteer = require('puppeteer');
Const url =进程.argv [2];
if (!url) {
    抛出"请提供URL作为第一个参数";
}
异步函数run () {
    Const浏览器=等待木偶师.发射();
    Const page = await浏览器.newPage ();
    等待页面.转到(url);
    等待页面.截图({路径:“截图.png的});
    浏览器.close ();
}
run ();

你可以通过在项目的根目录下执行以下命令来运行它:

节点截图.js http://github.com

等一下,然后砰! 我们的无头浏览器刚刚创建了一个名为 截图.png 你可以看到其中渲染的GitHub主页. 太好了,我们有一个工作的Chrome网页刮板!

让我们停下来一分钟,探索一下发生了什么 run() 上面的函数.

First, 我们启动一个新的无头浏览器实例, 然后打开一个新页面(选项卡)并导航到命令行参数中提供的URL. Lastly, 我们使用傀儡师。的内置方法进行截图, 我们只需要提供应该保存它的路径. 我们还需要确保关闭无头浏览器后,我们完成了我们的自动化.

现在我们已经介绍了基础知识,让我们继续学习一些更复杂的东西.

第二个木偶师刮痧的例子

对于我们的木偶教程的下一部分, 假设我们想从黑客新闻上抓取最新的文章.

创建一个名为 ycombinator-scraper.js 并粘贴以下代码片段:

Const puppeteer = require('puppeteer');
函数run () {
    return new Promise(async (resolve, reject) => {
        try {
            Const浏览器=等待木偶师.发射();
            Const page = await浏览器.newPage ();
            等待页面.转到(" http://news.ycombinator.com/”);
            让url = await页面.evaluate(() => {
                让结果 = [];
                让items = document.querySelectorAll(的.storylink”);
                items.forEach((item) => {
                    结果.({推
                        url:项.getAttribute(“href”),
                        文本:条目.innerText,实现
                    });
                });
                返回结果;
            })
            浏览器.close ();
            返回解决(url);
        } catch (e) {
            返回拒绝(e);
        }
    })
}
run().(控制台.log).抓住(控制台.错误);

好了,和之前的例子相比这里有更多的内容.

首先你可能会注意到 run() 函数现在返回一个承诺,所以 async Prefix已经移动到promise函数的定义中.

我们还将所有代码包装在一个try-catch块中,这样我们就可以处理导致承诺被拒绝的任何错误.

最后,我们使用傀儡师。的内置方法 evaluate (). 这个方法让我们运行自定义JavaScript代码,就好像我们在DevTools控制台中执行它一样. 该函数返回的任何内容都由promise解析. 当涉及到收集信息或执行自定义操作时,这种方法非常方便.

代码传递给 evaluate () 方法是非常基本的JavaScript,它构建一个对象数组,每个对象都有 url and text 字段表示我们在上面看到的故事url http://news.ycombinator.com/.

脚本的输出看起来像这样(但最初有30个条目):

[{url: 'http://www . '.自然.com/articles/d41586 - 018 - 05469 - 3 ',
    “偏见侦探:研究人员努力使算法公平”},
  {url: 'http://mino-games.可行的.com/jobs/415887’,
    文本:“Mino Games正在蒙特利尔招聘程序员”},
  {url: 'http://srobb.net/pf.html',
    text: '初学者使用pf防火墙指南'},
  // ...
  {url: 'http://tools.ietf.org/html/rfc8439’,
    text: 'ChaCha20和Poly1305 for IETF协议'}]

我得说,相当整洁!

好了,我们继续. 我们只返回了30个项目,而还有更多的可用项目——它们只是在其他页面上. 我们需要点击“更多”按钮来加载下一页的结果.

让我们稍微修改一下脚本,添加对分页的支持:

Const puppeteer = require('puppeteer');
函数 run (pagesToScrape) {
    return new Promise(async (resolve, reject) => {
        try {
            if (!pagesToScrape) {
                pagesToScrape = 1;
            }
            Const浏览器=等待木偶师.发射();
            Const page = await浏览器.newPage ();
            等待页面.转到(" http://news.ycombinator.com/”);
            let 当前页 = 1;
            let urls = [];
            while (当前页 <= pagesToScrape) {
                let newUrls = 等待页面.evaluate(() => {
                    让结果 = [];
                    让items = document.querySelectorAll(的.storylink”);
                    items.forEach((item) => {
                        结果.({推
                            url:项.getAttribute(“href”),
                            文本:条目.innerText,实现
                        });
                    });
                    返回结果;
                });
                urls = urls.concat(newUrls);
                if (当前页 < pagesToScrape) {
                    await Promise.all([
                        等待页面.click('a.morelink'),
                        等待页面.waitForSelector('a.storylink')
                    ])
                }
                当前页++;
            }
            浏览器.close ();
            返回解决(url);
        } catch (e) {
            返回拒绝(e);
        }
    })
}
run(5).(控制台.log).抓住(控制台.错误);

让我们回顾一下我们在这里做了什么:

  1. 我们添加了一个参数 pagesToScrape 到我们的主 run() 函数. 我们将使用它来限制脚本抓取的页面数量.
  2. 还有一个新变量名为 当前页 哪个表示我们当前查看的结果页数. 它被设置为 1 最初. 我们还包装了 evaluate () a中的函数 while 循环,这样它就会一直运行 当前页 是小于还是等于 pagesToScrape.
  3. 我们添加了移动到新页面的块,并在重新启动之前等待页面加载 while loop.

你会注意到我们用了 page.click () 方法让无头浏览器点击“更多”按钮. 我们还使用了 waitForSelector () 方法来确保我们的逻辑暂停,直到页面内容被加载.

这两个都是高层次的傀儡师。 API方法,可以开箱即用.

在使用傀儡师。进行抓取时,您可能会遇到的一个问题是等待页面加载. Hacker News的结构相对简单,等待页面加载完成相当容易. 对于更复杂的用例, 傀儡师。提供了广泛的内置功能, 你可以在 GitHub上的API文档.

这一切都很酷,但我们的木偶教程还没有涵盖优化. 让我们看看怎样才能让木偶师跑得更快.

优化我们的木偶剧本

一般的想法是不让无头浏览器做任何额外的工作. 这可能包括加载图像、应用CSS规则、触发XHR请求等.

和其他工具一样, 傀儡师。的优化取决于确切的用例, 所以请记住,其中一些想法可能不适合你的项目. 例如, 如果我们在第一个例子中避免加载图像, 我们的截图可能不是我们想要的样子.

Anyway, 这些优化可以通过在第一次请求时缓存资产来实现, 或者直接取消由网站发起的HTTP请求.

让我们先看看缓存是如何工作的.

您应该意识到,当您启动一个新的无头浏览器实例时, 傀儡师。为它的配置文件创建一个临时目录. 它在浏览器关闭时被删除,并且在启动新实例时不可用——因此所有的图像都不可用, CSS, 饼干, 存储的其他对象将无法再访问.

我们可以强制傀儡师。使用自定义路径来存储cookie和缓存等数据, 每次我们再次运行它时,哪些将被重用—直到它们过期或被手动删除.

Const浏览器=等待木偶师.发射({
    userDataDir:“./数据”,
});

这应该会给我们的性能带来很大的提升, 因为在第一次请求时,大量的CSS和图像将缓存在数据目录中, Chrome不需要一次又一次地下载它们.

但是,在呈现页面时仍将使用这些资产. 在我们对Y Combinator新闻文章的抓取需求中, 我们真的不需要担心任何视觉效果, 包括图片. 我们只关心纯HTML输出,所以让我们尝试阻止每个请求.

幸运的是, 和木偶师一起工作很酷, 在这种情况下, 因为它支持自定义钩子. 我们可以为每个请求提供一个拦截器,并取消那些我们不需要的请求.

拦截器可以用以下方式定义:

等待页面.setRequestInterception(真正的);
page.on('请求', (请求) => {
    如果请求.resourceType() === 'document') {
        请求.继续();
    } else {
        请求.abort ();
    }
});

如您所见,我们可以完全控制发起的请求. 我们可以编写自定义逻辑来允许或终止基于它们的特定请求 resourceType. 我们还可以访问很多其他数据,比如 请求.url 所以如果我们想的话,我们可以只屏蔽特定的url.

在上面的例子中,我们只允许资源类型为的请求 “文档” 通过我们的过滤器, 这意味着我们将屏蔽所有图像, CSS, 以及除了原始HTML响应之外的所有内容.

这是我们的最终代码:

Const puppeteer = require('puppeteer');
函数 run (pagesToScrape) {
    return new Promise(async (resolve, reject) => {
        try {
            if (!pagesToScrape) {
                pagesToScrape = 1;
            }
            Const浏览器=等待木偶师.发射();
            Const page = await浏览器.newPage ();
            等待页面.setRequestInterception(真正的);
            page.on('请求', (请求) => {
                如果请求.resourceType() === 'document') {
                    请求.继续();
                } else {
                    请求.abort ();
                }
            });
            等待页面.转到(" http://news.ycombinator.com/”);
            let 当前页 = 1;
            let urls = [];
            while (当前页 <= pagesToScrape) {
                等待页面.waitForSelector('a.storylink”);
                let newUrls = 等待页面.evaluate(() => {
                    让结果 = [];
                    让items = document.querySelectorAll(的.storylink”);
                    items.forEach((item) => {
                        结果.({推
                            url:项.getAttribute(“href”),
                            文本:条目.innerText,实现
                        });
                    });
                    返回结果;
                });
                urls = urls.concat(newUrls);
                if (当前页 < pagesToScrape) {
                    await Promise.all([
                        等待页面.waitForSelector('a.morelink'),
                        等待页面.click('a.morelink'),
                        等待页面.waitForSelector('a.storylink')
                    ])
                }
                当前页++;
            }
            浏览器.close ();
            返回解决(url);
        } catch (e) {
            返回拒绝(e);
        }
    })
}
run(5).(控制台.log).抓住(控制台.错误);

在利率限制下保持安全

无头浏览器是非常强大的工具. 他们能够执行几乎任何类型的网络自动化任务,而傀儡师。使这更容易. 尽管有各种可能性, 我们必须遵守网站的服务条款,以确保我们不会滥用该系统.

因为这方面与体系结构更相关, 我不会在这个木偶教程中深入介绍这一点. 也就是说,减慢傀儡师。脚本的最基本方法是向它添加一个sleep命令:

js 等待页面.等待(5000);

这条语句将强制脚本休眠5秒(5000毫秒)。. 你可以把它放在任何地方 浏览器.close ().

就像限制你使用第三方服务一样, 还有很多其他更强大的方法来控制你对木偶的使用. 一个例子是建立一个工人数量有限的排队系统. 每次你想用木偶师, 你会把一个新任务推到队列中, 但只有有限数量的工人能够完成其中的任务. 在处理第三方API速率限制时,这是一种相当普遍的做法,也可以应用于傀儡师。 web数据抓取.

木偶师在快速移动的网络中的位置

在这个木偶教程中,我展示了它作为网页抓取工具的基本功能. 然而, 它有更广泛的用例, 包括无头浏览器测试, PDF生成, 以及性能监控, 在许多其他方面.

Web技术正在快速发展. 有些网站如此依赖JavaScript渲染,以至于几乎不可能执行简单的HTTP请求来抓取它们或执行某种自动化. 幸运的是, 无头浏览器正变得越来越容易处理我们所有的自动化需求, 感谢像傀儡师。这样的项目及其背后的优秀团队!

了解基本知识

  • 你说的“无头浏览器”是什么意思??

    无头浏览器是一种没有任何用户界面(UI)的web浏览器. 相反,它遵循软件开发人员用不同编程语言定义的指令. 无头浏览器主要用于运行自动化的质量保证测试, 或者去抓取网站.

  • 抓取网站是否合法?

    网站通常允许其他软件抓取其内容. 请参考机器人排除标准(robots).TXT文件)的网站,你打算抓取, 因为它通常描述了你可以抓取哪些页面. 你还应该检查一下服务条款,看看你是否被允许刮胡子.

  • 什么是无头环境?

    无头意味着给定的设备或软件没有用户界面或输入机制,如键盘或鼠标. 术语“无头环境”更常用于描述设计为其他计算机或服务器提供服务的计算机软件.

  • 什么是无头Chrome?

    无头Chrome本质上是没有图形用户界面(GUI)的谷歌Chrome浏览器。, 基于相同的底层技术. Chrome是由软件开发者编写的脚本控制的.

  • 什么是谷歌木偶师?

    木偶操纵者是一个节点.js库由谷歌Chrome的开发团队维护. 傀儡师。提供了一个高级API来控制无头Chrome或铬或与DevTools协议交互.

  • Selenium是一个框架吗??

    Yes, but not a front-end web framework like Angular or React; Selenium is a software testing framework for web applications. 它的主要用例是在无头浏览器上自动化质量保证测试, 但它也经常被用来自动化网站上的管理任务.

就这一主题咨询作者或专家.
预约电话
尼克·奇科瓦尼的头像
尼克Chikovani

位于 第比利斯,格鲁吉亚

成员自 2018年6月7日

作者简介

拥有超过6年的开发和团队领导经验, 尼克酷爱强健的体魄, 可伸缩和干净的代码设计.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

9

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.