Scrapy与Python 3网页抓取实战:从入门到精通

引言

网页抓取,通常被称为网络爬虫或网络蜘蛛,是指以程序化方式浏览一系列网页并从中提取数据的行为。它是处理网络数据的强大工具。

使用网络爬虫,你可以提取关于一系列产品的数据、获取大量文本或数值数据进行处理、从没有官方API的网站检索数据,或者仅仅满足个人的好奇心。

在本教程中,您将了解网页抓取和爬虫过程的基本知识,同时探索一个有趣的数据集。我们将使用“Quotes to Scrape”,这是一个托管在专门用于测试网络爬虫的网站上的引语数据库。通过本教程的学习,您将得到一个功能完备的Python网络爬虫,它能够浏览包含引语的一系列页面,并在您的屏幕上显示它们。

这个网络爬虫将很容易进行扩展,你可以随心所欲地修改它,并将其作为从网络上抓取数据的基础,用于你自己的项目。

先决条件

完成本教程,你需要一个适用于Python 3的本地开发环境。你可以按照《如何安装和配置Python 3的本地编程环境》的指南来配置所需的一切。

第一步 – 创建基本的网络爬虫

抓取数据是一个两步过程:

  1. 系统地查找和下载网页。
  2. 从下载的网页中提取信息。

这两个步骤可以用多种语言的多种方式来实现。

您可以使用编程语言提供的模块或库从头开始构建一个爬虫,但随着爬虫变得更加复杂,您可能需要处理一些潜在的麻烦。例如,您需要处理并发,以便同时抓取多个页面。您可能还想办法将抓取的数据转换为不同的格式,如CSV、XML或JSON。有时您还需要处理一些需要特定设置和访问模式的网站。

如果你在现有处理这些问题的库的基础上构建你的爬虫,你会更顺利。在本教程中,我们将使用Python和Scrapy来构建我们的爬虫。

Scrapy是最受欢迎、功能强大的Python爬虫库之一;它采用“电池已装入”的方式来进行爬虫,这意味着它处理了大部分常见的功能,避免了开发者在每次开发中重复造轮子。

Scrapy和大多数Python包一样,可以在PyPI(也称为pip)上找到。PyPI是Python软件的社区维护仓库,收录了所有已发布的Python软件。

如果您已经按照本教程先决条件中所概述的方式安装了Python,那么您的电脑上已经安装了pip,因此您可以使用以下命令安装Scrapy:

  1. pip install scrapy

如果您在安装中遇到任何问题,或者想要在不使用pip的情况下安装Scrapy,请查阅官方的安装文档。

在安装了Scrapy后,为我们的项目创建一个新的文件夹。你可以通过在终端运行以下命令来实现:

  1. mkdir quote-scraper

现在,导航进入你刚刚创建的新目录:

  1. cd quote-scraper

然后,在我们的爬虫中创建一个名为scraper.py的新Python文件。在本教程中,我们将把所有的代码都放在这个文件中。您可以使用您选择的编辑软件创建此文件。

通过使用Scrapy作为基础,开始项目的过程。要做到这一点,您需要创建一个Python类,该类以scrapy.Spider为基类,并由Scrapy提供的基本爬虫类。这个类将有两个必需的属性:

  • name — 爬虫的名称。
  • start_urls — 你开始抓取的URL列表。我们将从一个URL开始。

在文本编辑器中打开scraper.py文件,并添加以下代码来创建基本的爬虫:

爬虫.py

import scrapyclass QuoteSpider(scrapy.Spider):    name = 'quote-spider'    start_urls = ['https://quotes.toscrape.com']

让我们逐行分析这段代码:

首先,我们导入Scrapy以便我们可以使用该软件包提供的类。

接下来,我们使用Scrapy提供的Spider类,并以其为基类创建一个名为QuoteSpider的子类。将子类视为其父类的更专业形式。Spider类具有定义如何跟随URL和从找到的页面提取数据的方法和行为,但它不知道要查找的位置和要查找的数据。通过创建一个子类,我们可以提供这些信息给它。

最后,我们命名这个类为quote-spider,并给我们的爬虫一个起始URL:https://quotes.toscrape.com。如果你在浏览器中打开这个URL,它会带你进入一个搜索结果页面,展示了许多著名引语的第一页。

现在,测试一下爬虫。通常,Python文件会通过类似python path/to/file.py这样的命令来运行。然而,Scrapy自带了自己的命令行界面,以简化启动爬虫的过程。请使用以下命令开始你的爬虫:

  1. scrapy runspider scraper.py

该命令将会输出类似以下内容的结果:

输出
2022-12-02 10:30:08 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.epollreactor.EPollReactor
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet Password: b4d94e3a8d22ede1
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
...
'scrapy.extensions.logstats.LogStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
...
'scrapy.downloadermiddlewares.stats.DownloaderStats']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
...
'scrapy.spidermiddlewares.depth.DepthMiddleware']
2022-12-02 10:30:08 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider opened
2022-12-02 10:30:08 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2022-12-02 10:30:08 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2022-12-02 10:49:32 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com> (referer: None)
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Closing spider (finished)
2022-12-02 10:30:08 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 226,
...
'start_time': datetime.datetime(2022, 12, 2, 18, 30, 8, 492403)}
2022-12-02 10:30:08 [scrapy.core.engine] INFO: Spider closed (finished)

