Python 3和Scrapy实战:网页爬虫教程

简介

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

通过使用网络爬虫,您可以挖掘有关一组产品的数据,获取大量文本或定量数据以进行分析,从没有官方 API 的网站检索数据,或者只是满足您自己的个人好奇心。

在本教程中,您将学习有关爬取和蜘蛛过程的基础知识,同时探索一个有趣的数据集。我们将使用Quotes to Scrape,这是一个托管在专门用于测试网络蜘蛛的网站上的引用数据库。通过本教程结束时,您将拥有一个完全功能的 Python 网络爬虫,它可以浏览包含引用的一系列页面,并在屏幕上显示它们。

该爬虫将很容易扩展,因此您可以对其进行调整,并将其用作从网络上爬取数据的自己项目的基础。

先决条件

要完成本教程,您需要一个用于 Python 3 的本地开发环境。您可以按照《如何安装和设置 Python 3 的本地编程环境》中的说明配置所需的一切。

步骤 1 —— 创建基本爬虫

爬取是一个两步过程:

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

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

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

如果您构建的爬虫基于一个已存在的库,该库可以为您处理这些问题,那么您将会更加顺利。在本教程中,我们将使用 Python 和 Scrapy 来构建我们的爬虫。

Scrapy 是最流行和强大的 Python 爬取库之一;它采用“电池包含”方法来进行爬取,这意味着它处理了所有爬虫都需要的常见功能,因此开发人员不必每次都重新发明轮子。

Scrapy,像大多数 Python 包一样,位于 PyPI(也称为 pip)上。PyPI 是所有已发布的 Python 软件的社区拥有的存储库。

如果您的 Python 安装与本教程的先决条件中概述的一样,那么您的机器上已经安装了 pip,因此您可以使用以下命令安装 Scrapy:

pip install scrapy

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

安装了 Scrapy 后,为我们的项目创建一个新文件夹。您可以在终端中运行以下命令来执行此操作:

mkdir quote-scraper

现在,进入您刚刚创建的新目录:

cd quote-scraper

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

