☰
基于Selenium的裁判文书网爬虫:登录态、反爬破解与数据入库实践
2026/11/3 23:53:16 网站建设 项目流程

简介:基于Selenium的裁判文书网爬虫资源,整合了登录模拟与文书采集完整流程,面向Python爬虫学习者以及计算机、人工智能、物联网等专业的在校学生和教师,可广泛应用于课程设计、毕业设计、项目答辩或日常练手。资源共19个文件,压缩包仅233KB,其中9个Python脚本构成核心,覆盖登录、请求头构造、参数配置、docid获取及文书解析等关键环节;7个txt文件保存地域、页码、状态等可调参数,方便直接修改使用;另附JavaScript辅助脚本、Markdown详细说明和Git忽略规则,整体目录结构清晰,便于按模块学习与二次开发。登录资料齐全,代码经过测试运行成功,可直接运行,也可在此框架上扩展其他爬虫功能。已有181人学习下载,对从入门到进阶的爬虫实践和答辩展示均有实际帮助,整体体量轻巧,适合快速部署与动手实践。

1. 基于 selenium 裁判文书网爬虫:为什么老方案仍是登录采集最稳的起点

拿到一份“基于 selenium 裁判文书网爬虫”的源码包,先别嫌它不如 requests 快。裁判文书网这种登录态、验证码、JS 渲染三样全占的站点,Selenium 把浏览器当真实用户操作,绕开了接口签名逆向这个黑匣子,反而是从零到一跑通全流程成本最低的方案。

“文书网登录资料齐全”这句话,意味着项目里通常带了登录后的 cookie 或手动登录说明,你要做的是把“登录态恢复 → 检索翻页 → 详情解析 → 入库”这条链路接通。源码.zip 则是交付方式,重点在里头的文档怎么带,配置怎么改。

这套方案适合一个人或小团队做中小规模采集,十万级文书以内、每天跑批一两次的稳定场景。想明白了再动手,比先跑代码重要得多。

2. 先选型再动手:Selenium 方案的边界、登录态生命周期与交付结构

2.1 为什么是 Selenium 而不是 requests:三套方案的取舍

裁判文书网目前需要登录后才能正常检索,页面主体由 JS 渲染,列表和详情都是动态加载。用 requests 直接拉接口,并不是拉不到,而是要先把接口签名、请求头校验、加密参数这些环节逆干净。问题在于这类参数一旦调整,之前逆向的代码就可能整体失效,维护成本远高于采集本身。

Selenium 的思路是打开一个真实浏览器,让页面自己的 JS 去完成渲染和请求,爬虫只负责“像人一样操作”。登录、点击、翻页、等待加载这些动作都由浏览器驱动完成,那些加密参数对你来说就是一个黑匣子,不需要打开它。优点是稳定、可读、好排错,代价是单页耗时高、资源占用大,不适合千万级全量抓取。

我一般用下面这个对比表来决定到底选哪套方案:

方案登录成本反爬对抗单页成本最适合场景
requests + 接口逆向高,要逆向签名和参数高低有抓包逆向经验、大规模采集
Selenium低,手动登录一次复用 cookie中,需处理 webdriver 特征中中小规模、稳定优先
Playwright低,同样复用 cookie中,CDP 支持更好中需要多开并发、任务较复杂

从标题选型来看,作者把“能跑通、能看懂、能改”放在“跑得快”前面,这是对的做法。新手入门和中小规模数据采集,Selenium 的调试体验明显优于接口逆向,只在遇到大规模量产瓶颈时才需要考虑换方案。

2.2 登录态生命周期:人工过验证码,自动化复用

裁判文书网的登录验证码以滑块为主,纯自动化识别不是不能做,但成功率很难稳定到 99% 以上。常见的做法是反着来:人只登录一次,把登录后的 cookie 保存成文件,后面每次启动,程序把 cookie 塞回浏览器即可。这一步做对了,后面所有采集任务都不需要再碰登录。

登录态生命周期大致是五步:手动登录并完成验证码 → 导出 cookie → 启动时加载 cookie → 访问页面校验登录态 → 失效时暂停并人工介入。导出 cookie 的代码非常简单:

import json from selenium import webdriver driver = webdriver.Chrome() driver.get("https://wenshu.court.gov.cn") # 此处手动完成登录与滑块验证 cookies = driver.get_cookies() with open("cookies.json", "w", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False, indent=2) print(f"已保存 {len(cookies)} 条 cookie")

