十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python3裁判文书网爬虫实战:验证码识别与DocID增量采集

Python3裁判文书网爬虫实战:验证码识别与DocID增量采集 简介本资源是一款面向法学研究者、法律数据分析人员及Python初学者的裁判文书网自动化采集工具解决公开司法文书获取难、人工筛选效率低、验证码阻碍批量下载等核心痛点。系统基于Python3开发集成分类检索、DocID精准查询、时间范围过滤、法院地域切割及图形验证码自动识别功能支持按年份、案由、法院层级与行政区划定向抓取文书显著提升法律实证研究的数据准备效率。压缩包共22个文件含2个核心爬虫脚本.py、2个前端交互逻辑.js、11张验证码训练样本.jpg、2个说明文档.txt/.md及1个示例判决书.doc总大小仅79KB轻量易部署。已有118人学习下载提供开箱即用的完整工具链——包括可直接运行的checkcode.py识别模块、court.py法院地域配置模板、vl5x.js反爬适配脚本及详细README说明兼顾功能完整性与二次开发友好性。1. 项目背景与核心需求拆解做了几年法律数据相关的工作我越来越意识到裁判文书网这类公开数据源的价值。无论是做实证研究的法学院师生还是分析审判趋势的律师团队乃至研究司法透明度的社会学者都绕不开这个数据宝库。但真正上手去采集的时候问题就来了裁判文书网的检索系统交互逻辑复杂翻页、筛选、验证码、详情页跳转一环扣一环手动操作几百次还能忍受几千次上万次就完全不现实。这个项目标题里其实已经透露出核心信息一个基于Python3的裁判文书网爬虫工具支持分类检索、DocID查询、文书下载和验证码识别附带时间条件过滤和法院地域切割功能。说白了就是一套从检索到下载、从清洗到归档的完整流水线。它不是那种能跑就行的玩具脚本而是把法律数据采集过程中常见的痛点都提前考虑进去了。我在设计这个工具时给自己定了几个目标也建议准备做同类项目的朋友先想清楚自己的需求边界检索要灵活不能写死分类检索必须支持按案由、法院层级、文书类型等维度组合筛选。下载要精准通过DocID查询可以跳过检索页的复杂性直接定位到特定文书这在补采和增量更新时非常有用。验证码不能卡脖子识别准确率不追求100%但至少要达到能用、够用的水平配合人工兜底。数据要带元信息时间字段和法院地域必须保留下来后续做统计分析和地域切割才有的放矢。这套工具适合谁用首先是法律实证研究者他们需要批量获取某个时间段、某个地域、某类案由的文书样本其次是司法数据分析团队需要把公开文书作为语料进行文本挖掘还有一部分是法律科技公司的研发人员拿来做原型验证或训练数据准备。如果你只是偶尔查一两篇文书那没必要折腾直接上网站手动检索就行。我在这篇文章里会把项目的整体设计思路、关键技术点的实现方式、踩过的坑以及最终的实操流程完整拆解一遍希望能给正在做或准备做同类数据采集系统的朋友一些参考。2. 系统整体架构与核心设计思路2.1 为什么选Python3作为主力语言这个问题其实没什么悬念。Python3在爬虫领域几乎是事实标准生态太成熟了。requests库处理HTTP请求BeautifulSoup和lxml解析HTMLPillow做图像处理pytesseract接OCR引擎这些库组合起来开发效率极高代码量可以压缩到Java或C版本的几分之一。而且Python3的字符串处理和多线程支持也让数据清洗和并发下载变得相对简单。当然性能上Python3确实不如编译型语言但裁判文书网这种站点本身有反爬限制并发太高反而容易被封IP所以单线程加控制频率的策略完全够用还更安全。2.2 整体模块划分把整个系统按功能拆成六个模块每个模块各司其职模块职责关键技术点检索模块构造检索条件组合筛选维度翻页获取文书列表请求参数构造、分页逻辑DocID管理模块维护DocID索引支持批量导入和快速查询SQLite存储、索引优化验证码识别模块识别登录和检索过程中的验证码图像预处理、OCR引擎下载模块根据DocID或检索结果批量下载文书详情会话保持、超时重试数据清洗模块解析文书正文提取结构化字段XML解析、正则匹配分析模块时间过滤、地域切割、基础统计pandas、matplotlib每个模块之间通过数据接口衔接不搞重耦合。比如检索模块产出的结果统一格式化为包含案号、DocID、法院、日期等字段的JSON行下载模块只需要吃这个JSON就能工作两者互不干扰。这样一个设计的好处是如果哪天裁判文书网改版了检索接口只需要修复检索模块下载和分析模块可以完全不动。2.3 为什么要同时支持分类检索和DocID查询在实际使用中这两种查询方式适用的场景完全不同。分类检索更像是地毯式搜索。比如你想研究近五年某省范围内劳动争议案件的判决结果分布那就要通过组合筛选条件把所有符合条件的文书一条条翻出来。这个场景下检索条件的设计灵活性决定了你采集到的数据质量。DocID查询则是精确打击。裁判文书网每一篇文书都有唯一标识如果你从其他渠道比如新闻报道、学术论文的引用列表拿到了具体文书的DocID直接查询能省掉大量中间检索步骤。还有个典型场景是做增量补采上次采集到某个DocID位置下次从这个位置继续往下拉不需要重新跑全量检索。所以我把两条路径都做进去了用户可以根据实际需求选择也可以混合使用先用分类检索拉一个大范围的数据集再对其中缺失的部分用DocID做定点补采。2.4 存储方案的选型考量采集下来的数据需要有个地方安置。我最终选择了SQLite原因有三点第一简单。整个系统就一个数据库文件备份迁移都方便不需要额外安装数据库服务。第二够用。个人研究或中小团队使用单表几百万条记录SQLite完全扛得住。第三Python3标准库自带sqlite3模块少一个依赖就是少一分麻烦。数据表的设计上core_docs表存核心字段包括DocID、案号、法院、省份、案由、文书类型、发布日期、正文路径等。另外建了一张fetch_log表记录每次采集的批次信息、采集时间、成功失败数量方便回溯问题。3. 核心功能细节与实现要点3.1 分类检索参数构造与翻页逻辑裁判文书网的检索页面本质上是一个前端SPA数据通过后端接口返回。这里有几个关键点需要处理。首先是检索条件的参数化。案由、法院层级、文书类型、审判程序、时间范围这些条件要在构造请求时转换成后端接口能识别的参数格式。我的做法是先通过浏览器的开发者工具抓包把一次完整检索请求的所有参数记录下来然后用Python字典重新组织这些参数把固定的值写死把可变的值留成变量。# 检索参数构造示例 def build_search_params(case_causeNone, court_nameNone, doc_typeNone, start_dateNone, end_dateNone): params { caseType: doc_type or , # 文书类型 caseCause: case_cause or , # 案由 searchType: 1, courtName: court_name or , # 法院名称 startDate: start_date or , # 起始日期 endDate: end_date or , # 截止日期 pageNum: 1, # 页码 pageSize: 10, # 每页数量 } return params其次是翻页逻辑。裁判文书网的检索结果分页通常通过pageNum参数控制每页返回的记录数在系统限制范围内可以调整但调太大容易被反爬机制盯上。我实测比较稳妥的做法是每页10到20条之间宁可请求次数多一些也不要触发风控。翻页过程中还有一个容易忽略的细节页面总数是动态变化的。有时候第一页查询返回100页翻到后面可能只剩80页了这是因为数据源本身在实时更新也可能是因为检索结果集太大系统只保留了前N页。所以代码里要做边界判断循环到当前页大于总页数或返回结果为空时自动终止。3.2 DocID查询机制与增量更新策略DocID查询是本系统的一个亮点功能。裁判文书网的URL结构里其实隐藏了DocID信息掌握了这个规律就可以实现跳过检索、直接访问。实现思路并不复杂把已知的DocID拼接到标准详情页URL模板中然后带上会话请求直接访问详情页解析页面内容即可。这种方式的优点是速度快、目标明确缺点是必须先知道DocID才能查。所以实际使用中通常是把DocID查询和分类检索配合起来形成一个先粗筛、后精补的流程。增量更新策略是这样的每个DocID在数据表里都记录了一个created_at或fetched_at时间戳。下次跑增量更新时只需要查询上次采集时间之后新增的DocID区间逐个请求详情补全数据即可。这样既避免全量重刷浪费时间和流量也保证了数据的新鲜度。# DocID增量更新逻辑 def sync_docs_by_docid(docid_list): for docid in docid_list: detail_url fhttps://wenshu.court.gov.cn/website/wenshu/181107ANFZ0BXSK4/index.html?docId{docid} try: detail_html fetch_with_retry(detail_url) parsed parse_detail_page(detail_html, docid) save_to_database(parsed) except Exception as e: log_error(docid, str(e)) continue这个模块还有一个细节值得说一下请求详情页时必须保持会话状态也就是requests.Session要复用Cookie。因为详情页的数据加载依赖前面检索时建立的会话上下文如果每次请求都新建会话很容易被服务器判定为异常行为。3.3 文书下载会话保持与断点续传文书正文的下载是整个流程的最后一步但恰恰是最容易出问题的环节。裁判文书网的下载接口做了不少限制其中最核心的一点是依赖会话状态和时间戳校验。实际操作中我总结了几条经验第一必须用Session对象发起下载请求而且这个Session最好和检索、查详情用的是同一个保证整个操作链路上Cookie和Token的一致性。第二下载接口有频率限制连续请求几十篇之后会强制要求验证码所以不能闷头猛下要控制并发数。第三下载失败时要重试但重试不能立即进行最好加随机延迟否则重试次数多了反而会被封。断点续传的实现思路是给每篇待下载文书记录一个状态字段pending、downloading、success、failed。程序启动时先查询failed状态的记录重新加入下载队列下载过程中如果异常中断下次启动也可以从上次的断点位置继续。# 下载队列状态管理 def get_download_queue(): 获取需要下载的文书队列未下载的 下载失败的 conn get_db_connection() cursor conn.execute( SELECT docid, doc_url FROM core_docs WHERE download_status IN (pending, failed) ORDER BY created_at LIMIT 100 ) return cursor.fetchall()3.4 验证码识别图像处理与OCR组合方案验证码识别是很多爬虫项目里最劝退的一个环节。我在这个项目里的做法是图像预处理 OCR引擎识别 人工兜底三管齐下。裁判文书网的验证码图像相比其他站点不算太复杂是经典的4位字母数字组合有少量噪点和干扰线。直接用tesseract识别准确率大概在60%左右根本不够用。但经过几轮图像预处理之后准确率能提升到85%以上已经基本满足自动化流程的需求了。预处理的核心步骤包括灰度化把彩色图像转换成灰度图减少颜色通道对识别的干扰。二值化设定阈值把灰度图转换成黑白两色突出文字轮廓。去噪点通过连通域分析把面积小于阈值的小像素块清除掉。分割如果OCR引擎识别效果不好可以尝试先把字符分割开再单独识别。# 验证码图像预处理示例 from PIL import Image, ImageFilter def preprocess_captcha(image_path): img Image.open(image_path) img img.convert(L) # 灰度化 # 二值化处理阈值可根据实际情况调整 threshold 128 img img.point(lambda x: 0 if x threshold else 255, 1) # 去噪 img img.filter(ImageFilter.MedianFilter(size3)) return img整个识别流程封装成一个服务当自动识别置信度低于阈值时把验证码图片保存下来并推送到一个人工处理队列由操作人员人工识别后填入。这种机器为主、人工为辅的模式在实际运行中非常实用既不阻塞主流程又能保证关键时刻不掉链子。3.5 时间条件过滤解析与范围校验时间条件过滤看似简单但实际上牵扯到几个容易踩坑的细节。裁判文书网上的日期字段有两种一种是文书的发布日期另一种是裁判日期。两者通常只差几天但含义不同做数据分析时必须区分清楚。我在系统里保留了两种日期字段检索条件里也能分别指定。时间格式的处理也是个问题。前端传过来的日期是YYYY-MM-DD格式但后端接口可能要求YYYY年MM月DD日的中文格式也可能要求时间戳。我的做法是写一个统一的日期转换函数在构造请求和解析响应时都走这个函数避免格式混乱。另一个需要注意的点是时间范围跨度。一次检索的时间范围不能设得太宽实测下来跨度超过三年的检索请求经常超时。这是因为服务端需要扫描的数据量太大。所以我在前端限制了一次请求最多跨越一年如果用户需要长时间段的数据就循环遍历每一年分别采集最后merge到一起。3.6 法院地域切割基于层级体系的分类逻辑法院地域切割这个功能说白了就是按行政区域和法院层级对采集到的文书进行分类归档。裁判文书网的数据里有法院名称字段但这个字段是文本形式比如北京市第一中级人民法院广东省深圳市南山区人民法院要把它还原成省、市、区三级行政区划需要一套分类逻辑。我在实现过程中维护了一张法院映射表手动整理了几百个常见法院的名称与行政代码对应关系。匹配时先按名称做精确匹配匹配不到的再用关键词推理比如法院名称包含北京就归到北京市包含广东就归到广东省。推理规则的优先级要处理好省和市的包含关系不能匹配错。# 法院地域切割示例 def split_court_by_region(court_name): province None city None district None for p in PROVINCE_LIST: if p in court_name: province p break for c in CITY_LIST.get(province, []): if c in court_name: city c break for d in DISTRICT_LIST.get(city, []): if d in court_name: district d break return {province: province, city: city, district: district}地域切割的价值在于后续的数据分析。有了每篇文书的省市区标签就可以按地区做统计对比比如不同省份劳动争议案件的调解率差异、不同城市知识产权案件的数量分布等。4. 实操过程与核心环节实现4.1 环境准备与依赖安装在开始跑代码之前先把环境整好。我的开发环境是Windows 11 Python 3.10这个组合跑起来没有任何问题。如果你用的是Linux或macOS逻辑也是一样的只是个别系统级依赖的安装命令略有差异。首先创建一个虚拟环境避免和系统Python环境互相污染python -m venv wenshu_env wenshu_env\Scripts\activate # Windows # source wenshu_env/bin/activate # Linux/macOS然后安装依赖包pip install requests beautifulsoup4 lxml pillow pytesseract pandas openpyxl这里说明一下每个库的用途requests做HTTP请求beautifulsoup4配合lxml解析HTMLpillow做验证码图像处理pytesseract是OCR引擎的Python封装pandas和openpyxl用于数据分析和结果导出。pytesseract本身只是封装还需要安装底层的Tesseract OCR引擎。Windows用户需要去GitHub下载安装包安装时注意勾选中文简体语言包否则识别中文文字时会报错。4.2 主流程串联从检索到下载的一体化脚本这里给出一个完整的主流程脚本示例把检索、DocID管理、下载、清洗这几个模块串起来。import json import time import random import logging import requests from bs4 import BeautifulSoup logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) class WenshuCollector: def __init__(self): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://wenshu.court.gov.cn/, Accept-Language: zh-CN,zh;q0.9, }) def search(self, params): 执行一次检索返回列表结果 url https://wenshu.court.gov.cn/website/wenshu/181217BMTKHNT2W0/index.html resp self.session.post(url, dataparams) if resp.status_code 200: try: results resp.json() return results.get(data, {}).get(list, []) except json.JSONDecodeError: logging.warning(响应不是合法JSON可能触发了验证码) return [] return [] def crawl(self, params, pages1): 按检索条件爬取多页数据 all_items [] for page in range(1, pages 1): params[pageNum] str(page) items self.search(params) if not items: break all_items.extend(items) # 控制请求频率避免被风控 time.sleep(random.uniform(2, 5)) return all_items这里有几个细节需要解释。首先是请求头的User-Agent一定要设置为真实的浏览器标识不要用Python默认的User-Agent否则一眼被识别。然后是请求频率控制我在每翻一页之后sleep了2到5秒随机延迟这个时间间隔是我多次试错后的经验值太短容易被封太长浪费时间。另外一个经验如果单次检索的页数非常多几十页以上建议中途定时更换一个有效的Cookie。因为旧Cookie在高频请求下寿命会缩短提前做好Cookie池管理可以显著降低验证码出现频率。4.3 验证码识别模块的完整封装验证码识别这一块我把图像预处理和OCR整个流程封装成一个类方便其他模块调用。import pytesseract from PIL import Image, ImageFilter, ImageOps class CaptchaRecognizer: def __init__(self): # 如果pytesseract找不到tesseract路径需要手动指定 pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe def preprocess(self, img): 图像预处理流水线 img img.convert(L) # 自适应二值化 img ImageOps.autocontrast(img) img img.point(lambda x: 0 if x 140 else 255, 1) img img.filter(ImageFilter.MedianFilter(size3)) return img def recognize(self, image_bytes): 识别验证码返回文本及置信度 img Image.open(image_bytes) img self.preprocess(img) # 配置tesseract参数只识别字母数字PBM模式 text pytesseract.image_to_string( img, config--psm 7 -c tessedit_char_whitelist0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ) return text.strip()验证码识别准确率实测在85%左右。如果你的场景要求更高有几个优化方向一是收集样本数据微调tesseract的字符训练集二是换用深度学习的OCR方案比如PaddleOCR或者基于CNN的专用验证码识别模型。但考虑到大多数情况一个人的使用量不会太大85%的准确率配合人工兜底已经足够了。4.4 数据清洗与结构化落地采集到的原始HTML里文书正文混在大量的标签和脚本代码中不能直接拿来用。数据清洗模块负责把正文内容提取出来并做基础的结构化处理。def parse_detail_page(html, docid): 解析文书详情页提取结构化字段 soup BeautifulSoup(html, lxml) result {docid: docid} # 定位正文容器裁判文书网详情页的正文通常在一个特定div里 content_div soup.find(div, class_content) if content_div: result[content] content_div.get_text(\n, stripTrue) # 提取标题 title soup.find(title) if title: result[title] title.get_text(stripTrue) # 从正文中正则提取案号 import re case_match re.search(r(\d{4})[()]?\w*?[号第]\d号, result.get(content, )) if case_match: result[case_number] case_match.group(0) return result这个模块我建议多花心思调优因为后续所有数据分析都建立在清洗后的数据质量之上。如果正文里混入了页面脚注、上一篇下一篇链接等噪音分析结果就会受影响。清洗完成后的数据统一写入SQLite正文部分我选择把全文存成单个文本文件数据库里只保留文件路径。这样做的好处是数据库体积不会随着文书量增长而急剧膨胀备份和迁移都更轻量。5. 常见问题与排查技巧实录5.1 请求被重定向到验证码页面这是爬取裁判文书网时最常遇到的现象。症状很典型明明之前还能正常返回数据某个时间点之后响应内容变成了验证码页面。排查思路第一步看请求头是否完整有时候缺少Referer字段就会被拦第二步看Cookie是否过期长时间运行的Session要定期刷新第三步看请求频率高频连续请求一定会触发验证码。我的解决方案是做一个三层防护正常请求时每两次之间间隔2到5秒每翻10页检查一次是否出现验证码页面如果出现了就调用验证码识别模块自动识别并继续。如果连续三次识别失败就停下来人工介入。5.2 页面结构变更导致解析失败网站前端改版是爬虫开发者最头疼的问题之一。裁判文书网在观察期内有过几次结构微调最典型的是字段名的class属性变化。应对这个问题我在解析模块里做了一个降级策略优先使用最新的选择器规则如果匹配不到内容就回退到旧版本的选择器如果还是匹配不到就把原始HTML存储下来方便人工排查。PARSING_RULES [ {title: h1.title, content: div.content}, {title: .doc-title, content: #mainContent}, {title: div.rich_media_title, content: div.rich_media_content}, ]5.3 验证码识别失败率突然飙升有时候验证码识别准确率会突然从85%掉到50%这种情况通常是目标站点换了验证码样式。比如从纯数字变成了数字字母混合或新增了背景干扰。遇到这种情况不要急着改代码先把最近的验证码样本收集一批肉眼观察变化然后针对性调整预处理参数。比如干扰线多了就加大中值滤波的窗口大小背景噪点多了就调高二值化阈值。5.4 常见问题速查表问题现象可能原因解决方案请求返回403User-Agent被识别更换浏览器UA补全请求头检索返回结果为空参数格式有问题用抓包工具比对真实请求参数详情页解析不出正文页面结构变更更新选择器规则保存原始HTML排查下载文件总是不完整网络超时或频率限制加超时重试降低请求速度程序跑一会儿就卡住可能触发了IP限流暂停几分钟或更换代理IP5.5 批量采集过程中的实战经验批量采集是一个长期作战的过程我总结了几条实用经验分享给有类似需求的朋友。第一日志比代码更重要。上线初期花半小时把日志系统配好记录每次请求的URL、状态码、耗时、失败原因之后排查问题能省十倍时间。我用的Python标准库logging模块输出到文件并按天滚动方便按日期回溯。第二数据要经常备份。SQLite文件虽然小但积累到几百万条记录之后文件损坏的风险也随之增加。我每天跑完采集之后会执行一次数据库完整性检查然后把文件复制到远程存储。第三采集任务的管理建议用任务表。不要把所有逻辑都堆在一个长循环里而是把每个批次的任务检索条件、时间范围、目标DocID区间写入任务表由调度器逐个执行并更新状态。这样即使程序中途崩溃重启后也能从断点继续。6. 项目扩展方向与合规边界6.1 数据分析层面的扩展采集只是第一步数据拿到手里之后能做的分析方向非常多。按案由维度的统计分析比如把近五年的劳动争议案件按年份、地区拉一个趋势图能直观看出劳动纠纷的变化趋势。按法院层级的审判周期分析从立案日期到结案日期的平均时长不同层级的法院是否有显著差异。甚至可以用简单的关键词词频统计分析判决书里出现频率最高的法律条文引用。文本挖掘方面可以用jieba分词工具把裁判文书分成词向量再配合TF-IDF做案由的自动分类。这个方向上采集工具采集到的干净结构化数据就是最大的价值基础。6.2 合规使用边界在这里必须以一个有经验的从业者身份明确提示裁判文书网的数据爬取必须遵守相关法律法规和网站的使用协议。我设计这套系统时用途定位是法律研究和数据分析所有功能都应服务于这个合法目的。在实际使用中有几个边界一定要守住控制请求频率不给目标站点服务器造成压力采集的数据仅限公开信息不做二次转售如果目标站点明确禁止了批量采集行为应当立即停止并尊重其规则。另外从道义和利益上说采集工具做得再好最终的价值还是要靠分析结果来体现。把爬虫当成一把钥匙打开数据宝库之后的挖掘工作才是真正有长期价值的部分。6.3 后续迭代方向如果这个项目要继续做下去我个人觉得有几个方向值得投入。一是把验证码识别模块升级成机器学习方案。通过积累样本数据训练一个轻量的CNN模型识别准确率可以从85%提升到98%以上人工介入的次数会大幅减少。二是增加一个定时调度功能比如每天凌晨自动采集当天新增的特定类型文书形成持续更新的数据库。三是做一个简单的前端可视化界面把存储、查询、统计功能都集成进去团队里不懂技术的成员也能方便使用。不过也要提醒一句任何迭代都要先把基础的数据采集链路做稳做扎实别急着上花哨的功能。数据链路稳定、数据质量可靠所有上层应用才能立得住。本文还有配套的精品资源点击获取
返回列表