Scrapy与Python 3网页抓取实战:从入门到精通
引言
网页抓取,通常被称为网络爬虫或网络蜘蛛,是指以程序化方式浏览一系列网页并从中提取数据的行为。它是处理网络数据的强大工具。
使用网络爬虫,你可以提取关于一系列产品的数据、获取大量文本或数值数据进行处理、从没有官方API的网站检索数据,或者仅仅满足个人的好奇心。
在本教程中,您将了解网页抓取和爬虫过程的基本知识,同时探索一个有趣的数据集。我们将使用“Quotes to Scrape”,这是一个托管在专门用于测试网络爬虫的网站上的引语数据库。通过本教程的学习,您将得到一个功能完备的Python网络爬虫,它能够浏览包含引语的一系列页面,并在您的屏幕上显示它们。
这个网络爬虫将很容易进行扩展,你可以随心所欲地修改它,并将其作为从网络上抓取数据的基础,用于你自己的项目。
先决条件
完成本教程,你需要一个适用于Python 3的本地开发环境。你可以按照《如何安装和配置Python 3的本地编程环境》的指南来配置所需的一切。
第一步 – 创建基本的网络爬虫
抓取数据是一个两步过程:
- 系统地查找和下载网页。
- 从下载的网页中提取信息。
这两个步骤可以用多种语言的多种方式来实现。
您可以使用编程语言提供的模块或库从头开始构建一个爬虫,但随着爬虫变得更加复杂,您可能需要处理一些潜在的麻烦。例如,您需要处理并发,以便同时抓取多个页面。您可能还想办法将抓取的数据转换为不同的格式,如CSV、XML或JSON。有时您还需要处理一些需要特定设置和访问模式的网站。
如果你在现有处理这些问题的库的基础上构建你的爬虫,你会更顺利。在本教程中,我们将使用Python和Scrapy来构建我们的爬虫。
Scrapy是最受欢迎、功能强大的Python爬虫库之一;它采用“电池已装入”的方式来进行爬虫,这意味着它处理了大部分常见的功能,避免了开发者在每次开发中重复造轮子。
Scrapy和大多数Python包一样,可以在PyPI(也称为pip)上找到。PyPI是Python软件的社区维护仓库,收录了所有已发布的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开始。
在文本编辑器中打开scraper.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自带了自己的命令行界面,以简化启动爬虫的过程。请使用以下命令开始你的爬虫:
- 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部分)。
- 首先,通过查找页面上我们想要的数据部分,获取每个引用。
- 然后,针对每个引用,通过从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've just found 10,000 ways that won'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()
,因为我们只需要匹配选择器的第一个元素。这会返回一个字符串,而不是一个元素列表。
保存文件并再次运行爬虫程序。
- 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(),
}
保存您所做的更改并再次运行抓取程序。
- 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">→</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爬取网页”的教程。