Python爬虫实战:裁判文书网2021年数据抓取全流程解析
1. 项目概述:为什么需要关注裁判文书数据
裁判文书网,作为司法公开的重要窗口,沉淀了海量的判决、裁定等法律文书。对于法律从业者、学术研究者、金融风控分析师乃至关注社会动态的普通人而言,这些文书都是极具价值的原始数据矿藏。2021年的数据,既具有时效性上的“新鲜度”,又因为司法实践的相对稳定而具备典型的研究价值。手动一篇篇下载显然不现实,这就引出了我们今天要深入探讨的核心:如何系统性地、自动化地抓取这些公开数据。
这不仅仅是一个简单的“爬虫”任务。它涉及到对目标网站反爬策略的理解、对海量数据结构的解析、对抓取伦理与法律边界的把握,以及最终将非结构化的网页文本转化为结构化、可分析的数据集。整个过程,更像是一次小型的“数据工程”实践。我把自己在类似项目中的经验教训梳理出来,希望能帮你绕过我踩过的那些坑,高效、稳定地拿到你想要的数据。
2. 核心思路与架构设计:不只是写爬虫
在动手写代码之前,清晰的顶层设计能避免后期大量的返工。针对裁判文书网这类具有一定反爬机制的政府类网站,我们的设计需要兼顾效率、稳定性和合规性。
2.1 目标分析与难点预判
首先,我们需要明确抓取的目标边界。裁判文书网通常提供高级检索功能,我们可以通过案由、法院、裁判年份(如2021年)、当事人等条件组合筛选。我们的爬虫核心任务就是模拟这些检索条件,遍历所有结果页,并解析每一份文书的详情页。
主要难点集中在以下几点:
- 反爬机制:网站可能采用动态加载(Ajax)、请求头校验、IP访问频率限制、验证码(特别是在频繁访问后)等手段。
- 数据量大与结构复杂:2021年全年文书数量可能达到千万级,需要设计可靠的分片、去重和断点续爬策略。文书详情页的HTML结构可能嵌套较深,字段提取需要精细处理。
- 法律与伦理风险:必须严格遵守网站的
robots.txt协议,控制请求频率,避免对目标网站服务器造成压力。抓取的数据应仅用于个人学习或科学研究,严禁用于商业牟利或侵犯个人隐私。
2.2 技术选型与工具链
基于以上分析,一个稳健的技术栈组合如下:
- 编程语言:Python。其丰富的生态库(如Requests, Scrapy, Selenium)是网络爬虫的首选。
- 爬虫框架:对于大规模、复杂的抓取任务,推荐使用Scrapy。它提供了完整的爬虫生命周期管理、异步处理、中间件扩展等机制,能极大地提升开发效率和程序健壮性。对于初学者或小规模抓取,
Requests+BeautifulSoup组合更轻量灵活。 - 解析工具:BeautifulSoup4或lxml。用于解析HTML,提取文书标题、案号、法院、裁判日期、当事人信息、判决正文等字段。
- 动态页面处理:如果发现数据是通过JavaScript动态渲染的,简单的HTML解析无法获取,则需要引入Selenium或Playwright来模拟浏览器行为。但这类工具资源消耗大、速度慢,应作为备选方案。
- 请求会话与代理:使用
requests.Session()维持会话,处理Cookie。为应对IP封锁,需要准备IP代理池。可以考虑付费的代理服务,或者使用一些云服务提供的弹性IP。 - 数据存储:根据后续分析需求,可选择MySQL/PostgreSQL(关系型,便于关联查询)、MongoDB(文档型,适应半结构化数据)或直接存储为JSON Lines或CSV文件。
- 任务调度与监控:使用Scrapy内置的扩展,或结合Celery进行分布式任务调度。记录详细的日志,便于监控抓取状态和排查错误。
注意:在正式开始前,务必仔细阅读目标网站的
robots.txt文件(通常在网站根目录,如https://wenshu.court.gov.cn/robots.txt),尊重其设定的抓取规则。过高的请求频率不仅不道德,还可能引发法律风险。
3. 关键环节实现与核心代码解析
接下来,我们以 Scrapy 框架为例,拆解几个最关键的实现环节。假设我们的项目名为wenshu_2021。
3.1 环境搭建与项目初始化
首先,创建并进入一个干净的虚拟环境,然后安装必要的库并创建Scrapy项目。
# 创建虚拟环境(可选,但强烈推荐) python -m venv venv_wenshu source venv_wenshu/bin/activate # Linux/Mac # venv_wenshu\Scripts\activate # Windows # 安装核心库 pip install scrapy beautifulsoup4 pymysql # 根据存储选择安装 pymongo 等 # 创建Scrapy项目 scrapy startproject wenshu_2021 cd wenshu_20213.2 定义数据模型(Items)
在items.py中,我们定义要抓取的数据结构。这就像为我们的数据设计一张表格。
import scrapy class Wenshu2021Item(scrapy.Item): # 定义文书的数据字段 doc_id = scrapy.Field() # 文书唯一ID(通常从URL或页面中提取) title = scrapy.Field() # 文书标题 case_number = scrapy.Field() # 案号 court = scrapy.Field() # 法院 date = scrapy.Field() # 裁判日期 case_type = scrapy.Field() # 案件类型 parties = scrapy.Field() # 当事人(原告、被告等) content = scrapy.Field() # 文书正文内容 url = scrapy.Field() # 原文书详情页URL crawl_time = scrapy.Field() # 抓取时间戳3.3 构建爬虫核心(Spider)
这是最核心的部分。我们需要在spiders/目录下创建一个爬虫文件,例如wenshu_spider.py。
核心思路:
- 起始请求:构造一个符合2021年文书高级检索条件的初始请求URL。这通常需要分析网站搜索接口的规律。
- 列表页解析:解析搜索结果列表页,提取出当前页所有文书的详情页链接,并发起抓取请求(
yield scrapy.Request)。同时,需要解析出“下一页”的链接,实现翻页。 - 详情页解析:在详情页的回调函数中,使用
BeautifulSoup或Scrapy Selector解析出我们在Item中定义的各个字段,并yield一个填充好的Item对象。
import scrapy from wenshu_2021.items import Wenshu2021Item from bs4 import BeautifulSoup import urllib.parse import time class WenshuSpider(scrapy.Spider): name = 'wenshu_2021' allowed_domains = ['wenshu.court.gov.cn'] # 起始URL:这里需要根据网站实际的搜索接口构造,以下为示例格式 def start_requests(self): base_url = "https://wenshu.court.gov.cn/website/wenshu/181217BMTKHNT2W0/index.html?" # 构造查询参数:例如,查询2021年1月1日至2021年12月31日的民事一审判决书 params = { 'page': '1', # 起始页码 'sortType': '1', 'conditions': 'searchWord 1 2021-01-01 TO 2021-12-31 裁判日期:2021-01-01 TO 2021-12-31', # conditions 参数非常关键,需要仔细研究网站前端如何生成这个字符串 } start_url = base_url + urllib.parse.urlencode(params) # 添加必要的请求头,模拟浏览器 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://wenshu.court.gov.cn/' } yield scrapy.Request(url=start_url, headers=headers, callback=self.parse_list) def parse_list(self, response): """ 解析列表页,提取详情页链接和翻页链接。 """ soup = BeautifulSoup(response.text, 'html.parser') # 1. 提取当前页所有文书链接 (示例,需根据实际HTML结构调整CSS选择器) doc_links = soup.select('.doc-link a') for link in doc_links: doc_url = response.urljoin(link.get('href')) # 对每个详情页发起请求,指定回调函数为 parse_detail yield scrapy.Request(url=doc_url, callback=self.parse_detail, meta={'original_url': doc_url}) # 2. 处理翻页 (示例) next_page = soup.select_one('.next-page a') if next_page and next_page.get('href'): next_page_url = response.urljoin(next_page.get('href')) # 重要:添加延迟,避免请求过快 time.sleep(1) yield scrapy.Request(url=next_page_url, callback=self.parse_list) def parse_detail(self, response): """ 解析文书详情页,提取具体字段。 """ item = Wenshu2021Item() soup = BeautifulSoup(response.text, 'html.parser') # 提取各个字段,这里的选择器路径需要你通过浏览器开发者工具仔细分析 try: item['title'] = soup.select_one('.doc-title').get_text(strip=True) item['case_number'] = soup.select_one('.case-number').get_text(strip=True) item['court'] = soup.select_one('.court-name').get_text(strip=True) # ... 提取其他字段 item['content'] = soup.select_one('#contentDiv').get_text(strip=True) # 正文可能在一个id为contentDiv的容器内 item['url'] = response.meta['original_url'] item['crawl_time'] = time.strftime('%Y-%m-%d %H:%M:%S') except AttributeError as e: self.logger.warning(f"字段提取失败于URL: {response.url}, 错误: {e}") # 可以选择跳过此项或存储部分数据 return yield item3.4 应对反爬策略:中间件(Middleware)配置
Scrapy的中间件是我们增强爬虫能力、应对反爬的利器。需要在settings.py中启用并配置。
- User-Agent轮换:在
middlewares.py中创建一个类,为每个请求随机分配一个合理的User-Agent。 - IP代理池:同样在中间件中集成,从可靠的代理服务商获取IP,并在请求失败时自动更换。
- 请求延迟与并发控制:在
settings.py中设置。 - 处理Cookies和Session:确保爬虫能维持一个有效的会话状态。
settings.py关键配置示例:
# 启用自定义的下载器中间件 DOWNLOADER_MIDDLEWARES = { 'wenshu_2021.middlewares.RandomUserAgentMiddleware': 543, 'wenshu_2021.middlewares.ProxyMiddleware': 544, 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, } # 配置自动限速扩展,非常有用 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 3.0 # 初始下载延迟(秒) AUTOTHROTTLE_MAX_DELAY = 60.0 # 在高延迟情况下设置的最大下载延迟 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # 平均每个远程网站应同时收到的请求数,调低更友好 # 并发请求数设置,初期建议调低 CONCURRENT_REQUESTS = 2 DOWNLOAD_DELAY = 2 # 每个请求之间的基本延迟(秒) # 重试设置 RETRY_ENABLED = True RETRY_TIMES = 3 # 重试次数 RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429] # 遇到这些状态码重试 # 遵守robots.txt ROBOTSTXT_OBEY = True # 务必设置为True3.5 数据存储(Pipeline)
抓取到的Item会经过Pipeline进行处理,这里是进行数据清洗和持久化的地方。在pipelines.py中定义。
import pymysql import json from itemadapter import ItemAdapter class JsonWriterPipeline: """ 将数据写入JSON Lines文件,简单可靠 """ def open_spider(self, spider): self.file = open('wenshu_2021.jl', 'a', encoding='utf-8') def close_spider(self, spider): self.file.close() def process_item(self, item, spider): line = json.dumps(ItemAdapter(item).asdict(), ensure_ascii=False) + "\n" self.file.write(line) return item class MySQLPipeline: """ 将数据存入MySQL数据库 """ def __init__(self, mysql_host, mysql_db, mysql_user, mysql_pwd): self.host = mysql_host self.db = mysql_db self.user = mysql_user self.pwd = mysql_pwd @classmethod def from_crawler(cls, crawler): return cls( mysql_host=crawler.settings.get('MYSQL_HOST'), mysql_db=crawler.settings.get('MYSQL_DATABASE'), mysql_user=crawler.settings.get('MYSQL_USER'), mysql_pwd=crawler.settings.get('MYSQL_PASSWORD') ) def open_spider(self, spider): self.connection = pymysql.connect( host=self.host, user=self.user, password=self.pwd, database=self.db, charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) self.cursor = self.connection.cursor() # 创建表(如果不存在) create_table_sql = """ CREATE TABLE IF NOT EXISTS `wenshu_2021` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `doc_id` VARCHAR(255), `title` TEXT, `case_number` VARCHAR(255), `court` VARCHAR(255), `date` DATE, `case_type` VARCHAR(100), `parties` TEXT, `content` LONGTEXT, `url` VARCHAR(500), `crawl_time` DATETIME, UNIQUE KEY `uniq_doc` (`doc_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ self.cursor.execute(create_table_sql) self.connection.commit() def close_spider(self, spider): self.connection.close() def process_item(self, item, spider): # 构建插入或更新语句 sql = """ INSERT INTO `wenshu_2021` (`doc_id`, `title`, `case_number`, `court`, `date`, `case_type`, `parties`, `content`, `url`, `crawl_time`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE title=VALUES(title), content=VALUES(content), crawl_time=VALUES(crawl_time) """ self.cursor.execute(sql, ( item.get('doc_id'), item.get('title'), item.get('case_number'), item.get('court'), item.get('date'), item.get('case_type'), item.get('parties'), item.get('content'), item.get('url'), item.get('crawl_time') )) self.connection.commit() return item然后在settings.py中启用并配置Pipeline:
ITEM_PIPELINES = { 'wenshu_2021.pipelines.JsonWriterPipeline': 300, # 优先级数字越小越先执行 'wenshu_2021.pipelines.MySQLPipeline': 800, } MYSQL_HOST = 'localhost' MYSQL_DATABASE = 'wenshu_data' MYSQL_USER = 'your_username' MYSQL_PASSWORD = 'your_password'4. 实战部署、监控与问题排查
4.1 运行与部署
在本地测试无误后,可以部署到更稳定的服务器上长期运行。
# 本地测试运行,并将日志输出到文件 scrapy crawl wenshu_2021 -s LOG_FILE=wenshu_crawl.log # 使用Scrapy的JOBDIR功能支持断点续爬(非常重要!) scrapy crawl wenshu_2021 -s JOBDIR=crawls/wenshu-2021-resume对于大规模抓取,建议使用Scrapyd(Scrapy官方的部署工具)或Scrapy Cluster等分布式方案,将爬虫任务分发到多台机器上执行。
4.2 常见问题与排查技巧实录
在长达数周甚至数月的抓取过程中,你一定会遇到各种问题。以下是我总结的“避坑指南”:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 返回空白页或状态码403 | IP被封锁、请求头不完整、Cookie失效、触发了风控。 | 1.检查当前IP:访问https://httpbin.org/ip看是否与代理IP一致。2.检查请求头:特别是 User-Agent,Referer,Accept-Language,用浏览器开发者工具对比。3.引入验证码识别:如果出现验证码,需要接入打码平台(如超级鹰、图鉴)或尝试机器学习方案,但成本会剧增。 4.大幅降低请求频率:增加 DOWNLOAD_DELAY,减少CONCURRENT_REQUESTS。 |
| 解析不到数据,选择器返回空 | 网页结构发生变化、数据是JavaScript动态加载。 | 1.手动检查:用浏览器打开目标URL,查看元素是否还在原位置。 2.查看响应体:在爬虫中打印 response.text[:2000],看是否包含预期数据。如果不包含,说明数据可能是JS加载。3.启用Selenium/Playwright:对于动态页面,这是最终解决方案。但需注意,它们比纯HTTP请求慢几个数量级。 |
| 数据库重复数据或插入错误 | 未正确处理唯一键、数据编码问题。 | 1.设计唯一标识:使用doc_id或url的MD5值作为唯一键。2.使用 ON DUPLICATE KEY UPDATE:如上面MySQL示例,避免重复插入。3.确保数据库字符集:使用 utf8mb4以支持所有Emoji和生僻字。 |
| 爬虫运行一段时间后卡住或崩溃 | 内存泄漏、网络异常未处理、遇到未预料的页面结构。 | 1.启用详细日志:在settings.py中设置LOG_LEVEL='DEBUG'。2.使用 try...except:在解析函数中包裹关键代码,记录错误URL并跳过,避免整个爬虫停止。3.监控资源:定期检查服务器内存和CPU使用情况。 4.利用Scrapy的扩展:如 LogStats扩展监控抓取速度。 |
| 抓取速度极其缓慢 | 请求延迟设置过高、代理IP速度慢、目标网站响应慢。 | 1.平衡速度与稳定性:在不被封的前提下,逐步调整AUTOTHROTTLE_TARGET_CONCURRENCY和CONCURRENT_REQUESTS。2.测试代理IP质量:单独测试代理IP的延迟和可用率,建立高质量代理池。 3.分片抓取:不要用一个爬虫抓全部。可以按日期范围、法院地域等维度,启动多个爬虫实例同时抓取不同区间。 |
4.3 数据清洗与后续处理建议
抓取下来的原始数据通常很“脏”,需要清洗:
- 去重:基于唯一标识符(如案号)去除完全重复的记录。
- 字段标准化:例如,将“裁判日期”从各种字符串格式(“2021年3月15日”、“2021-03-15”)统一转换为标准的
YYYY-MM-DD格式。 - 正文清洗:去除文书正文中的无关字符(如多余空格、换行符、页眉页脚信息)。
- 关键信息提取:可以使用正则表达式或NLP工具从正文中进一步提取更结构化的信息,如“诉讼金额”、“判决结果”(支持/驳回)、“审理程序”等。这是一个更高级的课题,可以结合
jieba(分词)、pyltp或HanLP等工具进行。
5. 法律合规与伦理思考
这是所有技术操作的前提,必须单独强调。
- 尊重
robots.txt:这是与网站管理员的契约。如果明确禁止抓取某些目录,请遵守。 - 限制访问频率:你的爬虫不应该影响网站的正常服务。将你的请求模拟成一个非常有耐心的、间隔时间很长的正常用户。
- 明确数据用途:抓取的数据应限于个人学习、学术研究或公益用途。严禁用于商业售卖、骚扰当事人、或任何非法活动。
- 处理个人信息:裁判文书中包含大量自然人姓名、身份证号(部分脱敏)、住址等个人信息。即使数据公开,在后续的分析、展示、分享中,也应考虑进行适当的脱敏处理,避免造成二次伤害。
- 关注服务条款:仔细阅读网站的用户协议或服务条款,了解其对数据使用的具体规定。
我个人在实际操作中的体会是,裁判文书网的抓取是一场“持久战”和“精细活”。最大的挑战往往不是技术本身,而是在对抗反爬和维护爬虫长期稳定运行之间找到那个微妙的平衡点。我的策略通常是“慢就是快”——一开始就采用最保守的请求策略(高延迟、低并发),然后根据日志和响应情况极其缓慢地向上调整,直到触及那个看不见的“阈值”边缘。这样虽然初始速度慢,但能保证爬虫可以安静地运行数周而不被中断,总体效率反而更高。另外,建立一个详尽的错误日志和重试机制至关重要,它能帮你自动绕过那些临时性的问题页面,确保数据集的完整性。最后,请永远对数据抱有敬畏之心,技术是工具,如何使用它取决于你的初衷。