返回博客

用 Ruby 实现爬虫抓取

Augustas Pelakauskas

2022-11-17

网页抓取,也叫做数据抓取,正迅速成为全球公司的必备工具。虽然大多数企业都知道数据收集和分析,但可能对使用网页抓取技术收集公共数据知之甚少。作为一种网页抓取方法,Ruby 可以有效地处理所有与网页抓取相关的任务。

Ruby 是一种成熟的开源编程语言。第一版在 1996 年发布,而最新的主要迭代 3 在 2020 年被弃用。本文介绍了在最新版本 3 中使用 Ruby 进行网页抓取的工具和技术。

我们先来介绍一下抓取静态公共网页的步骤,然后再介绍抓取动态网页的方法。虽然第一种方法适用于大多数网站,但并不适用于使用 JavaScript 渲染内容的动态页面。为了处理这些网站,我们先来了解一下无头浏览器。

安装 Ruby

要在 Windows 上安装 Ruby,请下载并运行 Ruby 安装程序

或使用像 Chocolatey 这样的软件包管理器。如果您使用的是 Chocolatey,打开命令提示符并运行以下命令:

choco install ruby

这样可以下载并安装使用 Ruby 所需的所有内容。

macOS 默认捆绑了 Ruby 版本 2。建议不要使用这一版本,最好进行单独安装。由于 Ruby 的最新版本是版本 3,因此在本文中我们将使用该版本。尽管如此,我们即将编写的代码也向后兼容 2.6 版本。

要在 macOS 上安装 Ruby,请使用类似于 Homebrew 的软件包管理器。在终端中输入以下内容:

brew install ruby

这样就下载并安装了 Ruby 开发环境。

对于 Linux 系统,请使用发行版的软件包管理器。例如,在 Ubuntu 上运行如下命令:

sudo apt install ruby-full

抓取静态页面

在本节中,我们将编写一个网络爬虫,可从 https://books.toscrape.com 上抓取数据。这是一个用静态网站练习网页抓取的样书。

安装必要的 gem

Ruby 包叫做 “gem”,可以通过命令行安装。第一个必要的 gem 是 HTTParty。其目的是为了得到网站的回应。还有一些其他选项,如 Net/HTTP、Open URI 等。在本例中,我们将使用 HTTParty,因为它是最流行的主流 gem 之一。

为了解析网站的回应,我们将使用另一种叫做 Nokogiri 的 gem。

最后,为了将数据导出为 CSV 文件,我们将使用 CSV gem。

要安装这些 gem,请打开终端并运行以下命令:

gem install httparty
gem install nokogiri
gem install cs

或者,在存放代码文件的目录中创建一个新文件,并将其保存为 Gemfile。在这个文件中,输入以下几行:

source 'https://rubygems.org'
gem 'nokogiri'
gem 'httparty'
gem 'csv'

打开终端,导航到该目录,并执行以下命令:

bundle

如此即可安装 Gemfile中列出的所有 gem。

提出 HTTP 请求

用 Ruby 进行网页抓取的第一步是发送一个 HTTP 请求。

在浏览器中加载一个页面时,浏览器向该网页发送 HTTP GET 请求。这个操作可以使用 HTTParty 复制,只需要以下两行

require 'httparty'
response = HTTParty.get('https://books.toscrape.com/')

第一行是一个 require 语句,只需要写一次。实质上,发送 HTTP 请求只需要一行代码。

使用此方法返回的响应对象包含许多有用的属性。例如,response.code 包含 HTTP 请求的状态代码。成功的 HTTP 状态代码是 200。实际返回的 HTML 包含在 response.body 中。

如果您只是想看一下响应的 HTML,可以在代码中添加以下几行:

if response.code == 200
puts response.body
else
puts "Error: #{response.code}"
exit
end

对于我们的网络爬虫来说,这个特定的 HTML 字符串并没那么有用,这意味着还需要另一个步骤来从这个页面提取特定的信息。

此时就要用到 HTML 解析,由 Nokogiri gem 来表示

