news 2026/9/10 3:10:26

Scrapy爬取裁判文书网:加密参数还原与中间件实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy爬取裁判文书网:加密参数还原与中间件实战

简介:这是一份基于 Scrapy 框架实现裁判文书网爬虫的完整项目源码,主要面向正在学习 Python 网络爬虫、准备毕业设计或课程设计的高校学生,也适合需要参考真实项目结构、希望快速上手数据采集的中级开发者。项目已经过本地编译验证,评审分达九十八分,内容由助教老师审定,难度适中,能够直接运行或作为二次开发的基础。压缩包共包含二十九个文件,其中十七个 Python 文件构成核心爬虫逻辑,覆盖爬虫主程序、中间件、数据管道、请求头配置等模块;五个 XML 配置与四个 JavaScript 文件分别用于工程描述和前端加密破解逻辑,另有 Scrapy 配置、IDEA 工程说明及 Markdown 文档,整体仅一百零二 KB,目录组织清晰,便于导入开发工具后快速阅读。当前已有约一百三十人学习使用,可帮助读者理解裁判文书网的请求参数加密、列表页解析、数据持久化等关键环节,也能直接用于课程答辩、毕业设计展示或后续功能扩展。实际运行中遇到的问题与解决思路,都直观体现在源码结构中。

1. 先理清楚:裁判文书网爬虫的难点不在请求,在请求之前的加密参数

用 Scrapy 写裁判文书网爬虫,很多人第一次写完就能拿到状态码 200,却在 parse 里半天提取不到一条数据。原因是裁判文书网的数据接口在请求前需要生成一组带时间戳、随机数和签名校验的参数,这组参数由前端 JS 配合 cookie 动态生成,直接用 Requests 或 Scrapy 裸请求会被当异常流量拦掉。这个基于 Scrapy 的高分项目恰恰是把这层参数还原做成独立模块encrypt.py,再配合TheUserAgent.py、下载中间件和标准的 Scrapy 管道,构成一个能跑、能演示、能扩展毕设功能点的完整项目。项目难度适中,既不需要你啃大型 JS 混淆框架,又能真正讲清楚 Scrapy 爬虫在反爬站点上的使用方法,适合毕业设计、期末课程设计以及想从 Requests 转向 Scrapy 的开发者反复读代码并动手改。

2. 工程目录拆解:从文件分布看懂一台爬虫的调用链路

拿到压缩包先不要急着执行scrapy crawl wenshu_list,先看目录分布。这个工程同时存在wenshu_jia包和根目录下的demo.pydemo2.pyziji_demo.py,说明作者刻意保留了两种调试方式:一种是纯 Python 脚本用来验证接口参数和 cookie 组合,另一种是真正的 Scrapy 工程收口。这样的结构对毕设答辩很有价值,你可以先讲一个请求如何被逆向分析出来,再讲 Scrapy 如何把它工程化落地。

2.1 目录骨架:哪些文件负责调度、解析、反爬、落库

. ├── scrapy.cfg ├── encrypt.py # 参数加密/签名生成 ├── demo.py # 快速验证列表接口 ├── demo2.py # 验证详情页接口 ├── ziji_demo.py # 自写脚本测试 UA/cookie 组合 ├── wenshuliebiao.py # 独立演示列表翻页逻辑 └── wenshu_jia ├── __init__.py ├── items.py # 文书字段定义 ├── pipelines.py # 清洗、去重、入库 ├── middlewares.py # 请求头/签名/重试中间件 ├── settings.py # 项目配置 ├── TheUserAgent.py # UA 池 └── spiders ├── __init__.py └── wenshuliebiao.py # Scrapy 爬虫主逻辑

这个结构里最容易被忽略的是wenshuliebiao.py在根目录和spiders下同时出现。根目录那个通常是不依赖 Scrapy 的列表页脚本,主要是为了方便单文件断点调试;spiders下那个才进入真正的 Scrapy 调度器。两者解析逻辑一致,改造字段时只需要同步修改items.py和解析函数。

文件在调用链中的位置出了问题常见表现
encrypt.py发出请求前接口返回 -1 或 403
middlewares.pyScrapy 下载器前置状态码 200 却没有数据,或频繁重试
TheUserAgent.py每个 Request 发出前连续请求被拒绝
items.pyparse 字典收口字段缺失但在日志中不报错
pipelines.pyitem 经过之后数据库无记录或重复记录

由表格可以看出,大部分故障最终都不是解析代码的问题,而是请求链路上的中间环节没有对齐。所以调试时优先看middlewares.py是否真正生效,再看数据解析。

2.2 items.py:先把一条裁判文书收口成固定结构

Scrapy 的 Item 相当于给字典字段做了一层契约。常见做法是先在items.py里定义好文书的关键字段,再写解析逻辑,否则爬完才发现取证字段对不上数据库列。

import scrapy class WenshuItem(scrapy.Item): doc_id = scrapy.Field() # 文书唯一 ID,用于详情页去重 case_name = scrapy.Field() # 案件名称 case_number = scrapy.Field() # 案号 court_name = scrapy.Field() # 法院名称 case_type = scrapy.Field() # 案件类型:民事/刑事/行政 trial_date = scrapy.Field() # 裁判日期 content = scrapy.Field() # 文书正文,详情页解析 source_url = scrapy.Field() # 来源 URL,方便回溯

这里有一个参数设计细节:doc_id不要直接用标题或案号,最好用接口返回的文档 ID,字段短且稳定;trial_date建议统一输出为YYYY-MM-DD字符串,后续写入 MySQL 时不需要二次转换。字段一旦固定,管道和建表语句都围绕它展开,后期改字段的代价最小。

2.3 settings.py:并发、延迟、去重三个参数直接影响能不能拿全数据

下面这段 settings 是从该资源中提炼出来最值得抄走的骨架,一个标准 Scrapy 工程只要调整这几处,就能适配大多数反爬站点:

BOT_NAME = 'wenshu_jia' SPIDER_MODULES = ['wenshu_jia.spiders'] NEWSPIDER_MODULE = 'wenshu_jia.spiders' ROBOTSTXT_OBEY = False CONCURRENT_REQUESTS = 4 DOWNLOAD_DELAY = 1.5 RANDOMIZE_DOWNLOAD_DELAY = True DOWNLOADER_MIDDLEWARES = { 'wenshu_jia.middlewares.WenshuDownloaderMiddleware': 543, } ITEM_PIPELINES = { 'wenshu_jia.pipelines.WenshuPipeline': 300, }

第一组参数影响请求频率。CONCURRENT_REQUESTS不建议调成 16 的默认值,裁判文书网对并发很敏感,4 并发出错率最低;DOWNLOAD_DELAY设置在 1.5 秒左右,配合RANDOMIZE_DOWNLOAD_DELAY可以让请求间隔自然抖动。第二组中间件数值 543 表示在默认下载中间件之后执行,保证 UA 和签名能覆盖最终请求。第三组管道数值 300 表示 item 被爬虫解析后立即进入清洗流程。按这套参数改,速度不快,但能稳定抓到大部分数据。

3. 参数还原与中间件实战:encrypt.py、TheUserAgent.py、middlewares.py 的配合逻辑

这一章是整个项目分值最高的部分。裁判文书网的反爬不是简单校验 Referer,而是在调用查询接口前生成动态签名。签名参数一旦缺失或过期,请求直接返回空列表。我在调试时见过很多人把时间花在解析列表上,最后发现是 header 里少了一个 token 拼接字段。

3.1 encrypt.py 中加密参数的还原原理

这类站点的动态签名通常由四部分组成:固定 key、业务查询参数、服务器种下的 cookie token、当前时间戳,再加上一个随机数做混淆。最终通过 MD5 或 SHA256 输出签名。项目里的encrypt.py核心逻辑可以简化为下面这段:

import hashlib import time import random def make_sign(key, query, cookie_token): ts = str(int(time.time() * 1000)) nonce = ''.join(random.choices('0123456789', k=6)) raw = f"{key}{query}{cookie_token}{ts}{nonce}" sign = hashlib.md5(raw.encode('utf-8')).hexdigest() return sign, ts, nonce

参数含义分别是:key是从前端 JS 里找到的固定字符串,query是列表页查询条件序列化后的内容,cookie_token是首次访问首页时服务器下发并写入 cookie 的标识,nonce是随机数。把这四部分拼接后做 MD5,再将签名、时间戳和随机数放在请求头里,服务端会按相同拼接规则重新计算一次。要注意cookie_token有时效性,encrypt.py本身不负责刷新 token,需要爬虫在启动时先访问一次首页获取最新值,否则返回的 JSON 里会带-1状态码。

3.2 TheUserAgent.py 的 UA 池结构

UA 池在反爬站点里的作用是避免同一个浏览器标识在几十秒内被反复检测。Scrapy 默认不设置 User-Agent,如果不覆盖,对方很容易判断为脚本请求。

import random UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/125.0", ] def get_user_agent(): return random.choice(UA_POOL)

这个模块只做一件事:每次调用返回一个随机 UA。实践证明,UA 更换频率比DOWNLOAD_DELAY更容易触发风控,所以不要把 UA 固定在 settings 的DEFAULT_REQUEST_HEADERS里,而是通过中间件在每个 Request 发出前动态设置。

3.3 middlewares.py 在 Scrapy 请求生命周期里拼签名

Scrapy 下载中间件的process_request方法会在请求真正发送前执行,这里就是拼 UA、拼签名、补 Referer 的最佳位置。

from wenshu_jia.TheUserAgent import get_user_agent from encrypt import make_sign class WenshuDownloaderMiddleware: def process_request(self, request, spider): request.headers['User-Agent'] = get_user_agent() if '/api/query' in request.url: cookie_token = request.cookies.get('wenshu_token') sign, ts, nonce = make_sign('9b2f...', request.url, cookie_token) request.headers['X-Sign'] = sign request.headers['X-Ts'] = ts request.headers['X-Nonce'] = nonce request.headers['Referer'] = 'https://wenshu.court.gov.cn/' return None

process_request返回值是 None 时,请求会继续走下一层中间件并最终发出;如果返回 Response 对象,则跳过网络请求直接进入爬虫解析。这里的判断条件是'/api/query' in request.url,避免对首页、静态资源也做多余签名。还要注意request.cookies.get在首次请求时可能取不到值,所以启动时要在start_requests中先把首页 cookie 种到 Request 里。

3.4 到底要不要维护 cookie:不处理 session 就会无限重定向

裁判文书网的签名参数依赖 cookie 中的 token,但 cookie 不是永久有效。如果不处理 session,可能会出现列表页正常、翻页请求全部重定向到首页的情况。常见解决方案是在本地缓存 cookie 文件,并记录过期时间:

import pickle import os COOKIE_FILE = '/tmp/wenshu.cook' def load_cookie(): if os.path.exists(COOKIE_FILE): with open(COOKIE_FILE, 'rb') as f: return pickle.load(f) return {} def save_cookie(cookie_obj): with open(COOKIE_FILE, 'wb') as f: pickle.dump(cookie_obj, f)

load_cookie在 spider 构造时调用,将 cookie 绑定到第一个请求;当状态码变成 302 或 JSON 中返回格式异常时,就清理本地 cookie,重新访问首页获取新 token。缓存路径建议放在/tmp,不要提交到 git 仓库,避免把个人会话泄漏到代码包里。

4. 从列表页到详情页:wenshuliebiao.py 与 spiders 里的完整实现

前面把 request 层面的事做完,接下来就是典型的scrapy 爬虫案例:列表解析、翻页、详情页提取、管道落库。这个工程里根目录的demo.pyspiders/wenshuliebiao.py逻辑基本一致,差别在前者可以脱离 Scrapy 框架独立运行,方便在 PyCharm 里断点观察响应结构。

4.1 start_requests 先验证一个查询再批量翻页

在构造批量请求前,先用一个请求验证签名和 cookie 是否有效,能省去大量调试时间:

