
简介这是一份基于 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.py、demo2.py、ziji_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 或 403middlewares.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, k6)) 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 Noneprocess_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.py和spiders/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( urlapi, methodPOST, bodyjson.dumps(params), headers{Content-Type: application/json}, cookiesself._init_cookie(), dont_filterTrue, callbackself.parse_list )这里使用methodPOST是因为查询接口接收的是 JSON body不能直接拼在 URL 后面。dont_filterTrue确保分页请求即使 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( urlresponse.url, methodPOST, bodyjson.dumps({type: case, page: next_page, size: 20}), headersresponse.request.headers, cookiesresponse.request.cookies, dont_filterTrue, callbackself.parse_list )解析逻辑里每个get都提供默认值避免字段缺失直接抛异常。注意翻页请求复用了上一次的response.request.headers这样签名参数不会因为重新构造 header 而丢失。如果某个翻页请求返回异常可以先在ziji_demo.py中手动改 page 值确认接口是否对新页码有效。4.3 pipelines.py在入库前完成去重和正文截断爬取大量文书后最明显的问题就是重复记录。这里通过doc_id做内存去重并在管道里把结构化字段落成 CSVfrom itemadapter import ItemAdapter from scrapy.exceptions import DropItem class WenshuPipeline: def __init__(self): self.seen set() self.db open(wenshu.csv, a, encodingutf-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(fduplicate 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_LEVELINFO在 PyCharm 里给middlewares.py的process_request打上断点观察request.headers和request.cookies是否在上一个中间件里被覆盖。项目根目录的demo2.py也可以单独执行它会打印详情页的响应前 500 个字符用来判断详情接口是否需要额外拆分参数。5. 反爬升级后的两条改造路径Playwright 接管动态 iframeScrapy 扩展成分布式当裁判文书网把字段渲染迁移到动态 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(headlessTrue) page browser.new_page(user_agentuser_agent) page.goto(url, wait_untilnetworkidle) for frame in page.frames: if /detail in frame.url: return frame.content() browser.close()wait_untilnetworkidle表示等待页面所有请求结束适合依赖异步加载的列表。page.frames可以遍历当前页面所有 iframe找到真正包含文书信息的 frame 后提取 HTML。注意headlessTrue在部分服务器上需要安装 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-Ts、X-Nonce这类动态 header只对 URL 和方法做指纹。另一个坑是 cookie 在多节点之间不能共享若 token 在 A 节点过期B 节点仍然往 Redis 队列里压任务会导致大量无效请求。解决思路是把 token 和过期时间一起存入 Redis每次取任务前检查过期状态。改造完成后可以从之前 4 并发的 settings 开始逐步把CONCURRENT_REQUESTS提到 8观察 Redis 队列积压情况和错误码比例。对比单机日志你会更直观理解 Scrapy 调度器与下载中间件在分布式环境下的协作边界。本文还有配套的精品资源点击获取