前言
Scrapy 是一个基于 python 开发的爬虫框架,用于抓取 web 站点并从页面中提取结构化的数据,提供很多爬虫组件、基类,可扩展性强,通过其可以将爬虫中重复冗杂的逻辑步骤抽离出来,将通用步骤做成组件,至规则化通用爬虫,后续只需要添加或更改特定的逻辑,将代码编写简洁化,也提升了后期的可维护性。
Scrapy 文档:https://doc.scrapy.org/
Scrapy 框架的架构组成
Scrapy 框架的架构主要由以下五部分组成:
- 引擎(Engine):整个架构的核心,处理整个系统的数据流和事件,负责数据的流转和逻辑的处理,类似于框架的中央处理器控制整个流程
- 调度器(Scheduler):接收 Engine 发送的 Request 并将其加入队列中,同时也可以将 Request 发回给 Engine 供 Downloader 执行,负责维护 Request 的调度逻辑,通过它来决定下一个抓取的网址是什么,同时去除重复的网址
- 下载器(Downloader):用于高速地下载网络上的资源,Engine 向 Downloader 发送 Request,其得到 Response 再发送给 Engine 处理,建立在高效的异步模型 twisted 上
- 爬虫(Spider):定义了站点的爬取逻辑和页面的解析规则,负责响应并生成 Item 和新的 Request 并发送给 Engine 进行处理,可从网页中提取所需的信息
- 项目管道(Item Pipeline):负责处理 Spider 从页面中抽取的 Item,进行数据清洗、验证和存储等工作
整体架构如下图所示:
- 下载器中间件(Downloader Middlewares):位于 Engine 与 Downloader 之间的 Hook 框架,负责实现 Engine 与 Downloader 之间的请求和响应的处理过程
- 爬虫中间件(Spider Middlewares):位于 Spider 与 Downloader 之间的 Hook 框架,负责实现 Spider 与 Downloader 之间的 Item、请求和响应的处理过程
- Item:保存爬取到的数据的容器
Hook:https://blog.csdn.net/Yy_Rose/article/details/124216720
Scrapy 框架的数据流过程
Scrapy 框架中,由 Engine 负责了整个数据流的分配和处理,数据流主要包括 Item、Request、Response,如上图所示,基本数据流如下:
- 启动爬虫时,Engine 根据要爬取的目标站点找到处理该站点的 Spider,Spider 会生成最初需要爬取的页面对应的一个或多个 Request,然后发送给 Engine
- Engine 从 Spider 中获取这些 Request,然后把它们交给 Scheduler 等待被调度
- Engine 向 Scheduler 索取下一个要处理的 Request,这时候 Scheduler 根据其调度逻辑选择合适的 Request 发送给 Engine
- Engine 将 Scheduler 发送的 Request 转发给 Downloader 进行下载执行,将 Request 发送给 Downloader 的过程会经由许多定义好的 Dwonloader Middlewares 处理
- Downloader 将 Request 发送给目标服务器,得到对应的 Response,然后将其返回给 Engine,将 Response 返回给 Engine 的过程同样会经由许多定义好的 Downloader Middlewares 处理
- Engine 从 Downloader 处接收到的 Response 里包含了爬取的目标站点的内容,Engine 会将此 Response 发送给对应的 Spider 进行处理,将 Response 发送给 Spider 的过程中会经由定义好的 Spider Middlewares 处理
- Spider 处理 Response,解析 Response 的内容,这时候 Spider 会产生一个或多个爬取结果 Item 或者后续要爬取的目标页面对应的一个或多个 Request,然后再将这些 Item 或 Request 发送给 Engine 进行处理,将 Item 或 Request 发送给 Engine 的过程经由定义好的 Spider Middlewares 处理
- Engine 将 Spider 发回的一个或多个 Item 转发给定义好的 Item Pipelines 进行数据处理或存储的一系列操作,将 Spider 发回的一个或多个 Request 转发给 Scheduler 等待下一次被调度
重复 2 至 8 过程,直至 Scheduler 中没有更多的 Request,这时候 Engine 会关闭 Spider,整个爬取过程结束。
转载于:https://cuiqingcai.com/17777.html
Scrapy 爬取 NBA 中文网球员季后赛数据
创建项目(scrapy startproject 项目名称):
scrapy startproject nbainfo
切换到项目文件夹,创建 spider(scrapy genspider spider名称 网站域名):
scrapy genspider nba china.nba.cn
scrapy 框架创建完成:
nbainfo # 项目文件夹
spiders # spider文件夹
__init__.py
nba.py # spider 名称,网站链接的配置,抓取逻辑,解析逻辑的实现
__init__.py
items.py # 定义爬取的数据结构,创建 Item
middlewares.py # 定义爬取时的中间件,设置请求头,设置代理,设置 cookie,处理重定向等
pipelines.py # 定义数据管道,存储数据等
settings.py # 配置文件
scrapy.cfg # Scrapy 部署时的配置文件
直接获取网页响应代码看不到球员数据内容,所以可以推测球员数据为 Ajax 动态加载,开启开发者工具 → Network → Preview,Ajax 请求内容在 XHR 中可以看到,如图最下一个文件中可以看到对应的球员全部数据信息:
items.py → 创建 Item
from scrapy import Item, Field
class NbainfoItem(Item):
rank = Field()
playerName = Field()
country = Field()
team = Field()
game_num = Field()
game_started = Field()
ave_score = Field()
ave_backboard = Field()
ave_assists = Field()
time = Field()
efficiency = Field()
two_rates = Field()
three_rates = Field()
penalty_shot_rates = Field()
attack = Field()
defense = Field()
ave_steals = Field()
ave_capping = Field()
errors = Field()
fouls = Field()
nba.py → Spider,其作用为:
- 定义爬取网站的动作
- 分析爬取下来的页面
from scrapy import Spider, Request
from nbainfo.items import NbainfoItem
import json
class NbaSpider(Spider):
# 爬虫名称
name = 'nba'
# 允许爬取的域名
allowed_domains = ['china.nba.cn']
# 起始 URL 列表
start_urls = ['https://china.nba.cn']
def start_requests(self):
base_url = 'https://china.nba.cn/stats2/league/playerstats.json?conference=All&country=All&individual=All&locale=zh_CN&pageIndex=0&position=All&qualified=false&season=2021&seasonType=4&split=All+Team&statType=points&team=All&total=perGame'
headers = {
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
'cookie': 'cna=c91496a55d67443ba13a743cee516a6a; privacyV2=true; AMCVS_248F210755B762187F000101%40AdobeOrg=1; s_cc=true; i18next=zh_CN; locale=zh_CN; countryCode=CN; _abfpc=b3bf192770bcca9001aecb42fb3f52762c2e59aa_2.0; AMCV_248F210755B762187F000101%40AdobeOrg=-1712354808%7CMCIDTS%7C19118%7CMCMID%7C17427397163401041232983010658028957487%7CMCAAMLH-1652423791%7C11%7CMCAAMB-1652423791%7CRKhpRz8krg2tLO6pguXWp5olkAcUniQYPHaMWWgdJ3xzPWQmdj0y%7CMCOPTOUT-1651826191s%7CNONE%7CMCAID%7CNONE%7CvVersion%7C4.3.0; tp=3632; s_ppv=cn%253Astats%253Aplayers%253Amain%2C12%2C12%2C450; s_gpv=no%20value',
'referer': 'https://china.nba.cn/statistics/',
}
yield Request(base_url, callback=self.parse_detail, headers=headers)
def parse_detail(self, response):
html = response.text
results = json.loads(html)
result = results.get('payload').get('players')
item = NbainfoItem()
for result_info in result:
name_info = result_info.get('playerProfile')
item['rank'] = result_info.get('rank')
item['country'] = name_info.get('country')
item['playerName'] = name_info.get('displayName')
tema_info = result_info.get('teamProfile')
item['team'] = tema_info.get('name')
ave_info = result_info.get('statAverage')
item['game_num'] = ave_info.get('games')
item['game_started'] = ave_info.get('gamesStarted')
item['ave_score'] = ave_info.get('pointsPg')
item['ave_backboard'] = ave_info.get('rebsPg')
item['ave_assists'] = ave_info.get('assistsPg')
item['time'] = ave_info.get('minsPg')
item['efficiency'] = ave_info.get('efficiency')
item['two_rates'] = ave_info.get('fgpct')
item['three_rates'] = ave_info.get('tppct')
item['penalty_shot_rates'] = ave_info.get('ftpct')
item['attack'] = ave_info.get('offRebsPg')
item['defense'] = ave_info.get('defRebsPg')
item['ave_steals'] = ave_info.get('stealsPg')
item['ave_capping'] = ave_info.get('blocksPg')
item['errors'] = ave_info.get('turnoversPg')
item['fouls'] = ave_info.get('foulsPg')
yield item
settings.py → 配置文件,这里需进行几个设置:
1. robots.txt 文件遵循 Robot 协议,告诉搜索引擎爬虫,本网站哪些目录下的网页不希望你进行爬取,在 Scrapy 启动后,会在第一时间访问网站的 robots.txt 文件,然后决定该网站的爬取范围,由于我们所想爬取的内容一般都是不被允许的,所以拒绝遵守 Robot 协议:
ROBOTSTXT_OBEY = False
2. 在使用 scrapy crawl -o file.json 将获取到的内容写入 json 文件时,对于中文,保存的是 unicode 编码字符,输出结果为编码字符串,因此需要转换为 utf-8 中文编码:
FEED_EXPORT_ENCODING = 'utf-8'
也可以使用以下命令,不过直接在 settings.py 中配置会使后续测试更为便捷:
scrapy crawl -o file.json -s FEED_EXPORT_ENCODING=UTF-8
3. 添加 MongoDB 相关变量,开启 MongoDB 管道:
# MongoDB 连接字符串
MONGODB_CONNECTION_STRING = 'localhost'
# 数据库名称
MONGODB_DATABASE = 'nba_data'
# 启用项目管道,300 为调用优先级
ITEM_PIPELINES = {
'nbainfo.pipelines.MongoDBPipeline': 300,
}
pipelines.py → Item Pipelines 项目管道,其作用为:
- HTML 数据清洗
- 验证爬取数据,检查爬取字段
- 查重并丢弃重复内容
- 将爬取结果存储到数据库中
class MongoDBPipeline(object):
def __init__(self, connection_string, database):
# 初始化数据库对象
self.connection_string = connection_string
self.database = database
@classmethod
# from_crawler 获取 settings.py 中配置信息
def from_crawler(cls, crawler):
return cls(
connection_string = crawler.settings.get(
'MONGODB_CONNECTION_STRING'),
database = crawler.settings.get('MONGODB_DATABASE')
)
# 通过 from_crawler 赋值的 connection_string 创建一个 MongoDB 连接对象
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.connection_string)
self.db = self.client[self.database]
# 接收从 Spider 生成的 item 对象,存入到数据库中
def process_item(self, item, spider):
name = 'nba_player_data'
self.db[name].insert_one(dict(item))
return item
# Spider 运行结束时关闭 MongoDB 连接
def close_spider(self, spider):
self.client.close()
保存文件内容至 json 文件:
scrapy crawl -o filename.json
保存文件内容至 csv 文件:
scrapy crawl -o filename.csv
Matplotlib 图表数据分析
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.ticker import FuncFormatter
data_file = pd.read_csv('./nbainfo.csv')
data = pd.Series([data_file['playerName'], data_file['three_rates']])
# 窗体设置
plt.figure(figsize=(25, 13)).canvas.set_window_title('球员三分命中率比较')
# 文本设置
plt.rc('font', family='SimHei', size=15)
# 标题设置
plt.title('球员三分命中率比较')
# x轴标签
plt.xlabel('球员排名')
# y轴标签
plt.ylabel('三分命中率')
# plt.xticks(ticks, [labels], **kwargs), x轴刻度设置
plt.xticks(range(0, 50), data[0], rotation=270, fontsize=8)
def to_percent(temp, position):
return '%1.0f' % temp + '%'
# y轴刻度百分比设置
plt.gca().yaxis.set_major_formatter(FuncFormatter(to_percent))
# plt.text(x,y,s,fontsize,verticalalignment,horizontalalignment,rotation,**kwargs)
for a, b, i in zip(data[0], data[1], range(len(data[0]))): # zip 函数
plt.text(a, b+0.5, "%1.0f" % data[1][i] + '%', ha='center', fontsize=11)
# 箭头设置
plt.annotate('卢卡·东契奇', xy=(0, 0), xytext=(-8, 8), color='r', size=10,
arrowprops=dict(facecolor='r', shrink=0.05, width=4.5))
# 绘制条形图 barh 为横向条形图, bar 为纵向条形图
plt.bar(data[0], data[1], color='b', width=0.25)
# 显示网格
plt.grid(True)
# savefig(fname, dpi=None, facecolor=’w’, edgecolor=’w’, orientation=’portrait’, papertype=None, format=None, transparent=False, bbox_inches=None, pad_inches=0.1, frameon=None, metadata=None)
plt.savefig('./nbainfo.png')
plt.show()
plt.savefig() 各参数用法可参考:https://blog.csdn.net/wilbeok/
Scrapyd
Scrapyd 提供一些列 HTTP 接口帮助部署、启动、停止和删除爬虫程序,可以对 Scrapy 爬虫任务进行有效便捷的管理。
安装
pip3 install scrapyd
以下结果即为启动成功:
由上图可知,Scrapyd 启动在 6800 端口,网址链接为 http://127.0.0.1:6800/,访问服务器的该端口,就能看到 Web UI 页面,以下即为成功:
Scrapyd 的相关接口方法可参考官方文档:https://python-scrapyd-api.readthedocs.io/en/latest/
总结
以上是对 scrapy 框架的学习总结,以及爬取 ajax 动态加载数据项目实践,如有见解欢迎评论区交流指正~
参考资料:
https://blog.csdn.net/ck784101777/article/details/104468780
https://cuiqingcai.com/17777.html
https://blog.csdn.net/You_are_my_dream/