import json import scrapy from wenshu_jia.items import WenshuItem class WenshuListSpider(scrapy.Spider): name = 'wenshu_list' def start_requests(self): api = 'https://wenshu.court.gov.cn/api/query' params = { 'type': 'case', 'page': 1, 'size': 20 } yield scrapy.Request( url=api, method='POST', body=json.dumps(params), headers={'Content-Type': 'application/json'}, cookies=self._init_cookie(), dont_filter=True, callback=self.parse_list )

这里使用method='POST'是因为查询接口接收的是 JSON body,不能直接拼在 URL 后面。dont_filter=True确保分页请求即使 URL 相同也不会被 Scrapy 的去重机制过滤掉。_init_cookie返回的是从encrypt.py缓存文件中加载的 token。

4.2 翻页解析:列表接口返回 JSON,不要用 XPath 硬碰

裁判文书网列表页返回的是结构化 JSON,比解析 HTML 要稳定得多:

def parse_list(self, response): data = response.json() rows = data.get('result', {}).get('rows', []) for row in rows: item = WenshuItem() item['doc_id'] = row.get('docId') item['case_name'] = row.get('caseName') item['case_number'] = row.get('caseNumber') item['court_name'] = row.get('courtName') item['case_type'] = row.get('caseType') item['trial_date'] = row.get('judgeDate') item['source_url'] = response.url yield item current_page = data.get('result', {}).get('page', 1) total_page = data.get('result', {}).get('totalPage', 1) if current_page < total_page: next_page = current_page + 1 yield scrapy.Request( url=response.url, method='POST', body=json.dumps({'type': 'case', 'page': next_page, 'size': 20}), headers=response.request.headers, cookies=response.request.cookies, dont_filter=True, callback=self.parse_list )

解析逻辑里每个get都提供默认值,避免字段缺失直接抛异常。注意翻页请求复用了上一次的response.request.headers,这样签名参数不会因为重新构造 header 而丢失。如果某个翻页请求返回异常,可以先在ziji_demo.py中手动改 page 值,确认接口是否对新页码有效。

4.3 pipelines.py:在入库前完成去重和正文截断

爬取大量文书后,最明显的问题就是重复记录。这里通过doc_id做内存去重,并在管道里把结构化字段落成 CSV:

from itemadapter import ItemAdapter from scrapy.exceptions import DropItem class WenshuPipeline: def __init__(self): self.seen = set() self.db = open('wenshu.csv', 'a', encoding='utf-8-sig') def process_item(self, item, spider): adapter = ItemAdapter(item) doc_id = adapter.get('doc_id') if doc_id in self.seen: raise DropItem(f'duplicate doc_id: {doc_id}') self.seen.add(doc_id) line = ','.join([ str(adapter.get('doc_id', '')), str(adapter.get('case_name', '')), str(adapter.get('case_number', '')), str(adapter.get('court_name', '')) ]) self.db.write(line + '\n') return item def close_spider(self, spider): self.db.close()

DropItem抛出的异常会被 Scrapy 捕获,管道继续处理下一个 item,不会中断爬虫。CSV 文件使用utf-8-sig编码是为了让 Excel 打开时不出现中文乱码。这段去重逻辑只适合单机运行;单机量小,内存足够支撑几十万条doc_id去重需求。

4.4 调试顺序:先用 scrapy shell 验证 response,再改 spider

每改一次签名逻辑都跑完整爬虫并不划算。常见做法是先启动scrapy shell,快速查看当前请求能不能拿到预期结构:

scrapy shell "https://wenshu.court.gov.cn/" >>> response.status >>> len(response.text)

如果response.text长度异常短,说明被反爬拦了,优先回头检查encrypt.py的签名是否过期。签名正确时,再运行一次完整爬虫:

scrapy crawl wenshu_list -s LOG_LEVEL=INFO

在 PyCharm 里给middlewares.pyprocess_request打上断点,观察request.headersrequest.cookies是否在上一个中间件里被覆盖。项目根目录的demo2.py也可以单独执行,它会打印详情页的响应前 500 个字符,用来判断详情接口是否需要额外拆分参数。