通过创建一个以 Scrapy 为基础的非常基本的爬虫来开始项目。为此,您需要创建一个 Python 类,它是 scrapy.Spider 的子类,这是 Scrapy 提供的一个基本蜘蛛类。该类将具有两个必需的属性:

  • name —— 蜘蛛的名称。
  • start_urls —— 从中开始爬取的 URL 列表。我们将从一个 URL 开始。
  • 在您的文本编辑器中打开 scrapy.py 文件,并添加以下代码以创建基本蜘蛛:

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

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

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

    接下来,我们使用 Scrapy 提供的 Spider 类,并将其作为 BrickSetSpider子类。将子类视为其父类的更专业化形式。Spider 类具有定义如何跟踪 URL 并从找到的页面中提取数据的方法和行为,但它不知道要查找的位置或要查找的数据。通过对其进行子类化,我们可以提供这些信息。

    最后,我们将类命名为 quote-spider,并为我们的爬虫指定一个起始 URL:https://quotes.toscrape.com。如果您在浏览器中打开该 URL,它将带您到一个搜索结果页面,显示许多著名引语的第一页。

    现在,测试一下爬虫。通常,Python 文件是通过类似 python path/to/file.py 的命令运行的。但是,Scrapy 配备了自己的命令行界面,以简化启动爬虫的过程。使用以下命令启动您的爬虫:

    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)
    

    这是大量的输出,让我们逐一解释一下。

  • 爬虫初始化并加载了所需的其他组件和扩展,以处理从 URL 读取数据。
  • 它使用我们在 start_urls 列表中提供的 URL,并获取了 HTML,就像您的网络浏览器一样。
  • 它将该 HTML 传递给 parse 方法,该方法默认情况下不执行任何操作。由于我们从未编写过自己的 parse 方法,因此蜘蛛在不执行任何工作的情况下就完成了。
  • 现在让我们从页面中提取一些数据。

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

    我们已经创建了一个非常基本的程序,用于下载页面,但它还没有进行任何爬取或蜘蛛行为。让我们给它一些要提取的数据。

    如果你查看我们要爬取的页面,你会发现它具有以下结构:

  • 每个页面都有一个标题。
  • 有一个登录链接。
  • 然后是引语本身,显示在一个类似表格或有序列表的结构中。每个引语都有类似的格式。
  • 在编写爬虫时,你需要查看 HTML 文件的源代码,并熟悉其结构。以下是源代码,为了可读性,已删除了与我们目标无关的标签:

    [secondary_label quotes.toscrape.com]
    <body>
    ...
        <div class="quote" itemscope itemtype="http://schema.org/CreativeWork">
            <span class="text" itemprop="text">“I have not failed. I've just found 10,000 ways that won't work.”</span>
            <span>by <small class="author" itemprop="author">Thomas A. Edison</small>
            <a href="/author/Thomas-A-Edison">(about)</a>
            </span>
            <div class="tags">
                Tags:
                <meta class="keywords" itemprop="keywords" content="edison,failure,inspirational,paraphrased" /    > 
                
                <a class="tag" href="/tag/edison/page/1/">edison</a>
                
                <a class="tag" href="/tag/failure/page/1/">failure</a>
                
                <a class="tag" href="/tag/inspirational/page/1/">inspirational</a>
                
                <a class="tag" href="/tag/paraphrased/page/1/">paraphrased</a>
                
            </div>
        </div>
    ...    
    </body>
    

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

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

    scrapy 根据你提供的 选择器 来获取数据。选择器是我们可以使用的模式,以便找到页面上的一个或多个元素,以便我们可以处理元素内的数据。scrapy 支持 CSS 选择器或 XPath 选择器。

    我们现在将使用 CSS 选择器,因为 CSS 完美适用于查找页面上的所有集合。如果你查看 HTML,你会发现每个引语都是用类 quote 指定的。由于我们正在寻找一个类,我们将使用 .quote 作为我们的 CSS 选择器。选择器的 . 部分搜索元素上的 class 属性。我们只需在我们的类中创建一个名为 parse 的新方法,并将该选择器传递到 response 对象中,如下所示:

    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> 标签中:

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

    我们正在循环遍历的 quote 对象有其自己的 css 方法,因此我们可以传入一个选择器来定位子元素。修改你的代码如下,以查找集合的名称并显示它:

    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(),
                }
    

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

  • 我们在引语和作者的选择器后附加了 ::text。这是一个 CSS 伪选择器,它获取标签内的文本,而不是标签本身。
  • 我们对 quote.css(TEXT_SELECTOR) 返回的对象调用了 extract_first(),因为我们只想要匹配选择器的第一个元素。这给我们一个字符串,而不是元素的列表。
  • 保存文件并再次运行爬虫:

    scrapy runspider scraper.py
    

    这次输出将包含引语及其作者:

    ...
    2022-12-02 11:00:53 [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'}
    2022-12-02 11:00:53 [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'}
    2022-12-02 11:00:53 [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'}
    ...
    
    

    让我们继续扩展这个功能,通过添加新的选择器来获取关于作者的页面链接和引语标签的链接。通过调查每个引语的 HTML,我们发现:

  • 存储作者 about 页面的链接是在其名称后面紧跟的一个链接中。
  • 标签存储为一组 a 标签,每个都有 tag 类,存储在具有 tags 类的 div 元素内。
  • 因此,让我们修改爬虫以获取这些新信息:

    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(),
                }
    
    

    保存你的更改并再次运行爬虫:

    scrapy runspider scraper.py
    

    现在输出将包含新的数据:

    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']}
    

    现在让我们将这个爬虫转变为一个可以跟踪链接的蜘蛛。

    步骤 3 —— 爬取多个页面

    你已经成功地从初始页面提取了数据,但我们并没有继续查看其余的结果。爬虫的整个目的是检测和遍历到其他页面的链接,并从这些页面中获取数据。

    你会注意到每个页面的顶部和底部都有一个小右尖括号(>),它链接到下一页的结果。以下是该部分的 HTML 代码:

    [secondary_label 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 标签。我们所要做的就是告诉爬虫,如果存在下一页,就跟随该链接。

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

    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 条引语。在整个过程中,这并不是一个巨大的数据块,但现在你知道了自动查找新页面进行抓取的过程。

    以下是本教程的完整代码:

    import scrapy
    
    
    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 的信息,请查看 Scrapy 的官方文档。关于从网页中提取数据的更多信息,请参阅我们的“如何使用 Beautiful Soup 和 Python 3 抓取网页”的教程。

    作者:白如意i

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python 3和Scrapy实战:网页爬虫教程

    发表回复