单个 Scrapy 项目与多个项目

Posted

技术标签:

【中文标题】单个 Scrapy 项目与多个项目【英文标题】:Single Scrapy Project vs. Multiple Projects 【发布时间】:2020-01-11 15:39:30 【问题描述】:

我在如何存储我所有的蜘蛛上遇到了两难境地。这些蜘蛛将通过命令行调用和从stdin 读取的项目被馈送到 Apache NiFi 中。我还计划让这些蜘蛛的一个子集在单独的 Web 服务器上使用 scrapyrt 返回单个项目的结果。我将需要在具有不同项目模型的许多不同项目中创建蜘蛛。它们都将具有相似的设置(例如使用相同的代理)。

我的问题是构建我的 scrapy 项目的最佳方式是什么?

    将所有蜘蛛放在同一个存储库中。 提供了一种为项目加载器和项目管道创建基类的简单方法。 将我正在处理的每个项目的蜘蛛分组到单独的存储库中。这样的好处是允许项目成为每个项目的焦点,并且不会变得太大。无法共享通用代码、设置、蜘蛛监视器 (spidermon) 和基类。尽管有一些重复,但这感觉最干净。 只打包我计划在 NiFi 存储库中使用非实时的蜘蛛和另一个存储库中的实时蜘蛛。 优点是我将蜘蛛保留在实际使用它们但仍然集中/卷积的项目中哪些蜘蛛用于哪些项目。

感觉正确的答案是#2。与特定程序相关的蜘蛛应该在自己的scrapy项目中,就像为项目A创建Web服务一样,你不会说哦,我可以将项目B的所有服务端点都扔到同一个服务中,因为那是即使某些设置可能重复,我的所有服务都将驻留在其中。可以说,一些共享代码/类可以通过单独的包共享。

你怎么看?你们都如何构建你的scrapy项目以最大限度地提高可重用性?您在哪里划清同一项目与单独项目的界限?它是基于您的 Item 模型还是数据源?

【问题讨论】:

我个人对不同的网站使用不同的项目。这确实意味着我不会按项目分离蜘蛛,因为一个网站可以为我提供多个蜘蛛。当您说“可以说某些共享代码/类可以通过单独的包共享”时。在树的顶部或适合您的任何其他位置创建一个模块目录,并将您的spiders.py 模块中的sys.path 的路径添加到import 您自定义的“共享代码/类”模块。更一般地说,如果您限制行数、提高可读性、限制错误,任何模式都是有效的。 感谢您的反馈。似乎创建单独项目的主要区别在于项目模型。如果您有 10 个网站都提供相同类型的商品,例如购买的产品,这些商品总是有名称、价格和描述,然后基于您正在处理的特定项目以及该项目的数据模型项目似乎是一个很好的方法。我有点喜欢在路径上保留一个包含共享类的公共文件夹的想法,但在这一点上,看起来像选项一这样的共享存储库会更可取。 没有其他想法了吗?有没有专家可以参与进来?我对其他方法持开放态度。 我不太同意你说它以物品模型为中心。也许明天我会写一个答案来向你展示不同的东西。我不知道你的编程水平,但你的项目结构(我不是指scrapy项目,而是一般意义上的项目)首先取决于你的技能。我没有像开始时那样组织我的项目,我知道这是因为缺乏技能。你经历的越多,你的结构就会发生更多的变化。 python给出的函数式编程非常强大。好吧,我会尽快这样做。 感谢@AvyWam,这对了解您如何布局项目非常有帮助。 【参考方案1】:

首先,当我写像'/path' 这样的路径时,那是因为我是 Ubuntu 用户。如果您是 Windows 用户,请调整它。那是文件管理系统的问题。

简单示例

假设您想要抓取 2 个或更多不同的网站。 第一个是泳装零售网站。二是关于天气。您想要抓取两者,因为您想要观察泳衣价格和天气之间的联系,以便预测较低的购买价格。

注意pipelines.py 我将使用 mongo 集合,因为这是我使用的,我暂时不需要 SQL。如果您不了解 mongo,请考虑将集合等同于关系数据库中的表。

scrapy 项目可能如下所示:

spiderswebsites.py,这里可以写你想要的蜘蛛数。

import scrapy
from ..items.py import SwimItem, WeatherItem
#if sometimes you have trouble to import from parent directory you can do
#import sys
#sys.path.append('/path/parentDirectory')

class SwimSpider(scrapy.Spider):
    name = "swimsuit"
    start_urls = ['https://www.swimsuit.com']
    def parse (self, response):
        price = response.xpath('span[@class="price"]/text()').extract()
        model = response.xpath('span[@class="model"]/text()').extract()
        ... # and so on
        item = SwimItem() #needs to be called -> ()
        item['price'] = price
        item['model'] = model
        ... # and so on
        return item

class WeatherSpider(scrapy.Spider):
    name = "weather"
    start_urls = ['https://www.weather.com']
    def parse (self, response):
        temperature = response.xpath('span[@class="temp"]/text()').extract()
        cloud = response.xpath('span[@class="cloud_perc"]/text()').extract()
        ... # and so on
        item = WeatherItem() #needs to be called -> ()
        item['temperature'] = temperature
        item['cloud'] = cloud
        ... # and so on
        return item

items.py,这里可以写你想要的item pattern的数量。

import scrapy
class SwimItem(scrapy.Item):
    price = scrapy.Field()
    stock = scrapy.Field()
    ...
    model = scrapy.Field()

class WeatherItem(scrapy.Item):
    temperature = scrapy.Field()
    cloud = scrapy.Field()
    ...
    pressure = scrapy.Field()

pipelines.py,我使用 Mongo 的地方

