第一部分:Scrapy
Spider
在本教程的第 1 部分:基本 Scrapy Spider中,我们将介绍:
- 什么是 Scrapy?
- 如何设置我们的 Python 环境
- 如何设置我们的 Scrapy 项目
- 创建我们的 Scrapy Spider
- 使用 Scrapy Shell 查找我们的 CSS 选择器
- 如何运行我们的 Scrapy Spider,以及以 CSV 或 JSON 格式输出数据
- 如何浏览页面
在本系列中,我们将从Chocolate.co.uk抓取产品,因为它将是一个很好的示例,说明如何抓取电子商务商店的数据。另外,谁不喜欢巧克力呢!
什么是 Scrapy
?
Scrapy由Zyte的联合创始人Pablo Hoffman 和 Shane Evans 开发,是一个专为网络抓取而设计的 Python 框架。
使用 Scrapy,您可以轻松构建高度可扩展的抓取工具,它将检索页面 HTML、解析和处理数据,并将其存储为您选择的文件格式和位置。
为什么以及何时应该使用 Scrapy
?
不过,还有其他 Python 库也可用于网页抓取:
- Python 请求/ BeautifulSoup:适用于小规模网页抓取,其中数据在 HTML 响应中返回。需要构建自己的蜘蛛管理功能来管理并发、重试、数据清理、数据存储。
- Python Request-HTML:将 Python 请求与解析库相结合,Request-HTML 是 Python Requests/BeautifulSoup 组合和 Scrapy 之间的中间立场。
- Python Selenium:如果您正在抓取网站,并且它仅在 Javascript 呈现后返回目标数据,或者您需要与页面元素交互以获取数据,请使用它。
Python Scrapy具有更多功能,非常适合开箱即用的大规模抓取:
- CSS 选择器和 XPath 表达式解析
- 数据格式(CSV、JSON、XML)和存储(FTP、S3、本地文件系统)
- 强大的编码支持
- 并发管理
- 自动重试
- Cookie 和会话处理
- 爬行蜘蛛和内置分页支持
您只需在您的设置文件中进行自定义,或者添加开发人员开源的众多 Scrapy 扩展和中间件之一。
学习曲线最初比使用 Python Requests/BeautifulSoup 组合更陡峭,但是,从长远来看,在部署生产抓取工具和大规模抓取时,它将为您节省大量时间。
Scrapy 初学者
教程
介绍完毕,让我们开始开发 Spider。首先,我们需要设置 Python 环境。
步骤 1 - 设置 Python
环境
为了避免以后出现版本冲突,最佳做法是为每个 Python 项目创建一个单独的虚拟环境。这意味着您为某个项目安装的任何软件包都与其他项目分开,因此您不会无意中破坏其他项目。
根据您机器的操作系统,这些命令会略有不同。
MacOS 或
Linux
在 MacOS 或任何 Linux 发行版上设置虚拟环境。
首先,我们要确保安装了最新版本的软件包。
$ sudo apt-get update
$ apt install tree
<font style="color:rgb(28, 30, 33);">python3-venv</font>
如果你还没有安装,那么请安装
$ sudo apt install -y python3-venv
接下来,我们将创建我们的 Python 虚拟环境。
$ cd /scrapy_tutorials
$ python3 -m venv venv
$ source venv/bin/activate
最后,我们将在虚拟环境中安装 Scrapy。
$ apt-get install python3-pip
$ sudo pip3 install scrapy
窗户
在 Windows 上设置虚拟环境。
<font style="color:rgb(28, 30, 33);">virtualenv</font>
在您正在使用的 Windows 命令 shell、Powershell 或其他终端中安装。
pip install virtualenv
导航到您想要创建虚拟环境的文件夹,然后启动 virtualenv。
cd /scrapy_tutorials
virtualenv venv
激活虚拟环境。
source venv\Scripts\activate
最后,我们将在虚拟环境中安装 Scrapy。
pip install scrapy
测试 Scrapy 已
安装
为了确保一切正常,如果你<font style="color:rgb(28, 30, 33);">scrapy</font>
在命令行中输入命令,你应该得到如下输出:
$ scrapy
Usage:
scrapy <command> [options] [args]
Available commands:
bench Run quick benchmark test
check Check spider contracts
commands
crawl Run a spider
edit Edit spider
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
list List available spiders
parse Parse URL (using its spider) and print the results
runspider Run a self-contained spider
第 2 步 - 设置我们的 Scrapy
项目
现在我们已经设置好了环境,可以开始做有趣的事情了。构建我们的第一个 Scrapy 蜘蛛!
创建我们的 Scrapy
项目
我们要做的第一件事是创建我们的 Scrapy 项目。该项目将保存我们抓取工具的所有代码。
执行此操作的命令行语法是:
scrapy startproject <project_name>
因此,在这种情况下,由于我们要抓取一个巧克力网站,因此我们将项目命名为<font style="color:rgb(28, 30, 33);">chocolatescraper</font>
。但您可以使用任何您想要的项目名称。
scrapy startproject chocolatescraper
理解 Scrapy 项目
结构
为了帮助我们理解我们刚刚做了什么,以及 Scrapy 如何构建它的项目,我们将暂停一会儿。
首先,我们来看看<font style="color:rgb(28, 30, 33);">scrapy startproject chocolatescraper</font>
刚刚做了什么。在命令行中输入以下命令:
$ cd /chocolatescraper
$ tree
你应该看到类似这样的内容:
├── scrapy.cfg
└── chocolatescraper
├── __init__.py
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
└── __init__.py
当我们运行该<font style="color:rgb(28, 30, 33);">scrapy startproject chocolatescraper</font>
命令时,Scrapy会自动生成一个模板项目供我们使用。
我们不会在这个初学者项目中使用大多数这些文件,但我们会对每个文件进行简要解释,因为每个文件都有特殊的用途:
- settings.py包含所有项目设置,例如激活管道、中间件等。在这里,您可以更改延迟、并发性以及更多内容。
- items.py是提取数据的模型。您可以定义一个自定义模型(如 ProductItem),该模型将继承 Scrapy Item 类并包含您抓取的数据。
- pipelines.py是蜘蛛传递所产生的项目的地方,它主要用于清理文本并连接到文件输出或数据库(CSV、JSON SQL 等)。
- 当您想要修改请求的方式以及 scrapy 处理响应的方式时,middlewares.py很有用。
- scrapy.cfg是一个配置文件,用于更改一些部署设置等。
步骤3-创建我们的
蜘蛛
好的,我们已经创建了一般的项目结构。现在,我们将创建用于抓取数据的爬虫。
Scrapy 提供了许多不同类型的蜘蛛,但在本教程中,我们将介绍最常见的蜘蛛,即通用蜘蛛。以下是一些最常见的蜘蛛:
- Spider -获取 start_urls 列表并使用
<font style="color:rgb(28, 30, 33);">parse</font>
方法抓取每一个。 - CrawlSpider -旨在通过跟踪其找到的任何链接来抓取整个网站。
- SitemapSpider -旨在从站点地图中提取 URL
要创建一个新的通用蜘蛛,只需运行genspider命令:
# syntax is --> scrapy genspider <name_of_spider> <website>
$ scrapy genspider chocolatespider chocolate.co.uk
现在一个新的蜘蛛将被添加到您的<font style="color:rgb(28, 30, 33);">spiders</font>
文件夹中,它应该看起来像这样:
import scrapy
class ChocolatespiderSpider(scrapy.Spider):
name = 'chocolatespider'
allowed_domains = ['chocolate.co.uk']
start_urls = ['http://chocolate.co.uk/']
def parse(self, response):
pass
这里我们看到命令<font style="color:rgb(28, 30, 33);">genspider</font>
以类的形式创建了一个模板蜘蛛供我们使用<font style="color:rgb(28, 30, 33);">Spider</font>
。这个蜘蛛类包含:
- name - 为蜘蛛命名的类属性。稍后运行蜘蛛时我们会用到它
<font style="color:rgb(28, 30, 33);">scrapy crawl <spider_name></font>
。 - allowed_domains - 一个类属性,告诉 Scrapy 只应抓取该
<font style="color:rgb(28, 30, 33);">chocolate.co.uk</font>
域的页面。这可防止蜘蛛程序出差错并抓取大量网站。这是可选的。 - start_urls - 一个类属性,告诉 Scrapy 应该抓取的第一个 URL。我们稍后会对此进行一些更改。
- 解析——
<font style="color:rgb(28, 30, 33);">parse</font>
从目标网站收到响应后调用该函数。
要开始使用这个 Spider,我们必须做两件事:
- 将 更改
<font style="color:rgb(28, 30, 33);">start_urls</font>
为我们要抓取的 URL https://www.chocolate.co.uk/collections/all。 - 将我们的解析代码插入到
<font style="color:rgb(28, 30, 33);">parse</font>
函数中。
步骤 4 - 更新起始
网址
这很简单,我们只需要替换数组中的 url <font style="color:rgb(28, 30, 33);">start_urls</font>
:
import scrapy
class ChocolatespiderSpider(scrapy.Spider):
name = 'chocolatespider'
allowed_domains = ['chocolate.co.uk']
start_urls = ['https://www.chocolate.co.uk/collections/all']
def parse(self, response):
pass
接下来,我们需要创建 CSS 选择器来解析页面中所需的数据。为此,我们将使用 Scrapy Shell。
步骤 5 - Scrapy Shell:查找我们的 CSS
选择器</font
要从 HTML 页面中提取数据,我们需要使用XPath或CSS 选择器来告诉 Scrapy 数据在页面中的哪个位置。XPath 和 CSS 选择器就像 Scrapy 的小地图,用于导航 DOM 树并找到我们需要的数据的位置。在本指南中,我们将使用 CSS 选择器来解析页面中的数据。为了帮助我们创建这些 CSS 选择器,我们将使用Scrapy Shell。
Scrapy 的一大优点是它带有内置 shell,可让您快速测试和调试 XPath 和 CSS 选择器。您无需运行整个抓取程序来查看 XPath 或 CSS 选择器是否正确,而是可以直接将它们输入到终端并查看结果。
要打开 Scrapy shell,请使用以下命令:
scrapy shell
注意:如果您想使用 IPython 作为您的 Scrapy shell(功能更强大并提供智能自动完成和彩色输出),那么请确保您已安装 IPython:
pip3 install ipython
然后<font style="color:rgb(28, 30, 33);">scrapy.cfg</font>
像这样编辑你的文件:
## scrapy.cfg
[settings]
default = chocolatescraper.settings
shell = ipython
打开我们的 Scrapy shell 后,你应该会看到类似这样的内容:
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x0000025111C47948>
[s] item {}
[s] settings <scrapy.settings.Settings object at 0x0000025111D17408>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
In [1]:
获取
页面
为了创建我们的 CSS 选择器,我们将在以下页面上测试它们:
https://www.chocolate.co.uk/collections/all
我们要做的第一件事是在我们的 Scrapy shell 中获取巧克力网站的主要产品页面。
fetch('https://www.chocolate.co.uk/collections/all')
我们应该看到如下响应:
In [1]: fetch('https://www.chocolate.co.uk/collections/all')
2021-12-22 13:28:56 [scrapy.core.engine] INFO: Spider opened
2021-12-22 13:28:57 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.chocolate.co.uk/robots.txt> (referer: None)
2021-12-22 13:28:57 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.chocolate.co.uk/collections/all> (referer: None)
我们可以看到,我们成功的从 检索到了页面<font style="color:rgb(28, 30, 33);">chocolate.co.uk</font>
,并且 Scrapy shell 已经自动将 HTML 响应保存在响应变量中。
In [2]: response
Out[2]: <200 https://www.chocolate.co.uk/collections/all>
查找产品 CSS
选择器
为了找到正确的 CSS 选择器来解析产品详细信息,我们首先在浏览器的 DevTools 中打开该页面。
打开网站,然后打开开发者工具控制台(右键单击页面并单击检查)。
使用检查元素,将鼠标悬停在项目上并查看各个产品的 ID 和类别。
在这种情况下,我们可以看到每盒巧克力都有自己的特殊组件,称为<font style="color:rgb(28, 30, 33);">product-item</font>
。我们可以使用它来引用我们的产品(见上图)。
现在使用我们的 Scrapy shell 我们可以看看是否可以使用此类提取产品信息。
response.css('product-item')
我们可以看到它找到了所有与该选择器匹配的元素。
In [3]: response.css('product-item')
Out[3]:
[<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item pro...'>,
<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item " r...'>,
<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item " r...'>,
<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item " r...'>,
<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item " r...'>,
<Selector xpath='descendant-or-self::product-item' data='<product-item class="product-item pro...'>,
...
获取第一个
产品
为了获得我们使用的第一个产品,<font style="color:rgb(28, 30, 33);">.get()</font>
将其附加到命令的末尾。
response.css('product-item').get()
这将返回 DOM 树中此节点的所有 HTML。
In [4]: response.css('product-item').get()
Out[4]: '<product-item class="product-item product-item--sold-out" reveal><div class="product-item__image-wrapper product-item__image-wrapper--multiple"><div class="product-item__label-list label-list"><span class="label label--custom">New</span><span class="label label--subdued">Sold out</span></div><a href="/products/100-dark-hot-chocolate-flakes" class="product-item__aspect-ratio aspect-ratio " style="padding-bottom: 100.0%; --aspect-ratio: 1.0">\n
...
获取所有
产品
现在我们已经找到了包含产品项目的 DOM 节点,我们将获取所有产品项目并将这些数据保存到响应变量中,然后循环遍历这些项目并提取我们需要的数据。
因此可以使用以下命令执行此操作。
products = response.css('product-item')
产品变量现在是页面上所有产品的列表。
要检查产品变量的长度,我们可以看看有多少个产品。
len(products)
输出如下:
In [6]: len(products)
Out[6]: 24
提取产品
详细信息
现在让我们从产品列表中提取每个产品的名称、价格和网址。
products 变量是产品列表。当我们更新蜘蛛代码时,我们将循环遍历此列表,但是,为了找到正确的选择器,我们将在列表的第一个元素上测试 CSS 选择器<font style="color:rgb(28, 30, 33);">products[0]</font>
。
单一产品 –获取单一产品。
product = products[0]
名称 -可以通过以下方式找到产品名称:
product.css('a.product-item-meta__title::text').get()
In [5]: product.css('a.product-item-meta__title::text').get()
Out[5]: '100% Dark Hot Chocolate Flakes'
价格 -可以通过以下方式查看产品价格:
product.css('span.price').get()
您可以看到返回的价格数据包含大量多余的 HTML。我们将在下一步中将其删除。
In [6]: product.css('span.price').get()
Out[6]: '<span class="price">\n <span class="visually-hidden">Sale price</span>▒8.50</span>'
要从价格中删除多余的 span 标签,我们可以使用该<font style="color:rgb(28, 30, 33);">.replace()</font>
方法。当我们需要清理数据时,replace 方法很有用。
在这里我们将<font style="color:rgb(28, 30, 33);"><span></font>
用空引号替换部分<font style="color:rgb(28, 30, 33);">''</font>
:
product.css('span.price').get().replace('<span class="price">\n <span class="visually-hidden">Sale price</span>','').replace('</span>','')
In [7]: product.css('span.price').get().replace('<span class="price">\n <span class="visually-hidden">Sale price</span>','').replace('</span>','')
Out[7]: '8.50'
产品 URL -接下来让我们看看如何提取每个产品的产品 URL。为此,我们可以使用<font style="color:rgb(28, 30, 33);">products.css('div.product-item-meta a')</font>
product.css('div.product-item-meta a').attrib['href']
In [8]: product.css('div.product-item-meta a').attrib['href']
Out[8]: '/products/100-dark-hot-chocolate-flakes'
更新的[Spider](https://scrapeops.io/python-scrapy-playbook/scrapy-beginners-guide/#updated-spider)
现在,我们已经找到了正确的 CSS 选择器,让我们更新我们的蜘蛛。使用` exit()` 命令退出 Scrapy shell。我们更新的 Spider 代码应如下所示:
import scrapy
class ChocolatespiderSpider(scrapy.Spider)
#the name of the spider
name = 'chocolatespider'
#the url of the first page that we will start scraping
start_urls = ['https://www.chocolate.co.uk/collections/all']
def parse(self, response):
#here we are looping through the products and extracting the name, price & url
products = response.css('product-item')
for product in products:
#here we put the data returned into the format we want to output for our csv or json file
yield{
'name' : product.css('a.product-item-meta__title::text').get(),
'price' : product.css('span.price').get().replace('<span class="price">\n <span class="visually-hidden">Sale price</span>','').replace('</span>',''),
'url' : product.css('div.product-item-meta a').attrib['href'],
}
在这里,我们的蜘蛛执行以下步骤:
- 向 发出请求
<font style="color:rgb(28, 30, 33);">'https://www.chocolate.co.uk/collections/all'</font>
。 - 当它得到响应时,它会使用从页面中提取所有产品
<font style="color:rgb(28, 30, 33);">products = response.css('product-item')</font>
。 - 循环遍历每个产品,并使用我们创建的 CSS 选择器提取名称、价格和url 。
- 产生这些项目以便它们可以存储在 CSV、JSON、DB 等中。
步骤 5 - 运行我们的
Spider
现在我们有了一个蜘蛛,我们可以通过进入 scrapy 项目的顶层并运行以下命令来运行它。
scrapy crawl chocolatespider
它将运行,您应该会在屏幕上看到日志。以下是最终统计数据:
2021-12-22 14:43:54 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 707,
'downloader/request_count': 2,
'downloader/request_method_count/GET': 2,
'downloader/response_bytes': 64657,
'downloader/response_count': 2,
'downloader/response_status_count/200': 2,
'elapsed_time_seconds': 0.794875,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2021, 12, 22, 13, 43, 54, 937791),
'httpcompression/response_bytes': 268118,
'httpcompression/response_count': 2,
'item_scraped_count': 24,
'log_count/DEBUG': 26,
'log_count/INFO': 10,
'response_received_count': 2,
'robotstxt/request_count': 1,
'robotstxt/response_count': 1,
'robotstxt/response_status_count/200': 1,
'scheduler/dequeued': 1,
'scheduler/dequeued/memory': 1,
'scheduler/enqueued': 1,
'scheduler/enqueued/memory': 1,
'start_time': datetime.datetime(2021, 12, 22, 13, 43, 54, 142916)}
2021-12-22 14:43:54 [scrapy.core.engine] INFO: Spider closed (finished)
从上面的统计数据我们可以看出,我们的蜘蛛抓取了 24 个项目:<font style="color:rgb(28, 30, 33);">'item_scraped_count': 24</font>
。
如果我们想将数据保存到 JSON 文件,我们可以使用该<font style="color:rgb(28, 30, 33);">-O</font>
选项,后跟文件名。
scrapy crawl chocolatespider -O myscrapeddata.json
如果我们想将数据保存到 CSV 文件,我们也可以这样做。
scrapy crawl chocolatespider -O myscrapeddata.csv
步骤 6 - 导航至“下一页
”
到目前为止,代码运行良好,但我们只从网站的第一页获取产品,即我们在 start_url 变量中列出的 url。
因此,下一个合乎逻辑的步骤是转到下一页(如果有的话),并从中抓取商品数据!以下是我们的实现方法。
首先,让我们再次打开 Scrapy shell,获取页面并找到正确的选择器以获取下一页按钮。
scrapy shell
然后再次获取该页面。
fetch('https://www.chocolate.co.uk/collections/all')
然后获取包含下一页 url 的 href 属性。
response.css('[rel="next"] ::attr(href)').get()
In [2]: response.css('[rel="next"] ::attr(href)').get()
Out[2]: '/collections/all?page=2'
现在,我们只需要更新我们的蜘蛛,让它在解析完页面上的所有项目后请求此页面。
import scrapy
class ChocolateSpider(scrapy.Spider):
#the name of the spider
name = 'chocolatespider'
#these are the urls that we will start scraping
start_urls = ['https://www.chocolate.co.uk/collections/all']
def parse(self, response):
products = response.css('product-item')
for product in products:
#here we put the data returned into the format we want to output for our csv or json file
yield{
'name' : product.css('a.product-item-meta__title::text').get(),
'price' : product.css('span.price').get().replace('<span class="price">\n <span class="visually-hidden">Sale price</span>','').replace('</span>',''),
'url' : product.css('div.product-item-meta a').attrib['href'],
}
next_page = response.css('[rel="next"] ::attr(href)').get()
if next_page is not None:
next_page_url = 'https://www.chocolate.co.uk' + next_page
yield response.follow(next_page_url, callback=self.parse)
这里我们看到我们的蜘蛛现在找到了下一页的 URL,如果不是,它会将其附加到基本 URL 并发出另一个请求。
现在我们在 Scrapy 统计信息中看到我们已经抓取了 5 个页面并提取了 73 个项目:
2021-12-22 15:10:45 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 2497,
'downloader/request_count': 5,
'downloader/request_method_count/GET': 5,
'downloader/response_bytes': 245935,
'downloader/response_count': 5,
'downloader/response_status_count/200': 5,
'elapsed_time_seconds': 2.441196,
'feedexport/success_count/FileFeedStorage': 1,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2021, 12, 22, 14, 10, 45, 62280),
'httpcompression/response_bytes': 986800,
'httpcompression/response_count': 5,
'item_scraped_count': 73,
'log_count/DEBUG': 78,
'log_count/INFO': 11,
'request_depth_max': 3,
'response_received_count': 5,
'robotstxt/request_count': 1,
'robotstxt/response_count': 1,
'robotstxt/response_status_count/200': 1,
'scheduler/dequeued': 4,
'scheduler/dequeued/memory': 4,
'scheduler/enqueued': 4,
'scheduler/enqueued/memory': 4,
'start_time': datetime.datetime(2021, 12, 22, 14, 10, 42, 621084)}
2021-12-22 15:10:45 [scrapy.core.engine] INFO: Spider closed (finished)