scrapy框架学习笔记

scrapy运行机制

详见Architecture overview

安装

直接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

  1. project/spider下创建Spider文件(scrapy genspider SpiderName website.com可快速创建示例)
    定义Spider类,必须继承scrapy.Spider类,并且必须指定类参数namescrapy crawl通过这个name来启动爬虫。

  2. 实现start_requests方法,定义爬虫入口,返回一个Request对象。
    如果只是简单的get请求,可以设定类参数start_urls = [url list]以使用默认方法(默认回调是parse方法)

  3. 实现parse(self, response)方法,用于处理请求返回的响应,返回Item对象或者Request对象。
    经常有请求完初始链接后要从响应拿到下一步的链接,这时可以在parse方法中返回一个Request对象,callback指定另一个函数,且可以通过kw_args传递额外参数(如本次处理的数据)给callback。
    返回Item则会交给pipeline处理

  4. Request对象和Response对象Selectors

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等可以定制更丰富的功能,这些都可以直接查询官方文档,根据自己需要进行定制

posted @ 2024-10-11 19:35  faf4r  阅读(16)  评论(0编辑  收藏  举报