这段代码的逻辑是先打开官网建立会话上下文,再让操作者手动过掉验证码,最后把浏览器当前持有的 cookie 落盘。get_cookies() 返回的是一个列表,每项包含 name、value、domain、path、expiry 等字段,json 格式方便后续按需筛选。

参数说明:保存文件必须指定 encoding="utf-8",否则含中文的 cookie 值在 Windows 下重载会乱码;indent=2 只是让文件可读,不影响加载。另外一点经验:cookie 里的 domain 字段决定了哪个域名能用,重放时浏览器只接受与当前访问域名匹配的 cookie,跨域写入会直接报 InvalidCookieDomainException。

2.3 拿到 zip 别急着跑:源码目录与运行前的核对清单

“源码.zip”这类交付包,我拿到手的第一件事不是直接 python main.py,而是先把目录结构读一遍。常见做法是入口脚本、登录态文件、配置、数据层、解析层分开放,结构大致像这样:

wenshu_crawler/ ├── main.py # 采集入口,调度检索与翻页 ├── config.py # 浏览器路径、超时、日志级别 ├── cookies.json # 手动登录后生成的登录态文件 ├── db.py # SQLAlchemy 模型与入库封装 ├── parser.py # 详情页字段提取与标准化 ├── logs/ # 运行日志 └── README.md # 环境要求与运行步骤

这个结构的好处是各层只做一件事:main 管流程,parser 管解析,db 管存储。改字段不用动调度逻辑,换存储方式也不用动解析代码。

运行前还要逐项核对环境:Python 版本是否与项目要求一致,Chrome 与 chromedriver 版本是否匹配,selenium 是 3.x 还是 4.x。这三个版本只要有一个对不上,启动时就会在创建 driver 那一步报错,而且报错信息常常看不出真实原因。先花十分钟核对环境,比跑起来再排错省时间得多。

3. 把登录和采集跑通:从 cookie 恢复到详情入库的核心代码

3.1 加载 cookie 并校验登录态的核心代码

有了 cookies.json 之后,启动脚本要做三件事:创建浏览器实例、把 cookie 写回去、确认登录态有效。下面是我惯用的起手式:

import json, time from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def make_driver(): options = Options() options.add_argument("--start-maximized") # 关闭自动化控制提示条,避免页面感知异常 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) return webdriver.Chrome(options=options) def inject_cookies(driver, path="cookies.json"): with open(path, "r", encoding="utf-8") as f: cookies = json.load(f) driver.get("https://wenshu.court.gov.cn") time.sleep(2) for c in cookies: if "wenshu.court.gov.cn" in c.get("domain", ""): driver.add_cookie(c) driver.refresh()

这段代码的逻辑是先打开一次主页,让浏览器建立当前域名的上下文,之后再逐个写入 cookie。直接 new 一个页面就 add_cookie 会失败,因为浏览器需要先“站”在对应域名上。过滤 domain 这一步是为了避免把第三方统计域的 cookie 也写进去,那些值对登录态没有帮助,还可能触发参数校验异常。

参数说明:excludeSwitches 里关掉的 enable-automation,是浏览器地址栏下方的“正在受自动化软件控制”提示条,关掉后页面环境更接近普通浏览器。如果你想让 UA 也更真实,可以在 options 里手动加一个与当前 Chrome 版本一致的 user-agent,不要盲目用一个网上抄来的旧 UA。

cookie 写入后需要确认是否真的登录成功,不能只看页面能打开。我一般用在页面上找登录入口的方式判断:

def check_login(driver): driver.get("https://wenshu.court.gov.cn") text = driver.page_source if "登录" in text and "用户名" in text: return False return True

逻辑是通过页面源码是否包含登录特征词来判断登录态。更稳妥的做法是访问个人中心或发起一次检索,看是否弹出登录框,但那样成本更高。文本匹配作为日常检查已经够用,发现失效就停止采集并告警。

3.2 检索与翻页:显式等待和空结果判定的正确写法

登录态没问题后,进入检索采集主流程。这里最容易翻车的是等待策略。固定 sleep 看似简单,但页面在负载高的时候会加载变慢,固定 3 秒可能不够;负载低的时候又是在干等。正确的做法是显式等待关键元素出现,再开始操作。

