使用Scrapy从HTML标签中提取数据
Scrapy是一个用于创建Web爬虫应用的Python框架。它提供了相关编程接口,可以通过识别新链接来抓取Web数据,并可以从下载的内容中提取结构化数据。
本指南将为您提供构建Spider爬虫的说明,它可通过递归方式来检查网站的所有
<a>
标记并跟踪记录无效的链接。本指南是为3.4或更高版本的Python以及Scrapy 1.4版来编写的,它并不适用于Python 2环境。
准备工作
- 熟悉我们的 入门 指南并完成设Linode主机名和时区的设置步骤。
-
本指南将尽可能使用
sudo
实现指令。请完成“ 保护您的服务器 ”部分以创建标准用户帐户,同时加强SSH访问并删除不必要的网络服务。 -
更新您的系统:
sudo apt update && sudo apt upgrade -y
注意 本指南是为非root用户编写的。需要提升权限的命令请使用
sudo
前缀执行。如果您不熟悉该sudo
命令,请参阅“ 用户和组” 指南。
安装Python 3环境
在包括Debian 9和CentOS 7的大多数系统上,默认的Python版本是2.7,并且需要手动安装
pip
包安装管理工具。
在Debian 9系统上安装
-
Debian 9自身同时携带了Python 3.5和2.7,但其中2.7是默认的版本。请修改版本:
update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
update-alternatives --install /usr/bin/python python /usr/bin/python3.5 2
-
检查您使用的是否是Python 3版本:
python --version
-
安装
pip
,Python包安装管理工具:sudo apt install python3-pip
在CentOS 7系统下安装
-
在CentOS系统上,请从EPEL包管理存储库安装Python、PIP和一些依赖项:
sudo yum install epel-release
sudo yum install python34 python34-pip gcc python34-devel
-
将
/usr/bin/python
程序链接从原先默认的Python2 替换为新安装的Python 3:sudo rm -f /usr/bin/python
sudo ln -s /usr/bin/python3 /usr/bin/python
-
检查是否使用了正确的版本:
python --version
安装Scrapy
系统级别下安装(不推荐)
虽然系统级别下的安装是最简单的方法,但可能其会与其他需要不同版本库的Python脚本冲突。请在当您的系统仅专用于Scrapy时才使用此方法:
sudo pip3 install scrapy
在虚拟环境下安装Scrapy
这是推荐的安装方法。Scrapy将安装在一个
virtualenv
环境中,以防止与系统级别的库发生冲突。
-
在CentOS系统上,Python 3版本的
virtualenv
将随Python一起安装。但在Debian 9上它需要几个步骤进行安装:sudo apt install python3-venv
sudo pip3 install wheel
-
创建虚拟环境:
python -m venv ~/scrapyenv
-
激活虚拟环境:
source ~/scrapyenv/bin/activate
然后,shell提示符将显示您正在使用的环境。 -
在虚拟环境中安装Scrapy。请注意,您不再需要添加
sudo
前缀,库将仅安装在新创建的虚拟环境中:pip3 install scrapy
创建Scrapy项目
以下所有命令均在虚拟环境中完成。如果您是重新开始会话,请不要忘记重新激活
scrapyenv
。
-
创建一个目录来保存您的Scrapy项目:
mkdir ~/scrapy
cd ~/scrapy
scrapy startproject linkChecker
-
定位到新的Scrapy项目目录并创建一个Spider爬虫程序。本文进行抓取的模板网站为
http://www.example.com
,请将其调整到您要抓取的网站。cd linkChecker
scrapy genspider link\_checkerwww.example.com
此操作将创建一个带有基本Spider爬虫的~/scrapy/linkChecker/linkChecker/spiders/link_checker.py
文件。
注意 以下部分中的所有路径和命令都是基于
~/scrapy/linkChecker
这个srapy项目目录的。
开启Spider爬虫程序
-
开始Spider爬虫程序:
scrapy crawl
Spider爬虫程序会在Scrapy中注册自己的名称,该名称是在您的Spider类中的name
属性中进行制定的。 -
启动
link_checker
Spider爬虫程序:cd ~/scrapy/linkChecker
scrapy crawl link\_checker
新创建的Spider爬虫只会下载www.example.com
页面,之后我们将创建爬取逻辑。
使用Scrapy Shell
Scrapy提供了两种简单的从HTML中提取内容的方法:
-
response.css()
方法使用CSS选择器来获取标签。检索btn
CSS类中的所有链接,请使用:response.css("a.btn::attr(href)")
-
response.xpath()
方法从XPath查询中获取标签。要检索链接内所有图像的资源地址,请使用:response.xpath("//a/img/@src")
您可以尝试使用交互式的Scrapy shell:
-
在您的网页上运行Scrapy shell:
scrapy shell http://www.example.com
-
对选择器进行测试,直到其结果达到你的预期:
response.xpath("//a/@href").extract()
有关选择器的更多信息,请参阅 Scrapy选择器文档 。
编写爬虫爬取逻辑
Spider爬虫使用
parse(self,response)
方法来解析所下载的页面。此方法返回一个包含新的URL资源网址的
迭代对象
,这些新的URL网址将被添加到下载队列中以供将来进行爬取数据和解析。
-
1.编辑
linkChecker/spiders/link_checker.py
文件以提取所有<a>
标签并获取href
链接文本。返回带有yield
关键字的URL网址并将其添加到下载队列:
import scrapy
class LinkCheckerSpider(scrapy.Spider):
name = 'link_checker'
allowed_domains = ['www.example.com']
start_urls = ['http://www.example.com/']
def parse(self, response):
""" Main function that parses downloaded pages """
# 打印spider正在进行的事务
print(response.url)
# 获取所有<a>标签
a_selectors = response.xpath("//a")
# 对每个标签进行循环操作
for selector in a_selectors:
# 解析出链接的文本
text = selector.xpath("text()").extract_first()
# 解析出链接的网址
link = selector.xpath("@href").extract_first()
# 创建一个新的Request对象
request = response.follow(link, callback=self.parse)
# 基于生成器返回该对象
yield request
-
2.运行更新后的Spider爬虫:
scrapy crawl link_checker
然后,您将看到Spider爬虫爬取所有链接。由于allowd_domains
属性的限制,它不会超出 www.example.com 域。根据网站的大小不同,这可能需要一些时间。如果需要停止进程,请使用Ctrl+C
指令。
添加Request请求的元信息
Spider爬虫将以递归方式遍历队列中的链接。在解析所下载的页面时,它没有先前解析页面的任何信息,例如哪个页面链接到了新页面。为了将更多信息传递给
parse
方法,Scrapy提供了一种
Request.meta()
方法,可以将一些键值对添加到请求中,这些键值对在
parse()
方法的响应对象中可用。
元信息用于两个目的:
-
为了使
parse
方法知道来自触发请求的页面的数据:页面的URL资源网址(from_url
)和链接的文本(from_text
) -
为了计算
parse
方法中的递归层次,来限制爬虫的最大深度。 -
1.从前一个spider爬虫开始,就添加一个属性来存储最大深度(
maxdepth
)并将parse
函数更新为以下内容:
# 添加最大深度参数
maxdepth = 2
def parse(self, response):
# 设置首页的默认元信息
from_url = ''
from_text = ''
depth = 0;
# 如果有信息的话,解析响应中的源信息
if 'from' in response.meta:
from_url = response.meta['from']
if 'text' in response.meta:
from_text = response.meta['text']
if 'depth' in response.meta:
depth = response.meta['depth']
# 更新输出逻辑,来展现包含当前页面链接的页面和链接的文本信息
print(depth, reponse.url, '<-', from_url, from_text, sep=' ')
# 在还未到达最大深度的情况下才可以浏览标签
if depth < self.maxdepth:
a_selectors = response.xpath("//a")
for selector in a_selectors:
text = selector.xpath("text()").extract_first()
link = selector.xpath("@href").extract_first()
request = response.follow(link, callback=self.parse)
# 元信息:当前页面的URL资源网络地址
request.meta['from'] = response.url
# 元信息:链接的文本信息
request.meta['text'] = text
# 元信息:链接的深度
request.meta['depth'] = depth + 1
yield request
-
2.运行更新后的spider爬虫:
scrapy crawl link_checker
您的爬虫程序爬取深度不能超过两页,并且当所有页面下载完毕将会停止运行。其输出结果将显示链接到下载页面的页面以及链接的文本信息。
设置需处理的HTTP状态
默认情况下,Scrapy爬虫仅解析请求成功的HTTP请求;,在解析过程中需要排除所有错误。为了收集无效的链接,404响应就必须要被解析了。创建
valid_url
和
invalid_url
两个数组,,分别将有效和无效的链接存入。
-
1.设置在spider爬虫属性
handle_httpstatus_list
中解析的HTTP错误状态列表:handle_httpstatus_list = [404]
- 2.更新解析逻辑以检查HTTP状态和填充正确的数组。爬虫程序现在看起来像:
class LinkCheckerSpider(scrapy.Spider):
name = "link_checker"
allowed_domains = ['www.example.com']
# 设置需要处理的HTTP错误码
handle_httpstatus_list = [404]
# 初始化有效和无效链接的数组
valid_url, invalid_url = [], []
maxdepth = 2
def parse(self, response):
from_url = ''
from_text = ''
depth = 0;
if 'from' in response.meta: from_url = response.meta['from']
if 'text' in response.meta: from_text = response.meta['text']
if 'depth' in response.meta: depth = response.met['depth']
# 出现了404错误,填充无效链接数组
if response.status == 404:
self.invalid_url.append({'url': response.url,
'from': from_url,
'text': from_text})
else:
# 填充有效链接数组
self.valid_url.append({'url': response.url,
'from': from_url,
'text': from_text})
if depth < self.maxdepth:
a_selectors = response.xpath("//a")
for selector in a_selectors:
text = selector.xpath("text()").extract_first()
link = selector.xpath("@href").extract_first()
request = response.follow(link, callback=self.parse)
request.meta['from'] = response.url;
request.meta['text'] = text
yield request
-
3.运行更新后的Spider爬虫:
scrapy crawl link_checker
这里的输出信息应该比以前的更多。这两个数组虽然已填充但从并未打印信息到控制台。爬虫程序必须在信息处理程序爬取结束时就转存它们。
设置信息处理程序
Scrapy允许您在爬取过程中的各个点中添加一些处理程序。信息处理程序使用
crawler.signals.connect()
方法进行设置,
crawler
对象在
Spider
类中的
from_crawler()
方法中可用。
要在爬取过程结束时添加处理程序以打印有关无效链接的信息,请重写
from_crawler
方法以注册处理
signals.spider_closed
信号的处理程序:
# 重写from_crawler方法
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
# 回调父方法以保障正常运行
spider = super(LinkCheckerSpider, cls).from_crawler(crawler, *args, **kwargs)
# 为spider_closed标记注册spider_closed处理程序
crawler.signals.connect(spider.spider_closed, signals.spider_closed)
return spider
# This method is the actual handler
def spider_closed(self):
# 打印爬取到i信息中的一些有效信息
print('There are', len(self.valid_url), 'working links and',
len(self.invalid_url), 'broken links.', sep=' ')
# 如果有的话,输出所有无效链接
if len(self.invalid_url) > 0:
print("Broken links are:")
for invalid in self.invalid_url:
print(invalid)
请参阅 Scrapy信号文档 来获取完整的可用信号列表。
再次运行Spider爬虫,您将在Scrapy统计信息之前看到无效链接的详细信息。
命令行的输入起始URL网址
初始的URL网址在spider爬虫的源代码中是硬编码的。如果我们可以在启动爬虫时就设置它而不是更改代码,效果会更好。
scrapy crawl
允许通过命令行使用
__init__()
类构造函数来传递参数。
-
1.使用
url
参数向爬虫程序添加__init__()
方法:
# 将url参数添加到自定义构造函数
def __init__(self, url='http://www.example.com', *args, **kwargs):
# 不要忘记调用父构造函数
super(LinkCheckerSpider, self).__init__(*args, **kwargs)
# 使用url参数设置start_urls属性
self.start_urls = [url]
-
2.使用
-a
命令行标志传递Spider爬虫参数:scrapy crawl linkChecker -a url="http://another\_example.com"
进行项目设置
爬虫程序的默认Scrapy设置在
settings.py
文件中定义。请将最大下载大小设置为3 MB,以防止Scrapy下载视频或二进制文件等大文件。
请编辑
~/scrapy/linkChecker/linkChecker/settings.py
并添加以下行:
移除域名限制
我们的爬虫程序有一个名为
allowed_domains
的参数来阻止下载不需要的URL 网址。如果没有此属性,爬虫可能会尝试遍历整个Web并且永远不会完成其任务。
如果
www.example.com
域中与外部域的链接中断,则将不会检测到该链接,因为爬虫不会对其进行爬取信息。删除该
allowed_domains
属性以添加下载外部网页的自定义逻辑,这不会造成递归浏览其链接。
- 1.添加URL网址和正则表达式管理包:
import re
from urllib.parse import urlparse
-
2.添加
domain = ''
属性将保存主域。主域未初始化,在其第一次下载时设置为实际URL网址。在HTTP重定向的情况下,实际URL可能与起始URL不同。 -
3.删除
allowed_domains
属性 -
4.初始化
parse
方法中的domain
属性:
if len(self.domain) == 0:
parsed_uri = urlparse(response.url)
self.domain = parsed_uri.netloc
- 5.更新表达式以添加域检查并对新的URL网址进行深度检查:
parsed_uri = urlparse(response.url)
# 对新链接采用先前的逻辑
if parsed_uri.netloc == self.domain and depth < self.maxdepth:
请参阅下一节中的完整spider爬虫,之前的相关设置回集成在此代码中。
完全实现的Spider爬虫程序
这是功能齐全的Spider爬虫程序。添加了一些技巧来获取响应域并阻止其他域链接的递归浏览。否则,您的Spider爬虫将尝试解析整个网络!
import re
from urllib.parse import urlparse
import scrapy
from scrapy import signals
class LinkCheckerSpider(scrapy.Spider):
name = 'link_checker'
# 设置需要处理的HTTP错误码
handle_httpstatus_list = [404]
valid_url = []
invalid_url = []
# 设置最大深度
maxdepth = 2;
domain = ''
def __init__(self, url='http://www.example.com', *args, **kwargs):
super(LinkCheckerSpider, self).__init__(*args, **kwargs)
self.start_urls = [url]
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = super(LinkCheckerSpider, cls).from_crawler(crawler, *args, **kwargs)
# 为spider_closed标记注册spider_closed处理程序
crawler.signals.connect(spider.spider_closed, signals.spider_closed)
return spider
def spider_closed(self):
"""spider_closed 标签的处理程序"""
print('----------')
print('There are', len(self.valid_url), 'working links and',
len(self.invalid_url), 'broken links.', sep=' ')
if len(self.invalid_url) > 0:
print('Broken links are:')
for invalid in self.invalid_url:
print(invalid)
print('----------')
def parse(self, response):
""" 解析下载页面的主方法"""
# 为没有元信息的首页设置默认值
from_url = ''
from_text = ''
depth = 0;
# 如果有的话,解析响应中的元信息
if 'from' in response.meta: from_url = response.meta['from']
if 'text' in response.meta: from_text = response.meta['text']
if 'depth' in response.meta: depth = response.meta['depth']
# 如果第一次响应,更新域信息(以管理重定向)
if len(self.domain) == 0:
parsed_uri = urlparse(response.url)
self.domain = parsed_uri.netloc
# 出现404错误,填充无效链接数组
if response.status == 404:
self.invalid_url.append({'url': response.url,
'from': from_url,
'text': from_text})
else:
#填充有效链接数组
self.valid_url.append({'url': response.url,
'from': from_url,
'text': from_text})
# 解析当前页面的域信息
parsed_uri = urlparse(response.url)
# 当以下情况解析新链接:
# - 如果当前页面不是外部域
# - 且其深度小于最大深度
if parsed_uri.netloc == self.domain and depth < self.maxdepth:
# 获得所有<a>标签
a_selectors = response.xpath("//a")
# 为每一个标签循环
for selector in a_selectors:
# 解析链接文本
text = selector.xpath('text()').extract_first()
# 解析链接网址
link = selector.xpath('@href').extract_first()
# 创建新的Request请求对象
request = response.follow(link, callback=self.parse)
request.meta['from'] = response.url;
request.meta['text'] = text
# 利用生成器返回
yield request
监控正在运行的Spider程序
Scrapy在6023端口上提供telnet接口以监控正在运行的spider爬虫程序。telnet会话是一个您可以在其中执行Scrapy公有对象上的方法的Python shell脚本。
- 在后台运行你的spider爬虫: