使用 Scrapy 搭建网络爬虫框架

大部分内容是直接翻译 Scrapy 文档

当今互联网上的文字信息绝大多数以 HTML 的形式存在。为了利用信息,我们需要使用网络爬虫,按照特定的规则爬取网络上的信息,并以适当的方式存储在数据库中。本小组开发的食物营养信息搜索引擎也需要爬取现有网络数据库的信息。本课程论文将简要介绍 Scrapy 爬虫框架,使用此框架开发简单爬虫程序的方法,并列举 Scrapy 的其它高级特性。

Scrapy 介绍

Scrapy 是一款快速的高级爬虫和页面抓取框架,它可以爬取网站,并从网页中提取结构化的数据。Scrapy 可以被用于多种不同用途,包括数据挖掘、网页检测以及自动化测试等。Scrapy 提供了交互式命令行操作、基于 CSS 选择器和 XPath 语法的网页提取操作、便捷的数据导出功能,以及丰富的可扩展性,使得爬虫工作变得简单方便。

Scrapy 架构

Scrapy 框架由若干模块组成,主要的模块包括:

  • 爬虫模块(Spider):这是用户自己创建的类,用于指定页面爬取逻辑。具体来讲,用户需要指定如何在每个响应(Response)中获取数据条目(item),以及生成新的请求(Request)。
  • 引擎模块(Engine):负责控制整个数据流,并适时触发事件处理。
  • 下载模块(Downloader):负责下载网页内容。
  • 调度模块(Scheduler):负责暂时保存爬虫模块生成的请求,将其加入请求队列,以备后续执行。
  • 条目管道(Item pipeline):负责处理爬虫模块产生的每个数据条目,包括数据清洗、验证以及持久化操作。

Scrapy 数据流

下图展示了 Scrapy 框架中数据的流动过程,理解该图有助于对 Scrapy 的工作原理有一个整体的认识:

Scrapy 数据流

  1. 引擎从爬虫模块中获取初始请求。
  2. 引擎将初始请求发送给调度模块。
  3. 引擎从调度模块中获取下一个请求。
  4. 引擎将请求发送给下载模块。这一过程中,请求会经过一系列下载中间件(Downloader Middlewares)。这些中间件可以修改(例如增加 cookie 和 HTTP 验证)、新增(例如自动重试)或过滤请求。
  5. 下载模块将响应传回引擎。这一过程中,引擎同样会经过下载中间件。这些中间件可能会修改响应内容。
  6. 引擎将响应发送给爬虫模块。
  7. 爬虫模块按照用户提供的逻辑,对响应内容进行分析,并返回抓取到的数据条目以及进一步的请求。
  8. 引擎将数据条目送到条目管道进行处理,将新的请求存入调度模块待后续执行。
  9. 从第 3 步开始,重复上述步骤,直到调度模块中不再有新的请求。

使用 Scrapy

安装与初始化

最简单的方式是使用 pip 安装 Scrapy:

pip install Scrapy

然后使用以下命令来初始化 Scrapy 项目:

scrapy startproject <project-name>

这将会在当前目录创建 <project-name> 文件夹,文件夹目录如下:

<project-name>/
    scrapy.cfg           # 部署 Scrapy 的配置文件
    <project-name>/            # Python 模块根目录
        __init__.py
        items.py         # 用于定义数据条目
        middlewares.py   # 用于定义中间件
        pipelines.py     # 用于定义条目管道
        settings.py      # 用于工程配置
        spiders/         # 爬虫模块目录
            __init__.py

其中的很多文件都和前面对 Scrapy 的描述对应。由于我们编写的爬虫程序较为简单,在 <project-name>/spiders 文件夹中创建 spider.py,在其中实现爬虫模块即可,其他文件只需要稍微改动。

创建初始请求

spider.py 中,我们需要新建一个爬虫类,这个类应该继承 scrapy.Spider。其中的类属性 name 是该爬虫类的名称,start_urls 是初始请求的 URL。

class MySpider(scrapy.Spider):
  name = 'my-spider'
  start_urls = [
    'http://example.com/starting-url'
  ]

