简介:面向自然语言处理初学者与法律科技产品开发者,这份下载包提供了一套中文法律智能问答系统的工程化参考实现。压缩包共25个文件,约371.77MB,核心包括40000条法律问答数据集、Chinese-BERT-wwm-ext预训练模型(含pytorch_model.bin、config.json、vocab.txt)、句子相似匹配与数据分析的Python脚本、基于Flask/Web的应用入口及main.py,以及模板、静态资源和Visio系统流程图,基本覆盖从数据到部署的完整链路。目前已吸引140人学习下载,适合用于课程设计、毕业项目或法律咨询机器人原型开发。借助其中已训练模型与脚本,可以快速理解中文文本语义匹配思路,复用数据预处理与Web交互代码,再结合流程图梳理业务逻辑,有效降低从零搭建法律问答系统的门槛。
1. 拿到“法律.rar”之后:先别急着看文件,把资料库管道搭起来
接手某团队的历史资料时,最常见的交接物就是一个“法律.rar”压缩包。解压之后里面往往混着几百份判决书、合同扫描件、公证书和“新建文档(5).docx”这类命名的 Word,目录结构全凭打包人当时的心情。等要找出某一份涉诉文书时,只能一个文件夹一个文件夹地开,文件名乱码、PDF 打不开、DOC 老格式读不出,检索基本靠肉眼。这篇文章要解决的问题就是:把“一个匿名压缩包”变成“能按关键词秒查、按案号/日期/当事人筛选的本地法律资料库”。
这套流程适合谁?法务、律师助理、档案数字化岗位,以及所有被“历史文件包”折磨过的人。整条路线从解压编码治理开始,到正文抽取、全文索引、结构化入库结束,不依赖外部服务,离线环境就能跑通。新手可以按步骤复现,熟手可以直接拿走命令和参数。整篇文章没有平台限制,只要是能跑 Python 的机器,都能把“法律.rar”变成你的本地检索系统。
2. 解压与编码治理:让 GBK 文件名不乱码的落地写法
2.1 先摸清包里有什么:文件清单与类型画像
解压之前先花 30 秒把压缩包内容读一遍。很多“法律.rar”是几年间陆续往里面塞文件的结果,里面有重复文件、加密项、嵌套压缩包,甚至文件名带路径穿越的恶意项。一上来就完整解压,遇到加密文件会卡住等待密码,遇到异常文件名会把目录结构搞乱。常见做法是先用 rarfile 读取文件列表,做一次画像再动手。
import rarfile from collections import Counter from pathlib import Path rf = rarfile.RarFile("法律.rar") infos = rf.infolist() print("文件总数:", len(infos)) # 统计扩展名分布:.pdf / .doc / .docx / .txt / 其他 exts = Counter(Path(i.filename).suffix.lower() for i in infos if not i.is_dir()) print("扩展名分布:", exts) # 检查包内是否存在加密项,避免解压中途等待密码 encrypted = [i.filename for i in infos if i.needs_password()] print("加密项数量:", len(encrypted))这里依赖 rarfile 库,它本身不实现解压算法,底层要能调用到 unrar 或 unar。infolist()返回的是压缩条目元数据,不实际解压,所以这一步很快,适合在拿到陌生包时先跑一遍。needs_password()判断条目是否加密,如果加密项超过文件总数的一半,最好先跟交接方确认口令再继续,否则后续流程会中途断掉。
看完扩展名分布和加密情况后,再决定下一步。比如全是 PDF,就重点准备 PDF 抽取工具;有大量 .doc,就要提前准备 LibreOffice 转换;有加密项,先把密码拿到手。这个前置检查看起来不起眼,实际上能避免后面 80% 的返工。
2.2 用 rarfile 解压并强制修正文件名编码
进入解压环节时会遇到一个典型问题:很多 Windows 上打包的 RAR 文件,文件名用的是 GBK 编码,但 Python 的 rarfile 读取时会按 cp437 解码,结果就是解压出一堆类似“鍚堝悓.docx”的乱码文件名。这个问题在涉外项目资料、跨平台交接的压缩包里几乎必然出现,处理方式是在解压时手动做编码修正。
import rarfile from pathlib import Path rar_path = "法律.rar" out_root = Path("./law_corpus/raw") out_root.mkdir(parents=True, exist_ok=True) rf = rarfile.RarFile(rar_path) for info in rf.infolist(): raw_name = info.filename # 针对 GBK 被误读成 cp437 的情况,反转再解一次 try: fixed_name = raw_name.encode("cp437").decode("gbk") except (UnicodeEncodeError, UnicodeDecodeError): fixed_name = raw_name # 本来就是 UTF-8 或 ASCII 的不用动 # 路径穿越防护:确保解压目标不越出 raw 目录 target = (out_root / fixed_name).resolve() if not str(target).startswith(str(out_root.resolve())): print(f"跳过越界条目: {fixed_name}") continue if info.is_dir(): target.mkdir(parents=True, exist_ok=True) continue target.parent.mkdir(parents=True, exist_ok=True) with rf.open(info) as src, open(target, "wb") as dst: dst.write(src.read()) print("解压完成:", len(rf.infolist()), "个条目")这段代码是解压流程的核心。encode("cp437").decode("gbk")的原理是:rarfile 已经用 cp437 把原字节解码成字符串,我们先用 cp437 把它还原成原始字节,再用 GBK 正确解码,从而恢复“合同.pdf”这样的真实文件名。resolve()配合startswith校验,是为了防止压缩包里的条目写作../../evil.sh把文件写到 raw 目录之外,这种条目名字在恶意压缩包里并不少见。
如果你拿到了加密包,在构造rarfile.RarFile(rar_path, pwd=b"123456")传入口令即可。注意RarFile的pwd参数接收的是字节串不是字符串。另外有些 RAR5 压缩包用新版算法,旧版 unrar 解不了,报错信息通常是“Unsupported RAR5”,这个放到第 5 章避坑里展开。
2.3 建工作目录并保存解压清单
解压完成后,不要急着把所有文件塞在一个目录里。后续要做正文抽取、全文索引、结构化入库,文件会经历多次形态变化。我会按四个子目录划分:raw/保存原始文件,只读不动;text/保存抽取出的 UTF-8 纯文本;meta/保存清单和分类结果;db/放最终生成的 SQLite 数据库。
from pathlib import Path import json base = Path("./law_corpus") for sub in ["raw", "text", "meta", "db"]: (base / sub).mkdir(parents=True, exist_ok=True) # 保存解压映射,方便回溯“数据库里某条记录到底来自压缩包里哪个文件” manifest = [] for info in rf.infolist(): if info.is_dir(): continue try: fixed_name = info.filename.encode("cp437").decode("gbk") except (UnicodeEncodeError, UnicodeDecodeError): fixed_name = info.filename manifest.append({ "src": info.filename, "dst": str((base / "raw" / fixed_name).relative_to(base)), "size": info.file_size }) with open(base / "meta" / "manifest.json", "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2)manifest 文件的价值在于可追溯。等到建完索引、数据库里的内容已经随时可查时,你可能会反过来问“这份文书在压缩包里的原始路径是什么”,这时只需查 manifest 就能立刻还原。文件名乱码、类型异常、夹带无关文件的判断,也都能基于这份清单完成。实际项目中我通常还会顺带统计重复文件(按文件大小和名字粗排),因为历史压缩包里的重复文件会拖慢后续索引速度。
3. 正文抽取与文件分类:把 PDF/DOC 变成能检索的纯文本
3.1 按扩展名分发的文本抽取函数
全文检索的前提是把 PDF、DOC、DOCX 等二进制格式转成纯文本。直接对二进制内容做索引不仅占空间,而且 MATCH 时无法利用分词,检索效果很差。这一步的目标是:对raw/下每个文件抽出一份 UTF-8 编码的.txt,写入text/目录,文件名和原始文件名保持一致(扩展名换成 .txt),方便前后对照。
from pathlib import Path import pdfplumber from docx import Document def extract_pdf(path: Path) -> str: pages = [] with pdfplumber.open(path) as pdf: for page in pdf.pages: # 扫描件这一页返回 None,不塞入结果 t = page.extract_text(x_tolerance=2, y_tolerance=2) if t: pages.append(t) return "\n".join(pages) def extract_docx(path: Path) -> str: doc = Document(path) return "\n".join(p.text for p in doc.paragraphs if p.text.strip()) raw_dir = Path("./law_corpus/raw") text_dir = Path("./law_corpus/text") text_dir.mkdir(exist_ok=True) for src in raw_dir.rglob("*"): if src.is_dir(): continue suffix = src.suffix.lower() try: if suffix == ".pdf": body = extract_pdf(src) elif suffix == ".docx": body = extract_docx(src) elif suffix == ".txt": body = src.read_text(encoding="utf-8", errors="ignore") else: continue # .doc / .wps 走 3.2 的转换流程 except Exception as exc: print(f"抽取失败: {src.name} -> {exc}") continue if body.strip(): out_path = text_dir / (src.stem + ".txt") out_path.write_text(body, encoding="utf-8")pdfplumber的两个参数值得单独解释:x_tolerance控制同一行内文字块的横向合并距离,法律文书经常是双栏排版,默认值容易把左右两栏串成同一行;调大这个值(比如 3~5)可以避免串栏,但调太大又会让分段丢失。y_tolerance控制行间合并,遇到表格类文书时如果发现表格内容被拆得七零八落,优先调大它。docx 抽取用 python-docx 的paragraphs就够了,表格内容会被漏掉,如果包里有大量表格型文书,需要补一段遍历doc.tables的逻辑,这里不展开。
3.2 老版 DOC 和其他格式:用 LibreOffice 批转换
.doc是老版 Word 的二进制格式,python-docx 不支持解析;.wps是办公软件的私有格式,Python 生态里也没有稳定的解析库。常见的可靠方案是装一个 LibreOffice,用命令行无界面模式批量转成 docx,再走 3.1 的抽取流程。
mkdir -p ./law_corpus/converted soffice --headless --convert-to docx --outdir ./law_corpus/converted ./law_corpus/raw/*.doc--headless表示无界面运行,适合在服务器或后台执行;--convert-to docx指定目标格式;--outdir指定输出目录;最后接要转换的文件列表。转换完成后,把converted/目录下的文件也纳入 3.1 的遍历范围(或者把它们复制回 raw 目录再重跑一遍抽取脚本)。整个过程可能耗时较长,几百份 DOC 文件转换大概要几分钟到十几分钟,看机器性能。如果包里有加密 DOC,LibreOffice 会跳过并输出错误报告,需要在转换日志里单独收集文件名。
这里有一个容易翻车的细节:LibreOffice 转换后的 docx 文件,文件名如果含有空格或特殊字符,包一层双引号。在 Python 里建议用subprocess.run传参数列表,不要用 shell 字符串拼接,否则遇到(5)这种文件名里的括号就有概率出问题。
3.3 用“文件特征表”给法律文书打分类标签
抽完正文后,下一步是给每份文档打一个粗分类标签。常见的法律文件类型包括:裁判文书、合同类、诉讼文书(起诉状/上诉状/申请书)、公证文书、法律法规。分类的意义在于查询时能按类别过滤,比如“只看合同类”和“只看判决书”的检索场景完全不同。
分类不需要引入机器学习模型。几百份文件的量级,用关键词加权规则表就够了;分类错了也不致命,因为最后检索是全文级召回,类别只做过滤维度。
RULE_SCORES = { "合同": {"合同类": 3, "裁判文书": 1}, "协议": {"合同类": 2}, "判决书": {"裁判文书": 4}, "裁定书": {"裁判文书": 4}, "调解书": {"裁判文书": 3}, "起诉状": {"诉讼文书": 3}, "上诉状": {"诉讼文书": 3}, "申请书": {"诉讼文书": 2}, "公证书": {"公证文书": 3}, "条例": {"法律法规": 4}, "办法": {"法律法规": 3}, "法": {"法律法规": 2}, } def classify_doc(title: str, head_text: str) -> str: scores = {} head = head_text[:800] for keyword, mapping in RULE_SCORES.items(): if keyword in title or keyword in head: for label, weight in mapping.items(): scores[label] = scores.get(label, 0) + weight if not scores: return "未分类" return max(scores, key=scores.get)classify_doc接收文件名和正文前 800 字,返回分类标签。为什么只取前 800 字?因为法律文书的首部通常是“××人民法院民事判决书”“甲方与乙方合同纠纷”这类信息,能覆盖绝大部分类型特征;取全文会引入尾部条款、落款等干扰。关键词权重上,出现“判决书”直接给 4 分,几乎可以一锤定音。这个方法对“未分类”的容忍度要够,宁可标记为未分类,也不要硬塞到错误类别里,未分类的文档在检索时不会丢,只是少了过滤入口。
运行完这步,建议把title + category写回到meta/classified.json,后面建索引时要读这份文件。
4. 本地全文检索与结构化抽取:用 SQLite FTS5 把卷宗变成数据
4.1 为什么本地检索不直接 grep:中文分词与命中排序
拿到几百份纯文本后,最朴素的做法是grep -r "合同纠纷" ./text/。文件少的时候确实够用,但一旦上千份,问题就来了:grep 不支持中文分词,“合同纠纷”四个字连续出现才能命中,而文书正文里到处是“因合同产生纠纷”“合同项下的纠纷”这类打断词;grep 也没有相关性排序,命中结果按文件名排,一次查出几十条,哪条最重要看不出来。这个规模下,常见做法是引入全文检索引擎。
完整部署一个搜索引擎在本地环境显得过重。SQLite 的 FTS5 扩展是更务实的选择:单文件、零服务、Python 标准库自带支持,几千份文书的体量完全能扛住。它支持分词、短语查询、snippet()高亮片段,和 grep 相比是从“能不能找到”进化到了“能不能找得快、找得准”。
4.2 建 FTS5 索引并写入分词结果
FTS5 默认的 unicode61 分词器对中文支持约等于按单字拆分,查询“合同”会变成查“合”和“同”两个字,噪音很大。实际做法是在写入索引前先用 jieba 做中文分词,把分词结果以空格分隔存进一个专门的列,MATCH 时对该列进行匹配。原文列保留,用于snippet()展示上下文。
import sqlite3 import json import jieba conn = sqlite3.connect("./law_corpus/db/law.db") conn.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS fts_docs USING fts5( title, category, tokens, content ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS doc_meta( id INTEGER PRIMARY KEY, title TEXT, category TEXT, case_no TEXT, case_date TEXT, parties TEXT, path TEXT ) """) def cut_to_tokens(text: str) -> str: # join 成空格分隔,FTS5 MATCH 时按空格切分 return " ".join(jieba.cut(text)) with open("./law_corpus/meta/classified.json", encoding="utf-8") as f: docs = json.load(f) data = [] for idx, doc in enumerate(docs): title = doc.get("title", "") category = doc.get("category", "未分类") body = doc.get("text", "") tokens = cut_to_tokens(title + "\n" + body) data.append((idx, title, category, tokens, body)) conn.executemany( "INSERT INTO fts_docs(rowid, title, category, tokens, content) VALUES (?,?,?,?,?)", [(i, t, c, tk, b) for i, t, c, tk, b in data] ) conn.commit() print("入库完成:", len(data), "条")这里让 FTS5 表的rowid和doc_meta.id保持一致,是为了之后用 rowid 关联两张表。tokens列存 jieba 分词后的文本,content列存原文。注意INSERT时显式指定了 rowid,保证两侧能对上号。cut_to_tokens里把 title 也拼进分词范围,这样“只看标题”的检索场景用title:关键词也能命中。
如果不想引入 jieba 依赖,还可以把建表语句改成tokenize = 'trigram',FTS5 的 trigram 分词器会把中文按每 3 个字切一组,查询时也能命中但边界效果差一些。我个人习惯用 jieba,控制力更强,而且分词结果本身也是后续统计的素材。
4.3 查询:关键词匹配与高亮
查询接口的设计要同时满足两类输入:用户可能输入一个完整词,也可能输入一句话。所以查询前同样要经过 jieba 分词,只是不再做拼接全文,而是把用户输入切成词后空格连接,交给 MATCH。snippet()函数负责生成命中片段的高亮文本。
def search(keyword: str, category: str = "", limit: int = 20): tokens = " ".join(jieba.cut(keyword)) sql = """ SELECT f.title, f.category, snippet(f, 0, '[', ']', '…', 8), m.case_no, m.case_date FROM fts_docs f LEFT JOIN doc_meta m ON m.id = f.rowid WHERE f.tokens MATCH ? """ params = [tokens] if category: sql = sql.replace("WHERE f.tokens MATCH ?", "WHERE f.tokens MATCH ? AND f.category = ?") params.append(category) sql += " LIMIT ?" params.append(limit) cur = conn.execute(sql, params) for title, cat, snip, case_no, case_date in cur.fetchall(): print(f"[{cat}] {title} | {case_no or '无案号'} | {case_date or '无日期'}") print(" ", snip)snippet(f, 0, '[', ']', '…', 8)的参数依次是:表名、列号(0 对应 title 列)、命中词前缀、后缀、省略号、最多显示的词块数。如果要高亮正文,把列号改成 3 即可(对应 content 列)。MATCH 语法里,空格分隔的多词默认是 AND 关系,比如“合同 纠纷”要求同一行同时出现两个词;“合同纠纷”作为一个短语查询,则需要用双引号包成"合同纠纷"。实际使用的法务同事通常会输入“某某公司 合同”这种形式,AND 语义正好符合预期。
4.4 案号、日期、当事人:用正则提前入库
全文检索解决了“找得到”的问题,但法律文书的查询往往更具体:“某法院 2023 年的所有判决书”“某个案号的合同”。所以在建索引之后,再用规则从正文里抽取案号、日期、当事人,写入 doc_meta 的结构化字段。
import re CASE_NO_RE = re.compile( r"[((]\s*(19|20)\d{2}\s*[))]\s*[\u4e00-\u9fa5A-Za-z]{1,10}\d+号" ) DATE_RE = re.compile( r"(19|20)\d{2}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日" ) PARTY_RE = re.compile( r"(原告|被告|上诉人|被上诉人|申请人|被申请人)[::]?\s*([^\s,。;]{1,30})" ) def parse_case_meta(text: str) -> dict: case_no = "" m = CASE_NO_RE.search(text) if m: candidate = m.group(0) # 案号长度范围校验,过滤掉“2023年第2号公告”之类干扰 if 6 <= len(candidate) <= 40 and candidate.endswith("号"): case_no = candidate date_str = "" m = DATE_RE.search(text) if m: y, mo, d = re.findall(r"\d+", m.group(0)) date_str = f"{y}-{int(mo):02d}-{int(d):02d}" parties = PARTY_RE.findall(text) if parties: parties_str = ";".join(p[1].strip() for p in parties[:6]) else: parties_str = "" return {"case_no": case_no, "case_date": date_str, "parties": parties_str}这套正则故意做得宽松。法律文书里的案号格式极度不统一,有“(2023)京01民终1234号”,也有“(2023)穗仲案字第5678号”,甚至还有“第〔2023〕19号”。框死每一种格式写出来的正则迟早被新格式打破。宽松匹配加后置校验的实际效果远好于严格正则,candidate.endswith("号")和长度限制能过滤掉大部分误匹配。日期直接转成 ISO 格式的意义在于:之后可以用case_date BETWEEN '2023-01-01' AND '2023-12-31'做区间筛选,这是原生态“2023年1月1日”很难做到的。
当事人抽取只做了最朴素的“角色 + 冒号 + 姓名”提取,遇到“原告认为”“被告辩称”这种句子会误抽,但作为初步结构化数据已经够用;重点案卷的精细化人员提取,可以之后用命名实体识别模型补。抽取结果直接UPDATE doc_meta SET case_no=?, case_date=?, parties=? WHERE id=?回写库,再把 doc_meta 增加一个review字段标记“待人工复核”的条目。
5. 避坑指南:法律文档处理的 5 个高频翻车现场
5.1 解压阶段的坑:乱码、依赖缺失、路径穿越
乱码。现象:解压出来文件名全是“鍚堝悓.docx”之类。原因:Windows 下打包时文件名按 GBK 编码,rarfile 却按 cp437 解码,形成了双编码错误。解决:用raw_name.encode("cp437").decode("gbk")逆操作恢复;但前提是探测结果确认包内确实属于“GBK 被误读成 cp437”的情形,如果包内本身就是 UTF-8 文件名,再用这套反转反而会把正常文件名弄坏。实操里我会抽样取 5~8 个乱码名字,分别用 GBK 和 UTF-8 试图解码,看哪个方向能恢复出可读的中文,再决定统一策略。
依赖缺失。现象:rarfile抛出RarCannotExec: Cannot find unrar或者Unsupported RAR5。原因:rarfile 只是封装层,真正的 RAR 解压算法需要外部可执行文件。解决:Linux 下装unar通常一劳永逸,处理 RAR5 也更稳定;unrar-free对付老 RAR 还行,新版格式容易中途失效。项目里如果允许调用外部命令,直接用unar 法律.rar -o ./law_corpus/raw/做预处理,再进入 Python 流程,能省掉不少底层兼容的力气。
路径穿越。现象:解压后raw目录之外多出了文件。原因:压缩包内的条目名呈现../../evil.html这种写法,解压工具没有做边界校验。解决:第 2 章代码里resolve()加startswith前缀判断。这个坑在正式的项目资料里不常见,但从陌生人处收到的压缩包必须防一手,放到自动化流程里成本又低。
5.2 文本抽取阶段的坑:扫描件、老版 DOC、PDF 加密
扫描件。现象:PDF 用 pdfplumber 抽出来是空字符串,或者只有页眉页脚。原因:文件本质是图片,没有文本层。解决:接 OCR,常见的做法是用 PaddleOCR 或 tesseract 把扫描页转成文字。OCR 的准确率在印刷体判决书上通常有 90% 以上,但案号里的“0/O”混淆、印章盖住正文的段落,会直接导致抽取字段出错。所以 OCR 文本进入索引时没问题,进结构化字段前必须要人工抽检。如果文档同时存在文本版和扫描版,优先用文本版,OCR 只是兜底。
老版 DOC。现象:python-docx 读.doc抛异常或者读出空文档。原因:.doc是 OLE 复合文档格式,python-docx 只支持 OOXML 的.docx。解决:用 3.2 的 LibreOffice 批转换。注意转换后的 docx 表格样式可能轻微变形,但纯文本内容基本无损,对检索场景够用。
PDF 加密。现象:pdfplumber 报file has not been decrypted。原因:PDF 设置了打开口令或权限口令。解决:有授权口令时pdfplumber.open(path, password="...");口令未知时把文件名记入blocked.txt,随后单独找交接方核实。千万别在批量脚本里遇到加密 PDF 就中断,捕获异常、记录清单、继续下一个,是这类批处理任务的基本素养。
5.3 检索与入库阶段的坑:分词失效、正则误伤、大批量插入慢
中文搜不出。现象:正文里明明有“合同纠纷”,搜索“合同纠纷”却一条都查不到。原因:FTS5 默认分词器按单字拆中文,MATCH 短语时匹配行为不符合预期。解决:写入时用 jieba 分词并单独存 tokens 列,查询时对用户输入同样分词,两边口径一致才能命中。加了 jieba 还不行的场景,通常是词库没有收录人名地名,例如“某某建设集团”被切成单字,需要加载自定义词典,把高频当事人名称加进jieba.load_userdict。
正则误伤。现象:案号字段里混进“2023年第2号公告”“2023年1号令”。原因:案号正则写得太宽。解决:第 4 章的案号正则加了两层后置校验(长度限制、号结尾),但仍会在特殊文书上误伤。稳妥起见,结构化入库后按案号做一次 DISTINCT 检查,看到明显不是案号的字符串,当场补正则规则。
大批量插入慢。现象:几千份文档入库耗时几分钟以上。原因:逐条execute并且每条都commit,事务开销全花在写盘同步上。解决:改为executemany一次性批量写入,结束时commit一次。如果内存紧张,按 500 条一批处理,每批 commit 一次,速度和可靠性平衡点在这个量级最合适。
6. 把整套流程封成一个命令:查询、验证与后续扩展
6.1 一键管道:从压缩包到可检索数据库
前面的步骤拆开讲是为了把原理和坑说透,实际使用时要把它们串成一条管道。日常做法是把“解压→抽取→分类→入库→结构化”这些函数收进同一个脚本,对外暴露一个入口:
import sys from pathlib import Path def legal_pipeline(rar_path: str): # 1. 解压并修正编码,写 manifest unrar_with_fix(rar_path) # 2. 正文抽取:PDF/DOCX -> text/ extract_all_text() # 3. 分类 + 文本入库 + 结构化抽取 build_law_index() print("完成: ./law_corpus/db/law.db") if __name__ == "__main__": target = sys.argv[1] if len(sys.argv) > 1 else "法律.rar" legal_pipeline(target)入口参数只有一个压缩包路径,默认取当前目录下的“法律.rar”。日常查询时,直接跑一条命令:
python query.py "某某公司 合同纠纷" --category 裁判文书 --limit 10query.py内部调用第 4 章的search()函数,把结果打印成“标题 | 案号 | 日期 + 高亮片段”的格式。这个封装完成后,处理新的压缩包不再需要打开编辑器改代码,交接多少个“法律.rar”都能在同一个命令下处理干净。
6.2 正确性验证与后续扩展
第二步是验证索引和结构化字段的准确性。实操时我通常挑三份不同类型且内容已知的文件作为“金标准”,一份裁判文书、一份合同、一份公证书,跑完管道后人工核对这三份的案号、日期、当事人是否抽对,再抽查一批:
SELECT title, case_no, case_date, parties FROM doc_meta WHERE case_no IS NOT NULL LIMIT 20;全文检索侧的验证方式是用 5~10 组关键词做召回测试:把关键词换成“不在标题但确定在正文”的写法,确认仍能命中。这一步通过后,这套管道才算交付。
后续扩展的方向通常有三条:一是给扫描件专门加 OCR 队列,没抽出文本的文件自动排队进 OCR;二是当文书量到几万份时,在 FTS5 基础上加一个语义检索接口,用嵌入模型把每条正文向量化,支持“合同是否显失公平”这类的语义查询;三是利用已入库的案号和日期字段做统计分析,比如按月统计案件量、按当事人汇总涉诉次数,这些数据在 doc_meta 结构化之后都是现成的。我自己第一次整理某团队的历史卷宗时,就因为没做案号抽取,事后按当事人反查全靠肉眼扫文档,白白耗掉一整天。后来所有“法律.rar”拿到手,第一件事就是跑一遍管道再交差,几个压缩包处理下来,检索和统计省下的时间远超搭建脚本的投入。希望这套路线对你也有同样的效果。
本文还有配套的精品资源,点击获取