这是大量的输出,所以让我们分开来看看。

这是文章《如何使用Scrapy和Python 3抓取网页》的第2部分(共9部分)。

  • 爬虫初始化并加载了处理URL数据读取所需的额外组件和扩展。
  • 它使用了我们在start_urls列表中提供的URL,并抓取了HTML,就像你的网络浏览器所做的那样。
  • 它将该HTML传递给parse方法,该方法默认情况下不执行任何操作。由于我们从未编写自己的parse方法,因此爬虫在未执行任何工作的情况下就结束了。

现在,让我们从页面中提取一些数据。

第二步 – 从页面中提取数据

我们已经创建了一个非常基础的程序,它可以下载一个页面,但目前还没有进行任何数据获取或爬取操作。让我们给它一些需要提取的数据。

如果你看一下我们想要抓取的页面,你会看到它具有以下的结构:

  • 有一个在每个页面上都存在的页眉。
  • 有一个登录链接。
  • 然后是引言本身,以表格或有序列表的形式显示。每个引言都具有相似的格式。

当撰写网络爬虫时,你将需要查看HTML文件的源代码并熟悉其结构。这是简化版的源代码,为了便于阅读已经移除了与我们目标无关的标签。

quotes.toscrape.com
<body>
...
    <div class="quote" itemscope itemtype="http://schema.org/CreativeWork">
        <span class="text" itemprop="text">“我没有失败。我只是发现了一万种行不通的方法。”</span>
        <span>作者:<small class="author" itemprop="author">托马斯·A·爱迪生</small>
        <a href="/author/Thomas-A-Edison">(关于)</a>
        </span>
        <div class="tags">
            标签:
            <meta class="keywords" itemprop="keywords" content="爱迪生,失败,励志,转述" /> 
            
            <a class="tag" href="/tag/edison/page/1/">爱迪生</a>
            
            <a class="tag" href="/tag/failure/page/1/">失败</a>
            
            <a class="tag" href="/tag/inspirational/page/1/">励志</a>
            
            <a class="tag" href="/tag/paraphrased/page/1/">转述</a>
            
        </div>
    </div>
...
</body>

爬取这个页面是一个两步过程。

这是文章《如何使用Scrapy和Python 3抓取网页》的第4部分(共9部分)。

  1. 首先,通过查找页面上我们想要的数据部分,获取每个引用。
  2. 然后,针对每个引用,通过从HTML标签中提取数据来获取我们想要的数据。

Scrapy根据您提供的选择器来获取数据。选择器是我们用来在页面上查找一个或多个元素的模式,以便我们可以对元素内的数据进行操作。Scrapy支持使用CSS选择器或XPath选择器。

我们现在将使用CSS选择器,因为CSS非常适合用于查找页面上的所有集合。如果您查看HTML,您会看到每个引用都使用class="quote"进行了指定。因为我们正在寻找一个类,所以我们会使用.quote作为我们的CSS选择器。选择器中的.部分会在元素的class属性上进行搜索。我们只需要在类中创建一个名为parse的新方法,并将该选择器传递给响应对象,就像这样:

爬虫.py

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            pass

这段代码获取页面上的所有集合,并对它们进行循环以提取数据。现在让我们从这些引文中提取数据,以便我们能够显示它。

再看一下我们解析的页面的源代码,它告诉我们每个引用的文本都存储在一个带有text类的<span>标签内,而引用的作者则在一个带有author类的<small>标签内。

quotes.toscrape.com... <span class="text" itemprop="text">“I have not failed. I&#39;ve just found 10,000 ways that won&#39;t work.”</span> <span>by <small class="author" itemprop="author">Thomas A. Edison</small> ...

我们正在循环遍历的引用对象具有自己的css方法,因此我们可以传入一个选择器来定位子元素。请按照以下方式修改您的代码,以找到集合的名称并显示出来。

刮刮器.py
class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        
        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
            }

注意:extract_first() 后面的逗号不是拼写错误。在 Python 中,字典对象后面的逗号是有效语法,也是为了给添加更多元素留出空间,在后面我们会看到。

在这段代码中,你会注意到两件事:

  • 我们为引用和作者的选择器添加了 ::text。这是一个 CSS 伪选择器,用于获取标签内部的文本,而不是标签本身。
  • 我们对 quote.css(TEXT_SELECTOR) 返回的对象调用了 extract_first(),因为我们只需要匹配选择器的第一个元素。这会返回一个字符串,而不是一个元素列表。

保存文件并再次运行爬虫程序。

  1. scrapy runspider scraper.py

这次的输出将包括引用及其作者。