start_urls 中的每个 URL 对应一个初始请求,这些请求是爬虫的起点。我们也可以创建 self.start_requests 函数,手动控制此过程:

def start_requests(self):
  urls = [
    'http://example.com/starting-url'
  ]
  for url in urls:
    yield scrapy.Request(url=url, callback=self.parse)

这些请求会被引擎模块抓取,送往下载模块,得到返回响应,再交给 Spider 对象中的解析函数。默认情况下,解析函数为 self.parse,也可以使用 callback 参数来指定。我们可以在解析函数中对响应做任何事情,比如,将响应内容(即抓取的网站内容)保存到文件:

def parse(self, response):
  filename = 'page.html'
  with open(filename, 'wb') as f:
    f.write(response.body)

提取网页数据

当然,解析函数的主要功能是提取页面信息。我们可以在解析函数中直接使用其它解析工具(如 lxml 和 BeautifulSoup)来处理网站内容,也可以利用 Scrapy 自带的解析工具完成信息提取。这里我们使用后者,因为 Scrapy 的解析功能足够强大。

使用 CSS 选择器

我们可以直接使用 CSS 选择器选择对应的元素:

headers = response.css('.text-box > h4')

该函数返回的是一个 SelectorList 类型的对象,表示一组包装了符合条件的 HTML 元素的 Selector 对象。对 SelectorList 对象可以继续选择:

anchors = result.css('a')

也可以使用 for 循环分别处理 SelectorList 中的每个元素:

for header in headers:
  anchors = header.css('a')
  # 分别处理 anchors

SelectorSelectorList 使用 get 方法,可以得到首个结果对应的字符串:

result.css('a').get()
'<a href="http://example.com/some/link">Link</a>'

使用 getall 方法,可以得到含有所有结果的字符串数组:

result.css('a').getall()
['<a href="https://example.com/some/link">Link</a>', '<a href="https://example.com/some/other/link">Link 2</a>']

这样写,字符串会包括 HTML 标签。由于 CSS 选择器不能选择标签内的文本,Scrapy 对 CSS 选择器做了扩展,添加 ::text 可以获得标签内文本:

result.css('a::text').getall()
['Link', 'Link 2']

添加 ::attr(href) 可以获取标签的 href 属性(括号里的内容可以是任意属性名):

result.css('a::attr(href)').getall()
['https://example.com/some/link', 'https://example.com/some/other/link']

使用 XPath

XPath 是比 CSS 选择器更强大的选择工具,实际上,Scrapy 的 CSS 选择器功能就是用 XPath 实现的。例如,上面的操作可以用 XPath 实现:

links = response.xpath('//*[@class="text-box"]/h4/a/@href')

XPath 还可以执行更复杂的操作。它可以读取标签内容,并选中特定一个元素,比如,选择「含有『分类』字样的 b 标签后面的第二个 p 元素的 class 属性」:

classes = response.xpath('//b[contains(., "分类")]/following-sibling::p[2]/@class')

使用 XPath 选择时,也可以使用 getgetall 方法。

提取数据条目

每个数据条目可以作为普通的 dict 导出,也可以作为 Item 对象导出。这里为了方便,我们直接使用 dict 导出数据。在 parse 方法中,直接使用 yield 返回数据条目:

yield {
  "links": links,
  "classes": classes
}

即可提取出这一条目。该条目会被 Scrapy 引擎送入条目管道进一步处理。

生成更多请求

有时,我们需要跟踪当前页面上的链接,爬取其他网页的数据。我们可以使用 yield 返回一个 Request 对象,该对象会被 Scrapy 引擎送入调度模块:

for link in links:
  link = response.urljoin(link)
  yield scrapy.Request(link, callback=self.parse_link)

由于 link 可能是相对 URL,我们需要使用 urljoin 方法得到绝对 URL,然后构建 Request 对象。在构建对象时,需要指定该请求的解析函数,因为不同页面可能需要不同的解析函数。

Scrapy 还提供了另外一个 API,可以自动处理相对 URL,省去 urljoin 的步骤:

