跳至主要内容

使用 Scrapy 在 Python 中构建网络爬虫

使用 Scrapy 开发网络爬虫,这是一款用于提取、处理和存储网页数据的强大框架。
· 14分钟 阅读

使用 AI 探索

如果您想先了解 Python 的网页抓取概览,建议学习 DataCamp 的 Web Scraping with Python 课程。

在本教程中,您将学习如何使用 Scrapy——一个能够处理海量数据的 Python 框架!我们将通过为电商网站 AliExpress.com 构建一个网页抓取器来学习 Scrapy。开始抓取吧!

具备基础的 HTML 和 CSS 知识将有助于您更轻松、更快速地理解本教程。想要快速回顾,可阅读这篇关于 HTML 和 CSS 的文章。

Scrapy 概览

scrapy overview
来源

网页抓取已成为从网络中提取信息以支持决策与分析的有效方式,也是数据科学工具箱中的重要组成部分。数据科学家应当掌握如何从网页收集数据,并将其以不同格式保存以供后续分析。

您在互联网上看到的任何网页都可以被爬取以获取信息,且网页上任何可见内容都可以被提取 [2]。每个网页都有其独特的结构与元素,因此您需要根据目标页面来编写相应的抓取器/蜘蛛。

Scrapy 提供了一个强大的框架,可用于提取数据、处理数据并将其保存。

Scrapy 使用 spiders(蜘蛛),它们是根据一组指令运行的独立爬虫 [1]。在 Scrapy 中,开发者可以复用代码,从而更容易构建与扩展大型爬取项目。

Scrapy 与 BeautifulSoup 对比

本节将概述另一款广泛使用的网页抓取工具 BeautifulSoup,并与 Scrapy 进行比较。

Scrapy 是一个用于网页抓取的 Python 框架,为开发者提供了完整的一揽子方案,无需过多担心代码的维护。

Beautiful Soup 也被广泛用于网页抓取。它是一个用于解析 HTML 和 XML 文档并从中提取数据的 Python 包,支持 Python 2.6+ 和 Python 3。

简而言之,它们的差异如下:

ScrapyBeautifulSoup
功能性---
Scrapy 是下载网页、处理内容并将其保存到文件或数据库的完整方案BeautifulSoup 本质上是 HTML 和 XML 解析器,需借助其他库如 requests、urlib2 来打开 URL 并存储结果 [6]
学习曲线---
Scrapy 是网页抓取的强力工具,提供多种抓取网页的方式。理解其工作机制需要更多时间,但一旦掌握,便可用一行命令创建并运行爬虫。要成为 Scrapy 专家可能需要一些练习与时间来熟悉所有功能。对编程新手来说,BeautifulSoup 相对容易上手,可在短时间内完成较小的任务
速度与负载---
Scrapy 能轻松完成大型任务。根据链接数量,可在一分钟内爬取一组 URL,并且运行十分流畅,因为它使用 Twister 进行异步(非阻塞)并发。BeautifulSoup 适合高效完成简单的抓取任务。如果不使用 multiprocessing,速度会比 Scrapy 慢。
可扩展性---
Scrapy 提供 Item pipelines,可在蜘蛛中编写函数处理数据,如校验、清洗与保存到数据库。它还提供用于测试蜘蛛的 spider Contracts,并支持创建通用或深度爬虫。此外,可管理重试、重定向等诸多变量。若项目逻辑较少,BeautifulSoup 足以胜任;但如需更多自定义功能(如代理、Cookie 管理、数据管道等),Scrapy 是更佳选择。

信息:同步表示必须等待当前任务完成后才能开始新任务;异步表示在前一个任务尚未完成前即可开始另一个任务。

这里有一篇有趣的 DataCamp BeautifulSoup 教程供您学习。

Scrapy 安装

scrapy

在安装了 Python 3.0(及以上版本)后,如果您使用的是 Anaconda,可通过 conda 安装 scrapy。在 Anaconda Prompt 中输入以下命令:

conda install -c conda-forge scrapy

安装 Anaconda 的方法可参考这些 DataCamp 教程:Mac 与 Windows。

或者,您也可以使用 Python 包管理器 pip。适用于 Linux、Mac 和 Windows:

pip install scrapy

Scrapy Shell

Scrapy 还提供了一个名为 Scrapy Shell 的抓取交互环境,开发者可以用它来测试对站点行为的假设。下面我们以电商网站 AliExpress 的平板电脑页面为例。您可以使用 Scrapy Shell 查看网页返回的组成部分,并据此满足您的需求。

打开命令行,输入以下命令:

scrapy shell

如果您使用的是 Anaconda,也可以在 Anaconda Prompt 中输入上述命令。命令行或 Anaconda Prompt 中的输出将类似如下:

run crawler