def collect(driver, keyword, max_pages=20): wait = WebDriverWait(driver, 15) kw_input = wait.until( EC.presence_of_element_located((By.ID, "searchBox")) ) kw_input.clear() kw_input.send_keys(keyword) driver.find_element(By.ID, "searchBtn").click() time.sleep(3) items = [] for page in range(max_pages): wait.until(EC.presence_of_element_located( (By.CLASS_NAME, "doc-list") )) cards = driver.find_elements( By.CSS_SELECTOR, ".doc-list .doc-item" ) if not cards: break for card in cards: title_el = card.find_element(By.CSS_SELECTOR, "h4 a") items.append({ "title": title_el.text.strip(), "url": title_el.get_attribute("href") }) next_btn = driver.find_elements( By.CSS_SELECTOR, ".pagination a.next" ) if not next_btn or "disabled" in next_btn[0].get_attribute("class"): break next_btn[0].click() time.sleep(2) return items

这段代码的核心是“等到了再操作”。presence_of_element_located 会在元素进入 DOM 后立即返回,不需要等页面完全加载。翻页按钮的判断用了“取不到或带 disabled 类”两个条件,避免最后一页点击后页面打转。

参数说明:max_pages 是每轮采集的页数上限,建议设 10 到 20,跑完入库后再继续,而不是一个进程无限翻页,这样任务可以被拆成小批次,失败重跑的粒度更小。time.sleep(2) 是翻页后的必要缓冲,让 JS 完成旧 DOM 替换,太短会造成下一轮查找时元素所在节点已被移除。这里的两个等待各有分工,不能互相替代。

3.3 详情页字段解析:标题、案号、法院与正文内容

列表拿到链接后,要进详情页提取完整字段。裁判文书详情页的字段集中在标题、案号、法院、审判程序、裁判日期和正文几个区域,用 ID 和固定 class 定位即可。

def parse_detail(driver, url): driver.get(url) wait = WebDriverWait(driver, 15) wait.until(EC.presence_of_element_located((By.ID, "content"))) doc = {"url": url} doc["title"] = driver.find_element( By.CLASS_NAME, "doc-title" ).text.strip() doc["case_no"] = driver.find_element( By.ID, "caseNo" ).text.strip() for row in driver.find_elements(By.CSS_SELECTOR, ".meta-row"): cells = row.find_elements(By.CSS_SELECTOR, "span") if len(cells) == 2: key = cells[0].text.strip(":") doc[key] = cells[1].text.strip() doc["content"] = driver.find_element( By.ID, "content" ).text.strip() return doc

逻辑上,先强制等待正文节点出现,因为正文是整个详情页最后渲染的部分,正文出现代表页面主体已就绪。meta-row 是一行行的元数据,比如法院名称、审判程序、裁判日期,统一按“span 成对”的规则抽取,字段名就取左侧文本。

参数说明:网页结构改版后,ID 可能变化,这时候优先改用 XPath 按文本定位,比如 //span[contains(text(),"案号")]/following-sibling::span。正文用 text 获取会有多余换行,入库前建议做一次空白字符归一化。这里返回的是 dict,下一步直接喂给数据层。

4. Selenium 爬裁判文书网避坑指南:登录过期、webdriver 特征与 DOM 刷新问题

4.1 登录态过期导致整轮采集白跑

现象:定时任务跑了 6 个小时,早上看日志一切正常,数据库新增条数却是 0,详情页抓回去的全是登录跳转页的空模板。

原因:cookie 有有效期限,网站端过期后页面外观变化很小,列表页甚至还能正常翻页,但详情接口开始返回登录提示。采集进程不懂这些,它只看到页面“打开成功”,就把空字段写进了库。

解决:在采集循环里每处理 100 到 200 个链接就调用一次 check_login,发现失效立即停止写入并发送告警,不要尝试自动重登。另一个习惯是把详情页正文长度为 0 的记录单独标记为“疑似登录失效”,跑批结束后统一人工复核。宁可第一小时发现过期,不要第六小时才发现。

4.2 webdriver 特征让风控页面偶发出现

现象:Selenium 打开页面后检索结果偶尔为空,手动刷新同一地址又正常,而且频率不固定,有点像玄学。

原因:部分风控策略会读取 window.navigator.webdriver 属性,自动化浏览器该值默认为 true,手工浏览器为 undefined。当页面检测到异常特征时,不直接拦截,而是悄然返回一个空数据或验证页。

