使用无头Chrome和Selenium爬取网站

在假期期间,我在进行一个需要从不同网站下载内容的项目。我需要一个网络爬虫,但典型的Perl选项如WWW:Mechanize无法满足需求,因为许多网站的内容由JavaScript控制,我需要一个启用JavaScript的浏览器。但是浏览器会消耗大量内存 - 该怎么办呢?

答案是使用无头Chrome,它的工作方式与正常Chrome完全一样,只是没有图形显示,从而减少了其内存占用。我可以使用Selenium::Remote::Driver和Selenium服务器来控制它。以下是我是如何做到这一点的。

非Perl配置

显然,我需要安装Chrome浏览器。在Linux上,通常需要添加Chrome仓库,然后安装Chrome软件包。在Fedora上,操作非常简单

sudo dnf install fedora-workstation-repositories
sudo dnf config-manager --set-enabled google-chrome
sudo dnf install google-chrome-stable

我还需要ChromeDriver,它实现了WebDriver的Chrome线协议。换句话说,它是Selenium与Chrome通信的途径

wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip
unzip chromedriver_linux64.zip

我将它放在/usr/bin

sudo chown root:root chromedriver
sudo chmod 755 chromedriver
sudo mv chromedriver /usr/bin/

我下载了Selenium服务器

wget https://selenium-release.storage.googleapis.com/3.14/selenium-server-standalone-3.14.0.jar

这个版本的Selenium需要Java版本8,我通过其软件包安装了它

sudo dnf install java-1.8.0-openjdk

最后,我启动了Selenium服务器

java -Dwebdriver.chrome.driver=/usr/bin/chromedriver -jar selenium-server-standalone-3.14.0.jar

必须运行此服务器,以便Perl可以通过Selenium与Chrome通信。

一个基本的爬虫

我编写了一个基本的爬虫脚本,这里是一个简化的版本

#!/usr/bin/env perl
use Selenium::Remote::Driver;
use Encode 'encode';

my $driver = Selenium::Remote::Driver->new(
  browser_name => 'chrome',
  extra_capabilities => { chromeOptions => {args => [
    'window-size=1920,1080',
    'headless',
  ]}},
);

my %visited = ();
my $depth = 1;
my $url = 'https://example.com';

spider_site($driver, $url, $depth);

$driver->quit();

此脚本初始化了一个Selenium::Remote::Driver对象。注意它如何将选项传递给Chrome:例如,window-size选项是一个键值对选项,而headless是一个布尔值。Chrome接受许多选项。以下是我认为有用的其他一些选项

  • allow-running-insecure-content - 允许Chrome加载带有无效安全证书的网站
  • disable-infobars - 禁用“Chrome正在被软件控制”的通知
  • no-sandbox - 禁用沙盒安全功能,允许您以root身份运行无头Chrome

脚本初始化一个%visited散列来存储浏览器访问的URL,以避免请求相同的URL两次。变量$depth确定爬虫应深入多少级别:值为1时,它将访问它加载的第一个页面上的所有链接,但之后不再访问。变量$url确定要访问的起始网页。

spider_site函数是递归的

sub spider_site {
  my ($driver, $url, $depth) = @_;
  warn "fetching $url\n";
  $driver->get($url);
  $visited{$url}++;

  my $text = $driver->get_body;
  print encode('UTF-8', $text);

  if ($depth > 0) {
    my @links = $driver->find_elements('a', 'tag_name');
    my @urls = ();
    for my $l (@links) {
      my $link_url = eval { $l->get_attribute('href') };
      push @urls, $link_url if $link_url;
    }
    for my $u (@urls) {
      spider_site($driver, $u, $depth - 1) unless ($visited{$u});
    }
  }
}

它获取给定的$url,将网页的文本内容打印到STDOUT。在打印之前对输出进行编码:我发现这是必要的,以避免多字节编码问题。如果爬虫尚未达到最大深度,它将获取页面上的所有链接,并爬取它尚未访问的每个链接。我将get_attribute方法调用包裹在eval中,因为它可能在链接从网站中消失后失败。

一个改进的爬虫

上面显示的爬虫脚本虽然功能齐全,但非常基础。我编写了一个更高级的版本,具有一些不错的功能

  • 启动时ping Selenium服务器,如果服务器没有响应则退出
  • 限制跟随的链接仅限于与起始URL的域匹配的链接,以避免从无关网站下载内容
  • 将静态变量如$depth转换为命令行选项
  • 添加调试模式以打印爬虫所做的决策
  • 接受URL列表而不是一次只接受一个
  • 使用Parallel::ForkManager并行抓取URL
  • 将网站内容打印为gzip文件,以分离不同起始URL的内容并节省磁盘空间

我还有其他想要改进的地方,但这些已经足够完成任务。

标签

David Farrell

David是一位专业的程序员,他经常在推特博客上分享关于代码和编程艺术的见解。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上创建一个问题或提交一个拉取请求来帮助我们。