> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Scrapy 分布式采集完全指南
- URL: https://blog.vercanti.com/scrapy-fen-bu-shi-cai-ji-wan-quan-zhi-nan/
- Published: 2026-08-28T14:34:46.000Z
- Updated: 2026-08-28T14:57:17.000Z
- Description: 1. Scrapy 架构总览(Scrapy分布式采集.md#1-scrapy-架构总览) 2. Spider 类详解(Scrapy分布式采集.md#2-spider-类详解) 3. Request 与 Response 对象(Scrapy分布式采集.md#3-request-与-response-对象) 4. Item 与 ItemLoader(Scrapy分布式采集.md#4-item-与-itemloader) 5. 下载器中间件(Scrapy分布式采集.md#5-下载器中间件) 6. Spider 中间件(Scrapy分布式采集.md#6-spid
- Author: yellowdog
- Tags: Python, 框架与库

> 官方文档：<https://docs.scrapy.org/>  
> 最后更新：2026-03-05

---

## 目录

1. [Scrapy 架构总览](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#1-scrapy-%E6%9E%B6%E6%9E%84%E6%80%BB%E8%A7%88)
2. [Spider 类详解](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#2-spider-%E7%B1%BB%E8%AF%A6%E8%A7%A3)
3. [Request 与 Response 对象](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#3-request-%E4%B8%8E-response-%E5%AF%B9%E8%B1%A1)
4. [Item 与 ItemLoader](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#4-item-%E4%B8%8E-itemloader)
5. [下载器中间件](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#5-%E4%B8%8B%E8%BD%BD%E5%99%A8%E4%B8%AD%E9%97%B4%E4%BB%B6)
6. [Spider 中间件](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#6-spider-%E4%B8%AD%E9%97%B4%E4%BB%B6)
7. [Item Pipeline](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#7-item-pipeline)
8. [重要 settings 配置项](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#8-%E9%87%8D%E8%A6%81-settings-%E9%85%8D%E7%BD%AE%E9%A1%B9)
9. [反反爬策略](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#9-%E5%8F%8D%E5%8F%8D%E7%88%AC%E7%AD%96%E7%95%A5)
10. [Scrapy-Redis 分布式改造](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#10-scrapy-redis-%E5%88%86%E5%B8%83%E5%BC%8F%E6%94%B9%E9%80%A0)
11. [分布式最佳实践](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#11-%E5%88%86%E5%B8%83%E5%BC%8F%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5)
12. [Scrapyd 部署](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#12-scrapyd-%E9%83%A8%E7%BD%B2)
13. [常见陷阱与注意事项](Scrapy%E5%88%86%E5%B8%83%E5%BC%8F%E9%87%87%E9%9B%86.md#13-%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1%E4%B8%8E%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)

---

## 1\. Scrapy 架构总览

### 1.1 核心组件

```
┌─────────────────────────────────────────────────────┐
│                    Scrapy Engine                    │
│  (核心调度器，协调所有组件之间的数据流)               │
└──────┬──────────┬──────────┬──────────┬─────────────┘
       │          │          │          │
  ┌────▼───┐ ┌───▼────┐ ┌───▼────┐ ┌───▼────┐
  │Scheduler│ │Downlo- │ │ Spider │ │  Item  │
  │ 调度器  │ │ ader   │ │ 爬虫   │ │Pipeline│
  │         │ │下载器  │ │        │ │        │
  └────┬────┘ └───┬────┘ └───┬────┘ └────────┘
       │          │          │
  ┌────▼──────────▼──────────▼────┐
  │         Middleware            │
  │   (下载器中间件 + Spider中间件)│
  └───────────────────────────────┘

```

### 1.2 数据流说明

| 步骤 | 流向                                  | 说明                               |
| -- | ----------------------------------- | -------------------------------- |
| 1  | Spider -> Engine                    | Spider 产生初始 Request              |
| 2  | Engine -> Scheduler                 | Engine 将 Request 交给 Scheduler 排队 |
| 3  | Scheduler -> Engine                 | Scheduler 按策略返回下一个 Request       |
| 4  | Engine -> Downloader Middleware     | Request 经过下载器中间件处理               |
| 5  | Downloader Middleware -> Downloader | 处理后的 Request 交给 Downloader       |
| 6  | Downloader -> Downloader Middleware | Response 经过下载器中间件处理              |
| 7  | Downloader Middleware -> Engine     | 处理后的 Response 返回 Engine          |
| 8  | Engine -> Spider Middleware         | Response 经过 Spider 中间件           |
| 9  | Spider Middleware -> Spider         | Response 交给 Spider 的 callback    |
| 10 | Spider -> Engine                    | Spider 产生 Item 和新的 Request       |
| 11 | Engine -> Item Pipeline             | Item 经过 Pipeline 处理              |
| 12 | Engine -> Scheduler                 | 新 Request 重新入队                   |

### 1.3 各组件职责

**Engine（引擎）**

- 控制所有组件之间的数据流
- 处理动作触发，不可自定义（框架核心）

**Scheduler（调度器）**

- 接收 Request 并排队
- 按顺序返回 Request 给 Engine
- 默认使用优先级队列 + 内存去重

**Downloader（下载器）**

- 执行实际的 HTTP 请求
- 将响应返回给 Engine/Spider
- 基于 Twisted 异步框架实现

**Spider（爬虫）**

- 定义爬取逻辑和解析逻辑
- 产生 Request 和 Item
- 可定义多个 callback 方法

**Item Pipeline（数据管道）**

- 处理 Spider 产生的 Item
- 清洗、验证、持久化数据
- 多个 Pipeline 按优先级串联执行

**Middleware（中间件）**

- 下载器中间件：处理 Request/Response
- Spider 中间件：处理 Spider 的输入/输出

---

## 2\. Spider 类详解

### 2.1 Spider（基础爬虫）

```python
import scrapy

class MySpider(scrapy.Spider):
    name = "my_spider"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com"]
    custom_settings = {
        "DOWNLOAD_DELAY": 1,
    }

    def parse(self, response):
        # 解析响应，产生 Item 或新 Request
        yield {"title": response.css("h1::text").get()}

```

**Spider 类属性**

| 属性               | 类型          | 默认值  | 说明                                    |
| ---------------- | ----------- | ---- | ------------------------------------- |
| name             | str         | 必填   | 爬虫唯一名称，在项目中必须唯一                       |
| allowed\_domains | list\[str\] | \[\] | 允许爬取的域名列表，不在此列表的 URL 会被过滤             |
| start\_urls      | list\[str\] | \[\] | 初始爬取 URL 列表，由 start\_requests() 使用    |
| custom\_settings | dict        | None | 爬虫级别的自定义 settings，覆盖全局 settings       |
| crawler          | Crawler     | 自动注入 | 关联的 Crawler 对象，可访问 settings、signals 等 |
| settings         | Settings    | 自动注入 | 当前生效的 settings 对象                     |
| logger           | Logger      | 自动创建 | 以 name 命名的 logger                     |

**Spider 核心方法**

| 方法                | 返回值                      | 说明                                           |
| ----------------- | ------------------------ | -------------------------------------------- |
| start\_requests() | Iterable\[Request\]      | 产生初始请求，默认对 start\_urls 每个 URL 生成 GET Request |
| parse(response)   | Iterable\[Item/Request\] | 默认回调，解析响应                                    |
| closed(reason)    | None                     | 爬虫关闭时调用                                      |

**`start_requests()` 方法签名（可重写）**

```python
def start_requests(self):
    for url in self.start_urls:
        yield scrapy.Request(url, dont_filter=True)

```

### 2.2 CrawlSpider（链接爬取爬虫）

CrawlSpider 扩展了 Spider，通过定义规则自动跟进链接，适合全站爬取。

```python
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MyCrawlSpider(CrawlSpider):
    name = "my_crawl_spider"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com"]

    rules = (
        # 跟进列表页链接，不解析
        Rule(LinkExtractor(restrict_css=".pagination a"), follow=True),
        # 跟进详情页链接，并调用 parse_item 解析
        Rule(LinkExtractor(restrict_css=".item a"), callback="parse_item"),
    )

    def parse_item(self, response):
        yield {
            "url": response.url,
            "title": response.css("h1::text").get(),
        }

```

**注意：CrawlSpider 不能重写 `parse()` 方法**，因为 CrawlSpider 内部使用 `parse()` 实现规则处理逻辑。自定义回调方法必须使用其他名称。

### 2.3 Rule 参数详解

```python
Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None, errback=None)

```

| 参数               | 类型             | 默认值  | 说明                                                           |
| ---------------- | -------------- | ---- | ------------------------------------------------------------ |
| link\_extractor  | LinkExtractor  | 必填   | 定义如何从页面提取链接                                                  |
| callback         | str / callable | None | 对匹配 URL 的响应调用此回调；None 表示只跟进不解析                               |
| cb\_kwargs       | dict           | None | 传递给 callback 的额外关键字参数                                        |
| follow           | bool           | None | 是否从匹配的响应继续跟进链接；callback 为 None 时默认 True，有 callback 时默认 False |
| process\_links   | callable       | None | 对提取的链接列表进行过滤/处理的函数，接收并返回链接列表                                 |
| process\_request | callable       | None | 对每个 Request 进行处理的函数，必须返回 Request 或 None                      |
| errback          | str / callable | None | 请求出错时的回调                                                     |

### 2.4 LinkExtractor 参数详解

```python
from scrapy.linkextractors import LinkExtractor

LinkExtractor(
    allow=(),
    deny=(),
    allow_domains=(),
    deny_domains=(),
    restrict_xpaths=(),
    restrict_css=(),
    tags=("a", "area"),
    attrs=("href",),
    canonicalize=False,
    unique=True,
    process_value=None,
    deny_extensions=None,
    restrict_text=None,
    strip=True,
)

```

| 参数               | 类型                | 默认值                 | 说明                                        |
| ---------------- | ----------------- | ------------------- | ----------------------------------------- |
| allow            | str / list\[str\] | ()                  | 正则表达式，只提取匹配的 URL；空则提取所有                   |
| deny             | str / list\[str\] | ()                  | 正则表达式，排除匹配的 URL                           |
| allow\_domains   | str / list\[str\] | ()                  | 只提取这些域名的链接                                |
| deny\_domains    | str / list\[str\] | ()                  | 排除这些域名的链接                                 |
| restrict\_xpaths | str / list\[str\] | ()                  | 只在 XPath 匹配的区域内提取链接                       |
| restrict\_css    | str / list\[str\] | ()                  | 只在 CSS 选择器匹配的区域内提取链接                      |
| tags             | str / list\[str\] | ("a", "area")       | 要扫描链接的 HTML 标签                            |
| attrs            | str / list\[str\] | ("href",)           | 要提取链接的属性                                  |
| canonicalize     | bool              | False               | 是否规范化 URL（使用 w3lib.url.canonicalize\_url） |
| unique           | bool              | True                | 是否对提取的链接去重                                |
| process\_value   | callable          | None                | 对提取的每个值进行处理，返回处理后的字符串或 None（丢弃）           |
| deny\_extensions | list\[str\]       | IGNORED\_EXTENSIONS | 排除这些文件扩展名                                 |
| restrict\_text   | str / list\[str\] | None                | 正则表达式，只提取链接文本匹配的链接                        |
| strip            | bool              | True                | 是否去除属性值中的空白字符                             |

### 2.5 其他内置 Spider 类型

| 类名            | 说明                    |
| ------------- | --------------------- |
| scrapy.Spider | 基础爬虫，适合简单场景           |
| CrawlSpider   | 规则驱动的全站爬虫             |
| XMLFeedSpider | 解析 XML Feed，按节点迭代     |
| CSVFeedSpider | 解析 CSV 文件，按行迭代        |
| SitemapSpider | 通过 Sitemap.xml 发现 URL |

---

## 3\. Request 与 Response 对象

### 3.1 Request 参数详解

```python
scrapy.Request(
    url,
    callback=None,
    method="GET",
    headers=None,
    body=None,
    cookies=None,
    meta=None,
    encoding="utf-8",
    priority=0,
    dont_filter=False,
    errback=None,
    flags=None,
    cb_kwargs=None,
)

```

| 参数           | 类型          | 默认值     | 说明                                                    |
| ------------ | ----------- | ------- | ----------------------------------------------------- |
| url          | str         | 必填      | 请求的 URL                                               |
| callback     | callable    | None    | 响应成功时调用的回调函数；None 时使用 Spider 的 parse()                |
| method       | str         | "GET"   | HTTP 请求方法，如 "GET" / "POST" / "PUT"                    |
| headers      | dict        | None    | HTTP 请求头                                              |
| body         | bytes / str | None    | 请求体，POST 请求时使用                                        |
| cookies      | dict / list | None    | cookies，dict 格式或 \[{"name": ..., "value": ...}\] 列表格式 |
| meta         | dict        | None    | 附加到请求的元数据，可在 callback 中通过 response.meta 访问            |
| encoding     | str         | "utf-8" | URL 和 body 的编码                                        |
| priority     | int         | 0       | 请求优先级，值越大优先级越高，Scheduler 优先处理                         |
| dont\_filter | bool        | False   | 是否跳过去重过滤器，True 时允许重复请求                                |
| errback      | callable    | None    | 请求出错时（网络错误、HTTP 错误等）的回调函数，接收 Failure 对象               |
| flags        | list        | None    | 请求标志列表，用于日志和扩展                                        |
| cb\_kwargs   | dict        | None    | 传递给 callback 的额外关键字参数                                 |

**FormRequest（表单请求）**

```python
scrapy.FormRequest(
    url,
    formdata=None,   # dict，表单数据，自动编码为 application/x-www-form-urlencoded
    **kwargs,        # 同 Request 的其他参数
)

```

`FormRequest.from_response()` 可自动填充页面中的表单：

```python
scrapy.FormRequest.from_response(
    response,
    formname=None,       # 按 name 属性定位表单
    formid=None,         # 按 id 属性定位表单
    formnumber=0,        # 按序号定位表单（从 0 开始）
    formdata=None,       # 覆盖/补充表单字段
    formxpath=None,      # 按 XPath 定位表单
    formcss=None,        # 按 CSS 选择器定位表单
    clickdata=None,      # 模拟点击的按钮
    dont_click=False,    # 不模拟点击，直接提交
    **kwargs,
)

```

**Request.meta 常用键**

| 键                        | 说明                                  |
| ------------------------ | ----------------------------------- |
| proxy                    | 指定代理，如 "http://127.0.0.1:8888"      |
| download\_timeout        | 本次请求的超时秒数                           |
| dont\_redirect           | 为 True 时不跟进重定向                      |
| dont\_retry              | 为 True 时不重试                         |
| handle\_httpstatus\_list | 允许处理的 HTTP 状态码列表，如 \[404, 403\]     |
| handle\_httpstatus\_all  | 为 True 时处理所有 HTTP 状态码               |
| cookiejar                | 指定 cookie 隔离组（整数），不同组之间 cookies 不共享 |
| dont\_merge\_cookies     | 为 True 时不合并 cookies                 |
| redirect\_urls           | 重定向历史 URL 列表（只读，由框架填充）              |
| redirect\_reasons        | 重定向原因列表（只读，由框架填充）                   |
| download\_slot           | 指定下载槽（并发控制单元），默认按域名分组               |
| download\_latency        | 下载延迟（只读，由框架填充，单位秒）                  |
| depth                    | 当前请求的深度（CrawlSpider 自动维护）           |

### 3.2 Response 属性与方法

| 属性/方法                  | 类型                  | 说明                       |
| ---------------------- | ------------------- | ------------------------ |
| url                    | str                 | 响应的 URL                  |
| status                 | int                 | HTTP 状态码                 |
| headers                | Headers             | 响应头                      |
| body                   | bytes               | 响应体原始字节                  |
| text                   | str                 | 响应体解码后的字符串（TextResponse） |
| encoding               | str                 | 响应编码                     |
| meta                   | dict                | 对应 Request 的 meta        |
| request                | Request             | 对应的 Request 对象           |
| flags                  | list                | 响应标志                     |
| css(query)             | SelectorList        | CSS 选择器查询                |
| xpath(query)           | SelectorList        | XPath 查询                 |
| json()                 | dict/list           | 将响应体解析为 JSON             |
| urljoin(url)           | str                 | 将相对 URL 转为绝对 URL         |
| follow(url, ...)       | Request             | 创建跟进请求，自动处理相对 URL        |
| follow\_all(urls, ...) | Iterable\[Request\] | 批量创建跟进请求                 |

**`response.follow()` 参数**

```python
response.follow(
    url,              # str 或 Selector，相对或绝对 URL
    callback=None,
    method="GET",
    headers=None,
    body=None,
    cookies=None,
    meta=None,
    encoding="utf-8",
    priority=0,
    dont_filter=False,
    errback=None,
    cb_kwargs=None,
    flags=None,
)

```

### 3.3 Selector 选择器

```python
# CSS 选择器
response.css("div.title::text").get()          # 获取第一个，不存在返回 None
response.css("div.title::text").getall()       # 获取所有，返回列表
response.css("a::attr(href)").get()            # 获取属性值
response.css("div.title::text").get(default="") # 指定默认值

# XPath
response.xpath("//div[@class='title']/text()").get()
response.xpath("//a/@href").getall()

# re 正则提取
response.css("div::text").re(r"\d+")           # 返回所有匹配列表
response.css("div::text").re_first(r"\d+")     # 返回第一个匹配

```

---

## 4\. Item 与 ItemLoader

### 4.1 Item 定义

```python
import scrapy

class ArticleItem(scrapy.Item):
    title = scrapy.Field()
    url = scrapy.Field()
    author = scrapy.Field()
    publish_date = scrapy.Field()
    content = scrapy.Field()
    tags = scrapy.Field()

```

**scrapy.Field() 参数**

`Field` 本质是一个字典，可以传入任意关键字参数作为元数据，这些元数据由 ItemLoader 或 Pipeline 使用：

```python
class ArticleItem(scrapy.Item):
    title = scrapy.Field(
        input_processor=MapCompose(str.strip),    # ItemLoader 输入处理器
        output_processor=TakeFirst(),             # ItemLoader 输出处理器
        serializer=str,                           # 序列化函数（自定义用途）
    )

```

**dataclass 风格（Scrapy 2.2+）**

```python
from dataclasses import dataclass, field

@dataclass
class ArticleItem:
    title: str = ""
    url: str = ""
    tags: list = field(default_factory=list)

```

### 4.2 ItemLoader

ItemLoader 提供了填充 Item 的机制，支持输入/输出处理器对数据进行清洗。

```python
from scrapy.loader import ItemLoader
from itemloaders.processors import TakeFirst, MapCompose, Join
import w3lib.html

class ArticleLoader(ItemLoader):
    default_item_class = ArticleItem
    default_output_processor = TakeFirst()   # 默认输出处理器：取第一个值

    title_in = MapCompose(w3lib.html.remove_tags, str.strip)   # title 输入处理器
    content_in = MapCompose(w3lib.html.remove_tags)
    tags_out = Identity()                    # tags 输出处理器：保留列表

def parse(self, response):
    loader = ArticleLoader(item=ArticleItem(), response=response)
    loader.add_css("title", "h1::text")
    loader.add_css("content", "div.content")
    loader.add_xpath("author", "//span[@class='author']/text()")
    loader.add_value("url", response.url)
    return loader.load_item()

```

**ItemLoader 初始化参数**

| 参数       | 类型         | 默认值  | 说明                                            |
| -------- | ---------- | ---- | --------------------------------------------- |
| item     | Item       | None | 要填充的 Item 实例；None 时使用 default\_item\_class 创建 |
| selector | Selector   | None | 用于 CSS/XPath 查询的 Selector                     |
| response | Response   | None | 自动创建 Selector 的 Response 对象                   |
| parent   | ItemLoader | None | 父 ItemLoader（嵌套使用）                            |

**ItemLoader 方法**

| 方法                                             | 参数             | 说明                      |
| ---------------------------------------------- | -------------- | ----------------------- |
| add\_css(field, css, \*processors, \*\*kw)     | field名, CSS选择器 | 通过 CSS 提取并添加到字段         |
| add\_xpath(field, xpath, \*processors, \*\*kw) | field名, XPath  | 通过 XPath 提取并添加到字段       |
| add\_value(field, value, \*processors, \*\*kw) | field名, 值      | 直接添加值到字段                |
| replace\_css(field, css, ...)                  |                | 替换字段值（而非追加）             |
| replace\_xpath(field, xpath, ...)              |                | 替换字段值                   |
| replace\_value(field, value, ...)              |                | 替换字段值                   |
| get\_css(css, \*processors, \*\*kw)            |                | 提取值但不填充 Item            |
| get\_xpath(xpath, \*processors, \*\*kw)        |                | 提取值但不填充 Item            |
| get\_value(value, \*processors, \*\*kw)        |                | 处理值但不填充 Item            |
| load\_item()                                   |                | 应用输出处理器，返回填充完毕的 Item    |
| nested\_css(css)                               |                | 创建嵌套 Loader（限定在子选择器范围内） |
| nested\_xpath(xpath)                           |                | 创建嵌套 Loader             |

**常用内置处理器**

| 处理器                     | 说明               | 示例                               |
| ----------------------- | ---------------- | -------------------------------- |
| TakeFirst()             | 取列表中第一个非空值       | TakeFirst()                      |
| Identity()              | 原样返回，不做处理        | Identity()                       |
| Join(separator=" ")     | 将列表用分隔符拼接为字符串    | Join(", ")                       |
| MapCompose(\*functions) | 对每个值依次应用函数列表     | MapCompose(str.strip, str.lower) |
| Compose(\*functions)    | 对整个列表依次应用函数      | Compose(lambda x: x\[:5\])       |
| SelectJmes(json\_path)  | JMESPath 处理 JSON | SelectJmes("data.items")         |

---

## 5\. 下载器中间件

### 5.1 中间件接口

自定义下载器中间件需实现以下一个或多个方法：

```python
class MyDownloaderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        # 可访问 crawler.settings
        instance = cls()
        return instance

    def process_request(self, request, spider):
        """
        对每个 Request 调用。
        返回值：
        - None：继续处理（交给下一个中间件或下载器）
        - Response：直接返回，不再下载
        - Request：将新 Request 放入调度器
        - 抛出 IgnoreRequest：调用 errback 或丢弃
        """
        return None

    def process_response(self, request, response, spider):
        """
        对每个 Response 调用。
        返回值：
        - Response（可以是新的）：继续处理
        - Request：将新 Request 放入调度器
        - 抛出 IgnoreRequest：调用 errback 或丢弃
        """
        return response

    def process_exception(self, request, exception, spider):
        """
        当 process_request() 或下载器抛出异常时调用。
        返回值：
        - None：继续处理异常
        - Response：停止异常处理，返回响应
        - Request：将新 Request 放入调度器
        """
        pass

```

### 5.2 内置下载器中间件（按默认优先级排序）

| 中间件类                      | 优先级 | 说明                   |
| ------------------------- | --- | -------------------- |
| HttpAuthMiddleware        | 300 | HTTP 基础认证            |
| DownloadTimeoutMiddleware | 350 | 下载超时处理               |
| DefaultHeadersMiddleware  | 400 | 添加默认请求头              |
| UserAgentMiddleware       | 500 | 设置 User-Agent        |
| RetryMiddleware           | 550 | 失败请求重试               |
| AjaxCrawlMiddleware       | 560 | Ajax 爬取支持            |
| MetaRefreshMiddleware     | 580 | 处理 meta refresh 重定向  |
| HttpCompressionMiddleware | 590 | 处理 gzip/deflate 压缩响应 |
| RedirectMiddleware        | 600 | 处理重定向                |
| CookiesMiddleware         | 700 | 管理 cookies           |
| HttpProxyMiddleware       | 750 | 处理 HTTP 代理           |
| DownloaderStats           | 850 | 统计下载信息               |
| HttpCacheMiddleware       | 900 | HTTP 缓存              |

**RetryMiddleware 相关 settings**

| 配置项                     | 类型   | 默认值                                        | 说明             |
| ----------------------- | ---- | ------------------------------------------ | -------------- |
| RETRY\_ENABLED          | bool | True                                       | 是否启用重试         |
| RETRY\_TIMES            | int  | 2                                          | 最大重试次数         |
| RETRY\_HTTP\_CODES      | list | \[500, 502, 503, 504, 522, 524, 408, 429\] | 触发重试的 HTTP 状态码 |
| RETRY\_PRIORITY\_ADJUST | int  | \-1                                        | 重试请求的优先级调整值    |
| RETRY\_EXCEPTIONS       | list | 网络异常列表                                     | 触发重试的异常类型      |

---

## 6\. Spider 中间件

### 6.1 中间件接口

```python
class MySpiderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        return cls()

    def process_spider_input(self, response, spider):
        """
        Response 传递给 Spider 之前调用。
        返回值：
        - None：继续处理
        - 抛出异常：调用 process_spider_exception()
        """
        return None

    def process_spider_output(self, response, result, spider):
        """
        Spider 处理 Response 后，对其产生的 Item/Request 调用。
        必须返回 Item 和 Request 的可迭代对象。
        """
        for item in result:
            yield item

    def process_spider_exception(self, response, exception, spider):
        """
        Spider 或 process_spider_input() 抛出异常时调用。
        返回值：
        - None：继续传播异常
        - 可迭代对象：正常处理
        """
        pass

    def process_start_requests(self, start_requests, spider):
        """
        对 Spider 的初始请求调用，必须返回 Request 的可迭代对象。
        """
        for request in start_requests:
            yield request

```

### 6.2 内置 Spider 中间件

| 中间件类                | 优先级 | 说明                        |
| ------------------- | --- | ------------------------- |
| HttpErrorMiddleware | 50  | 过滤非成功 HTTP 响应             |
| OffsiteMiddleware   | 500 | 过滤不在 allowed\_domains 的请求 |
| RefererMiddleware   | 700 | 自动设置 Referer 头            |
| UrlLengthMiddleware | 800 | 过滤超长 URL                  |
| DepthMiddleware     | 900 | 控制爬取深度                    |

---

## 7\. Item Pipeline

### 7.1 Pipeline 接口

```python
class MyPipeline:

    @classmethod
    def from_crawler(cls, crawler):
        # 可从 settings 读取配置
        instance = cls()
        instance.db_url = crawler.settings.get("DATABASE_URL")
        return instance

    def open_spider(self, spider):
        """Spider 启动时调用，适合初始化资源（数据库连接等）"""
        self.conn = create_connection(self.db_url)

    def close_spider(self, spider):
        """Spider 关闭时调用，适合释放资源"""
        self.conn.close()

    def process_item(self, item, spider):
        """
        对每个 Item 调用，必须返回 Item 或抛出 DropItem。
        返回值：
        - Item：继续传给下一个 Pipeline
        - 抛出 DropItem：丢弃此 Item，不再传给后续 Pipeline
        """
        if not item.get("title"):
            raise DropItem(f"Missing title: {item}")
        self.conn.insert(item)
        return item

```

### 7.2 常见 Pipeline 实现

**MongoDB Pipeline**

```python
import pymongo
from scrapy.exceptions import DropItem

class MongoPipeline:
    collection_name = "articles"

    @classmethod
    def from_crawler(cls, crawler):
        instance = cls()
        instance.mongo_uri = crawler.settings.get("MONGO_URI", "mongodb://localhost:27017")
        instance.mongo_db = crawler.settings.get("MONGO_DATABASE", "scrapy")
        return instance

    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):
        self.db[self.collection_name].insert_one(dict(item))
        return item

```

**去重 Pipeline**

```python
from scrapy.exceptions import DropItem

class DuplicatesPipeline:
    def __init__(self):
        self.ids_seen = set()

    def process_item(self, item, spider):
        if item["url"] in self.ids_seen:
            raise DropItem(f"Duplicate item: {item['url']}")
        self.ids_seen.add(item["url"])
        return item

```

### 7.3 在 settings 中启用 Pipeline

```python
ITEM_PIPELINES = {
    "myproject.pipelines.ValidationPipeline": 200,   # 数字越小越先执行
    "myproject.pipelines.DuplicatesPipeline": 300,
    "myproject.pipelines.MongoPipeline": 400,
}

```

---

## 8\. 重要 settings 配置项

### 8.1 爬虫行为

| 配置项                     | 类型   | 默认值         | 说明                               |
| ----------------------- | ---- | ----------- | -------------------------------- |
| BOT\_NAME               | str  | "scrapybot" | 爬虫名称，用于 User-Agent 默认值           |
| SPIDER\_MODULES         | list | \[\]        | Spider 所在模块列表                    |
| NEWSPIDER\_MODULE       | str  | ""          | 用 genspider 命令创建 Spider 的目标模块    |
| DEPTH\_LIMIT            | int  | 0           | 最大爬取深度，0 表示无限制                   |
| DEPTH\_PRIORITY         | int  | 0           | 非 0 时按深度调整优先级，1 表示广度优先，-1 表示深度优先 |
| CLOSESPIDER\_TIMEOUT    | int  | 0           | 爬虫运行超时秒数，0 表示禁用                  |
| CLOSESPIDER\_ITEMCOUNT  | int  | 0           | 采集 Item 数量达到此值后关闭爬虫              |
| CLOSESPIDER\_PAGECOUNT  | int  | 0           | 下载页面数量达到此值后关闭爬虫                  |
| CLOSESPIDER\_ERRORCOUNT | int  | 0           | 错误数量达到此值后关闭爬虫                    |

### 8.2 并发与限速

| 配置项                               | 类型    | 默认值   | 说明                                    |
| --------------------------------- | ----- | ----- | ------------------------------------- |
| CONCURRENT\_REQUESTS              | int   | 16    | 全局最大并发请求数                             |
| CONCURRENT\_REQUESTS\_PER\_DOMAIN | int   | 0     | 每个域名最大并发数，0 表示不限制                     |
| CONCURRENT\_REQUESTS\_PER\_IP     | int   | 0     | 每个 IP 最大并发数，0 表示不限制；设置后以 IP 而非域名为限速单元 |
| DOWNLOAD\_DELAY                   | float | 0     | 下载延迟秒数（每个域名/IP），支持小数                  |
| RANDOMIZE\_DOWNLOAD\_DELAY        | bool  | True  | 是否随机化延迟（0.5x \~ 1.5x DOWNLOAD\_DELAY） |
| AUTOTHROTTLE\_ENABLED             | bool  | False | 是否启用自动限速                              |
| AUTOTHROTTLE\_START\_DELAY        | float | 5.0   | 自动限速的初始延迟（秒）                          |
| AUTOTHROTTLE\_MAX\_DELAY          | float | 60.0  | 自动限速的最大延迟（秒）                          |
| AUTOTHROTTLE\_TARGET\_CONCURRENCY | float | 1.0   | 目标并发请求数（AutoThrottle 算法参数）            |
| AUTOTHROTTLE\_DEBUG               | bool  | False | 是否打印 AutoThrottle 调试信息                |

### 8.3 下载器

| 配置项                       | 类型    | 默认值                                     | 说明                     |
| ------------------------- | ----- | --------------------------------------- | ---------------------- |
| DOWNLOAD\_TIMEOUT         | float | 180                                     | 下载超时秒数                 |
| DOWNLOAD\_MAXSIZE         | int   | 1073741824                              | 最大响应体大小（字节），默认 1GB     |
| DOWNLOAD\_WARNSIZE        | int   | 33554432                                | 超过此大小时发出警告（字节），默认 32MB |
| DOWNLOAD\_HANDLERS        | dict  | 内置处理器                                   | 自定义 URL scheme 的下载处理器  |
| DOWNLOADER                | str   | "scrapy.core.downloader.Downloader"     | 下载器类                   |
| USER\_AGENT               | str   | "Scrapy/version (+https://scrapy.org)"  | 默认 User-Agent          |
| DEFAULT\_REQUEST\_HEADERS | dict  | {"Accept": ..., "Accept-Language": ...} | 默认请求头                  |

### 8.4 重试与错误处理

| 配置项                       | 类型   | 默认值                                 | 说明                    |
| ------------------------- | ---- | ----------------------------------- | --------------------- |
| RETRY\_ENABLED            | bool | True                                | 是否启用重试中间件             |
| RETRY\_TIMES              | int  | 2                                   | 最大重试次数                |
| RETRY\_HTTP\_CODES        | list | \[500,502,503,504,522,524,408,429\] | 触发重试的状态码              |
| HTTPERROR\_ALLOWED\_CODES | list | \[\]                                | 允许通过的非 2xx 状态码（不作为错误） |
| HTTPERROR\_ALLOW\_ALL     | bool | False                               | 允许所有状态码通过（不过滤错误状态码）   |

### 8.5 去重

| 配置项               | 类型   | 默认值                                | 说明             |
| ----------------- | ---- | ---------------------------------- | -------------- |
| DUPEFILTER\_CLASS | str  | "scrapy.dupefilters.RFPDupeFilter" | 去重过滤器类         |
| DUPEFILTER\_DEBUG | bool | False                              | 是否记录每个被过滤的 URL |

### 8.6 缓存

| 配置项                            | 类型   | 默认值                                                  | 说明                 |
| ------------------------------ | ---- | ---------------------------------------------------- | ------------------ |
| HTTPCACHE\_ENABLED             | bool | False                                                | 是否启用 HTTP 缓存       |
| HTTPCACHE\_EXPIRATION\_SECS    | int  | 0                                                    | 缓存过期时间（秒），0 表示永不过期 |
| HTTPCACHE\_DIR                 | str  | "httpcache"                                          | 缓存存储目录             |
| HTTPCACHE\_IGNORE\_HTTP\_CODES | list | \[\]                                                 | 不缓存这些状态码的响应        |
| HTTPCACHE\_STORAGE             | str  | "scrapy.extensions.httpcache.FilesystemCacheStorage" | 缓存存储后端             |
| HTTPCACHE\_POLICY              | str  | "scrapy.extensions.httpcache.DummyPolicy"            | 缓存策略               |

### 8.7 中间件与 Pipeline 启用

| 配置项                     | 类型   | 默认值 | 说明                                    |
| ----------------------- | ---- | --- | ------------------------------------- |
| DOWNLOADER\_MIDDLEWARES | dict | {}  | 下载器中间件，key 为类路径，value 为优先级（None 表示禁用） |
| SPIDER\_MIDDLEWARES     | dict | {}  | Spider 中间件，同上                         |
| ITEM\_PIPELINES         | dict | {}  | Item Pipeline，同上                      |
| EXTENSIONS              | dict | {}  | 扩展，同上                                 |

### 8.8 Feed 导出

| 配置项                    | 类型   | 默认值  | 说明                                  |
| ---------------------- | ---- | ---- | ----------------------------------- |
| FEEDS                  | dict | {}   | Feed 导出配置，key 为输出路径/URI，value 为格式选项 |
| FEED\_EXPORT\_ENCODING | str  | None | 导出文件编码，JSON 默认 "utf-8"              |
| FEED\_EXPORT\_FIELDS   | list | None | 指定导出字段及顺序                           |
| FEED\_STORAGES         | dict | {}   | 自定义 Feed 存储后端                       |
| FEED\_EXPORTERS        | dict | {}   | 自定义 Feed 导出格式                       |

**FEEDS 配置示例**

```python
FEEDS = {
    "output/%(name)s_%(time)s.json": {
        "format": "json",          # 格式：json, jsonlines, csv, xml
        "encoding": "utf8",
        "store_empty": False,      # 是否在没有 Item 时也生成文件
        "item_classes": [ArticleItem],  # 只导出这些 Item 类型
        "fields": ["title", "url"],     # 只导出这些字段
        "indent": 4,               # JSON 缩进
        "overwrite": True,         # 是否覆盖已有文件
    },
}

```

### 8.9 日志

| 配置项               | 类型   | 默认值                 | 说明                                     |
| ----------------- | ---- | ------------------- | -------------------------------------- |
| LOG\_ENABLED      | bool | True                | 是否启用日志                                 |
| LOG\_LEVEL        | str  | "DEBUG"             | 日志级别：DEBUG/INFO/WARNING/ERROR/CRITICAL |
| LOG\_FILE         | str  | None                | 日志文件路径，None 输出到 stderr                 |
| LOG\_FORMAT       | str  | 内置格式                | 日志格式字符串                                |
| LOG\_DATEFORMAT   | str  | "%Y-%m-%d %H:%M:%S" | 日志日期格式                                 |
| LOG\_STDOUT       | bool | False               | 是否将 Python 标准输出重定向到日志                  |
| LOG\_SHORT\_NAMES | bool | False               | 是否使用短 logger 名称                        |

---

## 9\. 反反爬策略

### 9.1 User-Agent 轮换

```python
# middlewares.py
import random

class RandomUserAgentMiddleware:
    USER_AGENTS = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
    ]

    def process_request(self, request, spider):
        request.headers["User-Agent"] = random.choice(self.USER_AGENTS)

```

```python
# settings.py
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None,  # 禁用默认
    "myproject.middlewares.RandomUserAgentMiddleware": 400,
}

```

也可使用 `fake-useragent` 库：

```python
from fake_useragent import UserAgent

class FakeUserAgentMiddleware:
    def __init__(self):
        self.ua = UserAgent()

    def process_request(self, request, spider):
        request.headers["User-Agent"] = self.ua.random

```

### 9.2 代理中间件

```python
import random

class ProxyMiddleware:
    PROXIES = [
        "http://proxy1.example.com:8080",
        "http://proxy2.example.com:8080",
        "http://user:password@proxy3.example.com:8080",
    ]

    def process_request(self, request, spider):
        request.meta["proxy"] = random.choice(self.PROXIES)

    def process_response(self, request, response, spider):
        # 代理失效时切换
        if response.status == 403:
            request.meta["proxy"] = random.choice(self.PROXIES)
            return request  # 重新发送请求
        return response

    def process_exception(self, request, exception, spider):
        # 连接失败时切换代理重试
        request.meta["proxy"] = random.choice(self.PROXIES)
        return request

```

### 9.3 Cookies 管理策略

```python
# settings.py

# 方案一：禁用 cookies（避免 session 追踪）
COOKIES_ENABLED = False

# 方案二：按 Spider 隔离 cookies（每个 Spider 独立 cookie jar）
# 在 Request.meta 中指定 cookiejar 编号
def parse(self, response):
    yield Request(url, meta={"cookiejar": 1})  # 使用 cookie jar 1
    yield Request(url, meta={"cookiejar": 2})  # 使用独立的 cookie jar 2

# 方案三：手动传入已有 cookies
yield Request(url, cookies={"session": "abc123", "token": "xyz"})

```

**COOKIES 相关配置**

| 配置项              | 类型   | 默认值   | 说明                |
| ---------------- | ---- | ----- | ----------------- |
| COOKIES\_ENABLED | bool | True  | 是否启用 cookies 中间件  |
| COOKIES\_DEBUG   | bool | False | 是否记录所有收发的 cookies |

### 9.4 下载延迟策略

```python
# settings.py

# 固定延迟
DOWNLOAD_DELAY = 2          # 每次请求等待 2 秒
RANDOMIZE_DOWNLOAD_DELAY = True  # 随机化为 1~3 秒

# 自动限速（推荐，根据服务器响应时间动态调整）
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 2
AUTOTHROTTLE_MAX_DELAY = 30
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0  # 目标同时向服务器发出 2 个并发请求
AUTOTHROTTLE_DEBUG = True

```

### 9.5 其他反反爬措施

```python
# 设置完整的浏览器请求头
DEFAULT_REQUEST_HEADERS = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
}

# 处理 Cloudflare 等 JS 挑战（需配合 scrapy-playwright 或 scrapy-splash）
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}

# 对特定请求启用 Playwright
yield Request(url, meta={"playwright": True})

```

### 9.6 Splash 渲染 JS 页面

```python
# settings.py
SPLASH_URL = "http://localhost:8050"
DOWNLOADER_MIDDLEWARES = {
    "scrapy_splash.SplashCookiesMiddleware": 723,
    "scrapy_splash.SplashMiddleware": 725,
    "scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 810,
}
SPIDER_MIDDLEWARES = {
    "scrapy_splash.SplashDeduplicateArgsMiddleware": 100,
}
DUPEFILTER_CLASS = "scrapy_splash.SplashAwareDupeFilter"

# Spider 中使用
from scrapy_splash import SplashRequest

yield SplashRequest(url, callback=self.parse, args={"wait": 2})

```

---

## 10\. Scrapy-Redis 分布式改造

### 10.1 分布式原理

Scrapy 默认使用内存作为请求队列，单机运行。Scrapy-Redis 将以下组件替换为 Redis 实现：

| 组件                | 默认实现                            | Scrapy-Redis 替换             |
| ----------------- | ------------------------------- | --------------------------- |
| Scheduler（请求队列）   | 内存优先级队列                         | Redis 列表/优先级队列              |
| DupeFilter（去重过滤器） | 内存集合（set）                       | Redis 集合（SADD/SISMEMBER）    |
| Spider（初始请求来源）    | start\_urls / start\_requests() | Redis 中指定 key 的 lpush/rpush |

多个 Scrapy 节点连接同一 Redis，共享请求队列和去重集合，实现真正的分布式爬取。

```
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Scrapy  │  │ Scrapy  │  │ Scrapy  │
│ Node 1  │  │ Node 2  │  │ Node 3  │
└────┬────┘  └────┬────┘  └────┬────┘
     │             │             │
     └─────────────┼─────────────┘
                   │ 共享
            ┌──────▼──────┐
            │    Redis     │
            │  请求队列    │
            │  去重集合    │
            │  起始URL     │
            └─────────────┘

```

### 10.2 安装

```bash
pip install scrapy-redis

```

### 10.3 基础配置

```python
# settings.py

# Redis 连接
REDIS_URL = "redis://username:password@localhost:6379/0"
# 或分别配置
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = None
REDIS_ENCODING = "utf-8"

# 使用 Scrapy-Redis 的 Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 使用 Redis 去重过滤器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 是否在 Redis 中持久化请求队列（爬虫关闭后保留，支持断点续爬）
SCHEDULER_PERSIST = True

# 请求队列类型
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
# 可选值：
# "scrapy_redis.queue.SpiderQueue"         FIFO 队列（先进先出）
# "scrapy_redis.queue.SpiderStack"         LIFO 队列（后进先出/深度优先）
# "scrapy_redis.queue.SpiderPriorityQueue" 优先级队列（默认，推荐）

# 队列中无请求时的等待时间（秒）
SCHEDULER_IDLE_BEFORE_CLOSE = 0   # 0 表示立即关闭；设置 > 0 可等待新任务

# Redis key 前缀（所有 Scrapy-Redis key 都以此为命名空间）
REDIS_ITEMS_KEY = "%(spider)s:items"
REDIS_START_URLS_KEY = "%(name)s:start_urls"

# 是否将 Item 序列化到 Redis（用于后续处理）
REDIS_ITEMS_SERIALIZER = "json"

```

**SCHEDULER\_QUEUE\_CLASS 对比**

| 队列类                 | 策略                      | 适用场景        |
| ------------------- | ----------------------- | ----------- |
| SpiderPriorityQueue | 按 priority 字段排序，高优先级先处理 | 通用场景，默认推荐   |
| SpiderQueue         | FIFO，先进先出，广度优先          | 需要广度优先遍历的场景 |
| SpiderStack         | LIFO，后进先出，深度优先          | 需要深度优先遍历的场景 |

### 10.4 Spider 改造

将 `scrapy.Spider` 替换为 `scrapy_redis.spiders.RedisSpider`：

```python
from scrapy_redis.spiders import RedisSpider

class MyDistributedSpider(RedisSpider):
    name = "my_distributed_spider"
    allowed_domains = ["example.com"]

    # Redis key，Spider 从此 key 读取起始 URL
    # 默认为 "%(name)s:start_urls"，即 "my_distributed_spider:start_urls"
    redis_key = "my_distributed_spider:start_urls"

    # 每次从 Redis 批量获取的 URL 数量（0 表示不限制）
    redis_batch_size = 16

    # 无任务时的最大等待时间（秒）
    max_idle_time = 0

    def parse(self, response):
        # 解析逻辑与普通 Spider 完全一致
        yield {
            "url": response.url,
            "title": response.css("h1::text").get(),
        }

```

**向 Redis 推送起始 URL**

```bash
# 使用 redis-cli 推送
redis-cli lpush my_distributed_spider:start_urls "https://example.com/page1"
redis-cli lpush my_distributed_spider:start_urls "https://example.com/page2"

# 批量推送
redis-cli rpush my_distributed_spider:start_urls \
    "https://example.com/page1" \
    "https://example.com/page2" \
    "https://example.com/page3"

```

**用 Python 推送**

```python
import redis
import json

r = redis.Redis(host="localhost", port=6379, db=0)

urls = [
    "https://example.com/page1",
    "https://example.com/page2",
]

for url in urls:
    r.lpush("my_distributed_spider:start_urls", url)

# 推送带 meta 的完整 Request（序列化后推送）
request_data = {
    "url": "https://example.com/category/1",
    "callback": "parse_category",
    "meta": {"category": "tech"},
}
r.lpush("my_distributed_spider:start_urls", json.dumps(request_data))

```

### 10.5 RedisCrawlSpider（链接爬虫分布式版本）

```python
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.spiders import Rule
from scrapy.linkextractors import LinkExtractor

class MyDistributedCrawlSpider(RedisCrawlSpider):
    name = "my_crawl_spider"
    allowed_domains = ["example.com"]
    redis_key = "my_crawl_spider:start_urls"

    rules = (
        Rule(LinkExtractor(allow=r"/list/"), follow=True),
        Rule(LinkExtractor(allow=r"/detail/"), callback="parse_item"),
    )

    def parse_item(self, response):
        yield {
            "url": response.url,
            "title": response.css("h1::text").get(),
        }

```

### 10.6 DUPEFILTER\_CLASS 详解

**RFPDupeFilter（Scrapy-Redis 默认去重）**

```python
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

```

工作原理：

1. 对每个 Request 计算指纹（fingerprint）：对 URL 规范化后与 method、body 组合进行 SHA1 哈希
2. 将指纹写入 Redis 集合（`SADD spider:dupefilter fingerprint`）
3. 通过 `SISMEMBER` 判断是否已处理

**Redis 中的 key**

| key 模式                    | 说明           |
| ------------------------- | ------------ |
| {spider.name}:dupefilter  | 去重指纹集合       |
| {spider.name}:requests    | 请求队列         |
| {spider.name}:start\_urls | 起始 URL 列表    |
| {spider.name}:items       | Item 数据（启用时） |

**自定义指纹算法**

```python
from scrapy_redis.dupefilter import RFPDupeFilter
from scrapy.utils.request import fingerprint

class CustomDupeFilter(RFPDupeFilter):
    def request_fingerprint(self, request):
        # 自定义指纹：只用 URL，忽略参数顺序
        from w3lib.url import canonicalize_url
        return hashlib.sha1(canonicalize_url(request.url).encode()).hexdigest()

```

### 10.7 Scheduler 详解

```python
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

```

**Scheduler 相关配置**

| 配置项                            | 类型   | 默认值                                       | 说明                                   |
| ------------------------------ | ---- | ----------------------------------------- | ------------------------------------ |
| SCHEDULER\_PERSIST             | bool | False                                     | Spider 关闭后是否保留 Redis 队列和去重集合（断点续爬关键） |
| SCHEDULER\_FLUSH\_ON\_START    | bool | False                                     | Spider 启动时是否清空 Redis 队列（True 则全新开始）  |
| SCHEDULER\_IDLE\_BEFORE\_CLOSE | int  | 0                                         | 队列为空时等待新任务的时间（秒）；0 立即关闭              |
| SCHEDULER\_SERIALIZER          | str  | "scrapy\_redis.picklecompat"              | Request 序列化方式，可替换为 "json" 但功能受限      |
| SCHEDULER\_QUEUE\_CLASS        | str  | "scrapy\_redis.queue.SpiderPriorityQueue" | 请求队列实现类                              |
| SCHEDULER\_QUEUE\_KEY          | str  | "%(spider)s:requests"                     | 请求队列在 Redis 中的 key                   |
| SCHEDULER\_DUPEFILTER\_KEY     | str  | "%(spider)s:dupefilter"                   | 去重集合在 Redis 中的 key                   |
| SCHEDULER\_DUPEFILTER\_CLASS   | str  | 由 DUPEFILTER\_CLASS 决定                    | Scheduler 内部使用的去重过滤器                 |

---

## 11\. 分布式最佳实践

### 11.1 多节点部署方案

**目录结构**

```
my_scrapy_project/
├── my_scrapy_project/
│   ├── spiders/
│   │   └── distributed_spider.py
│   ├── middlewares.py
│   ├── pipelines.py
│   └── settings.py
├── scrapy.cfg
└── requirements.txt

```

**settings.py 完整分布式配置**

```python
# ===== 基础配置 =====
BOT_NAME = "my_scrapy_project"
SPIDER_MODULES = ["my_scrapy_project.spiders"]
NEWSPIDER_MODULE = "my_scrapy_project.spiders"

# ===== Scrapy-Redis 分布式配置 =====
REDIS_URL = "redis://:yourpassword@192.168.1.100:6379/0"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True               # 断点续爬
SCHEDULER_FLUSH_ON_START = False       # 启动时不清空队列
SCHEDULER_IDLE_BEFORE_CLOSE = 60      # 空闲 60 秒后关闭
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"

# ===== 并发配置 =====
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_DELAY = 0.5
RANDOMIZE_DOWNLOAD_DELAY = True
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0

# ===== 中间件 =====
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": None,
    "my_scrapy_project.middlewares.RandomUserAgentMiddleware": 400,
    "my_scrapy_project.middlewares.ProxyMiddleware": 750,
}

# ===== Pipeline =====
ITEM_PIPELINES = {
    "my_scrapy_project.pipelines.ValidationPipeline": 200,
    "my_scrapy_project.pipelines.MongoPipeline": 400,
}

# ===== 日志 =====
LOG_LEVEL = "INFO"
LOG_FILE = "/var/log/scrapy/%(name)s.log"

```

**在多台机器上启动 Spider**

```bash
# 机器 1
scrapy crawl my_distributed_spider

# 机器 2（同一 Redis，不同节点，并行运行）
scrapy crawl my_distributed_spider

# 机器 3
scrapy crawl my_distributed_spider

```

所有节点从同一 Redis 队列取任务，结果写入同一 MongoDB，实现真正的分布式采集。

### 11.2 断点续爬

配置 `SCHEDULER_PERSIST = True` 后，爬虫关闭时 Redis 中的请求队列和去重集合会保留。

```bash
# 查看剩余待爬 URL 数量
redis-cli llen my_distributed_spider:requests

# 查看已去重 URL 数量
redis-cli scard my_distributed_spider:dupefilter

# 重新启动时，自动从上次中断处继续
scrapy crawl my_distributed_spider

```

**全新开始（清空状态）**

```bash
# 方法一：通过 settings
SCHEDULER_FLUSH_ON_START = True   # 启动时清空

# 方法二：手动清空 Redis
redis-cli del my_distributed_spider:requests
redis-cli del my_distributed_spider:dupefilter
redis-cli del my_distributed_spider:start_urls

```

### 11.3 URL 去重策略对比

| 策略                      | 实现                                     | 优点             | 缺点           |
| ----------------------- | -------------------------------------- | -------------- | ------------ |
| 内存 set（默认 Scrapy）       | scrapy.dupefilters.RFPDupeFilter       | 速度极快           | 重启后丢失，不支持分布式 |
| Redis set（Scrapy-Redis） | scrapy\_redis.dupefilter.RFPDupeFilter | 持久化，支持分布式      | 内存随 URL 数增长  |
| Redis Bloom Filter      | 第三方（scrapy-redis-bf 等）                 | 内存固定，可处理海量 URL | 存在误判率，不可删除   |

**Bloom Filter 去重（适合亿级 URL）**

```python
# 使用 scrapy-redis-bloomfilter
pip install scrapy-redis-bloomfilter

# settings.py
DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"
BLOOMFILTER_HASH_NUMBER = 6     # hash 函数数量，越多误判率越低（内存越大）
BLOOMFILTER_BIT = 30            # 2^30 bit = 128MB，控制内存占用

```

### 11.4 生产环境监控

```python
# 使用 Scrapy 内置的 stats
class MonitorPipeline:
    def open_spider(self, spider):
        self.start_time = time.time()

    def close_spider(self, spider):
        stats = spider.crawler.stats.get_stats()
        duration = time.time() - self.start_time
        spider.logger.info(f"采集完成: {stats.get('item_scraped_count', 0)} 条, 耗时 {duration:.1f}s")

```

```bash
# 通过 Scrapy 命令查看实时统计（Scrapyd 部署时）
curl http://localhost:6800/daemonstatus.json

# 通过 Redis 监控队列长度
redis-cli monitor   # 实时命令流
redis-cli info stats  # 统计信息

```

### 11.5 Item 后处理架构

在大规模分布式场景下，建议将 Item 写入 Redis 队列，由独立的消费者进程处理：

```python
# Pipeline：将 Item 写入 Redis 队列
import redis
import json

class RedisQueuePipeline:
    @classmethod
    def from_crawler(cls, crawler):
        instance = cls()
        instance.redis_url = crawler.settings.get("REDIS_URL")
        instance.redis_key = crawler.settings.get("REDIS_ITEMS_KEY", "%(spider)s:items")
        return instance

    def open_spider(self, spider):
        self.r = redis.from_url(self.redis_url)
        self.key = self.redis_key % {"spider": spider.name}

    def process_item(self, item, spider):
        self.r.rpush(self.key, json.dumps(dict(item), ensure_ascii=False))
        return item

```

```python
# 独立消费者进程
import redis
import json
import pymongo

r = redis.Redis(host="localhost", port=6379)
mongo = pymongo.MongoClient()["scrapy"]

while True:
    _, data = r.blpop("my_spider:items", timeout=30)
    if data:
        item = json.loads(data)
        mongo["articles"].insert_one(item)

```

---

## 12\. Scrapyd 部署

### 12.1 安装与配置

```bash
pip install scrapyd scrapyd-client

# 启动 Scrapyd 服务（默认监听 6800 端口）
scrapyd

```

**scrapyd.conf 配置文件**

```ini
[scrapyd]
eggs_dir    = eggs          # 项目 egg 存储目录
logs_dir    = logs          # 日志目录
items_dir   =               # Item 存储目录（空则不存储）
jobs_to_keep = 5            # 每个 Spider 保留的历史任务数
dbs_dir     = dbs           # 数据库目录
max_proc    = 0             # 最大并行进程数，0 表示 CPU 数量
max_proc_per_cpu = 4        # 每个 CPU 的最大并行进程数
finished_to_keep = 100      # 保留的已完成任务数
poll_interval = 5.0         # 轮询间隔（秒）
bind_address = 127.0.0.1    # 监听地址（改为 0.0.0.0 对外开放）
http_port   = 6800          # 监听端口
debug       = off
runner      = scrapyd.runner
application = scrapyd.app.application
launcher    = scrapyd.launcher.Launcher
webroot     = scrapyd.website.Root

```

### 12.2 部署项目

```bash
# scrapy.cfg 配置
[settings]
default = my_project.settings

[deploy:production]
url = http://192.168.1.200:6800/
project = my_project

[deploy:staging]
url = http://192.168.1.201:6800/
project = my_project

```

```bash
# 部署到生产环境
scrapyd-deploy production -p my_project

# 查看已部署版本
curl http://192.168.1.200:6800/listversions.json?project=my_project

```

### 12.3 Scrapyd API

| API                              | 方法   | 说明                               |
| -------------------------------- | ---- | -------------------------------- |
| GET /daemonstatus.json           | GET  | 查看 Scrapyd 状态                    |
| GET /listprojects.json           | GET  | 列出所有项目                           |
| GET /listversions.json?project=P | GET  | 列出项目的所有版本                        |
| GET /listspiders.json?project=P  | GET  | 列出项目的所有 Spider                   |
| GET /listjobs.json?project=P     | GET  | 列出所有任务（pending/running/finished） |
| POST /schedule.json              | POST | 启动一个爬虫任务                         |
| POST /cancel.json                | POST | 取消一个任务                           |
| POST /addversion.json            | POST | 上传项目代码（egg 文件）                   |
| POST /delversion.json            | POST | 删除项目版本                           |
| POST /delproject.json            | POST | 删除项目                             |

**启动爬虫任务**

```bash
curl http://localhost:6800/schedule.json \
    -d project=my_project \
    -d spider=my_distributed_spider \
    -d setting=DOWNLOAD_DELAY=2 \
    -d custom_arg=value

# 返回
{"status": "ok", "jobid": "6487ec79947edab326d6db28a2d86511e8247444"}

```

**查看任务日志**

```bash
# 日志路径：logs/{project}/{spider}/{jobid}.log
tail -f logs/my_project/my_distributed_spider/6487ec79947edab326d6db28a2d86511e8247444.log

```

### 12.4 多节点 Scrapyd 管理

对于大规模分布式部署，可使用 `scrapydweb` 进行可视化管理：

```bash
pip install scrapydweb
scrapydweb  # 默认 5000 端口

# 或使用 spiderkeeper
pip install spiderkeeper
spiderkeeper --server=http://localhost:6800

```

---

## 13\. 常见陷阱与注意事项

### 13.1 CrawlSpider 的 parse 方法陷阱

```python
# 错误：重写 parse 方法会破坏 CrawlSpider 的规则处理
class MyCrawlSpider(CrawlSpider):
    rules = (Rule(LinkExtractor(), callback="parse"),)  # 不要用 parse 作为 callback

    def parse(self, response):   # 不要重写 parse
        pass

# 正确：使用其他方法名
class MyCrawlSpider(CrawlSpider):
    rules = (Rule(LinkExtractor(), callback="parse_item"),)

    def parse_item(self, response):
        pass

```

### 13.2 Request 去重陷阱

```python
# 陷阱：同 URL 的 POST 请求默认被去重
# 默认去重指纹包含 method 和 body，但 FormRequest 可能出现去重问题

# 解决：对特殊请求禁用去重
yield Request(url, dont_filter=True)

# 或自定义去重指纹
from scrapy.utils.request import fingerprint

class CustomDupeFilter(RFPDupeFilter):
    def request_fingerprint(self, request):
        # 在指纹计算中加入自定义维度
        base = fingerprint(request)
        custom = request.meta.get("custom_id", "")
        return hashlib.sha1((base + custom).encode()).hexdigest()

```

### 13.3 Spider 关闭信号陷阱

```python
# 陷阱：在 close_spider 中访问 spider.crawler.stats 需要注意时序
class MyPipeline:
    def close_spider(self, spider):
        # 此时 stats 已最终化，可以安全访问
        count = spider.crawler.stats.get_value("item_scraped_count")
        spider.logger.info(f"共采集 {count} 条数据")

```

### 13.4 Item 与 dict 的区别

```python
# Item 在访问不存在的键时抛出 KeyError（与 dict 一致）
item = ArticleItem()
item["nonexistent"]   # KeyError

# 但 Item 只允许访问已定义的字段
item["undefined_field"] = "value"  # KeyError，不允许赋值未定义字段

# 安全访问
item.get("title")            # 不存在返回 None
item.get("title", "default") # 指定默认值

# 转换为 dict
dict(item)

```

### 13.5 Scrapy-Redis 的 SCHEDULER\_IDLE\_BEFORE\_CLOSE

```python
# 陷阱：默认值为 0，队列一空爬虫立即退出
# 在分布式场景中，其他节点可能还在产生新 URL，设置等待时间避免过早退出
SCHEDULER_IDLE_BEFORE_CLOSE = 60   # 队列为空后等待 60 秒再关闭

```

### 13.6 内存泄漏风险

```python
# 陷阱：在 Spider 类变量中积累数据导致内存泄漏
class MySpider(scrapy.Spider):
    seen_urls = []  # 类变量，所有实例共享，且不会被释放

    def parse(self, response):
        self.seen_urls.append(response.url)   # 持续增长，导致内存泄漏

# 正确：使用实例变量或依赖 Scrapy 的去重机制
class MySpider(scrapy.Spider):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.seen_urls = set()   # 实例变量，随 Spider 生命周期管理

```

### 13.7 Pipeline 中的异步操作

```python
# 在 Pipeline 中需要异步操作时，使用 Twisted 或 asyncio
from twisted.internet.defer import inlineCallbacks
from scrapy.utils.defer import maybe_deferred

class AsyncMongoPipeline:
    @inlineCallbacks
    def process_item(self, item, spider):
        # 使用 motor（异步 MongoDB 驱动）
        yield self.collection.insert_one(dict(item))
        return item

# Scrapy 2.0+ 支持原生 asyncio
class AsyncPipeline:
    async def process_item(self, item, spider):
        await self.collection.insert_one(dict(item))
        return item

```

### 13.8 allowed\_domains 与子域名

```python
# 陷阱：allowed_domains 中的域名会匹配子域名
# "example.com" 不会匹配 "www.example.com"，需要明确列出

class MySpider(scrapy.Spider):
    allowed_domains = ["example.com", "www.example.com", "api.example.com"]

# 或者禁用 OffsiteMiddleware
SPIDER_MIDDLEWARES = {
    "scrapy.spidermiddlewares.offsite.OffsiteMiddleware": None,
}

```

### 13.9 cookies 与多账号

```python
# 使用 cookiejar 隔离不同账号的 session
class MultiAccountSpider(scrapy.Spider):
    accounts = [
        {"username": "user1", "password": "pass1"},
        {"username": "user2", "password": "pass2"},
    ]

    def start_requests(self):
        for i, account in enumerate(self.accounts):
            yield scrapy.FormRequest(
                "https://example.com/login",
                formdata=account,
                meta={"cookiejar": i},    # 每个账号独立的 cookie jar
                callback=self.after_login,
                cb_kwargs={"account_id": i},
            )

    def after_login(self, response, account_id):
        yield scrapy.Request(
            "https://example.com/data",
            meta={"cookiejar": account_id},   # 继续使用同一 cookie jar
            callback=self.parse_data,
        )

```

### 13.10 分布式去重的一致性

Scrapy-Redis 使用 Redis SADD/SISMEMBER 实现去重，这些操作是原子的，但在极高并发下仍可能出现竞态条件（多个节点同时判断同一 URL 未爬取）。

```python
# 对于幂等操作（如读取），重复爬取影响不大，可以接受
# 对于非幂等操作（如写入），需要在 Pipeline 层面做去重
class DatabasePipeline:
    def process_item(self, item, spider):
        # 使用 upsert 操作而非 insert，避免重复写入
        self.collection.update_one(
            {"url": item["url"]},          # 查询条件
            {"$set": dict(item)},           # 更新内容
            upsert=True,                    # 不存在则插入
        )
        return item

```

---

## 快速参考

### 项目初始化

```bash
# 创建项目
scrapy startproject my_project
cd my_project

# 创建 Spider
scrapy genspider my_spider example.com
scrapy genspider -t crawl my_crawl_spider example.com

# 运行
scrapy crawl my_spider
scrapy crawl my_spider -o output.json
scrapy crawl my_spider -s LOG_LEVEL=INFO

# 调试
scrapy shell "https://example.com"
scrapy shell --nolog "https://example.com"

# 检查 Spider 输出
scrapy parse "https://example.com" --spider=my_spider -d 2

```

### 常用 Shell 命令

```python
# 在 scrapy shell 中
fetch("https://example.com")           # 重新获取 URL
response.css("h1::text").get()
response.xpath("//h1/text()").get()
response.json()
view(response)                          # 在浏览器中打开响应

```

### 分布式快速启动清单

```bash
# 1. 确认 Redis 可连接
redis-cli -h 192.168.1.100 ping

# 2. 推送起始 URL
redis-cli -h 192.168.1.100 rpush my_spider:start_urls "https://example.com"

# 3. 在所有节点启动 Spider
scrapy crawl my_spider

# 4. 监控队列
redis-cli -h 192.168.1.100 llen my_spider:requests    # 待爬数量
redis-cli -h 192.168.1.100 scard my_spider:dupefilter  # 已去重数量

# 5. 断点续爬（确认 SCHEDULER_PERSIST=True，直接重启即可）
scrapy crawl my_spider

```

---

## 最佳实践

**将 Item 定义为 dataclass 或用 `ItemLoader`，不要在 Spider 里手动构建字典**：`Item` 提供字段定义和校验；`ItemLoader` 支持输入/输出处理器链，适合复杂的字段清洗（去空格、拼接列表、类型转换），比分散的字符串处理更易维护。

**通过 `settings.py` 控制并发和速率，而非多进程启动多个 Scrapy 进程**：`CONCURRENT_REQUESTS`、`DOWNLOAD_DELAY`、`AUTOTHROTTLE_ENABLED` 可以精细控制爬取速率，避免对目标网站造成压力，也避免被封 IP。

**为所有 Spider 设置 `custom_settings`，实现 Spider 级别的配置覆盖**：不同 Spider 可能需要不同的并发数、User-Agent 或 Cookie 策略，通过 `custom_settings` 覆盖全局配置，无需启动多个项目。

**使用 Pipeline 做数据持久化，Item 只做数据传递**：Scrapy Pipeline 支持异步写入（MySQL、MongoDB、ES），与 Spider 解耦，便于切换存储后端。关闭 Pipeline 时不影响爬虫逻辑。

**分布式爬取时使用 `scrapy-redis` \+ Redis 实现请求队列共享和去重**：多台机器共用同一个 Redis 请求队列，`RFPDupeFilter` 确保 URL 不重复爬取，`SCHEDULER_PERSIST=True` 支持断点续爬。

---

## 常见陷阱

### 陷阱：在 `parse` 方法中使用同步阻塞 IO

**现象：** Spider 单线程爬取，即使设置了 `CONCURRENT_REQUESTS=32`，吞吐量也极低；或 Scrapy 控制台卡住不动。

**原因：** Scrapy 基于 Twisted 异步框架，`parse` 方法在 reactor 线程中执行，任何同步阻塞调用（`time.sleep()`、同步数据库写入、`requests.get()`）都会阻塞整个爬虫。

**解决：** 使用 `scrapy-playwright`/`scrapy-splash` 处理 JS 渲染，Pipeline 中用异步 DB 驱动（`aiomysql`、`motor`），不要在 Spider 中调用同步网络请求。

### 陷阱：`ROBOTSTXT_OBEY=True` 导致大量 URL 被跳过

**现象：** 爬虫启动后没有任何请求，或只处理了少量 URL，日志显示"Forbidden by robots.txt"。

**原因：** 默认配置 `ROBOTSTXT_OBEY=True` 会先获取并解析目标站点的 `robots.txt`，不允许的路径会被忽略。

**解决：** 在 `settings.py` 中设置 `ROBOTSTXT_OBEY = False`（合法合规前提下），或仔细检查 `robots.txt` 了解允许爬取的路径范围。

### 陷阱：重复爬取同一 URL（去重失效）

**现象：** 日志显示同一 URL 被请求多次，或数据库中出现大量重复数据。

**原因：** 在 `parse` 回调中用 `yield Request(url)` 时未检查 URL 是否已在队列或已访问，或自定义了 `fingerprint` 导致相同 URL 产生不同指纹。

**解决：** 确认 `DUPEFILTER_CLASS` 为默认的 `RFPDupeFilter`；自定义请求时不要随意修改 `meta`、`headers`（这些影响指纹），或显式传 `dont_filter=False`。

---

## 参见

- [httpx完全指南](https://blog.vercanti.com/httpx-wan-quan-zhi-nan/)
- [BeautifulSoup与lxml完全指南](https://blog.vercanti.com/beautifulsoup-yu-lxml-wan-quan-zhi-nan/)
- [Redis完全指南](https://blog.vercanti.com/redis-wan-quan-zhi-nan/)
- [Playwright完全指南](https://blog.vercanti.com/playwright-wan-quan-zhi-nan/)