【问题标题】:How to work with the scrapy contracts?如何使用scrapy合约?
【发布时间】:2014-11-04 00:43:53
【问题描述】:

Scrapy 合同问题

我开始研究 scrapy 框架。也实施了一些蜘蛛 提取,但我无法为蜘蛛编写单元测试用例,因为合同 scrapy 提供的包文档没有正确的程序来编写 测试用例。请帮我解决这个问题。

【问题讨论】:

    标签: python unit-testing python-2.7 web-scraping scrapy


    【解决方案1】:

    是的,Spiders Contracts 远非清晰和详细。

    我不是编写蜘蛛合约的专家(实际上在web-scraping tutorialnewcoder.io 工作时只写过一次)。但是每当我需要为 Scrapy 蜘蛛编写测试时,我更喜欢遵循 ​​suggested here 的方法——从本地 html 文件创建一个虚假的响应。如果这仍然是一个单元测试过程是有争议的,但这会为您提供更多的灵活性和稳健性。

    请注意,您仍然可以编写合约,但您很快就会感到需要扩展它们并编写自定义合约。没关系。

    相关链接:

    【讨论】:

    • 感谢@alecxe 对我的问题的快速回复。我将处理您建议的想法,例如创建虚假回复。再次感谢您的宝贵回答。
    • 嘿@alecxe 尝试了你的假响应方法,它向我展示了一个很好的结果,我的单元测试用例部分已经完成。再次感谢...... :)
    【解决方案2】:

    Scrapy 合约

    测试蜘蛛

    测试蜘蛛的两个最基本的问题可能是:

    1. 我的代码更改会/是否会破坏蜘蛛?
    2. 蜘蛛会/是否因为我正在抓取的页面更改而中断?

    合同

    Scrapy 提供了一种测试蜘蛛的方法:合约。

    合同看起来有点神奇。它们存在于多行文档字符串中。合约“语法”是:@contract_name <arg>。您可以创建自己的合约,这非常简洁。

    要使用合同,您需要在合同名称前加上 @。合约的名称由给定合约子类的.name 属性指定。这些合约子类要么是内置的,要么是您创建的自定义子类。

    最后,上述文档字符串必须存在于您的蜘蛛的回调中。这是parse回调中的一些基本合约的示例;默认回调。

    def parse(self, response):
      """This function gathers the author and the quote text.
    
      @url http://quotes.toscrape.com/
      @returns items 1 8
      @returns requests 0 0
      @scrapes author quote_text
      """
    

    您可以通过scrapy check 运行此合约;或者,使用scrapy check -l 列出您的合同。

    更深入的合同

    上述合约使用三个内置合约进行测试:

    • scrapy.contracts.default.UrlContract
    • scrapy.contracts.default.ReturnsContract
    • scrapy.contracts.default.ScrapesContract

    UrlContract 是强制性的,并不是真正的合同,因为它不用于验证。 @url 合约用于设置蜘蛛在通过scrapy check 测试蜘蛛时将爬取的 URL。在这种情况下,我们指定http://quotes.toscrape.com/。但我们可以指定http://127.0.0.1:8080/home-11-05-2019-1720.html,这是我使用scrapy view http://quotes.toscrape.com/ 命令保存的quotes.toscrape.com 的本地版本。

    ReturnsContract 用于检查您正在测试的回调的输出。如您所见,合约被调用了两次,参数不同。但是,您不能只将任何 ol' arg 放在那里。在引擎盖下,有一个预期 args 的字典:

    objects = {
      'request': Request,
      'requests': Request,
      'item': (BaseItem, dict),
      'items': (BaseItem, dict),
    }
    

    我们的合同规定我们的蜘蛛 @returns items 1 16116 是下限和上限。上限是可选的;如果未指定,则在引擎盖下设置为无穷大 ?。

    try:
        self.max_bound = int(self.args[2])
    except IndexError:
        self.max_bound = float('inf')
    

    但是,@returns 可以帮助您了解您的蜘蛛是否返回了预期数量的项目或请求。

    最后,@scrapes 合约是最后一个内置的。它用于检查抓取项目中是否存在字段。它只是遍历你的回调的输出字典并构造一个缺失属性的列表:

    class ScrapesContract(Contract):
        """ Contract to check presence of fields in scraped items
            @scrapes page_name page_body
        """
    
        name = 'scrapes'
    
        def post_process(self, output):
            for x in output:
                if isinstance(x, (BaseItem, dict)):
                    missing = [arg for arg in self.args if arg not in x]
                    if missing:
                        raise ContractFail(
                            "Missing fields: %s" % ", ".join(missing))
    
    

    运行合约

    运行:scrapy check

    如果一切顺利,你会看到:

    ...
    ----------------------------------------------------------------------
    Ran 3 contracts in 0.140s
    
    OK
    

    如果有东西爆炸,你会看到:

    F..
    ======================================================================
    FAIL: [example] parse (@returns post-hook)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/adnauseum/.virtualenvs/scrapy_testing-CfFR3tdG/lib/python3.7/site-packages/scrapy/contracts/__init__.py", line 151, in wrapper
        self.post_process(output)
      File "/Users/adnauseum/.virtualenvs/scrapy_testing-CfFR3tdG/lib/python3.7/site-packages/scrapy/contracts/default.py", line 90, in post_process
        (occurrences, self.obj_name, expected))
    scrapy.exceptions.ContractFail: Returned 10 items, expected 0
    
    ----------------------------------------------------------------------
    

    自定义合同

    假设您想要一份@has_header X-CustomHeader 合同。这将确保您的蜘蛛程序检查X-CustomHeader 的存在。 Scrapy 合约只是具有三个可覆盖方法的类:adjust_request_argspre_processpost_process。从那里开始,如果没有达到预期,您需要从pre_processpost_process 提高ContractFail

    from scrapy.contracts import Contract
    from scrapy.exceptions import ContractFail
    
    class HasHeaderContract(Contract):
      """Demo contract which checks the presence of a custom header
      @has_header X-CustomHeader
      """
      name = 'has_header' # add the command name to the registry
    
      def pre_process(self, response):
        for header in self.args:
          if header not in response.headers:
            raise ContractFail(f"{header} not present")
    

    为什么合同有用?

    看起来合同可以帮助您了解两件事:

    1. 您的代码更改没有破坏

      • 似乎对您正在抓取的页面的本地副本运行蜘蛛程序并使用合同来验证您的代码更改没有破坏任何东西可能是个好主意。在这种情况下,您正在控制被抓取的页面,并且您知道它没有改变。因此,如果您的合约失败,您就知道这是您的代码更改。
      • 在这种方法中,使用某种时间戳命名这些 HTML 固定装置可能会很有用,以便保存记录。即Site-Page-07-14-2019.html。您可以通过运行scrapy view <url> 来保存这些页面。 Scrapy 会在您的浏览器中打开此页面,但也会保存一个包含您需要的所有内容的 HMTL 文件。
    2. 您正在抓取的页面没有改变(以影响您的方式)

      • 然后你也可以让你的蜘蛛与真实的东西对比,让合同告诉你你正在抓取的东西已经改变了。

    虽然合同很有用,但您可能需要做更多工作来确保您的爬虫。例如,您正在抓取的项目数量不能保证始终保持不变。在这种情况下,您可能会考虑爬取模拟服务器并针对收集的项目运行测试。似乎缺乏文档和最佳实践。

    最后,有一个由 Scrapinghub 制作的项目 Spidermon,它可用于在蜘蛛运行时监控它:https://spidermon.readthedocs.io/en/latest/getting-started.html

    您可以根据模型定义验证抓取的项目并获取您的蜘蛛的统计信息(当前抓取的项目数、不符合验证的项目数等)。

    【讨论】:

      猜你喜欢
      • 2022-08-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-22
      • 1970-01-01
      • 2021-06-17
      • 1970-01-01
      • 2017-08-19
      相关资源
      最近更新 更多