尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

裁判文书网数据采集实战:Python爬虫与反爬策略全解析

裁判文书网数据采集实战:Python爬虫与反爬策略全解析 手头刚好有个法律数据相关的项目需要把裁判文书网上的公开判决书批量拉下来做案例分析。说实话刚开始看到这个需求我也觉得不就是写个爬虫嘛结果真正动手才发现里面的坑比想象中多得多。裁判文书网的反爬机制、登录验证、动态加载、分页限制每个环节单独拎出来都能写一篇长文。这篇文章我把整个项目的思路、踩坑过程和最终落地方案完整记录下来给后面要折腾这类数据采集的朋友一个参考。1. 项目背景与需求拆解1.1 为什么需要批量采集裁判文书先说清楚这个项目的价值。裁判文书网是目前国内公开的、体量最大的司法文书数据库上亿份生效判决书、裁定书都在上面。对律师来说办同类案件时需要检索大量相似判例看看法院对某个争议焦点的主流裁判观点对法学生和学术研究者来说做实证研究、统计某个地区的胜诉率、分析某类案由的赔偿标准都需要成规模的数据样本对企业法务来说尽调时批量筛查对手方的涉诉记录也是刚需。问题在于官网的检索体验是给人用的不是给数据用的。你想看某个法院近三年的全部民间借贷判决手动翻页翻到崩溃不说下载还要逐个点开详情页、再点下载按钮。一次两次还行动辄几百上千份的量纯手工操作根本不现实。所以这个项目的核心价值就是把“人肉翻页复制粘贴”变成“脚本自动跑”把零散的网页数据变成结构化的表格可以直接做统计分析。1.2 需求范围和边界界定动手之前我先把需求理清楚避免后期返工。这个项目需要满足几个基本要求按关键词或案由搜索自动遍历搜索结果列表页翻页到底对每个案件进入详情页提取案号、文书标题、法院名称、裁判日期、当事人信息、案件类型、正文内容等核心字段数据落盘为结构化格式方便后续用 Excel、SPSS 甚至数据库做二次分析出于合规考虑只采集法院已经主动公开的裁判文书不碰涉及国家秘密、未成年人犯罪、个人隐私等依法不公开的内容。这里要特别强调一个原则爬取公开数据不是目的合理利用才是。裁判文书网的数据本身是司法公开的产物采集后用于学术研究、法律服务等正当场景没有问题但绝不能拿去搞非法牟利、批量售卖个人信息或者恶意破坏网站的正常运行。整个项目过程中我只做低频次的请求控制绝不给服务器造成压力。1.3 项目的基本工作流程整个项目的技术链路大致是构造检索请求 → 获取列表页 HTML → 解析案件链接 → 逐个访问详情页 → 提取结构化字段 → 数据清洗 → 存储落盘。听起来不复杂但每一步都有它自己的麻烦。你先别急着写代码我建议把流程画出来标注清楚哪些环节可能被网站的反爬机制拦截、哪些环节数据格式不一致需要单独处理。后面我会把我在每个环节遇到的问题逐一展开告诉你实际跑项目时应该怎么应付。2. 技术选型与整体架构设计2.1 编程语言和核心库的选择这个项目我选的是 Python原因很简单爬虫生态最成熟遇到问题网上资料多处理文本和数据分析也顺手。核心用到的库如下requests负责发送 HTTP 请求获取网页源码。轻量、直观。lxml XPath解析 HTML定位和提取所需节点。比正则表达式稳定得多尤其是结构重复性高的列表页。json处理接口返回的 JSON 数据。裁判文书网的列表页底层其实是一个 JSON 接口用这个库直接解析非常方便。pandas数据清洗和导出把字段整理成规范的 DataFrame再输出为 CSV 或 Excel。time / random控制请求间隔模拟人工访问节奏降低被识别为爬虫的风险。我这里没有用 Selenium原因是它的运行速度慢、资源占用高而且随着页面结构变化脚本维护成本大。裁判文书网的列表数据本质上是从后端接口拿 JSON 再渲染到页面上的直接请求那个接口效率高得多。这个判断让我在后面省了大量时间也避开了浏览器自动化被检测的风险。2.2 请求策略和会话保持爬虫这个东西最忌讳的就是一股脑高频请求。裁判文书网的反爬机制非常成熟短时间内大量请求或者请求头特征异常直接触发验证码甚至封 IP。我的做法是先创建requests.Session()对象把登录后获取的 Cookie 挂在 session 上保证后续请求都带着同一个会话身份。同时在每次请求前随机休眠 2 到 5 秒模拟人工点击的节奏。数据量大的时候我会设置一个计数器每请求 50 次就暂停 1 到 2 分钟让服务器喘口气。注意请求频率这块不要抱着侥幸心理。一旦 IP 被封整个项目就得停下来等解封反而更慢。我实测下来控制好频率虽然整体耗时变长但胜在稳定最终完成时间反而比“狂跑然后被封”的方案快得多。2.3 数据模型设计在写爬虫之前我先把要存的数据结构定义好。字段设计上我这个项目是这样规划的字段类型说明case_number字符串案号如2024京01民终1234号title字符串文书标题court字符串审理法院case_type字符串案件类型民事、刑事、行政等judgment_date日期裁判日期party_info文本当事人信息judge字符串审判长/审判员content长文本文书正文url字符串详情页地址提前把字段定死有两个好处一个是解析页面时目标明确不会漏字段另一个是后期清洗数据时列名统一合并去重都方便。3. 核心难点拆解与应对方案3.1 登录态与验证码处理裁判文书网的正常使用需要通过实名注册账号登录部分场景下还会弹出验证码。这让不少人卡在了第一步。我的做法是先用浏览器手动登录一次通过开发者工具把请求头里的 Cookie 复制出来直接写进代码里。这种方式虽然土但最有效能够完美保留登录态避免脚本里去处理复杂的验证码识别逻辑。你可能会问Cookie 会过期吗会。实测大概几个小时到一天不等。解决方案是写一个 Cookie 校验函数每跑一段时间用最小代价的请求测试一下登录态是否仍然有效失效就停下来提示手动更新。我一开始没有加这个校验结果跑了一个小时后突然返回登录跳转页面所有解析逻辑全部报错排查了半天才发现是 Cookie 过期了。这个坑希望大家避开。3.2 列表页的动态加载与数据接口定位真正开始分析页面结构时我发现列表页的数据不是写在 HTML 源码里的而是通过 JavaScript 异步请求后端接口动态渲染的。这意味着直接用 requests 拿到的 HTML 里根本没有案件数据。解决思路是打开浏览器开发者工具切到 Network 面板在网页上手动搜索一次关键词观察浏览器发出了哪些 XHR 请求。找到返回 JSON 数据的那条接口请求后把它的 URL、请求方式、请求参数、请求头都记录下来然后用 Python 模拟这个接口请求直接拿结构化的 JSON 数据。这个思路很关键。很多人一看到动态加载页面就下意识掏出 Selenium其实先看接口是最优解。接口输出的是干净的 JSON解析难度比 HTML 低一个数量级而且数据字段规范化程度高字段命名也清晰。3.3 分页参数与总页数限制裁判文书网的分页逻辑隐藏在接口参数里比如当前页码、每页条数、排序方式等。你需要先发一页请求从响应结果里找出总记录数和总页数然后循环构造每一页的请求。这里要注意一个实际限制这个网站对单次检索的翻页深度是有限制的通俗讲就是你一口气翻到很后面的页码网站会拒绝返回数据。解决办法是在代码里加一个分页上限判断超过限制后自动更换关键词或添加更细的筛选条件把结果集拆分成多个小任务来跑。比如你要采集某个法院的全部民事判决书一次检索可能有 2 万条结果直接翻页翻到 200 页大概率被封或者被截断。我的做法是按裁判年份、案件类型分别检索每个子任务的查询结果控制在几百条以内翻几页就拉完了。虽然从一次任务变成十几次任务但每个任务都能稳定跑完整体效率反而更高。3.4 验证码的触发条件与对策即使你有登录态、控制了请求频率某些操作还是会触发验证码。我观察到的触发条件包括单位时间内请求次数过多、请求头特征异常、频繁切换 IP 等。我的对策有三个层次。第一个是预防严格限制请求频率模拟真实用户的操作节奏。第二个是降低触发概率请求头里带上完整的 User-Agent、Referer 等字段让请求看起来来自正常浏览器。第三个是兜底在代码里检测返回内容是否包含验证码特征一旦检测到就立即停止当前任务、进入冷却等待而不是继续盲目请求导致账号被限制。实操心得验证码弹出后不要尝试用打码平台硬刚。正道是让请求频率降下来等几分钟后继续。我后来把请求间隔统一调到 3 秒以上基本很少再触发验证码。4. 实操过程与核心代码实现4.1 环境准备先把环境搭好。我使用的 Python 版本是 3.10建议你用 3.8 以上。依赖安装执行一行命令pip install requests lxml pandas openpyxlopenpyxl是为了让 pandas 能导出 Excel 格式如果你只需要 CSV不装也没关系。开发调试我用的是 PyCharm当然你用 VS Code 或者其他编辑器都行影响不大。4.2 构造请求会话与请求头这一步是基础中的基础。请求头里最关键的是User-Agent和Referer。很多网站的反爬系统会校验这两个字段缺失或者不符合正常浏览器的特征很容易被识别为脚本。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://wenshu.court.gov.cn/, Accept: application/json, text/javascript, */*; q0.01, Accept-Language: zh-CN,zh;q0.9, } COOKIE 这里粘贴你浏览器里复制的完整 Cookie 字符串 session requests.Session() session.headers.update(HEADERS) session.cookies.update({Cookie: COOKIE}) # 实际操作中建议逐项解析 Cookie 或直接使用 dict 形式这里有个细节Cookie 字符串格式通常是namevalue; name2value2requests 的cookies参数可以直接传字典也可以传字符串到 headers 里。我在代码里用的是字典方式需要先把 Cookie 字符串解析成字典操作起来更可控。4.3 构造检索接口请求列表接口返回的是 JSON 数据格式大概是{ result: ..., data: { total: 12345, list: [...] } }。你需要自己先看一次接口响应结构再写对应的解析代码。def search_cases(keyword, page_num, page_size10): url https://wenshu.court.gov.cn/portal/action/advanced/search params { keyword: keyword, pageNum: page_num, pageSize: page_size, } resp session.post(url, dataparams, verifyFalse) data resp.json() return data注意这里的verifyFalse是因为网站证书链不完整加上这个参数后 requests 就不会在校验证书上报错但同时会输出一个警告。如果你看到警告觉得碍眼可以用urllib3.disable_warnings()关闭它。请求参数的具体字段名和 POST 的数据格式以你浏览器开发者工具里抓到的实际请求为准。不同时间网站可能调整参数名这个一定要现场核对。4.4 解析列表数据与提取关键信息拿到 JSON 之后列表页的核心信息是每条记录的唯一标识。通过这个标识可以拼接出详情页地址或者用接口获取详情。我当时的解析逻辑是遍历list里的每个元素从中提取出案例 ID、标题、案号、日期等字段存入一个列表最后返回供后续循环使用。def parse_list(json_data): result_list [] data json_data.get(data) or {} items data.get(list) or [] for item in items: result_list.append({ case_id: item.get(caseId), title: item.get(title), case_number: item.get(caseCode), court: item.get(courtName), judgment_date: item.get(judgeDate), }) return result_list这段代码有几个需要留神的地方接口返回的字段名我写的是按常见字段来举例的实际上你可能看到的是caseCode、caseNum之类的不同命名。所以拿到 JSON 后先用print(json.dumps(data, ensure_asciiFalse, indent2))把原始数据结构打出来仔细核对一遍再写字段名避免对着错误字段名白忙活。4.5 获取文书正文列表页只是摘要完整正文需要请求详情接口。我这里的做法是拿案例的唯一标识调用详情接口然后从 JSON 的指定字段里取正文 HTML再用BeautifulSoup或html库清洗掉 HTML 标签只保留纯文本。import re from lxml import etree def fetch_detail(case_id): detail_url https://wenshu.court.gov.cn/portal/action/advanced/getDocContentById resp session.post(detail_url, data{caseId: case_id}, verifyFalse) detail_json resp.json() html_content detail_json.get(data, {}).get(content) or # 将 HTML 转为纯文本 text re.sub(r[^], , html_content) return text这个转换方式属于最基础的。实际跑下来你可能会发现正文里有空格、换行符被吃掉的情况建议加上html html.replace(/p, \n)之类的预处理确保段落之间有换行否则导出的 Excel 里正文会是一坨挤在一起的文字。4.6 主流程控制与数据落盘主流程就是把上面几个函数串联起来控制翻页、循环、异常重试、频率控制和数据保存。import time import random import pandas as pd def main(): keyword 民间借贷纠纷 all_rows [] page 1 max_page 5 # 为了防止被限制最多只翻 5 页 while page max_page: print(f正在爬取第 {page} 页) json_data search_cases(keyword, page, page_size10) rows parse_list(json_data) if not rows: break for row in rows: row[content] fetch_detail(row[case_id]) all_rows.append(row) time.sleep(random.uniform(2, 4)) # 控制详情页请求频率 page 1 time.sleep(random.uniform(3, 6)) # 控制翻页请求频率 df pd.DataFrame(all_rows) df.to_excel(judgments.xlsx, indexFalse) print(f完成共保存 {len(all_rows)} 条数据)这里我把翻页上限调成 5 页、每页 10 条完全是出于演示目的。实际项目中数据量、分页大小、休眠时长都要根据你对目标网站反爬强度的判断来调整。跑一次先观察日志看看是否稳定、有没有出现异常返回再决定是否加大规模。4.7 断点续跑与日志输出项目跑了一半断掉是常态。可能是网络波动、IP 被封、Cookie 过期也可能只是电脑休眠了。所以一定要做断点续跑否则前功尽弃。我的方案是把已抓取的数据实时写入 SQLite 数据库而不是最后一次性导出。每抓取一条就INSERT一条如果脚本中断重启后查询数据库里已有的案例 ID跳过已抓取的部分继续跑。别小看这个设计我最后一次大规模采集连续跑了十几个小时中途断了三次全靠 SQLite 里的进度才能无缝续上。import sqlite3 conn sqlite3.connect(judgments.db) conn.execute( CREATE TABLE IF NOT EXISTS judgments ( case_id TEXT PRIMARY KEY, title TEXT, case_number TEXT, court TEXT, judgment_date TEXT, content TEXT ) ) def save_row(row): conn.execute( INSERT OR IGNORE INTO judgments (case_id, title, case_number, court, judgment_date, content) VALUES (?, ?, ?, ?, ?, ?), (row[case_id], row[title], row[case_number], row[court], row[judgment_date], row[content]) ) conn.commit()INSERT OR IGNORE配合主键约束能自动去重这个设计简单又实用。5. 数据清洗与二次加工5.1 清洗脏数据爬下来的数据不能直接拿去用里面脏数据不少。我碰到过的几类典型问题正文里有大量广告页脚、下载提示、页面水印等无关文字案号格式不统一有的带空格、有的是全角字符裁判日期有的精确到日、有的只有年月同一案件存在多份文书如一审、二审、再审裁定需要按案号去重或保留最新。清洗的时候我用 pandas 做字段处理核心操作无非是去空格、统一全半角、正则提取日期、按字符长度过滤混淆项。重点说下日期处理裁判日期我用正则(\d{4})年(\d{1,2})月(\d{1,2})日提取提取不了的就置为空值不强行猜测避免引入错误数据。5.2 结构化分析和统计的价值数据清洗完的下一步就是发挥价值。当时我用这批数据做了两个维度的分析按法院维度统计案由分布按月份维度观察案件数量的时间趋势。这些分析用 pandas 的groupby几分钟就能出结果但如果你没有前面爬虫阶段的积累这些数据根本无从谈起。这一步也验证了整个项目的意义批量采集本身不是目的把公开数据变成可量化的分析结论才是真正有价值的部分。做法律实证研究、做律师的类案检索报告、做法务的风险排查清单最终靠的都是一套能跑通的数据流水线。6. 常见问题与排查技巧实录6.1 Cookie 过期导致请求异常这个坑我在前面提过再补充一个判断技巧。当你请求列表接口时如果返回的 JSON 结构不是预期的数据格式而是带login或verify字样的提示基本可以断定是登录态失效。及时止损很重要不要继续空跑浪费请求额度。我建议每次请求后用一段代码快速校验返回值def check_response_valid(json_data): if data not in json_data or not isinstance(json_data.get(data), dict): raise RuntimeError(登录态可能已失效请更新 Cookie) return True6.2 翻页到后期返回空数据这种情况通常是网站的反爬策略在起作用。它不会直接拒绝你的请求而是返回空列表让脚本“成功”地跑完但什么都拿不到。我的解决办法是连续两页返回空数据就主动停止当前检索任务切换时间范围或增加关键词维度重新发起检索把结果集拆细。6.3 数据乱码和编码问题网页返回的中文偶尔会以 Unicode 转义符的形式存在比如\u6c11\u95f4。requests 拿到 JSON 后用json.loads一般会自动处理但如果你直接对 response.text 做正则提取就可能会遇到转义符残留。处理方式很简单用json.dumps再json.loads走一遍或者直接用json库的解析接口不要用手动字符串截取。6.4 常用排查工具和技巧我处理异常时的三板斧第一把resp.status_code和resp.text[:200]打出来看返回内容的开头第二把异常请求的完整参数、Cookie 前缀、时间戳都记录到日志文件中第三用浏览器的无痕模式手动复现一次操作对照抓包工具看请求差异。差一个Referer、差一个Content-Type都可能是被拒的原因。多对照排查速度能快不少。6.5 项目上线前的小建议最后给几个实用建议。先用小规模试跑。别一上来就全量采集先用 20 条数据跑通全流程确认字段解析正确、数据落盘正常再放大规模。用好日志模块。设计日志时把时间、页码、当前任务名、状态都打进去后期排障全靠它。做好数据备份。爬虫项目中断是家常便饭数据库设计上要能随时重跑重跑不能产生重复数据。预留反爬变化的应对方案。网站更新页面结构是不可避免的代码层尽量把解析逻辑和请求逻辑分离改起来方便。按照这个思路整个项目从立项到稳定产出数据我前后大概花了不到一周时间。如果你只是想快速拿到一批数据做研究不需要搞太重的架构用本文这套方案足够。跑数据这段经历让我最有感触的一点是真正难的不是写代码而是在限制条件下把代码跑到稳定可靠。希望这篇记录能帮你少走几步弯路。
返回列表