用 Nokogiri 解析 HTML

我们可以使用 Nokogiri 的 HTML4 模块来解析 HTML 字符串中的数据。

注意,在 Nokogiri 1.12 版本之前,模块 HTML4 还没出现。如果您使用的是早期版本,请使用 HTML 模块代替。事实上,在较新版本的 Nokogiri 中,您仍然可以使用 HTML 模块,这不仅仅是 HTML4 模块的别名。

第一步是添加 require语句。

require 'nokogiri'

接下来,调用 HTML4 并发送 HTML 字符串。在 response.body中可以找到这个字符串:

document = Nokogiri::HTML4(response.body)

这个文档对象包含解析后的数据,可以使用 CSS 选择器或 XPath 查询这些数据以获得特定的 HTML 元素。在本文中,我们将使用 CSS 选择器来定位 HTML 元素。

我们将从我们的目标网站上提取三种类型的公共数据:书名(title)价格(price)可得性(availability)。首先,我们将为保存所有这些信息的容器创建一个选择器。然后,我们将在所有这些容器上运行循环以提取数据。

每本书都包含在 HTML 元素 article中,其 class设置为 product_pod。使用此信息,以下代码行将返回所有书籍容器元素:

all_book_containers = document.css('article.product_pod')

生成的元素可以使用 CSS 选择器查询,以获得循环中特定的 HTML 元素:

all_book_containers.each do |container|
...
end

书名在容器 h3的标题中,最方便的检索方法如下:

title = container.css('h3 a').first['title']

或者,也可以使用缩略图的 alt属性来检索书名。注意,不是所有的网站都使用正确的 alt文本,因此您不能将其作为一种通用方法:

title = container.css('.image_container > a > img').first['alt']

.css() 函数返回一个数组。这就是为什么我们需要取第一个元素,然后检索出 alt 属性的值。

提取价格比较容易,因为它是文本。

price = container.css('.price_color').text

它还会返回一个货币符号。使用 delete功能可以很容易地排除符号。下面将删除数字和句号以外的所有内容:

price = container.css('.price_color').text.delete('^0-9.')

同样,可得性也可以提取如下:

availability = container.css('.availability').text.strip

请注意这里使用的是 strip 。这将删除换行符和空格。

我们刚刚提取的三个字段可用于创建一个数组,而该数组又可以附加到另一个数组中。因此,在循环结束时,所有信息都将以数组的形式出现。下面是整个循环:

all_book_containers = document.css('article.product_pod')
books = []
all_book_containers.each do |container|
title = container.css('.image_container > a > img').first['alt']
price = container.css('.price_color').text.delete('^0-9.')
availability = container.css('.availability').text.strip
book = [title, price, availability]
books << book
end

该网站的分页是一个简单的数字序列。我们可以看到,共有 50 个页面,每个页面的 URL 都遵循这个模式:

https://books.toscrape.com/catalogue/page-1.html
https://books.toscrape.com/catalogue/page-2.html
...
https://books.toscrape.com/catalogue/page-50.html

由于上面的序列并不复杂,我们可以用一个简单的循环来搞定。以下是分页的代码:

books = []
50.times do |i|
	url = "https://books.toscrape.com/catalogue/page-#{i + 1}.html"
  	response = HTTParty.get(url)
 	document = Nokogiri::HTML(response.body)
  	all_book_containers = document.css('article.product_pod')

 	all_book_containers.each do |container|
		title = container.css('.image_container > a > img').first['alt']
   	price = container.css('.price_color').text.delete('^0-9.')
	availability = container.css('.availability').text.strip
    	book = [title, price, availability]
   	books << book
 	end

 end

将抓取的数据写入 CSV 文件

在将数据提取到一个数组中后,可以使用 CSV gem 来导出数据。

首先在代码文件中添加 require 语句:

require 'csv'

第一步是在写入或追加模式下打开一个 CSV 文件。在打开 CSV 时,编写头文件也是一个好主意。

考虑到简洁性,我们可以发送一个书名、价格和可得性的数组。