您需要在 Scrapy Shell 中使用 fetch 命令对网页运行爬虫。爬虫(或蜘蛛)会遍历网页并下载文本与元数据。

fetch(https://pt.aliexpress.com/category/201005406/special-store.html)

注意:始终用引号包住 URL,单引号或双引号都可以

输出如下:

output

爬虫会返回一个response,您可以在 Shell 中使用 view(response) 查看:

view(response)

随后网页会在默认浏览器中打开。

web page

您可以在 Scrapy Shell 中使用以下命令查看原始 HTML 脚本:

print(response.text)

scrapy shell

您将看到用于生成网页的脚本。这与您在网页空白处右键单击并选择查看源代码或查看页面源代码时看到的内容相同。由于您只需要其中的关键信息,因此可借助浏览器开发者工具来检查所需元素。我们以以下元素为例:

  • 平板名称
  • 平板价格
  • 订单数量
  • 店铺名称

右键单击您想要的元素,然后选择 inspect(检查),如下所示:

inspect

浏览器的开发者工具对网页抓取非常有帮助。您可以看到该元素是一个带有 class 为product 的 <a> 标签,其文本即为产品名称:

inspect

使用 CSS 选择器进行提取

您可以通过元素属性或类等 CSS 选择器来提取。要提取产品名称,在 Scrapy Shell 中输入:

response.css(".product::text").extract_first()

输出为:

output

extract_first() 会提取与 CSS 选择器匹配的第一个元素。若要提取所有产品名称,使用 extract():

response.css(".product::text").extract()

extract product names

以下代码可提取产品的价格区间:

response.css(".value::text").extract()

extract product names

同理,您也可以尝试提取订单数量与店铺名称。

使用 XPath 进行提取

XPath 是一种用于在 XML 文档中选择节点的查询语言 [7]。您可以使用 XPath 在 XML 文档中导航。在底层,Scrapy 通过 Xpath 在 HTML 文档中定位元素。您在上文中使用的 CSS 选择器也会被转换为 XPath,但很多情况下 CSS 更易用。不过,您仍应了解 Scrapy 中 XPath 的工作方式。

回到 Scrapy Shell,像之前一样输入 fetch(https://pt.aliexpress.com/category/201005406/special-store.html/)。尝试以下代码片段 [3]:

response.xpath('/html').extract()

这将显示 <html> 标签下的所有代码。/ 表示节点的直接子节点。如果您想获取 html 标签下的 <div> 标签,可写为 [3]:

response.xpath('/html//div').extract()

在 XPath 中,您必须理解 / 与 // 的用法,以便在子节点与后代节点之间导航。这里有一篇关于 XPath 节点 的教程以及一些可尝试的示例。

如果您想获取所有 <div> 标签,也可以不使用 /html,直接向下钻取 [3]:

response.xpath("//div").extract()

您还可以通过使用属性及其值来进一步过滤起始节点并定位到目标节点。下面是使用类及其值的语法:

response.xpath("//div[@class='quote']/span[@class='text']").extract()

response.xpath("//div[@class='quote']/span[@class='text']/text()").extract()

使用 text() 提取节点内的全部文本

来看下面这段 HTML 代码:

您想获取 <a> 标签内的文本,该标签是带有类 site-notice-container container 的 <div> 的子节点,可以这样做:

response.xpath('//div[@class="site-notice-container container"]/a[@class="notice-close"]/text()').extract()

text inside tag

创建 Scrapy 项目与自定义蜘蛛

网页抓取可用于构建聚合器,以便进行数据对比。例如,您想购买一台平板,希望对比产品与价格,就可以爬取目标页面并将数据存入 Excel 文件。这里我们将抓取 aliexpress.com 上的平板信息。

现在,您将为同一页面创建一个自定义蜘蛛。首先需要创建一个 Scrapy 项目,以存放代码与结果。在命令行或 Anaconda Prompt 中输入:

scrapy startproject aliexpress

scrapy project

这将在您默认的 Python 或 Anaconda 安装目录下创建一个隐藏文件夹。aliexpress 将作为该文件夹名称(您也可自定义)。您可以通过资源管理器直接查看其内容。其结构如下:

folder names
file/folder用途
scrapy.cfg部署配置文件
aliexpress/项目的 Python 模块,您将从此处导入代码
__init.py__初始化文件
items.py项目 items 文件
pipelines.py项目 pipelines 文件
settings.py项目设置文件
spiders/您稍后将把蜘蛛文件放在此目录下
__init.py__初始化文件

创建项目后,切换到新建目录并输入以下命令:

[scrapy genspider aliexpress_tablets](https://pt.aliexpress.com/category/201005406/special-store.html)

command

这将在上述的 spiders 目录下创建名为 aliexpress_tablets.py 的模板文件。文件中的代码如下:

import scrapy

class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html']


    def parse(self, response):
         pass

在上述代码中,您可以看到 name、allowed_domains、sstart_urls 和 parse 函数。

  • name:蜘蛛的名称。合理命名有助于管理您创建的所有蜘蛛。名称必须唯一,因为运行蜘蛛时将使用 scrapy crawl name_of_spider。
  • allowed_domains(可选):一个可选的 Python 列表,包含允许被爬取的域名。不在此列表中的 URL 请求将不会被爬取。此处只应包含网站的域名(例如:aliexpress.com),而不是 start_urls 中的完整 URL,否则会收到警告。
  • start_urls:请求此列表中的 URL。当未指定特定 URL 时,蜘蛛会从这里开始爬取 [4]。因此,首先下载的是这里列出的页面,后续的请求将根据这些起始 URL 中包含的数据逐步生成 [4]。
  • parse(self, response):每当某个 URL 被成功爬取时会调用该函数,也称为回调函数。由爬取得到的 response(与 Scrapy Shell 中相同)将传入该函数,在其中编写提取逻辑!

信息:您可以在 Scrapy 蜘蛛的 parse() 函数中使用 BeautifulSoup 来解析 HTML 文档。

注意:您既可以像在 Scrapy Shell 部分那样,使用 response.css() 通过CSS 选择器提取数据,也可以使用XPath(XML) 访问子元素。您将在 pass() 函数处编辑的代码中看到 response.xpath() 的示例。

接下来我们修改 aliexpress_tablet.py 文件。我在 start_urls 中又添加了一个 URL。您可以将提取逻辑添加到 pass() 函数处,如下所示:

# -*- coding: utf-8 -*-
import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html',
                 'https://www.aliexpress.com/category/200216607/tablets/2.html?site=glo&g=y&tag=']


    def parse(self, response):

        print("procesing:"+response.url)
        #Extract data using css selectors
        product_name=response.css('.product::text').extract()
        price_range=response.css('.value::text').extract()
        #Extract data using xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        #Making extracted data row wise
        for item in row_data:
            #create a dictionary to store the scraped info
            scraped_info = {
                #key:value
                'page':response.url,
                'product_name' : item[0], #item[0] means product in the list and so on, index tells what value to assign
                'price_range' : item[1],
                'orders' : item[2],
                'company_name' : item[3],
            }

            #yield or give the scraped info to scrapy
            yield scraped_info

信息:zip() 接受 n 个可迭代对象并返回一个由元组组成的列表。第 i 个元组的元素由各可迭代对象的第 i 个元素构成。 [8]

yield 关键字用于定义生成器函数。生成器函数与普通函数类似,但用 yield 替代 return。当调用方需要一个值时,包含 yield 的函数会将值“产出”给调用方,同时保留其本地状态,并在下一次继续从中断处执行。这里 yield 将生成的字典交给 Scrapy,之后由其处理与保存!

现在可以运行蜘蛛:

scrapy crawl aliexpress_tablets

您将在命令行看到很长的输出,如下所示:

command
command

导出数据

为了便于后续分析,您可能需要将数据导出为 CSV 或 JSON。本节将介绍如何将这些数据保存为 CSV 与 JSON 文件。

要保存为 CSV 文件,打开项目目录下的 settings.py 并添加以下内容:

FEED_FORMAT="csv"
FEED_URI="aliexpress.csv"

保存 settings.py 后,在项目目录中重新运行 scrapy crawl aliexpress_tablets。

project directory

CSV 文件如下所示:

csv file

注意:每次运行蜘蛛都会向文件追加内容。

  • FEED_FORMAT [5]:设置数据存储的格式。支持的格式包括:
         + JSON
         + CSV
         + JSON Lines
         + XML
    
  • FEED_URI [5]:指定文件的位置。您可以将文件存储在本地文件系统或 FTP 上。

Scrapy 的 Feed Export 还能在文件名中添加时间戳与蜘蛛名称,或用于确定存储目录。

  • %(time)s:在创建导出文件时会被时间戳替换 [5]
  • %(name)s:会被蜘蛛名称替换 [5]

例如:

  • 按蜘蛛分别使用目录存储到 FTP [5]:

ftp://user:[email protected]/scraping/feeds/%(name)s/%(time)s.json

您在 settings.py 中的 Feed 设置会应用到项目中的所有蜘蛛。也可以为某个特定蜘蛛设置自定义配置,以覆盖 settings.py 中的设置。

# -*- coding: utf-8 -*-
import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html',
                 'https://www.aliexpress.com/category/200216607/tablets/2.html?site=glo&g=y&tag=']

    custom_settings={ 'FEED_URI': "aliexpress_%(time)s.json",
                       'FEED_FORMAT': 'json'}

    def parse(self, response):

        print("procesing:"+response.url)
        #Extract data using css selectors
        product_name=response.css('.product::text').extract()
        price_range=response.css('.value::text').extract()
        #Extract data using xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        #Making extracted data row wise
        for item in row_data:
            #create a dictionary to store the scraped info
            scraped_info = {
                #key:value
                'page':response.url,
                'product_name' : item[0], #item[0] means product in the list and so on, index tells what value to assign
                'price_range' : item[1],
                'orders' : item[2],
                'company_name' : item[3],
            }

            #yield or give the scraped info to Scrapy
            yield scraped_info

response.url 返回生成该响应的页面 URL。使用 scrapy crawl aliexpress_tablets 运行爬虫后,您可以查看生成的 json 文件:

json file

您可能已经注意到,在 start_urls 中有两个链接。第二个链接是同一平板搜索结果的第 2 页。将所有链接都手动添加是不现实的。理想状态是:爬虫应能自行遍历所有页面,start_urls 中只需提供起点。

如果某个页面有后续页面,您会在页面底部看到用于前后翻页的导航器。在本教程场景中,您会看到如下所示:

inspect

其对应的代码如下:

code

如您所见,其中有一个带有类 .ui-pagination-active 的 <span> 标签,表示当前页面;在其后方是所有指向下一页的 <a> 标签。每次都需要获取这个 <span> 标签之后的 <a> 标签。这里需要一点 CSS 知识!在这种情况下,您要获取的是兄弟节点而非子节点,因此需要编写一个 CSS 选择器,指示爬虫查找带有 .ui-pagination-active 类的 <span> 标签后面的 <a> 标签。

请记住!每个网页都有其自身的结构。您需要稍微研究一下结构,看看如何获取目标元素。务必先在 Scrapy Shell 中尝试 response.css(SELECTOR),再写入代码。

按如下方式修改 aliexpress_tablets.py:


import scrapy


class AliexpressTabletsSpider(scrapy.Spider):
    name = 'aliexpress_tablets'
    allowed_domains = ['aliexpress.com']
    start_urls = ['https://www.aliexpress.com/category/200216607/tablets.html']


    custom_settings={ 'FEED_URI': "aliexpress_%(time)s.csv",
                       'FEED_FORMAT': 'csv'}

    def parse(self, response):

        print("procesing:"+response.url)
        #Extract data using css selectors
        product_name=response.css('.product::text').extract()
        price_range=response.css('.value::text').extract()
        #Extract data using xpath
        orders=response.xpath("//em[@title='Total Orders']/text()").extract()
        company_name=response.xpath("//a[@class='store $p4pLog']/text()").extract()

        row_data=zip(product_name,price_range,orders,company_name)

        #Making extracted data row wise
        for item in row_data:
            #create a dictionary to store the scraped info
            scraped_info = {
                #key:value
                'page':response.url,
                'product_name' : item[0], #item[0] means product in the list and so on, index tells what value to assign
                'price_range' : item[1],
                'orders' : item[2],
                'company_name' : item[3],
            }

            #yield or give the scraped info to scrapy
            yield scraped_info


            NEXT_PAGE_SELECTOR = '.ui-pagination-active + a::attr(href)'
            next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
            if next_page:
                yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse)

