资讯动态

用Selenium爬裁判文书网:登录、解析与数据落库全攻略

发布时间:2026/10/3 18:26:34 来源:尧图企业网站定制
简介这份基于Selenium的裁判文书网爬虫项目覆盖登录验证、搜索、文书详情获取等完整流程尤其针对裁判文书网的登录机制做了完整封装适合Python爬虫学习者、计算机相关专业如人工智能、通信工程、自动化、电子信息等的在校学生、教师及企业开发者参考使用。资源包为ZIP压缩格式共19个文件包含9个Python脚本、7个文本文件、1个JavaScript脚本、1个Markdown文档及版本管理配置整体大小仅233KB。Python脚本分别实现参数解析、登录、文书搜索、docid获取等核心模块文本文件保存登录状态、页码、地域等运行参数JS脚本辅助模拟登录配合README文档和齐全的登录资料可快速理解项目结构并掌握登录难点。项目源自高分个人作品已获导师指导认可、答辩评审分95分代码经过完整测试运行成功。已有181人学习下载既可直接用于毕业设计、课程设计、作业及项目初期演示也可作为进阶学习素材在此框架上二次开发实现更多功能。1. 用selenium爬裁判文书网登录、解析、落库一次说清裁判文书网是司法数据公开的重要来源但它的反爬强度在国内公开网站里属于第一梯队。很多团队上来先写requests脚本结果卡在登录页和滑块验证上一两周都拿不到一条数据。基于selenium裁判文书网爬虫这套方案思路是让浏览器替你把JS渲染、登录交互和验证码流程全部跑完爬虫只负责指挥和收数据。它解决的核心问题只有一个当目标网站的请求链路里塞满了加密参数和浏览器指纹校验时直接用HTTP库模拟请求的成本已经高到不值得不如用真实浏览器做载体。这套方案适合需要持续拿文书数据做分析、做课题、做业务验证的人也是理解复杂站点反爬对抗的很好样本。2. 裁判文书网登录selenium的账号密码、验证码与Cookie三层搭法2.1 为什么登录是这座山最高的那道坎裁判文书网早期版本可以直接匿名访问检索但后来的改版把「查看文书全文」和「登录态」绑在了一起。你不登录能拿到案号和基础信息点进正文就被拦。更麻烦的是它的登录链路里塞了不止一道验证账号密码表单、滑块拼图验证、以及基于浏览器环境的风控检测。纯requests模拟登录要过的关卡太多而selenium天然绕开了大部分浏览器是真实的执行环境是真实的指纹特征也是真实的。我一般会把登录拆成三层来搭。第一层是账号密码的常规表单提交第二层是对抗滑块或点选验证码第三层是登录成功后把Cookie持久化下来避免每次启动都重新过一遍验证。这三层各自独立任何一个挂了都不影响另外两个的排查。2.2 最小登录代码显式等待与账号输入selenium最大的敌人是时序。页面加载快慢取决于网络、服务器负载和风控脚本的执行时间固定sleep很容易在慢网络下失败。所以登录表单的所有操作都应该基于显式等待而不是写死等待时间。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options webdriver.ChromeOptions() # 关闭自动化提示条降低被检测的概率 options.add_experimental_option(excludeSwitches, [enable-automation]) # 不关闭浏览器窗口方便观察验证码交互 driver webdriver.Chrome(optionsoptions) wait WebDriverWait(driver, 20) driver.get(https://wenshu.court.gov.cn/) # 打开登录框 wait.until(EC.element_to_be_clickable((By.XPATH, //a[contains(text(),登录)]))).click() # 等待账号输入框出现并输入 user_input wait.until(EC.presence_of_element_located((By.ID, username))) user_input.clear() user_input.send_keys(你的账号) driver.find_element(By.ID, password).send_keys(你的密码)这段代码的关键在于所有定位都套了WebDriverWait。presence_of_element_located等的是DOM节点出现element_to_be_clickable等的是节点可点两者对时机的敏感度不一样。账号输入框用presence就够了登录按钮必须用element_to_be_clickable因为按钮可能在DOM里但被遮罩层挡住。输入账号密码后先别急着点登录。裁判文书网的登录按钮有时会触发二次校验比如弹出滑块、点选汉字验证码。直接点会导致验证流程还没准备好就被跳过后面登录必然失败。我一般会先手动确认页面状态或者用代码检测验证码容器是否存在再决定点击时机。2.3 验证码处理的三种常见方案验证码是登录链路里最玄学的一环。它不像表单字段那样固定今天滑块明天点选后天直接放行完全看风控的心情。常见的处理方案有三种适用场景完全不同。第一种是半自动干预运行到验证码时程序停下来等你手动拖一下适合个人少量使用。实现很简单在代码里加一个input()阻塞或者用time.sleep给自己留出操作窗口。这是最稳的方案没有之一因为人脑识别滑块和点选的成功率接近百分之百。第二种是接打码平台。把验证码截图发给平台接口平台返回坐标或答案代码再模拟拖动或点击。对滑块验证码平台一般返回缺口坐标然后你用ActionChains把滑块从起点拖到目标位置。这类方案的瓶颈在拖动轨迹平台给准了坐标你的拖动轨迹太机械照样被判定为机器。from selenium.webdriver.common.action_chains import ActionChains # 假设已经拿到缺口横坐标 target_x slider wait.until(EC.presence_of_element_located((By.CLASS_NAME, yidun_slider))) ActionChains(driver).click_and_hold(slider).perform() # 先慢后快再微调模拟人手拖动节奏 ActionChains(driver).move_by_offset(target_x * 0.6, 0).perform() ActionChains(driver).move_by_offset(target_x * 0.3, 0).perform() ActionChains(driver).move_by_offset(target_x * 0.1 2, 0).perform() ActionChains(driver).pause(0.3).release().perform()轨迹曲线是这套方案里最值得花时间的参数。匀速直线拖动基本一秒就被识别标准做法是分段位移、每段之间加随机停顿最后一段做小幅回拉。这个回拉动作很关键人手动拖滑块很少一次到位总会有几像素的修正。第三种是纯模拟方案用图像识别算法定位缺口的x坐标本地算轨迹不依赖外部平台。这个方案适合批量部署的场景但开发成本最高。裁判文书网的验证码图片会做干扰线、噪点处理opencv的模板匹配不一定稳定需要针对具体图片样式做调参。我个人的建议是个人使用别折腾打码平台直接半自动人工处理最划算。打码平台要花钱接口调试要时间识别错了还要重试综合成本比人工点一下高得多。2.4 Cookie复用让登录态活过Session每次启动浏览器都重新走一遍登录和验证码既慢又容易触发风控。更合理的做法是登录成功后把Cookie持久化到本地文件下次启动直接注入Cookie跳过登录链路。import json # 登录成功后保存cookie cookies driver.get_cookies() with open(wenshu_cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) # 下次启动时加载cookie def load_cookies(driver, cookie_pathwenshu_cookies.json): with open(cookie_path, r, encodingutf-8) as f: cookies json.load(f) for cookie in cookies: cookie.pop(expiry, None) # 过期字段会导致注入失败 driver.add_cookie(cookie)注意cookie.pop(expiry, None)这行。selenium的add_cookie对expiry字段格式有要求如果你保存的Cookie里带了这个字段重新注入时很可能直接抛异常。去掉后让它沿用浏览器自身的会话管理逻辑反而更稳。Cookie也不是一劳永逸的。裁判文书网的登录态有有效期短则几小时长则几天过期后你再注入旧Cookie页面会静默跳回登录态但你以为自己还登着。所以代码里要有登录态检测访问一个需要登录的接口或页面检查是否出现了登录框特征元素。一旦发现掉登录态就重新走一遍登录流程并刷新Cookie文件。这套「登录→存Cookie→注入→检测掉线→重新登录」的闭环是这个爬虫能长期运行的骨架。3. 检索到详情selenium翻页、懒加载与文书字段抽取3.1 检索页URL参数构成与按条件搜索登录之后的核心操作是构造检索条件。裁判文书网的检索支持案由、法院层级、审判程序、文书类型、裁判日期等维度组合。直接用页面上的搜索表单操作最省事但速度慢直接改URL参数最快但参数名会随版本调整。我一般倾向混合方案用selenium操作表单输入关键词和筛选条件点搜索按钮让页面自己去拼URL。原因很简单URL参数里的加密值可能是JS动态生成的你自己拼URL等于把解密逻辑背在自己身上一旦网站改版就全线崩溃。而点页面按钮是站在网站自己的逻辑上改动成本低。# 输入搜索关键词 kw_box wait.until(EC.presence_of_element_located((By.ID, searchBox))) kw_box.clear() kw_box.send_keys(民间借贷纠纷) # 选择裁判日期区间 start_input driver.find_element(By.ID, startDate) start_input.clear() start_input.send_keys(2023-01-01) end_input driver.find_element(By.ID, endDate) end_input.clear() end_input.send_keys(2023-12-31) # 点击搜索 driver.find_element(By.XPATH, //button[contains(text(),检索)]).click()注意日期输入框有时不是原生input而是readonly属性配合日历控件。遇到这种情况send_keys可能不生效需要先执行JS把readonly去掉再输入。element driver.find_element(By.ID, startDate) driver.execute_script(arguments[0].removeAttribute(readonly), element) element.clear() element.send_keys(2023-01-01)3.2 懒加载下拉与分页点击的等待策略裁判文书网的列表页是经典的懒加载结构滚轮往下拉才会加载更多数据。selenium处理这个场景有两种思路一种是直接模拟滚动触发底部加载另一种是定位「下一页」按钮一页一页翻。模拟滚动的问题是加载时机不可控你不知道数据什么时候渲染完。我习惯的做法是死循环滚动加稳定检测滚动一次记录当前列表条数等待条数增长且连续几次不再增长才认为当前页加载完毕。from selenium.webdriver.common.by import By def load_full_list(driver, wait, list_selectordiv.listItem): prev_count -1 stable_rounds 0 while True: # 滚到底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1.2) items driver.find_elements(By.CSS_SELECTOR, list_selector) current_count len(items) if current_count prev_count: stable_rounds 1 else: stable_rounds 0 prev_count current_count # 连续3轮数量不再变化认为加载完成 if stable_rounds 3: break return items这个循环里的time.sleep(1.2)和stable_rounds 3是要调的核心参数。睡太久浪费时间睡太短可能一次滚动还没触发网络请求就进入了下一轮循环。1.2秒是在网络正常情况下的经验值网络差就调到2秒网络好可以压到0.8秒。分页按钮的点击比懒加载稳定得多但要注意裁判文书网的分页逻辑有时候不是真分页而是「加载更多」按钮。点「下一页」之前先确认按钮存在且可点点击后等待列表内容刷新。怎么判断刷新完成取第一条数据的文本等待它变成和点击前不同的内容。3.3 详情页字段抽取用XPath稳取关键信息列表页只能拿到案件名称、案号这种摘要信息完整文书内容必须进详情页。每条文书进一次详情页这个成本很高需要同时考虑效率和稳定性。进详情页前先把列表页所有文书的链接和案号收齐然后逐条打开。打开的方式建议新开一个tab而不是当前页跳转这样返回列表时不需要重新等待加载。# 打开详情页 original_window driver.current_window_handle driver.execute_script(window.open(关于案件的详情URL)) driver.switch_to.window(driver.window_handles[-1]) # 抽取字段 case_name wait.until(EC.presence_of_element_located((By.XPATH, //div[classcase-name]))).text case_num driver.find_element(By.XPATH, //div[classcase-number]).text court driver.find_element(By.XPATH, //div[classcourt-name]).text judge_date driver.find_element(By.XPATH, //div[classjudge-date]).text full_text driver.find_element(By.XPATH, //div[classfull-text]).text # 用完关掉详情tab切回列表 driver.close() driver.switch_to.window(original_window)XPath的定位依赖class名而这个class名在不同页面版本里并不一致。我踩过的坑是新版页面把case-name改成了case_name下划线风格变了导致一整批抽取全空。更稳的做法是先用文本特征定位比如「案号」「审理法院」这样的label再取其兄弟节点而不是硬记class名。文书的正文有时是分段渲染的直接用.text拿全文会遇到中间缺段落的情况。稳妥的做法是把正文区域的所有p标签分别取文本再拼接中间用换行符隔开。这样可以保住段落结构也避免.text对隐藏节点处理不一致的问题。paragraphs driver.find_elements(By.XPATH, //div[classfull-text]//p) full_text \n.join([p.text for p in paragraphs if p.text.strip()])3.4 把一条文书组织成结构化dict字段抽完不要立刻想着入库先组织成统一的dict结构。这个结构是你后续落库、去重、分析的基础字段名最好和你第4章要建的数据库表对齐。document { case_id: case_num, # 案号唯一标识 case_name: case_name, # 案件名称 court: court, # 审理法院 judge_date: judge_date, # 裁判日期 case_type: 民事, # 案件类型可按关键词粗判 full_text: full_text, # 文书全文 crawl_time: time.strftime(%Y-%m-%d %H:%M:%S), source_url: driver.current_url, }这里把case_id定为案号而不是数据库自增ID是为了后面做去重。同一个案件在检索结果里可能出现多次比如不同维度筛选时重复命中案号是天然的唯一键。crawl_time记录抓取时间source_url留底出问题的时候能回溯到原始页面。4. 数据落库与去重SQLAlchemy存储爬虫数据断点续爬不重不漏4.1 为什么选SQLAlchemy而不直接写CSV很多入门教程喜欢把爬虫结果存成CSV简单直接还能用Excel打开。但裁判文书网的数据有几个特点单条正文字数多、字段结构固定、抓取量大、需要频繁查重。CSV在这三个需求面前都很别扭。正文里如果有换行和逗号CSV的解析就会错乱几十万条文书放到一个CSV里任何查询都要全文件扫描。SQLAlchemy在这里的价值有两层。第一层是ORM让你不写原生SQL也能建表、插入、去重第二层是它隔离了数据库差异你本地用SQLite跑通逻辑部署到服务器换MySQL或PostgreSQL只需要改连接串代码不用动。4.2 建表与插入代码case_id去重from sqlalchemy import create_engine, Column, String, Text, DateTime, func from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base declarative_base() class JudgmentDocument(Base): __tablename__ judgment_documents id Column(String(64), primary_keyTrue) # case_id case_name Column(String(255)) court Column(String(128)) judge_date Column(String(32)) case_type Column(String(32)) full_text Column(Text) crawl_time Column(DateTime, defaultfunc.now()) source_url Column(String(512)) engine create_engine(sqlite:///judgments.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine)表结构里的id直接做成字符串主键存案号。这样做唯一约束和去重一并解决不用额外加索引。full_text用Text类型SQLite的Text没有长度限制但MySQL里的Text最多存64KB文书正文超过这个长度会被截断。如果你要上MySQL建议用LongText。插入时做去重常见做法是先查再插但高并发下存在竞态。更推荐的做法是依靠数据库的唯一约束插入时捕获异常或者用INSERT OR IGNORE语义。SQLAlchemy层面可以用sqlite_insert的on_conflict_do_nothing但为了兼容不同数据库我一般用先查询后插入的朴素写法def save_document(session, doc): existing session.get(JudgmentDocument, doc[case_id]) if existing: return False # 已存在跳过 session.add(JudgmentDocument(**doc)) session.commit() return True这个方案的缺点是在多进程并发抓取时两个进程同时查到不存在然后同时插入其中一个会撞主键抛异常。解决方案是捕获IntegrityError并回滚或者把抓取任务收敛到单进程。个人使用单进程足够别为了并发把复杂度拉高。全部抓完再统一commit是新手最常见的翻车点。一旦中间某个字段超长或类型不匹配整个事务回滚几小时的数据全丢。我的习惯是每抓完一条就commit一次虽然慢一点但最坏情况只丢当前这一条。4.3 断点续爬已抓文书ID列表与进度标记长周期抓取一定会遇到中断网络抖动、验证码卡死、电脑休眠任何一个都能让爬虫停下。断点续爬是这套方案必须有的能力。最简单的断点恢复是查数据库启动时把所有已经存在的case_id读进一个set抓取前先判断案号是否在这个set里。这个方案零额外存储但每次启动要把全表主键载入内存。几十万条数据没问题到了千万级就吃力了。更适合的量级做法是单独建一张进度表记录「抓取到哪个关键词、哪个日期区间、哪一页」。这样中断后可以从上次的位置继续而不是把已经抓完的页再跑一遍。class CrawlProgress(Base): __tablename__ crawl_progress id Column(Integer, primary_keyTrue, autoincrementTrue) keyword Column(String(128)) start_date Column(String(32)) end_date Column(String(32)) current_page Column(Integer, default1) finished Column(Integer, default0)我一般把进度的更新粒度做到关键词日期段页码。每处理完一页就更新一次current_page这样即使程序在某一页的中间崩了重启后从该页重新抓配合数据库去重最多重复抓一页的数据不会出现大段遗漏或重复。把进度持久化的时机很讲究。不要每处理完一条就写一次进度IO太频繁也不要等整个关键词跑完再写崩溃损失太大。折中方案是每翻完一页更新一次。一页的数据量正好是几十条重跑成本可接受。5. 裁判文书网selenium方案避坑8个翻车点与排查方法5.1 页面元素定位不到XPath报错现象脚本跑得好好的某天突然报NoSuchElementException定位不到登录按钮或搜索框。原因裁判文书网的前端做过版本升级页面结构或class名变更。另外登录框可能是异步加载的你定位时它还没渲染出来。解决先手动打开页面用开发者工具确认元素的当前class和id。不要盲改代码至少看三处同类页面确认结构统一。然后把所有定位全部升级为显式等待杜绝固定sleep后取元素的写法。5.2 滑块验证码拖了也过不去现象滑块拖到缺口位置松手后提示「拼图未完成」或直接刷新验证码。原因拖动轨迹太机械。代码一次性把滑块从起点移到终点没有加速减速过程风控直接判定为程序操作。解决把一次长拖动拆分成多段短拖动每段间隔随机。注意第一段要慢模拟手指按压后的起步过程中间段加快最后到位前要有一个小的回拉修正。调参时可以录一段自己手动拖动的轨迹数据来对照。5.3 登录成功后几秒就被踢下线现象登录成功跳转到首页刚点两个页面就发现又回到登录状态。原因风控检测到了自动化痕迹。比如navigator.webdriver属性为true或者浏览器窗口尺寸、缩放比例异常。解决启动参数里加上excludeSwitches和useAutomationExtension开关去掉自动化标识。窗口大小设置成常见的1920x1080。如果还是被踢可以考虑用undetected-chromedriver替代标准selenium驱动它的思路是直接patch掉webdriver特征。5.4 列表页数据加载不完整现象反复滚动十几次列表条数不增长停在某个数量。原因可能是触发了风控限制当前会话的加载被冻结。也可能是滚动的元素选错了选择器滚动的容器不是document而是某个内部div。解决先确认滚动容器。裁判文书网的列表在某个div内部滚动不是整个页面滚动。用driver.execute_script(arguments[0].scrollTop arguments[0].scrollHeight, list_container)滚动内部容器而不是window.scrollTo。另外检查网络面板看滚动后是否有XHR请求发出。没有请求说明被风控限制了需要重新启动会话。5.5 详情页打不开或一直在加载现象点击文书标题后新开的tab一直转圈或者页面白屏。原因详情页的接口需要登录态而你的Cookie已经失效。也可能是详情页做了单独的访问频率限制同一IP短时间内请求过多。解决打开详情页前先做一次登录态校验最简单的方式是访问一个已知需要登录的页面检查登录框是否存在。如果掉登录先重新走登录流程再继续。频繁白屏时插入一个随机延迟让每个详情页请求间隔拉开到3-5秒。5.6 数据库插入报错字段太长或类型不匹配现象DataError或者OperationalError提示某个字段超过最大长度。原因String(128)的字段收到了几百个字符的内容或者正文里包含特殊字符导致SQL拼接错误。解决把表字段长度放大尤其是court字段有些法院全称加后缀能到80字以上。正文用Text/LongText。如果某些字段的内容有异常插入前做长度截断不要直接入库后让数据库报错。5.7 程序跑着跑着Chrome就被强制关闭现象运行几小时后浏览器窗口消失selenium抛WindowClosedError。原因最常见的是内存不足Chrome长时间开多个tab会吃掉大量内存。也可能是浏览器崩溃或系统休眠。解决定时重启浏览器。我习惯每抓500条文档重启一次浏览器关闭全部tab重新打开列表页。这个操作同时也能清理Chrome累积的运行时缓存降低被风控识别的概率。5.8 抓到的文本中间少了段落现象正文里原本应有的大段内容丢失只剩开头和结尾。原因页面做了虚拟滚动未渲染的区域是空的.text只返回已经渲染出来的部分。你抓取的时机早于渲染完成。解决抓取前先判断正文区域子节点的数量是否稳定。或者用前面提到的按p标签逐段抽取的方式配合等待所有段落加载完成再取文本。6. 把方案调成长期能跑频控退避、日志与一天一跑的心得这套爬到能跑通只是第一步把它调成「明天还能跑、后天还能跑」才是真正的分水岭。我见过太多人费劲跑通了登录和解析结果上线跑一个周末就废弃了因为第二天起来发现程序卡在验证码上重试了一整夜。给个人使用的爬虫写一个简单的指数退避逻辑比什么都值钱。def fetch_with_backoff(driver, action_func, max_retries5): base_delay 2 for attempt in range(max_retries): try: return action_func() except Exception as e: wait base_delay * (2 ** attempt) print(f第{attempt 1}次失败: {e}, 等待{wait}秒) time.sleep(wait) raise RuntimeError(重试多次仍失败)退避策略只解决临时性错误。如果连续失败超过三到五次不要原地重试应当把当前的会话状态、URL、错误信息写进日志然后退出程序。原地重试只会让IP被风控盯得更紧。日志要包含时间戳、当前页码、案号、异常类型和堆栈这样第二天看日志就知道断在哪里。一天的抓取量也要控制在合理范围。裁判文书网的数据量巨大但单个IP的访问频率是有限的。我通常控制每秒最多一个请求每个详情页之间随机延迟2到4秒。一小时能抓取大约八百到一千条一天八千到一万条这个量级对大多数个人研究场景已经足够。想往更高量级走得考虑分布式方案多个IP和多个浏览器实例并行但那是另一个量级的工程投入。日志和退避都做好后整个爬虫可以挂一个定时任务每天凌晨跑。凌晨时段网站负载低风控相对宽松而且即使出问题白天有时间处理不会耽误工作。日志里除了错误信息我还会定期打印当前进度已抓多少条、还剩多少条、数据库总量多少。这些数字是判断爬虫是否健康的最直接依据。给这套方案做验收也很简单抓完一万条数据后随机抽二十条走一遍去重逻辑看是否有重复案号再抽三条去裁判文书网官网人工核对看字段是否和页面一致。我做这套方案最大的教训是第一个版本完全没写日志跑了两周数据才发现某一天的字段解析全错因为没有日志根本定位不了是哪次改代码引入的问题。后来所有爬虫项目我第一件事就是搭好日志第二件事才是写抓取逻辑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