介绍
Web 抓取,通常称为 Web 抓取或 Web 爬虫,或“以编程方式遍历网页集合并提取数据”,是处理 Web 数据的强大工具。
使用网络爬虫,您可以挖掘有关一组产品的数据,获取大量文本或定量数据以进行使用,从没有官方 API 的站点获取数据,或者只是满足您个人的好奇心。
在本教程中,您将在探索有趣的数据集时了解抓取和爬取过程的基础知识。 我们将使用 BrickSet,这是一个社区运营的网站,其中包含有关乐高套装的信息。 在本教程结束时,您将拥有一个功能齐全的 Python 网络爬虫,它会遍历 Brickset 上的一系列页面,并从每个页面中提取有关 LEGO 集的数据,并将数据显示到您的屏幕上。
刮板将很容易扩展,因此您可以对其进行修补,并将其用作您自己的项目从网络上刮取数据的基础。
先决条件
要完成本教程,您需要 Python 3 的本地开发环境。 您可以按照 如何为 Python 3 安装和设置本地编程环境来配置您需要的一切。
第 1 步 - 创建一个基本的刮板
刮削是一个两步过程:
- 您系统地查找和下载网页。
- 您获取这些网页并从中提取信息。
这两个步骤都可以用多种语言以多种方式实现。
您可以使用 modules 或您的编程语言提供的库从头开始构建刮板,但是随着刮板变得越来越复杂,您必须处理一些潜在的麻烦。 例如,您需要处理并发,以便一次可以抓取多个页面。 您可能想弄清楚如何将抓取的数据转换为不同的格式,例如 CSV、XML 或 JSON。 而且您有时必须处理需要特定设置和访问模式的网站。
如果您在为您处理这些问题的现有库之上构建您的爬虫,您将会有更好的运气。 在本教程中,我们将使用 Python 和 Scrapy 来构建我们的爬虫。
Scrapy 是最流行和最强大的 Python 抓取库之一; 它采用“包含电池”的方法进行抓取,这意味着它可以处理所有抓取工具所需的许多常见功能,因此开发人员不必每次都重新发明轮子。 它使抓取成为一个快速而有趣的过程!
与大多数 Python 包一样,Scrapy 位于 PyPI(也称为 pip
)上。 PyPI,即 Python 包索引,是一个社区拥有的所有已发布 Python 软件的存储库。
如果您有本教程先决条件中概述的 Python 安装,那么您的机器上已经安装了 pip
,因此您可以使用以下命令安装 Scrapy:
pip install scrapy
如果您在安装过程中遇到任何问题,或者您想在不使用 pip
的情况下安装 Scrapy,请查看 官方安装文档 。
安装 Scrapy 后,让我们为我们的项目创建一个新文件夹。 您可以通过运行在终端中执行此操作:
mkdir brickset-scraper
现在,导航到您刚刚创建的新目录:
cd brickset-scraper
然后为我们的爬虫创建一个名为 scraper.py
的新 Python 文件。 对于本教程,我们将把所有代码放在这个文件中。 您可以使用 touch
命令在终端中创建此文件,如下所示:
touch scraper.py
或者,您可以使用文本编辑器或图形文件管理器创建文件。
我们将从制作一个非常基本的刮板开始,它使用 Scrapy 作为其基础。 为此,我们将创建一个 Python 类,它是 Scrapy 提供的基本蜘蛛类 scrapy.Spider
的子类。 此类将具有两个必需的属性:
name
— 只是蜘蛛的名字。start_urls
— 您开始抓取的 URL 的 列表 。 我们将从一个 URL 开始。
在文本编辑器中打开 scrapy.py
文件并添加以下代码以创建基本蜘蛛:
刮板.py
import scrapy class BrickSetSpider(scrapy.Spider): name = "brickset_spider" start_urls = ['http://brickset.com/sets/year-2016']
让我们逐行分解:
首先,我们 import scrapy
以便我们可以使用包提供的类。
接下来,我们使用 Scrapy 提供的 Spider
类,并从中创建一个 子类,称为 BrickSetSpider
。 将子类视为其父类的更特殊形式。 Spider
子类具有定义如何跟踪 URL 并从它找到的页面中提取数据的方法和行为,但它不知道在哪里查找或查找什么数据。 通过对它进行子类化,我们可以为它提供信息。
然后我们将蜘蛛命名为 brickset_spider
。
最后,我们给我们的爬虫一个单一的 URL 开始:http://brickset.com/sets/year-2016。 如果您在浏览器中打开该 URL,它将带您进入搜索结果页面,显示包含 LEGO 套装的许多页面中的第一个。
现在让我们测试一下刮板。 您通常通过运行 python path/to/file.py
之类的命令来运行 Python 文件。 但是,Scrapy 带有 自己的命令行界面 来简化启动爬虫的过程。 使用以下命令启动刮板:
scrapy runspider scraper.py
你会看到这样的东西:
Output2016-09-22 23:37:45 [scrapy] INFO: Scrapy 1.1.2 started (bot: scrapybot) 2016-09-22 23:37:45 [scrapy] INFO: Overridden settings: {} 2016-09-22 23:37:45 [scrapy] INFO: Enabled extensions: ['scrapy.extensions.logstats.LogStats', 'scrapy.extensions.telnet.TelnetConsole', 'scrapy.extensions.corestats.CoreStats'] 2016-09-22 23:37:45 [scrapy] INFO: Enabled downloader middlewares: ['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware', ... 'scrapy.downloadermiddlewares.stats.DownloaderStats'] 2016-09-22 23:37:45 [scrapy] INFO: Enabled spider middlewares: ['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware', ... 'scrapy.spidermiddlewares.depth.DepthMiddleware'] 2016-09-22 23:37:45 [scrapy] INFO: Enabled item pipelines: [] 2016-09-22 23:37:45 [scrapy] INFO: Spider opened 2016-09-22 23:37:45 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) 2016-09-22 23:37:45 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023 2016-09-22 23:37:47 [scrapy] DEBUG: Crawled (200) <GET http://brickset.com/sets/year-2016> (referer: None) 2016-09-22 23:37:47 [scrapy] INFO: Closing spider (finished) 2016-09-22 23:37:47 [scrapy] INFO: Dumping Scrapy stats: {'downloader/request_bytes': 224, 'downloader/request_count': 1, ... 'scheduler/enqueued/memory': 1, 'start_time': datetime.datetime(2016, 9, 23, 6, 37, 45, 995167)} 2016-09-22 23:37:47 [scrapy] INFO: Spider closed (finished)
这是很多输出,所以让我们分解一下。
- 刮板初始化并加载了处理从 URL 读取数据所需的其他组件和扩展。
- 它使用我们在
start_urls
列表中提供的 URL 并抓取 HTML,就像您的 Web 浏览器一样。 - 它将 HTML 传递给
parse
方法,默认情况下该方法不执行任何操作。 由于我们从未编写过我们自己的parse
方法,蜘蛛只是完成了没有做任何工作。
现在让我们从页面中提取一些数据。
第 2 步 — 从页面中提取数据
我们创建了一个非常基本的程序来下拉页面,但它还没有进行任何抓取或爬取。 让我们给它一些数据来提取。
如果您查看 我们要抓取的页面,您会看到它具有以下结构:
- 每个页面上都有一个标题。
- 有一些顶级搜索数据,包括匹配的数量、我们正在搜索的内容以及网站的面包屑。
- 然后是集合本身,以表格或有序列表的形式显示。 每组都有类似的格式。
在编写爬虫时,最好查看 HTML 文件的源代码并熟悉其结构。 就是这样,为了便于阅读,删除了一些内容:
brickset.com/sets/year-2016<body> <section class="setlist"> <article class='set'> <a href="https://images.brickset.com/sets/large/10251-1.jpg?201510121127" class="highslide plain mainimg" onclick="return hs.expand(this)"><img src="https://images.brickset.com/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank" onError="this.src='/assets/images/spacer.png'" /></a> <div class="highslide-caption"> <h1>Brick Bank</h1><div class='tags floatleft'><a href='/sets/10251-1/Brick- Bank'>10251-1</a> <a href='/sets/theme-Creator-Expert'>Creator Expert</a> <a class='subtheme' href='/sets/theme-Creator-Expert/subtheme-Modular- Buildings'>Modular Buildings</a> <a class='year' href='/sets/theme-Creator- Expert/year-2016'>2016</a> </div><div class='floatright'>©2016 LEGO Group</div> <div class="pn"> <a href="#" onclick="return hs.previous(this)" title="Previous (left arrow key)">« Previous</a> <a href="#" onclick="return hs.next(this)" title="Next (right arrow key)">Next »</a> </div> </div> ... </article> </section> </body>
抓取此页面是一个两步过程:
- 首先,通过查找页面中包含我们想要的数据的部分来抓取每个乐高积木。
- 然后,对于每个集合,通过将数据从 HTML 标记中提取出来,从中获取我们想要的数据。
scrapy
根据您提供的 选择器 抓取数据。 选择器是我们可以用来在页面上查找一个或多个元素的模式,这样我们就可以处理元素中的数据。 scrapy
支持 CSS 选择器或 XPath 选择器。
我们现在将使用 CSS 选择器,因为 CSS 是更简单的选择,并且非常适合查找页面上的所有集合。 如果您查看页面的 HTML,您会看到每个集合都使用类 set
指定。 由于我们正在寻找一个类,我们将使用 .set
作为我们的 CSS 选择器。 我们所要做的就是将该选择器传递给 response
对象,如下所示:
刮板.py
class BrickSetSpider(scrapy.Spider): name = "brickset_spider" start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response): SET_SELECTOR = '.set' for brickset in response.css(SET_SELECTOR): pass
此代码抓取页面上的所有集合并循环它们以提取数据。 现在让我们从这些集合中提取数据以便我们可以显示它。
再看一下我们正在解析的页面的 source 告诉我们,每个集合的名称都存储在每个集合的 h1
标记中:
brickset.com/sets/year-2016<h1>Brick Bank</h1><div class='tags floatleft'><a href='/sets/10251-1/Brick-Bank'>10251-1</a>
我们循环的 brickset
对象有它自己的 css
方法,所以我们可以传入一个选择器来定位子元素。 如下修改您的代码以找到集合的名称并显示它:
刮板.py
class BrickSetSpider(scrapy.Spider): name = "brickset_spider" start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response): SET_SELECTOR = '.set' for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 ::text' yield { 'name': brickset.css(NAME_SELECTOR).extract_first(), }
注意:extract_first()
后面的逗号不是拼写错误。 我们将很快向本节添加更多内容,因此我们将逗号留在那里以便以后更容易添加到本节。
您会注意到这段代码中发生了两件事:
- 我们将
::text
附加到名称的选择器中。 这是一个 CSS 伪选择器 ,它获取a
标签的 inside 文本,而不是标签本身。 - 我们在
brickset.css(NAME_SELECTOR)
返回的对象上调用extract_first()
因为我们只想要匹配选择器的第一个元素。 这给了我们一个 string,而不是一个元素列表。
保存文件并再次运行刮板:
scrapy runspider scraper.py
这次你会看到集合的名称出现在输出中:
Output... [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'name': 'Brick Bank'} [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'name': 'Volkswagen Beetle'} [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'name': 'Big Ben'} [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'name': 'Winter Holiday Train'} ...
让我们通过为图像、碎片和微型人物或套装附带的 minifigs 添加新的选择器来继续扩展此功能。
再看一下特定集合的 HTML:
brickset.com/sets/year-2016<article class="set"> <a class="highslide plain mainimg" href="http://images.brickset.com/sets/images/10251-1.jpg?201510121127" onclick="return hs.expand(this)"> <img src="http://images.brickset.com/sets/small/10251-1.jpg?201510121127" title="10251-1: Brick Bank"></a> ... <div class="meta"> <h1><a href="/sets/10251-1/Brick-Bank"><span>10251:</span> Brick Bank</a> </h1> ... <div class="col"> <dl> <dt>Pieces</dt> <dd><a class="plain" href="/inventories/10251-1">2380</a></dd> <dt>Minifigs</dt> <dd><a class="plain" href="/minifigs/inset-10251-1">5</a></dd> ... </dl> </div> ... </div> </article>
通过检查这段代码,我们可以看到一些事情:
- 集合的图像存储在集合开始处
a
标记内的img
标记的src
属性中。 我们可以使用另一个 CSS 选择器来获取这个值,就像我们在获取每个集合的名称时所做的那样。 - 获取件数有点棘手。 有一个
dt
标签包含文本Pieces
,然后是一个dd
标签,其中包含实际的件数。 我们将使用 XPath,一种用于遍历 XML 的查询语言来获取它,因为它太复杂而无法使用 CSS 选择器来表示。 - 获取一组小人仔的数量类似于获取件数。 有一个包含文本
Minifigs
的dt
标签,紧随其后的是一个带有数字的dd
标签。
所以,让我们修改爬虫来获取这些新信息:
刮板.py
class BrickSetSpider(scrapy.Spider): name = 'brick_spider' start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response): SET_SELECTOR = '.set' for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 ::text' PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()' MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()' IMAGE_SELECTOR = 'img ::attr(src)' yield { 'name': brickset.css(NAME_SELECTOR).extract_first(), 'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(), 'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(), 'image': brickset.css(IMAGE_SELECTOR).extract_first(), }
保存您的更改并再次运行刮板:
scrapy runspider scraper.py
现在您将在程序的输出中看到新数据:
Output2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': '5', 'pieces': '2380', 'name': 'Brick Bank', 'image': 'http://images.brickset.com/sets/small/10251-1.jpg?201510121127'} 2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': None, 'pieces': '1167', 'name': 'Volkswagen Beetle', 'image': 'http://images.brickset.com/sets/small/10252-1.jpg?201606140214'} 2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': None, 'pieces': '4163', 'name': 'Big Ben', 'image': 'http://images.brickset.com/sets/small/10253-1.jpg?201605190256'} 2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': None, 'pieces': None, 'name': 'Winter Holiday Train', 'image': 'http://images.brickset.com/sets/small/10254-1.jpg?201608110306'} 2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': None, 'pieces': None, 'name': 'XL Creative Brick Box', 'image': '/assets/images/misc/blankbox.gif'} 2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016> {'minifigs': None, 'pieces': '583', 'name': 'Creative Building Set', 'image': 'http://images.brickset.com/sets/small/10702-1.jpg?201511230710'}
现在让我们把这个爬虫变成一个跟随链接的蜘蛛。
第 3 步 — 爬取多个页面
我们已经成功地从初始页面中提取了数据,但我们并没有越过它来查看其余的结果。 蜘蛛的全部意义在于检测和遍历其他页面的链接,并从这些页面中获取数据。
您会注意到每一页的顶部和底部都有一个小克拉 (>
),它链接到下一页结果。 这是它的HTML:
brickset.com/sets/year-2016<ul class="pagelength"> ... <li class="next"> <a href="http://brickset.com/sets/year-2017/page-2">›</a> </li> <li class="last"> <a href="http://brickset.com/sets/year-2016/page-32">»</a> </li> </ul>
如您所见,有一个 li
标签,其类为 next
,在该标签内,有一个 a
标签,带有指向下一页的链接。 我们所要做的就是告诉抓取工具如果存在该链接,则跟踪该链接。
修改您的代码如下:
刮板.py
class BrickSetSpider(scrapy.Spider): name = 'brick_spider' start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response): SET_SELECTOR = '.set' for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 ::text' PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()' MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()' IMAGE_SELECTOR = 'img ::attr(src)' yield { 'name': brickset.css(NAME_SELECTOR).extract_first(), 'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(), 'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(), 'image': brickset.css(IMAGE_SELECTOR).extract_first(), } NEXT_PAGE_SELECTOR = '.next 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 )
首先,我们为“下一页”链接定义一个选择器,提取第一个匹配项,并检查它是否存在。 scrapy.Request
是我们返回的一个值,表示“嘿,抓取此页面”,而 callback=self.parse
表示“一旦您从该页面获取 HTML,将其传递回此方法,以便我们可以解析它,提取数据,然后找到下一页。”
这意味着一旦我们进入下一页,我们将在那里寻找到下一页的链接,在那个页面上我们将寻找到下一页的链接,依此类推,直到我们找不到下一页的链接。 这是网络抓取的关键部分:查找和跟踪链接。 在这个例子中,它是非常线性的; 在我们到达最后一页之前,一个页面有一个指向下一页的链接,但是您可以点击指向标签的链接、其他搜索结果或您想要的任何其他 URL。
现在,如果您保存代码并再次运行爬虫,您会发现它不会在遍历集合的第一页时停止。 它在 23 页上继续浏览所有 779 场比赛! 从总体上看,这并不是一大块数据,但现在您知道了自动查找要抓取的新页面的过程。
这是本教程的完整代码,使用 Python 特定的突出显示:
刮板.py
import scrapy class BrickSetSpider(scrapy.Spider): name = 'brick_spider' start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response): SET_SELECTOR = '.set' for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 ::text' PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()' MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()' IMAGE_SELECTOR = 'img ::attr(src)' yield { 'name': brickset.css(NAME_SELECTOR).extract_first(), 'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(), 'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(), 'image': brickset.css(IMAGE_SELECTOR).extract_first(), } NEXT_PAGE_SELECTOR = '.next 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 )
结论
在本教程中,您构建了一个功能齐全的蜘蛛,它用不到 30 行代码从网页中提取数据。 这是一个很好的开始,但是你可以用这只蜘蛛做很多有趣的事情。 这里有一些方法可以扩展您编写的代码。 他们会给你一些抓取数据的练习。
- 现在我们只解析 2016 年的结果,正如您可能已经从
http://brickset.com/sets/year-2016
的2016
部分猜到的那样——您将如何抓取其他年份的结果? - 大多数套装都包含零售价。 您如何从该单元格中提取数据? 您将如何从中获得原始数字? Hint:您会在
dt
中找到数据,就像件数和人仔数一样。 - 大多数结果都有标签,这些标签指定了有关集合或其上下文的语义数据。 鉴于一组标签有多个标签,我们如何抓取这些标签?
这应该足以让你思考和尝试。 如果您需要更多关于 Scrapy 的信息,请查看 Scrapy 的官方文档。 有关处理来自网络的数据的更多信息,请参阅我们关于 “如何使用 Beautiful Soup 和 Python 3 抓取网页” 的教程。