最后,读取数据并立即写入,如果脚本在执行过程中失败,至少可以在 CSV 文件中保留一些块状数据。

下面的代码将所有内容放在一起:

CSV.open('books.csv', 'w+',
         write_headers: true,
         headers: %w[Title Price Availability]) do |csv|
 
  50.times do |i|
    url = "https://books.toscrape.com/catalogue/page-#{i + 1}.html"
    response = HTTParty.get(url)
    document = Nokogiri::HTML(response.body)
    all_book_containers = document.css('article.product_pod')

    all_book_containers.each do |container|
       title = container.css('h3 a').first['title']
       price = container.css('.price_color').text.delete('^0-9.')
       availability = container.css('.availability').text.strip
       book = [title, price, availability]
 
      csv << book
     end
   
 end

就是这样!您已经成功地用 Ruby 创建了一个网络爬虫,可以通过提取数据来创建 CSV 文件。

抓取动态网页

上一节中讨论的使用 HTTParty 的方法在从静态网页上抓取公共数据时非常有效。然而,它并不适合于动态页面。HTTParty 从请求的 URL 获得响应,但它并不渲染页面,这意味着任何单独加载的数据根本不会被加载。

有多种 gem 能帮助解决这个问题。其中最受欢迎的 gem 是 Kimurai。然而,Kimurai 还没有更新,不能与 Ruby 版本 3 一起使用。

幸运的是,能运行无头浏览器的 Selenium 可与 Ruby 完美配合。官方 Selenium 文档中有 Ruby 的代码示例。

必要的安装

在开始使用 Selenium 之前,请安装首选的互联网浏览器。最常用的是 Chrome 和 Firefox 浏览器。

浏览器安装好之后,按照说明安装 selenium-webdriver gem。

gem install selenium-webdriver

此外,如果需要,请安装 CSV gem。

gem install csv

加载动态网站

在本例中,我们选择了一个动态渲染的网站 https://quotes.toscrape.com/js/

在我们发送 HTTP 请求以加载页面之前,需要进行一些初始设置。

首先,在文件顶部添加 require语句。

require 'selenium-webdriver'

接下来,为 Chrome 创建一个 Selenium WebDriver 实例。

driver = Selenium::WebDriver.for(:chrome)

请注意,这将创建一个可见的 Chrome 浏览器实例,也就是有头浏览器。在开发过程中,这是可行的,但在理想情况下,最好使用不可见或无头浏览器。

要实现无头浏览器,首先要创建 Chrome::Options的实例,并将无头设置为 false。然后,发送一个实例作为 capabilities参数,如下所示:

options = Selenium::WebDriver::Chrome::Options.new
options.headless!
driver = Selenium::WebDriver.for(:chrome, capabilities: [options])

最后,调用 get方法来加载网页:

driver.get 'https://quotes.toscrape.com/js/'

页面现在已经加载,可以查询 HTML 元素了。

通过 CSS 选择器定位 HTML 元素

可以使用 driver.page_source方法提取 HTML。您也可以根据自己的喜好使用特定的 HTML 来创建 Nokogiri 文档的对象,如下所示:

document = Nokogiri::HTML(driver.page_source)

Selenium 有自己独特的方法来查询 HTML 元素。

使用的函数是 find_elements。它需要一个 cssxpath 参数来定位 HTML 元素,并返回一个所有匹配元素的数组。

如果您只需要一个元素,这个函数还有一个 find_element 的变体,只返回第一个匹配元素。

这个网络爬虫的总体方法与前一个相同:

  1. 使用 CSS 选择器找到包含每个引用的 HTML 元素。

  2. 在这些元素上运行循环。

  3. 使用 CSS 选择器提取引用文本和作者。

  4. 针对每个引用创建一个数组。

  5. 将该数组追加到一个外部数组。

每个引用都包含在一个带有 class quotediv元素中。CSS 选择器可以是简单的  .quote。第一段代码如下:

1 | quote_container = driver.find_elements(css: '.quote')
2 | quote_container.each do |quote_el|
3 | # Scrape each quote
4 | end

