简介:一份面向法学专业学生、考研与法考备考者的刑事诉讼法学复习资料,内容围绕课程核心框架展开,系统梳理了诉讼与刑事诉讼的基本概念、特征和分类,广义与狭义刑事诉讼的界定,刑事诉讼法渊源及其与宪法、刑法等相邻法律部门的关系。资料还详细讲解了刑事诉讼历史类型的划分,涵盖奴隶制、封建制、资本主义与社会主义刑事诉讼的本质特征,以及弹劾式、纠问式、混合式诉讼形式,并对职权主义与当事人主义两种现代诉讼模式进行了对比分析,适合期末考试冲刺和基础体系构建。包内为1个doc文档,大小约174KB,按章节组织,重要概念附有思考题,便于边学边练、查漏补缺。目前已有91人学习下载,适合希望快速建立刑事诉讼法学知识框架、理清历史沿革与诉讼模式差异的读者。
1. 一份 .doc 刑诉资料,凭什么值得当工程做
一份《刑事诉讼法学资料全.doc》,在绝大多数人手里是双击打开、滚动翻页、偶尔按几下 Ctrl+F。但如果你做过法律资料整理、知识库构建,或者给法务团队做过检索工具,就会知道 .doc 这种老式二进制格式是效率黑洞:段落内换行混乱、目录域残留、法条版本混排,复制出来全是乱码。把一份原本可以直接阅读的资料变成可检索、可比对、可追溯版本的结构化数据,需要一整套读取、清洗、切分和检索方案。
下面按处理链路讲:.doc 怎么无损转成文本,中文编码坑在哪,怎么把“编/章/条”骨架抽出来,再用 FTS5 和语义向量做本地检索。最后落到一个实际技巧:旧法条识别和重复段去除。整条链路在单机就够用,不依赖在线服务,适合法务、备考资料管理者和做法律文本工程的开发者。
2. 先把 .doc 请进文本世界:读取与编码防线
2.1 别用 open() 硬读:.doc 是 OLE2 复合文档
先说要打破的直觉:.doc 不是一个纯文本文件。它的物理结构是 OLE2 复合文档,本质是一个小型的“文件系统”,内部划分了若干 stream,正文数据存储在 WordDocument 流里,多数情况下按 UTF-16 编码保存。所以在 Linux 或 Windows 上直接用 open() 读出来,看到的是二进制乱码,而不是中文字符。
先做文件头探测,确认手里到底是不是真 .doc:
from pathlib import Path def sniff_doc_type(path: str) -> str: head = Path(path).read_bytes()[:8] if head.hex() == "d0cf11e0a1b11ae1": return "ole2 / .doc" if head[:4] == b"PK\x03\x04": return "zip / .docx" return "unknown" print(sniff_doc_type("刑事诉讼法学资料全.doc"))这个函数读取文件前 8 字节做判断。OLE2 文件固定以 d0cf11e0a1b11ae1 开头,docx 实质是 zip 容器,以 PK 开头。判明类型后再选工具,能避免拿文本工具硬解二进制文件的无效操作。
提示:如果文件头是 PK,说明这份资料虽然叫 .doc,实际是 docx 改名,直接交给 python-docx 处理即可,不需要走下面的转换链路。
2.2 两条转换路线:antiword 与 LibreOffice 无头模式
把 .doc 转成纯文本的常见做法有两种:轻量的 antiword,和功能更全的 LibreOffice 无头转换。antiword 胜在快,适合只要正文的场景,页眉页脚、表格和域代码会丢;LibreOffice 能最大程度保留版式,代价是转换慢、依赖体积大。
先看 antiword 路线:
# Debian/Ubuntu 系安装 sudo apt install antiword # 输出 UTF-8 纯文本 antiword -m UTF-8 刑事诉讼法学资料全.doc > corpus.txt-m 参数指定输入字符集并映射到 UTF-8 输出。对中文法律资料,这个参数必须加,否则会按 Latin-1 输出,中文全变成问号。
LibreOffice 路线更适合需要保留结构的场景:
soffice --headless --convert-to docx --outdir ./out 刑事诉讼法学资料全.doc soffice --headless --convert-to "txt:Text (UTF8)" --outdir ./out 刑事诉讼法学资料全.doc第一条命令把 .doc 转成 docx,供后续 python-docx 读取;第二条直接导出 UTF-8 文本。--headless 表示不启动图形界面,--outdir 指定输出目录。转换后建议先检查有没有目录残留:
grep -n "目录" out/刑事诉讼法学资料全.txt | head老文档的目录域在纯文本导出时经常残留,会出现“第 X 章……页码”这类行。发现后写个正则把整个目录区剔掉,而不是手工删。
2.3 中文乱码与编码探测的兜底策略
文本文件在反复转换过程中最容易翻车的就是编码。.doc 内部正文按 UTF-16 存储,但不同 Word 版本、不同输入法产生的碎片内容可能混入 GBK 字节。给一个自动探测的兜底函数:
from charset_normalizer import from_bytes raw = Path("unclear.txt").read_bytes() best = from_bytes(raw).best() text = str(best) print(best.encoding, best.percentage)charset_normalizer 会返回每个候选编码的置信度。实际操作中,如果 best.encoding 是 gb18030 或 gbk,说明文档在某个中间环节被当作简体中文编码处理过;如果返回 utf-8 且 percentage 偏低,要手动确认文件开头有没有 BOM。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 全文出现“锟斤拷”“锘锘” | UTF-8 被误按 GBK 解码再存盘 | 放弃损坏的中间文件,重新从 .doc 转换 |
| 打开正常,代码读入报 UnicodeDecodeError | 文件含 GBK 字符未指定编码 | 用 charset_normalizer 探测,或直接以 gb18030 读取 |
| 段落末尾大量 \u3000 全角空格 | Word 自动排版产生 | 正则替换为半角空格,再做后续切分 |
| 转换后正文是空文件 | 原文档内容在文本框或内容控件里 | 改用 LibreOffice 转 docx 后检查 document.xml |
这一章把“能读出来”做到位,下一步才是对文本结构化。
3. 按刑诉知识体系切块:从段落流到结构化条目
3.1 识别三类内容:法条、司法解释、复习笔记
一份敢叫“资料全”的刑诉文档,通常不只有《刑事诉讼法》条文。整理过的资料一般混着三类内容,处理前先做粗分类,否则把司法解释混入法条全文,后面的检索和版本对比都会失真。常见做法是:以“第X条”开头的段落入法条库;“解释”“规定”“纪要”标题后接的条目进入规则库;其余带“重点”“常考”“注意”等提示词的进入笔记库。
3.2 用正则建立「编/章/条」三级骨架
刑诉法按“编—章—条”组织,编下分章、章下分条、条内分款,这条层级天然适合正则抽取。先做字符规整,把全角符号清掉:
import re from pathlib import Path text = Path("corpus.txt").read_text(encoding="utf-8") text = text.replace("\u3000", " ").replace("(", "(").replace(")", ")")然后抽取出“编/章/条”的位置信息:
pattern = re.compile( r"(?ms)^(?P<level>第[一二三四五六七八九十百]+编|" r"第[一二三四五六七八九十百]+章|" r"第[一二三四五六七八九十百]+条)\s*(?P<title>.*?)$" ) items = [] for m in pattern.finditer(text): items.append({ "level": m.group("level"), "title": m.group("title").strip(), "start": m.start(), })有了每个节点的起始位置,把相邻节点之间的文本挂到前一个节点上:
for i, item in enumerate(items): item["end"] = items[i + 1]["start"] if i + 1 < len(items) else len(text) item["body"] = text[item["start"]:item["end"]].strip()这段切分逻辑把“第X编”作为最高层,“第X章”挂到当前编下,“第X条”挂到当前章下。正则里的 (?m) 让 ^ 匹配每行行首,配合 (?s) 让 .*? 跨行时保持非贪婪,避免一条规则把后面所有条文都吞进去。
提示:注意“第X条”和“第X款”的差异。款是条下面的段,行首出现“第X款”时不应切出新的条节点,上面正则只匹配编/章/条三个层级,正是为了规避这个问题。
3.3 法条引用与要点抽取
刑诉资料里大量出现“依照本法第一百七十五条的规定”“参见第七十一条”这类交叉引用。把引用提取出来,既能构建条文之间的关联关系,也能校验资料完整性——被引用的条号如果不在抽取结果里,说明资料有截断。
refs = [] for m in re.finditer(r"(本法|本规定)?第?[一二三四五六七八九十百零]+条", text): refs.append(m.group(0)) from collections import Counter for ref, cnt in Counter(refs).most_common(20): print(cnt, ref)再配合知识点词表做主题标注,比如“取保候审”“监视居住”“补充侦查”“不起诉”“速裁程序”这些制度和程序词,命中后把所在条目打上标签。主题标签是后面语义检索的稀疏信号,能显著提升精确检索的召回率。
切分结果可以直接导出为 JSON 或写入 SQLite 表。到这里,原始 .doc 已经从“一份文档”变成了“一份可查询的法律数据”。
4. 文档变知识库:本地检索与语义召回
4.1 用 SQLite FTS5 把章节提速到毫秒级
切分后的条文和笔记条目,量级通常在千条以内。这时候不需要上 Elasticsearch,单机 SQLite 自带的 FTS5 全文索引足够。建表和导入:
import sqlite3, json conn = sqlite3.connect("criminal.db") conn.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS doc_items USING fts5( level, title, body, tokenize = 'trigram' ) """) with open("items.json") as f: items = json.load(f) conn.executemany( "INSERT INTO doc_items(level, title, body) VALUES (?, ?, ?)", [(i["level"], i["title"], i["body"]) for i in items] ) conn.commit()这里把 tokenize 设为 trigram,不是默认的 unicode61。unicode61 按空格和标点切分 token,对中文句子基本只能整句匹配,MATCH '取保候审'无法命中“被取保候审的人”这种变体。trigram 是三个字符的滑动窗口,中文子串也能命中,代价是索引体积约为原文的 3 到 4 倍,千条级别无压力。
查询支持布尔组合和摘要高亮:
rows = conn.execute( "SELECT level, title, snippet(doc_items, 2, '[[', ']]', '...', 8) " "FROM doc_items WHERE doc_items MATCH ?", ('"取保候审" AND "监视居住"',) ).fetchall()snippet 返回命中片段,参数依次是表名、列序号、前缀、后缀、省略号、最大上下文词数。用 [[ ]] 包住命中词,方便界面直接做高亮。
| 分词器 | 中文子串匹配 | 索引体积 | 适用场景 |
|---|---|---|---|
| unicode61 | 不支持 | 小 | 英文或已用空格间隔的中文文本 |
| trigram | 支持 | 约为原文 3-4 倍 | 中文法律条款、笔记检索 |
| icu | 支持(需 ICU 扩展) | 中 | 已安装 SQLite ICU 扩展的环境 |
4.2 嵌入向量召回:给「取保候审和监视居住区别」这类问法找答案
FTS5 解决的是关键词命中,但用户经常不按条文原话提问。问“取保候审和监视居住有什么区别”,条文里根本不会同时出现这两个词和“区别”的组合,关键词检索无能为力,需要用向量召回。
常见做法:把切分出的条文和笔记条目用中文 embedding 模型编码成向量,查询时把问题也编码,再算余弦相似度取 top-k。整条链路本地可跑:
import numpy as np from sentence_transformers import SentenceTransformer model = SentenceTransformer("moka-ai/m3e-small") def embed_texts(texts, batch=32): return model.encode(texts, batch_size=batch, normalize_embeddings=True) vecs = embed_texts([i["body"] for i in items]) np.save("item_vectors.npy", vecs)normalize_embeddings=True 会把向量归一化为单位长度,这样余弦相似度可以退化为点积计算,查询更快。向量直接存成 .npy 文件,万条以内不必引入向量数据库。
查询阶段:
query_vec = embed_texts(["取保候审和监视居住有什么区别"])[0] scores = vecs @ query_vec topk = np.argsort(scores)[::-1][:5] for idx in topk: print(items[idx]["title"], round(float(scores[idx]), 3))这里有个现实问题:通用中文模型在法律术语上的语义空间和通用语料不完全一致。选型时可以在自己切分好的条文上对比几个候选模型的命中率,再决定用哪个,不要只看模型榜单分数。
4.3 一个 Streamlit 对话式检索样板
把关键词检索和向量召回拼到一个本地界面里,可以直接交给团队里非技术的成员使用:
import streamlit as st import sqlite3, numpy as np conn = sqlite3.connect("criminal.db") vecs = np.load("item_vectors.npy") st.header("刑诉资料检索") q = st.text_input("输入问题") top_k = st.slider("返回条数", 1, 20, 5) if q: rows = conn.execute( "SELECT title, body FROM doc_items WHERE doc_items MATCH ? LIMIT ?", (f'"{q[:12]}"', top_k) ).fetchall() for title, body in rows: st.markdown(f"**{title}**") st.write(body[:200])这段代码已经可以跑起来用。trigram 匹配模式下用问题前 12 个字符做查询串,命中率已经不错。正式使用再把语义召回按钮加进去,把上一节的 top-k 结果并列展示,用户对两种召回方式的差异会有直观感知。
5. 进阶:版本时效与重复内容的处理技巧
5.1 用修正时间线过滤过期条文
刑诉法从 1979 年制定至今经历多次修正,备考资料和法律数据库里混着旧条文是常态。作为工程问题,它本质上是给条文做版本标注。现行刑事诉讼法是 2018 年修正后的文本,资料里出现的旧条号在不同版本中的指向可能完全不同。
常见做法不需要理解法律内容:先从文本中抽“修正”“修改”“根据……的决定”这类标志性句子,判断一个段落属于旧版摘要还是现行条文;再对同一编号的条文做向量相似度比对,把相似度低于阈值的旧条目标记出来,提示人工复核。这一条比逐字阅读全文效率高得多,也适合交给非专业人员做初筛。
5.2 SimHash 去除笔记里的重复段
“资料全”的负面效果是同一个知识点在文档里出现多次:开头是条文原文,中间是讲义,最后是复习提纲。不去重的话,检索结果里会被相似段落刷屏。精确去重用 md5,但两段文字差几个字就完全匹配不上,更适合用 SimHash 做近似去重:
import re from hashlib import md5 def simhash(text: str, bits: int = 64) -> int: tokens = re.findall(r"[\u4e00-\u9fff]{2,}", text) v = [0] * bits for tok in tokens: h = int(md5(tok.encode()).hexdigest(), 16) for i in range(bits): v[i] += 1 if (h >> i) & 1 else -1 fingerprint = 0 for i in range(bits): if v[i] > 0: fingerprint |= 1 << i return fingerprintSimHash 的核心是把每段文字压成一个固定长度的指纹,汉明距离小于等于 3 的两个指纹视为重复。上面的实现对连续两个及以上中文字作为 token,足够处理法律文本。流程是:对每个 token 算 md5 哈希,按哈希每一位做投票(1 加 1、0 减 1),最后按位取符号得到指纹。
相似判断:
def is_duplicate(a: int, b: int, threshold: int = 3) -> bool: return (a ^ b).bit_count() <= threshold计算指纹后,把指纹按前 16 位分桶,新段落只和同桶内的指纹比汉明距离,避免全量两两比较。把这个去重步骤放在入库管道的最后,先去重再做版本过滤,剩下的内容才进检索库。整套流程闭环之后,再拿到任何一份 .doc 法律资料,跑一遍这个 pipeline,十分钟就能并进现有知识库。
本文还有配套的精品资源,点击获取