输出
2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: 从 <200 https://quotes.toscrape.com> 抓取 {'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein'} 2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: 从 <200 https://quotes.toscrape.com> 抓取 {'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen'} 2022-12-02 11:00:53 [scrapy.core.scraper] DEBUG: 从 <200 https://quotes.toscrape.com> 抓取 {'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe'} ...

让我们继续扩展这个功能,并为作者的页面添加新的选择器和标签来引用。通过研究每个引用的 HTML,我们发现:

  • 指向作者简介页面的链接存储在紧随其姓名之后的链接中。
  • 标签存储为 <a> 标签的集合,每个标签都带有 tag 类,存储在带有 tags 类的 <div> 元素中。

那么,让我们修改网页抓取工具来获取这些新的信息吧。

scraper.py
class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

保存您所做的更改并再次运行抓取程序。

  1. scrapy runspider scraper.py

现在输出将包含新数据。

Output

这是文章《如何使用Scrapy和Python 3抓取网页》的第7部分(共9部分)。

内容片段:

2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein', 'about': 'https://quotes.toscrape.com/author/Albert-Einstein', 'tags': ['inspirational', 'life', 'live', 'miracle', 'miracles']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen', 'about': 'https://quotes.toscrape.com/author/Jane-Austen', 'tags': ['aliteracy', 'books', 'classic', 'humor']}
2022-12-02 11:14:28 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com>
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe', 'about': 'https://quotes.toscrape.com/author/Marilyn-Monroe', 'tags': ['be-yourself', 'inspirational']}

现在,让我们将这个抓取器(Scraper)转换为一个能跟踪链接的爬虫(Spider)。

第三步 — 爬取多个页面

您已经成功地从初始页面提取了数据,但我们无法进一步查看其余的结果。爬虫的整个目的是检测并遍历其他页面的链接,并从这些页面中获取数据。

您会注意到每个页面的顶部和底部都有一个小的右箭头(>),它链接到下一页的结果。这是它的HTML代码:

<div class="secondary-code-label" title="quotes.toscrape.com">quotes.toscrape.com...
    <nav>
        <ul class="pager">
            
            
            <li class="next">
                <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
            </li>
            
        </ul>
    </nav>
...

在源代码中,您会找到一个带有“next”类的<li>标签,在该标签内部,有一个指向下一页的超链接<a>标签。我们唯一要做的就是告诉爬虫,如果存在该链接,就跟随这个链接。

请按照以下方式修改您的代码:

爬虫.py

这是文章《如何使用Scrapy和Python 3抓取网页》的第8部分(共9部分)。

class QuoteSpider(scrapy.Spider):
    name = 'quote-spdier'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(response.urljoin(next_page))

首先,我们为“下一页”链接定义一个选择器,并提取第一个匹配项,然后检查其是否存在。scrapy.Request 是一个新的请求对象,Scrapy 知道它意味着应该获取并解析下一页。

这意味着一旦我们翻到下一页,我们会在那里寻找下一页的链接,然后在那一页上再找到下一页的链接,直到我们找不到下一页的链接为止。这是网页爬取的关键部分:找到并跟随链接。在这个例子中,它是非常线性的;一个页面有一个链接指向下一页,直到我们到达最后一页。但是你也可以跟随链接到标签、其他搜索结果或任何你想要的 URL。

现在,如果你保存代码并再次运行爬虫,你会发现它不仅仅在遍历第一页的数据之后停止。它将继续遍历所有 10 页中的 100 个引用。就整体而言,这不是大量的数据,但现在你知道了自动查找新页面进行数据抓取的过程。

这是我们为本教程准备的完整代码。

爬虫.py
import scrapy


class QuoteSpider(scrapy.Spider):
    name = 'quote-spider'
    start_urls = ['https://quotes.toscrape.com']

    def parse(self, response):
        QUOTE_SELECTOR = '.quote'
        TEXT_SELECTOR = '.text::text'
        AUTHOR_SELECTOR = '.author::text'
        ABOUT_SELECTOR = '.author + a::attr("href")'
        TAGS_SELECTOR = '.tags > .tag::text'
        NEXT_SELECTOR = '.next a::attr("href")'

        for quote in response.css(QUOTE_SELECTOR):
            yield {
                'text': quote.css(TEXT_SELECTOR).extract_first(),
                'author': quote.css(AUTHOR_SELECTOR).extract_first(),
                'about': 'https://quotes.toscrape.com' + 
                        quote.css(ABOUT_SELECTOR).extract_first(),
                'tags': quote.css(TAGS_SELECTOR).extract(),
            }

        next_page = response.css(NEXT_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
            )

结论

在本教程中,您构建了一个功能完整的爬虫,它能够在不到三十行代码中从网页中提取数据。这是一个很好的开始,但您可以用这个爬虫做很多有趣的事情。这足以让您进行思考和实验。如果您需要关于Scrapy的更多信息,请参考Scrapy的官方文档。有关从网络中提取数据的更多信息,请参阅我们关于“如何使用Beautiful Soup和Python 3爬取网页”的教程。