引用文本的 CSS 选择器是  .text。为了获得这个内联元素内部的文本,我们可以调用带有 textContent参数的 attribute函数。

quote_text = quote_el.find_element(css: '.text').attribute('textContent')

同样,获取作者也很简单:

author = quote_el.find_element(css: '.author').attribute('textContent')

然后,这些信息可以存储在一个数组中。归纳起来,这就是现阶段代码的样子:

quotes = []
quote_elements = driver.find_elements(css: '.quote')
quote_elements.each do |quote_el|
  quote_text = quote_el.find_element(css: '.text').attribute('textContent')
  author = quote_el.find_element(css: '.author').attribute('textContent')
  quotes << [quote_text, author]
end

使用数组来存储抓取的公共数据的另一个好处是,我们可以轻易将其保存到 CSV 文件中。

我们的示例网站有一个指向下一页的按钮,因此我们可以创建一个查找下一个按钮的选择器。一旦找到按钮,就可以调用 click() 函数来按下这个按钮。在最后一页,找不到下一个按钮,造成错误。

我们可以用 while-true 循环来抓取数据。使用 begin-rescue-end 块来终止该循环,如下所示:

quotes = []
 while true do
   quote_elements = driver.find_elements(css: '.quote')
   quote_elements.each do |quote_el|
     quote_text = quote_el.find_element(css: '.text').attribute('textContent')
     author = quote_el.find_element(css: '.author').attribute('textContent')
     quotes << [quote_text, author]
   end
   begin
     driver.find_element(css: '.next >a').click
   rescue
     break # Next button not found
   end
 end

创建 CSV 文件

我们可以使用前一节中讨论的相同方法来创建 CSV 文件。

创建带有头信息的 CSV 文件。在所有引用上运行循环,并将其保存为一行:

require 'csv'
 
 CSV.open('quotes.csv', 'w+', write_headers: true,
          headers: %w[Quote Author]) do |csv|
   quotes.each do |quote|
     csv << quote
   end
 end

注意,除非您对自己的脚本有绝对的把握,否则将数据加载到内存中然后再写入可能不是最理想的方法。最好是边抓边写,以提升一致性。

就是这样!您已经成功创建了一个网络爬虫,用于从动态网站中提取公共数据,并将数据保存到 CSV 文件中。

结语

用 Ruby 进行网页抓取的关键在于寻找和选择正确的 gem。对于从发送 HTML 请求到创建 CSV 文件的网页抓取过程的所有步骤,都有相应的 gem 可使用。Ruby 的 gem,如 HTTParty 和 Nokogiri,非常适用于具有固定 URL 的静态网页。对于动态网页,Selenium 非常好用,而 Kimurai 是旧版本 Ruby 的首选 gem,因为它是基于 Nokogiri 的便捷版本。

点击此处找到本文中使用的完整代码,便于大家使用。如欲了解更多关于如何使用其他编程语言进行网页抓取的信息,请阅读我们的其他文章,如用 R 语言进行网页抓取用 C# 进行网页抓取,以及 Python 网页抓取教程

关于作者

Augustas Pelakauskas

文案

Augustas Pelakauskas 在 Oxylabs 担任一名文案策划人。拥有艺术家庭背景,他全身心地投入到各种创意项目中 - 最近的他都在写作。验证了他在自由新闻领域的能力后,他转到了科技内容创作。闲暇时,他喜欢阳光明媚的户外活动和运动康乐。事实证明,自行车则是他的第三个好朋友。

Oxylabs博客上的所有信息均按“原样”提供,仅供参考。对于您使用Oxylabs博客中包含的任何信息或其中可能链接的任何第三方网站中包含的任何信息,我们不作任何陈述,亦不承担任何责任。在从事任何类型的抓取活动之前,请咨询您的法律顾问,并仔细阅读特定网站的服务条款或取得抓取许可。

选择Oxylabs®,业务更上一层楼


隐私政策

oxylabs.cn© 2022 保留所有权利©