解决:至少做三件事。第一,在 Options 里关掉 enable-automation 开关;第二,有条件就引入 stealth 类脚本,在页面加载早期覆盖 navigator.webdriver 属性;第三,保证 chromedriver 和 Chrome 版本一致,版本错配本身就是一个明显的自动化指纹。这些手段只能降低概率,不能保证 100%,跑批时仍然要保留日志回查机制。

4.3 隐式等待与显式等待混用导致超时

现象:页面内容已经渲染完成,肉眼能看到数据,WebDriverWait 却抛出 TimeoutException,每次都在同一个元素上。

原因:脚本里同时设置了 driver.implicitly_wait(10) 和 WebDriverWait,两者叠加会让查找操作的总等待时间变成两者之和,且轮询节奏互相干扰,特别是在页面 DOM 频繁更新的场景下。

解决:全脚本统一用一种等待方式。我的习惯是全局不写 implicitly_wait,全部用 WebDriverWait 显式等待关键节点,超时设 15 秒,轮询间隔默认 0.5 秒。关键节点的选择要有讲究,不要等一个页面顶部永远先出现的元素,要等“你要操作的那个元素”真正可点击。

4.4 翻页后旧元素引用导致 NoSuchElementException

现象:第一页采集中,循环到了下一页,find_elements 返回空列表,或者点击翻页按钮直接抛元素失效异常。

原因:列表页翻页后,前端框架会整体替换列表 DOM,之前定位到的 WebElement 对象指向已经 detach 的旧节点,继续操作自然失败。

解决:每次进入新一页后,重新查找列表容器和翻页按钮,不要跨页复用任何 WebElement。写成代码就是在 for 循环内部重新执行 find_elements,而不是把第一页查到的 elements 列表缓存下来逐页遍历。这也是为什么 3.2 的 collect 函数把元素定位放在每一轮循环内的原因。

4.5 zip 交付包伪加密与中文名乱码导致源码打不开

现象:下载到的源码.zip 双击提示需要密码,输入 README 里记录的密码又失败;用常用解压软件解压后,文件名变成乱码或目录结构错乱。

原因:部分工具生成 zip 时设置了伪加密位,也就是压缩包头部把加密标志位置 1,但数据流本身没有真正加密;中文文件名则可能是压缩工具用 GBK 编码写入,而现代解压软件按 UTF-8 解析导致乱码。

解决:伪加密包先用 7-Zip 打开验证,如果能看到文件列表且能直接查看文件内容,说明只是标志位问题,用 zip 修复工具把加密位复原即可,这不是破解密码,是文件修复。中文乱码则尝试用 7-Zip 或 Python zipfile 以 GBK 方式重新读取文件名后解压。以后自己再打包交付时,统一用 UTF-8 文件名重新压缩,能少掉这一整类问题。

5. 数据入库与增量采集:用 SQLAlchemy 把爬下来的文书真正管起来

5.1 建表与模型:唯一键不能只用案号

采集解析完成后,最忌讳的就是把结果写进 CSV 了事。文书数据要做增量更新和断点续爬,必须进数据库。常见的做法是用 SQLAlchemy 做 ORM 模型,字段清晰,后续查询也方便。

from sqlalchemy import ( create_engine, Column, String, Text, Integer, DateTime, func ) from sqlalchemy.orm import declarative_base, sessionmaker Base = declarative_base() class Judgment(Base): __tablename__ = "judgment" id = Column(Integer, primary_key=True, autoincrement=True) doc_url = Column(String(500), unique=True, nullable=False) title = Column(String(500)) case_no = Column(String(100), index=True) court = Column(String(100)) procedure = Column(String(50)) judgment_date = Column(String(50)) content = Column(Text) fetched_at = Column(DateTime, server_default=func.now()) engine = create_engine("sqlite:///wenshu.db", echo=False) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)

这里一个关键设计是唯一键用 doc_url,而不是案号。裁判文书网存在同一案号对应多篇文书的情况,比如一审和二审、再审程序各自成文,案号相同但内容不同。用案号做唯一键会把合法文书拦截在库外。

字段类型也得按实际数据来。case_no 看起来像数字,但实际是“(2024)京01民终123号”这种格式,必须用字符串;judgment_date 先按文本存可以,后续做统计时再解析成日期列,不要在爬虫阶段强转。

5.2 增量去重:先批量查重再插入

爬虫跑第二遍时,大量链接是之前采过的,逐条去重会浪费大量请求。正确做法是把本次待入库的 url 集合一次查出来,在内存里做差集,再批量插入新记录。

