简介
网络爬虫,通常称为网络爬行或网络蜘蛛,是以编程方式浏览一系列网页并提取数据的行为,是处理网络数据的强大工具。
通过使用网络爬虫,您可以挖掘有关一组产品的数据,获取大量文本或定量数据以进行分析,从没有官方 API 的网站检索数据,或者只是满足您自己的个人好奇心。
在本教程中,您将学习有关爬取和蜘蛛过程的基础知识,同时探索一个有趣的数据集。我们将使用Quotes to Scrape,这是一个托管在专门用于测试网络蜘蛛的网站上的引用数据库。通过本教程结束时,您将拥有一个完全功能的 Python 网络爬虫,它可以浏览包含引用的一系列页面,并在屏幕上显示它们。
该爬虫将很容易扩展,因此您可以对其进行调整,并将其用作从网络上爬取数据的自己项目的基础。
先决条件
要完成本教程,您需要一个用于 Python 3 的本地开发环境。您可以按照《如何安装和设置 Python 3 的本地编程环境》中的说明配置所需的一切。
步骤 1 —— 创建基本爬虫
爬取是一个两步过程:
- 系统地查找并下载网页。
- 从下载的页面中提取信息。
这两个步骤可以用许多语言的许多方式来实现。
您可以使用编程语言提供的模块或库从头开始构建一个爬虫,但随着爬虫变得更加复杂,您可能会遇到一些潜在的问题。例如,您需要处理并发性,以便可以同时爬取多个页面。您可能希望找出如何将爬取的数据转换为不同的格式,如 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 scrapyclass 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>
爬取这个页面是一个两步过程:
- 首先,通过查找页面上具有我们想要的数据的部分来获取每个引语。
- 然后,对于每个引语,通过从 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">→</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 scrapyclass 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 抓取网页”的教程。