import pymongo
from .items import SwimItem,WeatherItem
from .spiders.spiderswebsites import SwimSpider , WeatherSpider

class ScrapePipeline(object):

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod #this is a decorator, that's a powerful tool in Python
    def from_crawler(cls, crawler):
        return cls(
        mongo_uri=crawler.settings.get('MONGODB_URL'),
        mongo_db=crawler.settings.get('MONGODB_DB', 'defautlt-test')
        )
    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]
        
    def close_spider(self, spider):
         self.client.close()

    def process_item(self, item, spider):
        if isinstance(spider, SwimItem):
            self.collection_name = 'swimwebsite'
        elif isinstance(spider, WeatherItem):
            self.collection_name = 'weatherwebsite'
        self.db[self.collection_name].insert(dict(item))

因此,当您查看我的示例项目时,您会发现该项目根本不依赖于项目模式,因为您可以在同一个项目中使用多种项目。 在上面的模式中,优点是如果需要,您可以在 settings.py 中保留相同的配置。但是不要忘记你可以“自定义”你的蜘蛛的命令。如果您希望您的蜘蛛运行与默认设置稍有不同,您可以设置为scrapy crawl spider -s DOWNLOAD_DELAY=35,而不是您在settings.py 中编写的25

函数式编程

此外,我的例子很简单。实际上,您很少对原始数据感兴趣。你需要很多代表很多线条的治疗方法。为了提高代码的可读性,您可以在模块中创建函数。但要小心spaghetti code。

functions.py,自定义模块

from re import search

def cloud_temp(response): #for WeatherSpider
    """returns a tuple containing temperature and percentage of clouds"""
    temperature = response.xpath('span[@class="temp"]/text()').extract() #returns a str as " 12°C"
    cloud = response.xpath('span[@class="cloud_perc"]/text()').extract() #returns a str as "30%"
    #treatments, you want to record it as integer
    temperature = int(re.search(r'[0-9]+',temperature).group()) #returns int as 12
    cloud = int(re.search(r'[0-9]+',cloud).group()) #returns int as 30
    return (cloud,temperature)

它提供spiders.py

import scrapy
from items.py import SwimItem, WeatherItem
from functions.py import *
...
class WeatherSpider(scrapy.Spider):
    name = "weather"
    start_urls = ['https://www.weather.com']
    def parse (self, response):
        cloud , temperature = cloud_temp(response) "this is shorter than the previous one
        ... # and so on
        item = WeatherItem() #needs to be called -> ()
        item['temperature'] = temperature
        item['cloud'] = cloud
        ... # and so on
        return item

此外,它在调试方面也有相当大的改进。假设我想做一个scrapy shell session。

>>> scrapy shell https://www.weather.com
...
#I check in the sys path if the directory where my `functions.py` module is present.
>>> import sys
>>> sys.path #returns a list of paths
>>> #if the directory is not present
>>> sys.path.insert(0, '/path/directory')
>>> #then I can now import my module in this session, and test in the shell, while I modify in the file functions.py itself
>>> from functions.py import *
>>> cloud_temp(response) #checking if it returns what I want.

这比复制和粘贴一段代码更舒服。由于 Python 是一种用于函数式编程的出色编程语言,因此您应该从中受益。这就是为什么我告诉你“更一般地说,如果你限制行数,提高可读性,限制错误,任何模式都是有效的。”它的可读性越高,您就越能限制错误。您编写的行数越少(例如避免复制和粘贴对不同变量的相同处理),您限制的错误就越少。因为当你纠正一个函数本身时,你纠正了所有依赖它的东西。

所以现在,如果你对函数式编程不是很熟悉,我可以理解你为不同的项目模式制作了几个项目。您可以利用您当前的技能并改进它们,然后随着时间的推移改进您的代码。

【讨论】:

【参考方案2】:

推荐来自 Google 群组主题“Single Scrapy Project vs. Multiple Projects for Various Sources”的 Jakob:

蜘蛛是否应该进入同一个项目主要是确定 根据他们抓取的数据类型,而不是根据数据的来源。

假设您正在从所有目标站点中抓取用户配置文件,然后 您可能有一个清理和验证用户头像的项目管道, 并将它们导出到您的“头像”数据库中。这说得通 将所有蜘蛛放入同一个项目中。毕竟,他们都使用 相同的管道,因为无论如何数据总是具有相同的形状 它是从哪里刮下来的。另一方面,如果你正在刮 来自 Stack Overflow 的问题、来自 Wikipedia 的用户资料,以及 来自 Github 的问题,您验证/处理/导出所有这些数据 类型不同,将蜘蛛放入 单独的项目。

换句话说,如果您的蜘蛛有共同的依赖关系(例如,它们 共享项目定义/管道/中间件),它们可能属于 进入同一个项目;如果他们每个人都有自己的特定 依赖关系,它们可能属于不同的项目。

Pablo Hoffman 是 Scrapy 的开发者之一,他在另一个帖子“Scrapy spider vs project”中回复:

...建议将所有蜘蛛放在同一个项目中以改进代码 可重用性(通用代码、辅助函数等)。

我们有时会在蜘蛛名称上使用前缀,例如 film_spider1, film_spider2 actor_spider1、actor_spider2 等。有时我们也 编写抓取多种项目类型的蜘蛛,因为它更有意义 当抓取的页面有很大的重叠时。

【讨论】:

以上是关于单个 Scrapy 项目与多个项目的主要内容,如果未能解决你的问题,请参考以下文章

scrapy 项目加载器返回列表不是单个值

scrapy突然创建多个项目

scrapy专题:scrapy-redis 框架分析

Scrapy-redis 组件

通过核心API启动单个或多个scrapy爬虫

使用scrapy-redis 搭建分布式爬虫环境