Python - 爬虫之Scrapy
Posted GitLqr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python - 爬虫之Scrapy相关的知识,希望对你有一定的参考价值。
欢迎关注微信公众号:FSA全栈行动 👋
一、scrapy 概念和流程
1、概念
Scrapy 是一个 python 编写的,被设计用于爬取网络数据、提取结构性数据的开源网络爬虫框架。
- 作用:少量的代码,就能够快速的抓取
- 官方文档:https://scrapy-chs.readthedocs.io/zh_CN/0.24/
补充:Scrapy 使用了 Twisted 异步网络框架,可以加快下载速度
2、工作流程
其流程描述如下:
- 爬虫中起始的 url 构造成 request 对象 --> 爬虫中间件 --> 引擎 --> 调度器
- 调度器把 request --> 引擎 --> 下载中间件 --> 下载器
- 下载器发送请求,获取 response 响应 --> 下载中间件 --> 引擎 --> 爬虫中间件 --> 爬虫
- 爬虫提取 url 地址,组装成 request 对象 --> 爬虫中间件 --> 引擎 --> 调度器,重复步骤 2
- 爬虫提取数据 --> 引擎 --> 管道处理和保存数据
注意:
- 图中绿色线条表示数据的传递
- 图中中间件的位置决定了其作用
- 引擎处于 4 个模块中间,各个模块之间相互独立,只和引擎进行交互
3、各模块的具体作用
模块 | 作用 | 是否实现 |
---|---|---|
Scrapy Engine(引擎) | 总指挥:负责数据和信号在不同模块之间传递 | scrapy 已经实现 |
Scheduler(调度器) | 一个队列,存放引擎发过来的 request 请求 | scrapy 已经实现 |
Downloader(下载器) | 下载把引擎发过来的 request 请求,并返回给引擎 | scrapy 已经实现 |
Spider(爬虫) | 处理引擎发来的 response,提取数据、url,并交给引擎 | 需要手写 |
Item Pipeline(管道) | 处理引擎传过来的数据,比如存储 | 需要手写 |
Downloader Middlewares(下载中间件) | 可以自定义的下载扩展,比如设置代理 | 一般不用手写 |
Spider Middlewares(爬虫中间件) | 可以自定义 requests 请求和进行 response 过滤 | 一般不用手写 |
注意:爬虫中间件 和 下载中间件 只是运行逻辑的位置不同,作用是重复的:如替换 UA 等。
二、scrapy 入门使用
1、安装 scrapy
scrapy 有 2 种安装方式:
-
命令:
sudo apt-get install scrapy
-
pip:
pip/pip3 install scrapy
2、项目开发流程
-
创建项目:
scrapy startproject <项目名称> eg: scrapy startproject myspider
-
生成一个爬虫:
scrapy genspider <爬虫名字> <允许爬取的域名> eg: cd myspider scrapy genspider example example.com
-
爬虫名字:作用爬虫运行时的参数
-
允许爬取的域名:为对于爬虫设置的爬取范围,设置之后用于过滤要爬取的 url,如果爬取的 url 与允许的域不同则被过滤掉
注意:执行完命令后,myspider/spiders 目录下,会多出一个 example.py 爬虫文件
-
-
提取数据:
根据网站结构在 spider 中实现数据采集相关内容
-
保存数据:
使用 pipeline 进行数据后续处理和保存
-
运行 scrapy
scrapy crawl <爬虫名字> scrapy crawl <爬虫名字> --nolog eg: scrapy crawl example
注意 :需要在项目录下执行命令;
--nolog
可以关闭框架日志信息输出
3、三个内置对象
- request 请求对象:由 url method post_data headers 等构成
- response 响应对象:由 url body status headers 等构成
- item 数据对象:本质是字典
1)定位元素以及提取数据
解析并获取 scrapy 爬虫中的数据:利用 xpath 规则字符串进行定位和提取
response.xpath()
:返回一个类似 List 的类型,其中包含的是 selector 对象,操作和列表一样,但是有一些额外方法:extract()
:返回一个包含有字符串的列表extract_first()
:返回列表中的第一个字符串,列表为空则返回 None
2)response 响应对象的常用属性
response.url
:当前响应的 url 地址response.request.url
:当前响应对应的请求的 url 地址response.headers
:响应头response.request.headers
:当前响应的请求头response.body
:响应体,也就是 html 代码,byte 类型response.status
:响应状态码
4、入门实战
1)创建项目&爬虫
scrapy startproject myspider
cd myspider
scrapy genspider itcast itcast.cn
2)完善爬虫(抓取数据)
在/myspider/myspider/spiders/itcast.py 中修改内容如下:
import scrapy
class ItcastSpider(scrapy.Spider):
name = 'itcast'
# 2.检查域名
allowed_domains = ['itcast.cn']
# 1.修改起始url
start_urls = ['http://itcast.cn/channel/teacher.shtml'] # 设置起始的url,我们只需要设置就好,通常会被自动的创建成请求发送
# 3.在parse方法中实现爬取逻辑
def parse(self, response):
# 解析方法,通常用于起始url对应响应的解析
# 定义对于网站的相关操作
# with open('itcast.html', 'wb') as f:
# f.write(response.body)
# 获取所有老师节点
node_list = response.xpath('//div[@class="li_txt"]')
# 遍历老师节点列表
for node in node_list:
temp = {}
# xpath方法返回的是选择器对象列表,extract()用于从选择器对象中提取数据
# temp['name'] = node.xpath('./h3/text()')[0].extract()
# temp['title'] = node.xpath('./h4/text()')[0].extract()
# temp['desc'] = node.xpath('./p/text()')[0].extract()
# xpath结果为只含有一个值的列表,可以使用extract_first(),如果为多个值则使用extract()
temp['name'] = node.xpath('./h3/text()').extract_first()
temp['title'] = node.xpath('./h4/text()').extract_first()
temp['desc'] = node.xpath('./p/text()').extract_first()
# {'name': [<Selector xpath='./h3/text()' data='黄老师'>], 'title': [<Selector xpath='./h4/text()' data='高级讲师'>], 'desc': [<Selector xpath='./p/text()' data='15年+的软件开发与教学经验,参与中国联通VOP系统,中国联通在线计费系统...'>]}
# extract()之后:
# {'name': '黄老师', 'title': '高级讲师', 'desc': '15年+的软件开发与教学经验,参与中国联通VOP系统,中国联通在线计费系统,道路交通事故救助基金管理系统等的开发工作;\\r\\n拥有丰富的教学经验。'}
print(temp)
yield temp
注意:
scrapy.Spider
爬虫类中必须有名为 parse 的解析- 如果网站结构层次比较复杂,也可以自定义其他解析函数
- 在解析函数中提取的 url 地址如果要发送请求,则必须属于
allowed_domains
范围内,但是start_urls
中的 url 地址不受这个限制 parse()
函数中使用yield
返回数值。- yield 能够传递的对象只能是:BaseItem,Request,dict,None
3)完善管道(保存数据)
- 定义一个管道类
- 重写管道类的
process_item()
方法 process_item()
方法处理完 item 之后必须返回给引擎
这里直接使用创建项目时默认创建好的管道类:/myspider/myspider/pipelines/MyspiderPipeline,修改内容如下:
import json
class MyspiderPipeline:
def __init__(self):
self.file = open('itcast.json', 'w')
def process_item(self, item, spider):
# print('item:', item)
# 将字典数据序列化
json_data = json.dumps(item, ensure_ascii=False) + ',\\n'
# 将数据写入文件
self.file.write(json_data)
# 默认使用完管道之后需要将数据返回给引擎
return item
def __del__(self):
self.file.close()
4)启用管道
ITEM_PIPELINES = {
'myspider.pipelines.MyspiderPipeline': 300,
# 'myspider.pipelines.MyspiderPipelineOther': 301,
}
- 配置项中,键为使用的管道类,管道类引用声明使用
.
进行分割,分别为:项目目录.文件.管理类
。 - 配置项中,值为管道的使用顺序,设置的数值越小越先执行,该值一般设置在 1000 以内。
5)运行爬虫
在控制台运行如下命令即可运行爬虫:
scrapy crawl itcast
scrapy crawl itcast --nolog
三、scrapy 数据建模(items)
1、数据建模
通常在项目开发过程中,需要在 items.py
中进行数据建模
1)为什么要建模
- 定义 item 即提前规划好哪些字段需要抓取,防止手误,因为定义好之后,在运行过程中,系统会自动检查
- 配置注释一起可以清晰的知道要抓取哪些字段,没有定义的字段不能抓取,在目标字段少的时候可以使用字典代替
- 使用 scrapy 的一些特定组件中需要 Item 做支持,如 scrapy 的 ImagesPipeline 管道类。
2)如何建模
在 items.py
文件中定义发提取的字段:
import scrapy
class MyspiderItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field() # 讲师的名字
title = scrapy.Field() # 讲师的职称
desc = scrapy.Field() # 讲师的介绍
# if __name__ == '__main__':
# item = MyspiderItem()
# item['name'] = 'lqr' # ok
# item['nama'] = 'lqr' # KeyError: 'MyspiderItem does not support field: nama'
注意:
scrapy.Item
可以理解为一个更高级的 “字典”,可以对键名进行限制、校验。但切记它不是字典,如果你需要对字典进行操作,可以使用dict()
将scrapy.Item
进行强制转换。
3)使用模板类
模板类定义以后,需要在爬虫中导入并且实例化,之后的使用方法和字典相同:
# 如果报包报错有2种解决办法:
# 1、PyCharm以myspider为根目录重新打开项目
# 2、不以myspider为根目录的话,可以右键myspider-->Make Directory as-->Sources Root
from myspider.items import MyspiderItem
def parse(self, response):
item = MyspiderItem()
...
item['name'] = node.xpath('./h3/text()').extract_first()
item['title'] = node.xpath('./h4/text()').extract_first()
item['desc'] = node.xpath('./p/text()').extract_first()
...
yield item
4)开发流程总结
-
创建项目
scrapy startproject 项目名
-
明确目标
- 在 items.py 文件中进行建模
-
创建爬虫
-
创建爬虫
scrapy genspider 爬虫名 允许的域
-
编写爬虫
修改start_urls 检查修改allowed_domains 编写解析方法
-
-
保存数据
- 在
pipelines.py
文件中定义对数据处理的管道 - 在
settings.py
文件中注册启用管道
- 在
四、scrapy 处理翻页(Request)
1、思路
- 找到下一页的 url 地址
- 构造 url 地址的请求对象,传递给引擎
2、实现步骤
-
确定 url 地址
-
构造请求
scrapy.Request(url, callback)
callback
:指定解析函数名称,表示该请求返回的响应使用哪一个函数进行解析
-
把请求交给引擎
3、实战(网易招聘爬虫)
1)思路分析
- 获取首页数据
- 寻找下一页地址,进行翻页,获取数据
2)代码实现
- 创建项目
scrapy startproject wangyi
cd wangyi
scrapy gespider job 163.com
- 定义模板类
import scrapy
class WangyiItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
link = scrapy.Field()
depart = scrapy.Field()
category = scrapy.Field()
type = scrapy.Field()
address = scrapy.Field()
num = scrapy.Field()
date = scrapy.Field()
- 编写爬虫(抓取数据)
import scrapy
from wangyi.items import WangyiItem
class JobSpider(scrapy.Spider):
name = 'job'
# 2.检查修改allowed_domains
allowed_domains = ['163.com']
# 1.修改start_urls
start_urls = ['https://hr.163.com/position/list.do']
def parse(self, response):
# 提取数据
# 获取所有职位节点列表
node_list = response.xpath('//*[@class="position-tb"]/tbody/tr')
# 遍历节点列表
for num, node in enumerate(node_list):
# 设置过滤条件,将目标节点获取出来
if num % 2 == 0:
item = WangyiItem()
item['name'] = node.xpath('./td[1]/a/text()').extract_first()
# item['link'] = 'https://hr.163.com' + node.xpath('./td[1]/a/@href').extract_first()
# response.urljoin()用于拼接相对路径的url,可以理解为自动补全
item['link'] = response.urljoin(node.xpath('./td[1]/a/@href').extract_first())
item['depart'] = node.xpath('./td[2]/text()').extract_first()
item['category'] = node.xpath('./td[3]/text()').extract_first()
item['type'] = node.xpath('./td[4]/text()').extract_first()
item['address'] = node.xpath('./td[5]/text()').extract_first()
item['num'] = node.xpath('./td[6]/text()').extract_first().strip()
item['date'] = node.xpath('./td[7]/text()').extract_first()
# print(item)
yield item
# 模拟翻页
part_url = response.xpath('/html/body/div[2]/div[2]/div[2]/div/a[last()]/@href').extract_first()
# 判断终止条件
if part_url != 'javascript:void(0)':
next_url = response.urljoin(part_url)
# 构建请求对象,并且返回给引擎
yield scrapy.Request(
url=next_url,
callback=self.parse
)
注意:引擎根据爬虫
yield
的对象类型,将对象分配给对应的模块处理,比如:如果是模板类对象或字典,则会交给管道(Item Pipeline);如果是 Request 对象,则会交给队列(Scheduler)。
- 编写管道(存储数据)
import json
class WangyiPipeline:
def __init__(self):
self.file = open('wangyi.json', 'w')
def process_item(self, item, spider):
item = dict(item)
str_data = json.dumps(item, ensure_ascii=False) + ',\\n'
self.file.write(str_data)
return item
def __del__(self):
self.file.close()
- 启用管道
ITEM_PIPELINES = {
'wangyi.pipelines.WangyiPipeline': 300,
}
- 运行爬虫
scrapy crawl job --nolog
3)扩展:
-
可以在 settings 中设置 ROBOTS 协议
# False表示忽略网站的robots.txt协议,默认为True ROBOTSTXT_OBEY = False
-
可以在 settings 中设置 User-Agent:
# scrapy发送的每一个请求的默认UA都是设置的这个User-Agent USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36'
4、scrapy.Request 的更多参数
scrapy.Request(url[, callback, method="GET", headers, body, cookies, meta, dont_filter=False])
注意:中括号[]里的参数为可选参数
参数解释:
- callback:表示当前 url 的响应交给哪个函数去处理
- meta:实现数据在不同的解析函数中传递,meta 默认带有部分数据,比如下载延迟,请求深度等
- dont_filter:默认为 False,会过滤请求的 url 地址,即请求过的 url 地址不会继续被请求,对需要重复请求的 url 地址可以把它设置为 True,比如贴吧的翻页请求,页面的数据总是在变化,start_urls 中的地址会被反复请求,否则程序不会启动
- method:指定 POST 或 GET 请求
- headers:接收一个字典,其中不包括 cookies
- cookies:接收一个字典,专门放置 cookies
- body:接收 json 字符串,为 POST 的数据,发送 payload_post 请求时使用
1)meta 参数的使用
-
meta 的使用:
- meta 可以实现数据在不同的解析函数中传递
-
meta 的注意事项:
- meta 参数是一个字典
- meta 字典中有一个固定的键 proxy,表示代理 ip
-
使用举例:
def parse(self, response):
...
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
...
def parse_detail(self,response):
# 获取之前传入的item
item = response.meta['item']
五、scrapy 模拟登录(cookie)
1、模拟登录的不同实现方式
- requests 模块
- 直接携带 cookies 请求页面
- 找到 url 地址,发送 post 请求存储 cookie
- selenium(浏览器自动处理 cookie)
- 找到对应的 input 标签,输入文本点击登录
- scrapy
- 直接携带 cookies
- 找 url 地址,发送 post 请求存储 cookie
2、scrapy 直接携带 cookies 获取需要登录信息的页面
应用场景:
- cookie 过期时间很长,常见于一些不规范的网站
- 能在 cookie 过期之前把所有的数据拿到
- 配合其他程序使用,比如其使用 selenium 把登录之后的 cookie 获取到并保存到本地,scrapy 发送请求之前先读取本地 cookie
1、start_url 携带 cookie(重写 start_requests 方法)
scrapy 中 start_url 是通过 start_requests 来进行处理的,可能通过重写该方法让 start_url 携带上请求头信息,实现代码如下:
import scrapy
class Git1Spider(scrapy.Spider):
name = 'git1'
allowed_domains = ['github.com']
start_urls = ['https://github.com/GitLqr']
def start_requests(self):
"""
重写start_requests,发送携带cookies的Request。
默认start_requests只是普通的get请求,不会携带自定义的头信息
"""
url = self.start_urls[0]
temp = '_octo=GH1.1.1045146750.1615451260; _device_id=cd8d64981fcb3fd4ba7f587873e97804'
# 把cookies字符串转成字典
cookies = {data.split('=')[0]: data.split('=')[-1] for data in temp.split('; ')}
yield scrapy.Request(
url=url,
callback=self.parse,
cookies=cookies
)
def parse(self, response):
print(response.xpath('/html/head/title/text()').extract_first())
注意:
- scrapy 中 cookie 不能够放在 headers 中,在构造请求的时候有专门的 cookies 参数,能够接收字典形式的 cookie
- 可能需要在 settings 中设置 ROBOTS 协议、USER_AGENT
2、scrapy.Request 发送 post 请求
scrapy.Request 发送 post 请求有两种方式:
-
通过
scrapy.Request()
指定 method、body 参数来发送 post 请求(不推荐) -
使用
scrapy.FormRequest()
来发送 post 请求(推荐)
注意:
scrapy.FormRequest()
能够发送表单和 ajax 请求,参考阅读 https://www.jb51.net/article/146769.htm
举例:
import scrapy
class Git2Spider(scrapy.Spider):
name = 'git2'
allowed_domains = ['github.com']
start_urls = ['http://github.com/login']
def parse(self, response):
username = 'GitLqr'
password = 'balabala'
# 从登录页面响应中解析出post数据
token = response.xpath('//input[@name="authenticity_token"]/@value').extract_first()
post_data = {
'commit': 'Sign in',
'authenticity_token': token,
'login': username,
'password': password,
'webauthn-support': 'supported',
}
print(post_data)
# 针对登录url发送post请求
yield scrapy.FormRequest(
url='https://github.com/session',
callback=self.after_login,
formdata=post_data
)
def after_login(self, response):
yield scrapy.Request('https://github.com/GitLqr', callback=self.check_login)
def check_login(self, response):
print(response.xpath('/html/head/title/text()').extract_first())
注意:在 settings.py 中通过设置
COOKIES_DEBUG = True
能够在终端查看到 cookie 的传递过程
六、scrapy 管道的使用
1、pipeline 中常用的方法
process_item(self, item, spider)
:- 管道类中必须有的函数
- 实现对 item 数据的处理
- 必须
return item
open_spider(self, spider)
:在爬虫开启的时候仅执行一次close_spider(self, spider)
:在爬虫关闭的时候仅执行一次
注意:以上 3 个方法,可以通过
spider.name
获取爬虫的名字
2、pipeline 判断数据来源
scrapy 的 Item Pipeline
模块可以有多个管道,当有一个 spider 把数据对象通过引擎交给 Item Pipeline
模块时, Item Pipeline
模块中的所有管道会按 settings.py
中指定的管道顺序一一被执行。但很多时候,我们需要管道针对特定爬虫做数据存储的,这时就需要在管道中对数据对象的来源做判断。
注意:不同的 pipeline 能够对一个或多个爬虫进行不同的数据处理的操作,比如一个进行数据清洗,一个进行数据的保存
举例:
- 爬虫 1:job.py
import scrapy
from wangyi.items import WangyiItem
class JobSpider(scrapy.Spider):
name = 'job'
...
def parse(self, response):
...
yield item
- 爬虫 2:job_simple.py
import scrapy
from wangyi.items import WangyiSimpleItem
class JobSimpleSpider(scrapy.Spider):
name = 'job_simple'
...
def parse(self, response):
...
yield item
- 2 个管道:pipelines.py
class WangyiPipeline:
def open_spider(self, spider):
if spider.name == 'job':
self.file = open('wangyi.json', 'w')
def process_item(self, item, spider):
if spider.name == 'job':
item = dict(item)
str_data = json.dumps(item, ensure_ascii=False) + ',\\n'
self.file.write(str_data)
return item
def close_spider(self, spider):
if spider.name == 'job':
self.file.close()
class WangyiSimplePipeline:
def open_spider(self, spider):
if spider.name == 'job_simple':
self.file = open('wangyi_simple.json', 'w')
def process_item(self, item, spider):
if spider.name == 'job_simple'以上是关于Python - 爬虫之Scrapy的主要内容,如果未能解决你的问题,请参考以下文章