def upsert_rows(session, docs): urls = [d["url"] for d in docs] exists = set( r[0] for r in session.query(Judgment.doc_url).filter( Judgment.doc_url.in_(urls) ) ) new_rows = [] for d in docs: if d["url"] in exists: continue new_rows.append(Judgment( doc_url=d["url"], title=d.get("title"), case_no=d.get("案号"), court=d.get("法院名称"), procedure=d.get("审判程序"), judgment_date=d.get("裁判日期"), content=d.get("content") )) if new_rows: session.bulk_save_objects(new_rows) session.commit() return len(new_rows)

逻辑上用一次 IN 查询拿到已存在的 url 集合,再在 Python 侧做差集。相对于逐条 SELECT 判断,这个方式把查询次数从 N 次降到 1 次,采集规模越大收益越明显。

参数说明:批量大小建议控制在 200 到 500 条一次提交。正文 content 字段很大时,事务提交时间会明显变长,过大的批量会把整个会话锁住。bulk_save_objects 会跳过 ORM 的事件钩子,但对这种纯写入场景性能最好。

入库后还可以顺手做个简单的统计视图,把当日新增、失败率、正文空值率输出到一个小页面,可视化观察采集质量,比人肉翻日志直观。入库统计是可选的,不是核心链路,但值得做。

5.3 断点续爬:以数据库为准而不是以文件为准

很多爬虫项目把“已爬链接”写在 txt 里,每爬一条追加一行。这在跑批中断后非常痛苦,文件最后一行可能写了一半,还要人工确认哪些成功哪些失败。以数据库为准的做法是加一张任务表,把每个待爬链接和它的状态都存起来。

class CrawlTask(Base): __tablename__ = "crawl_task" id = Column(Integer, primary_key=True, autoincrement=True) url = Column(String(500), unique=True, nullable=False) status = Column(Integer, default=0) # 0待爬 1采集中 2完成 3失败 retry_count = Column(Integer, default=0) @classmethod def next_batch(cls, session, size=50): return session.query(cls).filter( cls.status == 0 ).limit(size).all()

任务状态用数字表示,简单且便于索引。启动采集前先取一批 status=0 的任务,标记为采集中,完成后置为 2;解析异常或超时时置为 3,retry_count 加一。重启进程时,只需要查 status=0 或者“失败但 retry_count 小于 3”的记录继续跑。

这个设计把断点从“文件末尾”变成了数据库里一个可查询的状态。即使任务只跑了一半机器断电,重新启动后也能精确接着断点继续,不会重复采集,也不会漏采。再往后想上分布式,任务表就是天然的任务队列,多台机器各自取一批 status=0 的任务消费即可。

6. 进阶验证:全量跑批前的验收清单与按审级分片的技巧

真正把采集脚本挂到服务器之前,我会跑一轮小规模验收,用数据说话,而不是感觉“应该没问题”。验收清单通常包含五项:用 20 个关键词各取前 3 页做小样本,人工抽 10 篇文书,核对标题、案号、法院、正文四个核心字段是否完整;检查正文里是否混入“登录后查看”之类的占位文本;统计采集 100 条的成功率,正常应在 95% 以上,失败的大多是超时和连接中断;估算单页耗时,平均在 2 到 3 秒左右,以此推算全量时间;最后看增量入库的重复率是否为 0。

这五项有一项不过,都不要直接上全量。我自己吃过亏的一次是正文空值率到了 30%,原因是详情页还有一套懒加载逻辑,等待的节点选错了,HTML 里已经存在但内容是空的。小样本抽样能发现这种问题,跑全量才发现就是浪费几个小时。

分片技巧是按审级或法院维度拆分任务,而不是单浏览器并发翻页。比如把“基层法院”“中级法院”“高级法院”各自跑一个 Chrome 实例,多个实例互不共享翻页状态,每一片单独记录进度和失败任务。要上多机分布式,也只是把任务表进一步细化,让机器按队列取活。分片的同时要把浏览器回收做好,每个实例跑完 200 页重启一次,长期不重启的内存占用会导致页面响应越来越慢。

我现在拿到这类爬虫项目,第一件事永远是先补齐登录态校验和任务状态管理,再谈采集速度和并发量。速度再快,跑出来的数据不可信也是零;状态管理到位,跑批中断了还有后悔药吃。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询