5. 反爬升级后的两条改造路径:Playwright 接管动态 iframe,Scrapy 扩展成分布式

当裁判文书网把字段渲染迁移到动态 iframe 后,继续解析接口 JSON 就不太现实了。此时可以保留原有工程,只把spiders/wenshuliebiao.py的解析层换掉,用 Playwright 渲染页面再提取数据。

5.1 当接口签名升级,用 Playwright 处理动态 iframe

如果列表页内容出现在 iframe 且 iframe 的 src 由 JS 生成,直接抓原始 HTML 会拿不到。这时候可以单独写一个渲染函数:

from playwright.sync_api import sync_playwright def fetch_dynamic_list(url, user_agent): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(user_agent=user_agent) page.goto(url, wait_until='networkidle') for frame in page.frames: if '/detail' in frame.url: return frame.content() browser.close()

wait_until='networkidle'表示等待页面所有请求结束,适合依赖异步加载的列表。page.frames可以遍历当前页面所有 iframe,找到真正包含文书信息的 frame 后提取 HTML。注意headless=True在部分服务器上需要安装 Chromium 依赖,否则会报缺少动态链接库。这一类渲染逻辑不要放进中间件,否则会让所有静态请求都变得很重。

5.2 Scrapy 分布式改造与几个容易被忽略的坑

把单机 Scrapy 扩展成分布式爬虫,工程上最稳妥的方式是接入scrapy-redis,替换调度器和去重组件:

SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" REDIS_URL = "redis://localhost:6379/0"

这段配置会把所有请求指纹集中到 Redis,让多台机器共享同一个任务队列。但有一个很隐蔽的问题:中间件给请求添加了X-Ts时间戳,时间戳每次都在变,所以按完整 header 计算出的请求指纹也一直在变,Redis 里的指纹去重形同虚设。解决办法是在DUPEFILTER_CLASS基础上自定义指纹函数,剥离掉X-TsX-Nonce这类动态 header,只对 URL 和方法做指纹。另一个坑是 cookie 在多节点之间不能共享,若 token 在 A 节点过期,B 节点仍然往 Redis 队列里压任务,会导致大量无效请求。解决思路是把 token 和过期时间一起存入 Redis,每次取任务前检查过期状态。改造完成后,可以从之前 4 并发的 settings 开始,逐步把CONCURRENT_REQUESTS提到 8,观察 Redis 队列积压情况和错误码比例。对比单机日志,你会更直观理解 Scrapy 调度器与下载中间件在分布式环境下的协作边界。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 3:06:36

DevOps 平台有哪些?2026 主流企业级方案 4 类形态盘点

把 Jenkins 称作"DevOps 平台"&#xff0c;可能是过去十年选型里最贵的一个误会。 打开任何一份"DevOps 平台有哪些"的清单&#xff0c;Jenkins 几乎都在列&#xff1b;可它真正擅长的是流水线执行与插件生态&#xff0c;代码托管、制品策略、需求与审计都…

作者头像 李华
网站建设 2026/9/10 3:06:21

MoE混合精度量化实战:门控保精度+专家激进压缩

1. 这不是“把模型变小”的简单操作&#xff0c;而是给大模型装上智能节油系统你肯定见过这样的场景&#xff1a;一个70B参数的MoE大模型&#xff0c;在A100上推理时显存占用飙到85GB&#xff0c;吞吐量卡在3.2 token/s&#xff1b;而换用W8A8混合精度量化后&#xff0c;显存压…

作者头像 李华
网站建设 2026/9/10 3:05:05

Simulink铅酸电池建模:温度-老化耦合Thevenin模型实战

简介&#xff1a;本资源是一套面向MATLAB/Simulink初学者与电池建模实践者的铅酸蓄电池系统仿真教学包&#xff0c;适用于新能源、电力电子及车辆动力系统等方向的课程设计、毕业设计与工程验证场景。资源完整呈现基于Simulink的电化学模型构建过程&#xff0c;可实时输出充放电…

作者头像 李华
网站建设 2026/9/10 3:02:48

长期流量卡怎么选才不踩坑?23省套餐全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华