for link in links:
  yield response.follow(link, callback=self.parse_link)

response.follow 不仅支持传入 URL 字符串,还支持传入 a 标签的 Selector 对象。

接下来我们以如下两个网页为例,展示完整的爬虫类写法:

<body>
  <h1>首页</h1>
  <ul>
    <li><a href="/category-1">分类 1</a></li>
  </ul>
</body>
<body>
  <h1>分类 1</h1>
  <div class="quote">
    <p class="text">
      Genius is one percent inspiration, ninety-nine percent perspiration.
    </p>
    <p class="author">
      Thomas A. Edison
    </p>
  </div>
  <div class="quote">
    <p class="text">
      You’ll never find a rainbow if you’re looking down.
    </p>
    <p class="author">
      Charlie Chaplin
    </p>
  </div>
</body>

若要爬取以上两个网站的名人名言,爬虫类应该这样写:

class QuoteSpider(scrapy.Spider):
  name = 'quote-spider'
  start_urls = [
    'http://example.com'
  ]
  
  def parse(self, response):
    anchors = response.css('ul > li > a')
    for anchor in anchors:
      yield response.follow(anchor, self.parse_category)
  
  def parse_category(self, response):
    quotes = response.css('div.quote')
    for quote in quotes:
      yield {
        "text": quote.css('p.text::text').get(),
        "author": quote.css('p.author::text').get()
      }

导出数据条目

由于我们在解析函数中已经将数据进行了结构化处理,只需要将每个数据条目按照原样导出即可。为此,Scrapy 提供了直接将数据导出为 JSON、CSV 和 XML 等格式的 feed export 功能。要想使用 feed export,需要在 settings.py 中添加以下字段:

FEEDS = {
  'items.json': {
    'format': 'json',
    'encoding': 'utf8',
    'store_empty': False,
    'fields': None,
    'indent': 2,
    'item_export_kwargs': {
      'export_empty_fields': True,
    },
  }
}

该设置指定了输出文件名 items.json、文件格式 json,以及其它的配置。有了这一配置,我们不需要在 pipeline.py 中指定条目管道的具体操作,Scrapy 会为我们解决。

配置和运行爬虫

settings.py 中,还有许多可以配置的选项。例如,日志默认在控制台输出,以下配置可以指定日志的输出文件和级别:

LOG_FILE = 'scrapy.log'
LOG_LEVEL = 'INFO'

我们可以安装第三方的中间件 scrapy-user-agents,达到随机生成 user agent 的效果。使用 pip 下载:

pip install scrapy-user-agents

然后在 settings.py 中禁用掉默认的 user agent 中间件,并使用 scrapy-user-agents 提供的中间件:

DOWNLOADER_MIDDLEWARES = {
  'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
  'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
}

上面字典中的数字代表中间件的顺序。中间件数字越小,就会在请求从引擎发送到下载模块时先执行,响应从下载模块发送到引擎时后执行。

Scrapy 默认以尽量快的速度发送请求,这可能会导致目标服务器无法响应,或者触发反爬虫机制。为此,在 settings.py 中可以添加以下配置,指定同一网站请求的发送间隔:

DOWNLOAD_DELAY = 0.5  # 每隔 0.5 秒发送一个请求

配置完成后,只需使用以下命令启动爬虫:

scrapy crawl <project-name>

Scrapy 高级操作

在个人编写简单的爬虫框架时,以上功能已经完全足够。本节我们列举 Scrapy 的一些高级功能,以供进一步参考:

  • Scrapy 可以支持自动节流,根据响应的成功状态自动决定请求速率。这一功能需要用到内置的 AutoThrottle 插件。
  • Scrapy 可以部署到云服务器上,从而支持长期运行和大量的爬虫操作。
  • Scrapy 可以实现分布式部署、使用 IP 池等技术,以绕过目标网站的 IP 限制。
  • Scrapy 在流程中的许多步骤都会发送信号,用户可以编写自己的插件,捕获这些信号,扩展 Scrapy 的功能。

留下评论

注意 评论系统在中国大陆加载不稳定。

回到顶部 ↑