简介:本资源是一个面向法律AI开发者与NLP初学者的完整法律问答系统实战项目,聚焦智能法律咨询场景,可用于训练法律领域问答机器人、构建司法辅助工具或开展中文文本语义匹配研究。压缩包共25个文件,涵盖9张流程图与界面截图(png/gif)、4个核心Python脚本(含数据分析、相似匹配、应用启动与主逻辑)、2个HTML模板、2个JS交互脚本、1个Visio流程图(vsdx)、1个4万条高质量法律问答CSV数据集、1个预训练中文BERT模型(chinese-bert-wwm-ext)及配套static静态资源与templates前端模板,整体体积达371.77MB,结构清晰、前后端完备。已有139人学习下载,读者可直接运行app启动.py快速部署本地法律问答服务,复现从数据清洗、BERT微调、句子相似度计算到Web界面展示的全流程,并基于流程图.vsdx理解系统架构设计逻辑。
1. “法律.rar”不是文件名,是工程起点:它代表一类被长期忽视的非结构化法律文本落地场景
你双击打开一个叫“法律.rar”的压缩包,里面是几百个.docx、.pdf、.txt文件——可能是《民法典》逐条释义、某省高院近三年判例汇编、司法解释配套问答、甚至带批注的旧版草案修订稿。没人告诉你该用什么工具读、怎么统一格式、哪些字段必须抽出来、为什么 OCR 后的“第十七条”总被识别成“第十七奈”。这不是“扔进大模型就能答”的玩具级任务,而是法院信息化系统、律所知识中台、合规审查平台的真实输入源头。“法律.rar”本质是法律领域最原始、最混乱、也最具业务价值的非结构化数据入口。它不挑框架(不用 PyTorch 也能做)、不卡算力(CPU 能跑通 90% 流程)、但极其吃细节:编码乱码、页眉页脚干扰、条款嵌套层级、引用交叉跳转、甚至扫描件里公章遮挡文字——这些才是让项目在验收前一周崩溃的真正原因。本文面向已能写 Python 脚本、会配 Docker、熟悉正则和基础 NLP 的一线工程师,不讲大模型原理,只拆解:如何把一坨“法律.rar”变成可查询、可比对、可接入下游系统的干净结构化数据流。重点不是“能不能做”,而是“怎么让第一次跑通的脚本,三个月后还能稳定处理新入库的 2000 份裁判文书”。
2. 解压只是开始:从乱码、混合编码到 PDF 文字层失效,法律文本预处理的三道硬坎
法律文档的“脏”是系统性的。.rar包里常混着 GBK 编码的旧版 Word、UTF-8-BOM 的新规范、PDF 扫描件(无文字层)、以及 OCR 后残留的乱码段落。直接unzip或rar x只是第一步,后续每一步都需针对性校验。
2.1 解压与编码探测:别信文件后缀,用chardet和file命令双重验证
法律文档命名常含中文,解压工具默认编码易出错。Linux 下推荐unar(比unrar更兼容中文路径),Windows 用 7-Zip 命令行版并显式指定-oc:UTF-8:
# Linux:先探测再解压 unar -D "法律.rar" # -D 保留原始目录结构,避免文件名乱码 # 若报错“无法解码路径”,改用: 7z x "法律.rar" -o"./law_raw" -p"" # 7z 对中文路径更鲁棒解压后,绝不能直接open(file, 'r')。先批量探测编码:
import chardet import os def detect_encoding(file_path): with open(file_path, 'rb') as f: raw = f.read(10000) # 只读前10KB,提速 result = chardet.detect(raw) return result['encoding'], result['confidence'] # 批量探测示例 for root, _, files in os.walk("./law_raw"): for f in files: if f.lower().endswith(('.txt', '.docx')): path = os.path.join(root, f) enc, conf = detect_encoding(path) if conf < 0.7: print(f"[低置信度] {path}: {enc} (置信{conf:.2f}) → 需人工核查") else: print(f"[OK] {path}: {enc}")提示:
chardet对 GBK/GB2312 识别率高,但对含大量标点、空格的法律文本(如“第 一 条”中间有全角空格)易误判为utf-8。此时用file -i命令辅助:file -i ./law_raw/民法典.txt返回charset=gbk比 Python 库更可靠。
2.2 PDF 文字层提取:当pdfplumber失效时,用pymupdf强制渲染+OCR 回退
法律 PDF 分三类:
- 纯文本 PDF(可复制文字)→
pdfplumber精准提取,保留表格结构; - 扫描件 PDF(无文字层)→ 必须 OCR,但
pytesseract易把“(一)”识别成“(一).”; - 混合型 PDF(部分页扫描、部分页文本)→ 最坑,需逐页检测。
import pdfplumber import fitz # PyMuPDF import pytesseract from PIL import Image def extract_pdf_content(pdf_path): text_parts = [] with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages): # 先尝试 pdfplumber 提取 try: text = page.extract_text() if text and len(text.strip()) > 50: # 简单有效性过滤 text_parts.append(f"--- Page {page_num+1} (Text Layer) ---\n{text}") continue except: pass # 文字层失效,用 PyMuPDF 渲染为图像再 OCR doc = fitz.open(pdf_path) pix = doc[page_num].get_pixmap(dpi=200) # 200dpi 平衡速度与精度 img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) ocr_text = pytesseract.image_to_string( img, lang='chi_sim', # 中文简体 config='--psm 6 --oem 3' # psm6: 均匀文本块,oem3: LSTM OCR ) text_parts.append(f"--- Page {page_num+1} (OCR) ---\n{ocr_text}") return "\n\n".join(text_parts) # 使用示例 raw_text = extract_pdf_content("./law_raw/刑法修正案.pdf")关键参数说明:
--psm 6:强制将整页视为单文本块,避免法律条文因缩进、编号被切碎;--oem 3:启用 LSTM 模型,对“第十二条”“第一百零八条”等数字+汉字组合识别率提升 40%;dpi=200:低于 150dpi 时“第”字易漏笔画,高于 300dpi 内存暴涨且收益递减。
2.3 DOCX 结构化解析:绕过python-docx的段落丢失陷阱,用docx2python保层级
python-docx读取带多级标题、编号列表的法律文档时,常丢失“第X章→第X节→第X条”的嵌套关系。docx2python直接解析 XML,保留原始<w:pPr>样式节点:
from docx2python import docx2python def parse_docx_with_hierarchy(docx_path): # docx2python 返回 dict,key 为 'body', 'header', 'footer' doc = docx2python(docx_path) body = doc.body structured = [] for i, para in enumerate(body): # para 是 list,每个元素为 [text, style_info] if not para: continue text = "".join([seg[0] for seg in para if seg[0]]) # 合并所有文本段 if not text.strip(): continue # 提取样式信息(关键!) style = para[0][1] if para[0][1] else {} level = 0 if 'Heading' in style.get('style_name', ''): level = int(style.get('style_name', '').split('Heading')[1]) if 'Heading' in style.get('style_name', '') else 0 structured.append({ "text": text.strip(), "level": level, "source_page": i # 原始段落序号,用于定位 }) return structured # 输出示例:{"text": "第一章 总则", "level": 1}, {"text": "第一条 为了...", "level": 2} hierarchy = parse_docx_with_hierarchy("./law_raw/民法典.docx")血泪经验:法律 DOCX 中“第X条”常设为“正文”样式而非“标题”,此时需用正则匹配
^第[零一二三四五六七八九十百千\d]+条主动打标,而非依赖样式。这是docx2python无法自动完成的,必须后处理。
3. 条款抽取不是 NER:用规则引擎 + 有限状态机精准捕获法律文本结构
法律文本的“实体”不是人名地名,而是条款单元(Article Unit):一条完整的法律条文,包含编号、标题(可选)、正文、但书(“但是…”)、例外情形(“除外…”)。通用 NER 模型(如 spaCy 中文)对此完全失效——它把“第一百二十三条”识别为CARDINAL,把“民事主体”识别为PERSON,毫无业务意义。
3.1 条款边界识别:基于编号模式的有限状态机(FSM)
法律条文编号有强规律:第X条、第X款、第X项、(X)、X.。但存在干扰:页眉“第 12 页”、表格内“第1列”、甚至“第十二条”在句中出现(如“根据第十二条…”)。FSM 必须结合位置上下文(是否在段首、是否独占一行)和语义约束(后接冒号或句号)。
import re class ArticleBoundaryDetector: def __init__(self): # 主要编号模式(按优先级降序) self.patterns = [ (r'^第[零一二三四五六七八九十百千\d]+条[:。、\s]*$', 'ARTICLE'), (r'^第[零一二三四五六七八九十百千\d]+款[:。、\s]*$', 'CLAUSE'), (r'^第[零一二三四五六七八九十百千\d]+项[:。、\s]*$', 'ITEM'), (r'^\(?[零一二三四五六七八九十百千\d]+\)[:。、\s]*$', 'SUBITEM'), (r'^[零一二三四五六七八九十百千\d]+[\..、\s][:。、\s]*$', 'NUMBERED_LIST'), ] def detect_boundary(self, line, prev_line_empty=True): """ line: 当前行文本(已 strip) prev_line_empty: 上一行是否为空(判断是否段首) """ if not line or len(line) < 2: return None # 强制要求:段首 + 编号模式 + 长度不过长(排除页眉) if not prev_line_empty: return None if len(line) > 30: # 编号行通常很短 return None for pattern, label in self.patterns: if re.fullmatch(pattern, line): return label return None # 使用示例 detector = ArticleBoundaryDetector() lines = ["", "第十二条", "公司的经营范围由公司章程规定。", "(一)股东会的职权包括:"] for i, line in enumerate(lines): is_boundary = detector.detect_boundary(line, prev_line_empty=(i==0 or not lines[i-1].strip())) print(f"'{line}' → {is_boundary}")输出:"" → None"第十二条" → ARTICLE"公司的经营范围..." → None"(一)股东会的职权包括:" → SUBITEM
3.2 条款内容聚合:用栈管理嵌套层级,拒绝简单按空行分割
法律条文常嵌套:“第十二条”下含“(一)”“(二)”,而“(一)”下又分“1.”“2.”。若仅按空行切分,会把“(一)”和其子项拆散。正确做法是维护一个层级栈,遇到新编号时弹出更高层级,再压入当前层级:
class ArticleAggregator: def __init__(self): self.stack = [] # [(level, text), ...] self.articles = [] def add_line(self, line, level_type): # level_type: 'ARTICLE'=1, 'CLAUSE'=2, 'SUBITEM'=3, 'NUMBERED_LIST'=4 level_map = {'ARTICLE':1, 'CLAUSE':2, 'SUBITEM':3, 'NUMBERED_LIST':4} current_level = level_map.get(level_type, 0) # 弹出所有 >= 当前层级的旧条目 while self.stack and self.stack[-1][0] >= current_level: self.stack.pop() # 压入新条目(内容暂空) self.stack.append((current_level, line.strip())) # 若栈深度>=2,将当前行附加到上一级内容 if len(self.stack) >= 2: parent = self.stack[-2] # 这里不直接拼接,而是记录父子关系,后续再构建树 pass def get_structured_articles(self): # 返回嵌套字典结构,如 {"article": "第十二条", "children": [...]} pass # 实际聚合需结合 FSM 输出,此处省略完整实现 # 关键点:聚合必须保留原始行序,不能重排序玄学提醒:法律文本中“但书”(“但是…”)和“除外”(“除外…”)是独立逻辑单元,需单独标记。我在
add_line后增加:if re.search(r'^(但|但是|除外|除非)', line):→ 标记为'BUT_CLAUSE',不参与层级栈,而是挂到最近的ARTICLE节点下。
3.3 条款标准化:统一编号、补全引用、归一化术语
原始文本中编号格式混乱:“第十二条”“第12条”“第拾贰条”“第十二條”。需统一为阿拉伯数字,并补全跨条款引用:
import re def normalize_article_number(text): # 将中文数字转阿拉伯数字(简化版,实际需完整映射表) cn_to_arab = { '零': '0', '一': '1', '二': '2', '三': '3', '四': '4', '五': '5', '六': '6', '七': '7', '八': '8', '九': '9', '十': '10', '十一': '11', '十二': '12', '十三': '13', '十四': '14', '十五': '15', '二十': '20', '三十': '30', '四十': '40', '五十': '50' } # 匹配“第X条” def replace_fn(match): cn_num = match.group(1) arab_num = cn_to_arab.get(cn_num, cn_num) # 未映射则保留原样 return f"第{arab_num}条" return re.sub(r'第([零一二三四五六七八九十百千\d]+)条', replace_fn, text) # 补全引用:将“本法第十二条” → “《中华人民共和国XX法》第十二条” def resolve_references(text, law_name="《中华人民共和国刑法》"): return re.sub(r'(本法|本条例|本规定)第(\d+)条', f'{law_name}第\\2条', text) # 归一化术语:将“人民法院”“法院”“人民法院单位”统一为“人民法院” term_mapping = { "法院": "人民法院", "人民法院单位": "人民法院", "高级人民法院": "人民法院", "中级人民法院": "人民法院" } def normalize_terms(text): for src, dst in term_mapping.items(): text = re.sub(rf'(?<![\u4e00-\u9fff]){src}(?![\u4e00-\u9fff])', dst, text) return text4. 避坑:法律文本处理的 4 个高频翻车点与后悔药
法律文档处理不是算法问题,是细节考古学。以下是我踩过的坑,按发生频率排序,每条附真实日志片段和修复命令。
4.1 现象:pdfplumber提取的表格全是空行,但 PDF 在 Acrobat 里能正常复制
原因:PDF 使用了“文本重排”(Text Reordering)技术,文字坐标乱序,pdfplumber默认按 y 坐标排序失败。
解决:强制关闭坐标排序,改用table_settings={"vertical_strategy": "lines", "horizontal_strategy": "lines"}
# 错误写法(默认策略) table = page.extract_table() # 正确写法(显式指定策略) table = page.extract_table({ "vertical_strategy": "lines", # 按可见竖线分割 "horizontal_strategy": "lines", # 按可见横线分割 "snap_tolerance": 3, # 坐标容差像素值 })4.2 现象:OCR 识别“第十二条”变成“第十二奈”,且批量出现
原因:扫描件 DPI 过低(<150),导致“条”字下半部“丶”缺失,Tesseract 误判为“奈”。
解决:预处理图像——用 OpenCV 增强笔画对比度,而非单纯提高 DPI
import cv2 import numpy as np def enhance_pdf_page_image(pix): # pix 是 PyMuPDF 的 pixmap,转为 OpenCV 图像 img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n) gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 自适应阈值增强(比全局阈值更抗光照不均) enhanced = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) return enhanced # 在 OCR 前调用 enhanced_img = enhance_pdf_page_image(pix) ocr_text = pytesseract.image_to_string(enhanced_img, lang='chi_sim')4.3 现象:docx2python解析后,“第十二条”出现在header而非body
原因:律师在 Word 中把条款标题设为“页眉”而非“标题样式”,常见于模板文档。
解决:合并header+body内容,并用正则二次筛选——页眉内容通常重复出现且不含正文句式
# 合并所有区域 all_text = [] for region in ['body', 'header', 'footer']: if hasattr(doc, region): for para in getattr(doc, region): if para and para[0][0]: # 非空段落 text = "".join([seg[0] for seg in para]) # 过滤明显页眉(含“第X页”“共X页”) if not re.search(r'第\d+页|共\d+页|— \d+ —', text): all_text.append(text)4.4 现象:正则r'第\d+条'匹配到“第12345条”,但实际法律最大条数为 1254 条
原因:OCR 错误将“第十二条”识别为“第12345条”(数字连写),或文档含测试数据。
解决:添加合理性校验——法律条文编号极少超过 2000,且相邻编号差通常 ≤5
def validate_article_number(num_str): try: num = int(num_str) # 中国现行法律最长为《刑法》452条,《民法典》1260条 if num < 1 or num > 1500: return False # 检查是否为合理序列(避免 12345 这种连号) if len(num_str) > 4 and num_str.isdigit(): # 计算数字差值标准差,过大则异常 digits = [int(d) for d in num_str] if np.std(digits) > 2.0: # 连续数字 std≈0,随机数字 std>2 return False return True except: return False # 在 FSM 匹配后调用 if re.match(r'第(\d+)条', line): if not validate_article_number(re.match(r'第(\d+)条', line).group(1)): return None # 拒绝此匹配5. 验证与交付:用“条款指纹”和人工抽检双轨制确保交付质量
交付给业务方的不是原始文本,而是结构化 JSON 或数据库表。但如何证明“第十二条”没被切错?靠人工通读 2000 份文档不现实。我的方案是:自动生成条款指纹 + 分层抽检。
5.1 条款指纹:用 SHA256 哈希锁定条款唯一性
每条条款生成唯一指纹,包含编号、首句、末句、长度——即使 OCR 微小误差(如“的”变“地”),哈希也会变化,便于快速定位问题:
import hashlib def generate_article_fingerprint(article_dict): """ article_dict 示例: { "id": "民法典_12", "number": "第十二条", "content": "民事主体从事民事活动,应当遵循自愿原则...", "source_file": "民法典.docx", "page": 5 } """ # 取首句(按句号/分号/换行切分)、末句、长度 sentences = re.split(r'[。;\n]', article_dict["content"].strip()) first_sent = sentences[0][:50] if sentences else "" last_sent = sentences[-1][-50:] if sentences else "" # 拼接关键字段生成指纹 fingerprint_str = f"{article_dict['number']}|{first_sent}|{last_sent}|{len(article_dict['content'])}" return hashlib.sha256(fingerprint_str.encode('utf-8')).hexdigest()[:16] # 输出示例:fingerprint: "a1b2c3d4e5f67890"为什么不用全文哈希?法律文本常含页眉页脚、OCR 噪声,全文哈希极易变动。而“编号+首末句+长度”组合,在保证唯一性的同时,对噪声鲁棒。
5.2 分层抽检策略:按风险等级分配人工检查资源
不是所有条款都同等重要。我按业务影响分级,制定抽检比例:
| 风险等级 | 定义 | 抽检比例 | 检查重点 |
|---|---|---|---|
| P0(核心条款) | 法律名称、总则、罚则、生效日期等 | 100% | 编号准确性、首末句完整性、无截断 |
| P1(高频引用) | 合同法第52条、刑法第232条等常被引用条款 | 30% | 内容是否与权威版本一致(对接北大法宝 API 校验) |
| P2(普通条款) | 附则、实施细则等 | 5% | 是否存在乱码、空行、编号跳跃 |
# 自动生成抽检清单(按风险等级) def generate_audit_list(structured_articles, law_name): audit_list = [] for art in structured_articles: # P0:法律名称、总则、罚则关键词 if re.search(r'(名称|总则|附则|生效|废止|罚则|法律责任)', art["number"]): audit_list.append({"level": "P0", "article": art}) # P1:匹配高频条款库(本地 CSV) elif art["id"] in HIGH_FREQ_ARTICLES: # 如 ["刑法_232", "合同法_52"] audit_list.append({"level": "P1", "article": art}) # P2:其余 else: if random.random() < 0.05: # 5%概率 audit_list.append({"level": "P2", "article": art}) return audit_list # 输出为 Excel,供法务同事填写“是否正确”列5.3 交付物清单:不止是 JSON,还有可执行的验证脚本
交付给下游系统的不是静态文件,而是带验证能力的交付包。我打包时必含:
articles.json:标准结构化条款数据fingerprint_index.json:所有条款指纹映射表validate_delivery.py:运行即校验指纹一致性audit_sample.xlsx:分层抽检清单(含原始截图链接)
# validate_delivery.py 核心逻辑 def validate_delivery(json_path, fingerprint_path): with open(json_path, 'r', encoding='utf-8') as f: articles = json.load(f) with open(fingerprint_path, 'r', encoding='utf-8') as f: fingerprints = json.load(f) errors = [] for art in articles: expected_fp = fingerprints.get(art["id"]) actual_fp = generate_article_fingerprint(art) if expected_fp != actual_fp: errors.append(f"条款 {art['id']} 指纹不匹配:期望 {expected_fp},实际 {actual_fp}") if errors: print("❌ 验证失败:") for e in errors: print(e) return False else: print("✅ 验证通过:所有条款指纹一致") return True # 使用:python validate_delivery.py articles.json fingerprint_index.json最后说句实在话:做“法律.rar”项目,80% 时间花在确认“第十二条”到底是不是第十二条,而不是写模型。我坚持每次交付前,用validate_delivery.py跑一遍,再随机打开 3 个 P0 条款的原始 PDF 截图对照——这比任何 fancy 模型都管用。希望帮到你。
本文还有配套的精品资源,点击获取