上述代码中:

  • 您首先使用 next_page = response.css(NEXT_PAGE_SELECTOR).extract_first() 提取下一页的链接;若变量 next_page 得到非空链接,则进入 if 语句体。

  • response.urljoin(next_page):parse() 方法会用此方法构建新 URL,并创建一个新的请求,该请求稍后会发送至回调函数。 [9]

  • 获取新 URL 后,会再次抓取该链接,执行 for 语句体并继续寻找下一页。该过程会一直持续,直到找不到下一页链接。

现在您可以放松一下,欣赏蜘蛛自动抓取所有页面。上述蜘蛛将会从所有后续页面提取数据。这将是大量的抓取!但您的蜘蛛能胜任!下图可见文件大小已达到 1.1MB。

jupyter file

Scrapy 为您搞定一切!

本教程带您了解了 Scrapy、它与 BeautifulSoup 的比较、Scrapy Shell,以及如何在 Scrapy 中编写自己的蜘蛛。Scrapy 替您承担了大量繁琐的编码工作:从创建项目文件与文件夹到处理重复 URL,它让您在几分钟内完成高强度的网页抓取,并提供对常见数据格式的支持,方便您在其他程序中继续处理。此教程将帮助您理解 Scrapy 及其框架,以及它能为您做什么。要精通 Scrapy,您还需要深入学习它的各种强大功能,但本教程已让您能够高效地抓取成组网页。

欲进一步阅读,请参阅 Scrapy 官方文档。

此外,别忘了查看 DataCamp 的 Web Scraping with Python 课程。

参考资料

主题