基于Selenium的裁判文书网爬虫:登录态、反爬破解与数据入库实践

发布时间:2026/11/3 23:53:25
基于Selenium的裁判文书网爬虫:登录态、反爬破解与数据入库实践 简介基于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, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) print(f已保存 {len(cookies)} 条 cookie)这段代码的逻辑是先打开官网建立会话上下文再让操作者手动过掉验证码最后把浏览器当前持有的 cookie 落盘。get_cookies() 返回的是一个列表每项包含 name、value、domain、path、expiry 等字段json 格式方便后续按需筛选。参数说明保存文件必须指定 encodingutf-8否则含中文的 cookie 值在 Windows 下重载会乱码indent2 只是让文件可读不影响加载。另外一点经验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(optionsoptions) def inject_cookies(driver, pathcookies.json): with open(path, r, encodingutf-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_pages20): 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_keyTrue, autoincrementTrue) doc_url Column(String(500), uniqueTrue, nullableFalse) title Column(String(500)) case_no Column(String(100), indexTrue) court Column(String(100)) procedure Column(String(50)) judgment_date Column(String(50)) content Column(Text) fetched_at Column(DateTime, server_defaultfunc.now()) engine create_engine(sqlite:///wenshu.db, echoFalse) Base.metadata.create_all(engine) Session sessionmaker(bindengine)这里一个关键设计是唯一键用 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_urld[url], titled.get(title), case_nod.get(案号), courtd.get(法院名称), procedured.get(审判程序), judgment_dated.get(裁判日期), contentd.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_keyTrue, autoincrementTrue) url Column(String(500), uniqueTrue, nullableFalse) status Column(Integer, default0) # 0待爬 1采集中 2完成 3失败 retry_count Column(Integer, default0) classmethod def next_batch(cls, session, size50): return session.query(cls).filter( cls.status 0 ).limit(size).all()任务状态用数字表示简单且便于索引。启动采集前先取一批 status0 的任务标记为采集中完成后置为 2解析异常或超时时置为 3retry_count 加一。重启进程时只需要查 status0 或者“失败但 retry_count 小于 3”的记录继续跑。这个设计把断点从“文件末尾”变成了数据库里一个可查询的状态。即使任务只跑了一半机器断电重新启动后也能精确接着断点继续不会重复采集也不会漏采。再往后想上分布式任务表就是天然的任务队列多台机器各自取一批 status0 的任务消费即可。6. 进阶验证全量跑批前的验收清单与按审级分片的技巧真正把采集脚本挂到服务器之前我会跑一轮小规模验收用数据说话而不是感觉“应该没问题”。验收清单通常包含五项用 20 个关键词各取前 3 页做小样本人工抽 10 篇文书核对标题、案号、法院、正文四个核心字段是否完整检查正文里是否混入“登录后查看”之类的占位文本统计采集 100 条的成功率正常应在 95% 以上失败的大多是超时和连接中断估算单页耗时平均在 2 到 3 秒左右以此推算全量时间最后看增量入库的重复率是否为 0。这五项有一项不过都不要直接上全量。我自己吃过亏的一次是正文空值率到了 30%原因是详情页还有一套懒加载逻辑等待的节点选错了HTML 里已经存在但内容是空的。小样本抽样能发现这种问题跑全量才发现就是浪费几个小时。分片技巧是按审级或法院维度拆分任务而不是单浏览器并发翻页。比如把“基层法院”“中级法院”“高级法院”各自跑一个 Chrome 实例多个实例互不共享翻页状态每一片单独记录进度和失败任务。要上多机分布式也只是把任务表进一步细化让机器按队列取活。分片的同时要把浏览器回收做好每个实例跑完 200 页重启一次长期不重启的内存占用会导致页面响应越来越慢。我现在拿到这类爬虫项目第一件事永远是先补齐登录态校验和任务状态管理再谈采集速度和并发量。速度再快跑出来的数据不可信也是零状态管理到位跑批中断了还有后悔药吃。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询