scrapy框架学习笔记
scrapy运行机制
安装
直接pip install scrapy
即可
使用
命令行
scrapy startproject name
命令创建一个新的Scrapy项目scrapy crawl SpiderName
命令运行爬虫scrapy runspider SpiderName
命令运行脚本。
更多命令直接查Command line tool
概述
- 编写Spider,返回Request或自定义的Item对象
- 所有Request对象都会被scrapy调度请求,并在请求后调用设定的callback函数
- Item对象则是提取请求回的数据,结构化数据并交由item pipeline处理
- 定义好Item对象,存储必要的信息(个人认为以文本数据为主,二进制数据应保存链接后续pipeline处理)
- 定义item pipeline,处理Item对象,例如存储到数据库或文件
编写Spider
-
在
project/spider
下创建Spider文件(scrapy genspider SpiderName website.com
可快速创建示例)
定义Spider类,必须继承scrapy.Spider
类,并且必须指定类参数name
,scrapy crawl
通过这个name
来启动爬虫。 -
实现
start_requests
方法,定义爬虫入口,返回一个Request对象。
如果只是简单的get请求,可以设定类参数start_urls = [url list]
以使用默认方法(默认回调是parse
方法) -
实现
parse(self, response)
方法,用于处理请求返回的响应,返回Item对象或者Request对象。
经常有请求完初始链接后要从响应拿到下一步的链接,这时可以在parse
方法中返回一个Request对象,callback指定另一个函数,且可以通过kw_args
传递额外参数(如本次处理的数据)给callback。
返回Item则会交给pipeline处理
from scrapy.http import Request, JsonRequest
...
# 一般的Request请求构建
yield Request(
url=url,
method="GET", # 默认get,一般不写
headers={}, # 看需要写,Spider middleware好像也能操作
callback=self.parse, # 自定义回调函数
cb_kwargs={"additional_args": args},
)
# post请求
yield Request(
url=url,
method="POST",
headers={"Content-Type": "application/json"}, # 看需要写
callback=self.parse,
body=json.dumps(payload), # 注意转成JSON字符串
)
yield JsonRequest( # 省略content-type,基本就是post请求
url=url,
data=payload, # 注意这个直接传dict
callback=self.parse,
)
def parse(self, response, if_additional_args):
response.text
response.body # 取二进制数据,注意不是requests的content
response.json() # 如果是json数据,可以直接使用
# scrapy可以直接使用xpath和css选择器,非常方便
for quote in response.xpath("//div[@class='quote']"): # 返回的是SelectorList对象,可以遍历
yield {
"quote": quote.xpath("./span[1]/text()").get(), # 注意用get返回第一个结果值
"author": quote.xpath("span[2]/small/text()").get(),
"tags": quote.css("a.tag::text").getall(), # 注意用getall返回所有结果的列表
}
定义Item
在project/item
下定义Item类
Item类必须继承scrapy.Item
类,Item其实就是个字典,但是key只能是定义了的字段名
class CspProblemItem(scrapy.Item):
# contest info
contest_id = scrapy.Field()
contest_title = scrapy.Field()
contest_date = scrapy.Field()
# problem info
title = scrapy.Field()
description_url = scrapy.Field()
description_filepath = scrapy.Field()
description = scrapy.Field()
attachment_urls = scrapy.Field() # 最后下载的文件一般不保存在Item字段里,
attachment_filepaths = scrapy.Field() # 而是记录文件的url和路径,保存后记录在Item里
# flags to indicate whether the problem has been processed
done = scrapy.Field()
编写pipeline
- 在
project/pipeline
下定义pipeline类,不需继承,要实现process_item(self, item, spider)
方法 - 启用pipeline需要在
settings.py
中设置ITEM_PIPELINES
,将需要的pipeline类加上,并赋上priority(越小优先级越高) - 每个pipeline都必须返回自己处理的item,item之后会流经其它pipeline
- pipeline处理处理Item时,该Item就会被锁定该pipeline中直到被return
- 对于不需要的Item对象可以丢弃,通过
raise DropItem("drop item info")
丢弃 - item在这里其实是鸭子类型,scrapy提供了
ItemAdapter
来统一它们
# 因为Item是鸭子类型,在使用时需要判断类型和字段
from itemadapter import ItemAdapter
class MyPipeline:
def process_item(self, item, spider):
adapter = ItemAdapter(item)
adapter = ItemAdapter(item)
adapter.get("key") # 其实和dict.get()是一样的
adapter.is_item_class(MyItem) # 用于判断是否是需要的Item类型
if adapter.get("done"): # 对于非目标对象,要把Item返回
return item
- 有时需要下载item里的url,直接使用response的返回值,使用callback就很不方便,这时就要用到deferred了
# https://docs.scrapy.org/en/latest/topics/coroutines.html#inline-requests
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem
from scrapy import Request
from scrapy.http.request import NO_CALLBACK
from scrapy.utils.defer import maybe_deferred_to_future
from twisted.internet.defer import DeferredList
class MyPipeline:
async def process_item(self, item, spider): # 注意要使用async
adapter = ItemAdapter(item)
# 单个请求
request = Request(adapter["url"], callback=NO_CALLBACK) # 不使用callback
response = await maybe_deferred_to_future(
spider.crawler.engine.download(request)
)
if response.status != 200: # 一般要判断一下下载是否成功,并做一些处理
raise DropItem(f"Could not download {adapter['url']}")
# 多个请求,就是比单个的多了DeferredList包装
deferred_list = []
for url in url_list_:
request = Request(url, callback=NO_CALLBACK)
deferred = spider.crawler.engine.download(request)
deferred_list.append(deferred)
result = await maybe_deferred_to_future(DeferredList(deferred_list))
# result变量将包含一个列表,该列表中的每个元素是一个元组,表示每个Deferred的结果。这个元组通常有两种形式:
# (True, result):如果对应的Deferred成功完成,result是回调链最终返回的结果。
# (False, failure):如果对应的Deferred因错误而终止,failure是一个Failure实例,代表发生的异常。
for i, (success, response) in enumerate(result):
if not success:
continue
file_path.write_bytes(response.body)
关于settings.py
- 如果全局使用固定的cookie(在浏览器复制),需要在
settings.py
中注释修改DEFAULT_REQUEST_HEADERS
,并且将COOKIES_ENABLED
设置为False
,否则这里的headers不能使用到cookie。或者也可以在middleware中进行自定义 - 在settings可以定义自己需要的任何参数,使用Spider对象的
spider.settings["key"]
即可取到参数值
补充FilesPipeline
内置的媒体资源下载Pipeline,相较于自己实现,它内置有缓存策略等等,也不需要自己构建deferred list。两种方式,选择自己喜欢的就好。
使用FilesPipeline,必须在settings.py
定义文件存储路径FILES_STORE
。
继承scrapy.pipelines.files.FilesPipeline
主要override三个方法:
- file_path(self, request, response=None, info=None, *, item=None):该方法返回了文件的文件名
- get_media_requests(self, item, info):该方法返回
List[Request]
,根据需要从item
里获取请求url并发起请求 - item_completed(self, results, item, info):该方法定义了文件下载完成后进行的操作,然后返回item
# 使用内置的FilesPipeline替代我的AttachmentPipeline
# 启用FilesPipeline时必须设置FILES_STORE = "/path/to/valid/dir"
# 之后代码中的路径都应是相对路径
from scrapy.pipelines.files import FilesPipeline
class AttachmentFilesPipeline(FilesPipeline):
"""extract attachment urls(zip and images) and download attachment"""
def file_path(self, request, response=None, info=None, *, item=None):
"""use the url to extract file name"""
adapter = ItemAdapter(item)
file_name = request.url.split("/")[-1]
return f"{adapter['description_filepath']}/attachment/{file_name}"
def get_media_requests(self, item, info):
"""extract attachment urls and add them to request"""
adapter = ItemAdapter(item)
if not adapter.get("description"): # 未经ProblemPipeline处理
return []
if adapter.get("done"): # 已经处理过
return []
description = adapter["description"]
attachment_related_urls = re.findall("/staticdata/down/.*?.zip", description) # extract attachment url
image_related_urls = re.findall('src="(/staticdata/.*?)"', description) # extract image urls
attachment_related_urls.extend(image_related_urls)
# 后续要替换原文,所以需要保存提取出来的原文路径
adapter["attachment_urls"] = attachment_related_urls
for url in attachment_related_urls:
url = urljoin("https://sim.csp.thusaac.com", url)
yield Request(url, callback=NO_CALLBACK)
def item_completed(self, results, item, info):
"""replace path in description"""
"""results结构见https://docs.scrapy.org/en/latest/topics/media-pipeline.html#scrapy.pipelines.files.FilesPipeline.get_media_requests"""
adapter = ItemAdapter(item)
for i, (success, result) in enumerate(results):
if not success:
continue
# replace path in the description
related_path = Path(result["path"]).relative_to(adapter["description_filepath"])
adapter["description"] = adapter["description"].replace(
adapter["attachment_urls"][i], str(related_path)
)
adapter["done"] = True # 标记已处理
return item
"""
总结:FilesPipeline的使用和我写的AttachmentPipeline相似,
只是说把一个方法拆成了两部分,且FilesPipeline对下载的内容进行了管理,更灵活
"""
That's all
其实还有middleware等可以定制更丰富的功能,这些都可以直接查询官方文档,根据自己需要进行定制