使用无头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的内容并节省磁盘空间
我还有其他想要改进的地方,但这些已经足够完成任务。
标签
反馈
这篇文章有什么问题吗?请在GitHub上创建一个问题或提交一个拉取请求来帮助我们。