☰
GATT1994中文版PDF获取与Word兼容处理:法律文本工程化落地指南
2026/10/2 11:08:19 网站建设 项目流程

简介:这份PDF文档是关税及贸易总协定GATT1994的中文整理版,采用Word兼容格式,面向国际贸易法研究者、外贸从业者、法学专业学生及备考相关资格考试的人群,帮助读者便捷查阅GATT1994原始条款,理解多边贸易体制的法律框架。资源包内仅含1个PDF文件,大小约239KB,轻量易存,适合在电脑或移动设备上随时翻阅、检索与引用。文档完整收录协定正文,涵盖一般最惠国待遇、减让表、国内税与国内规章的国民待遇等核心条款,并涉及优惠待遇范围、贸易障碍取消及关税费用优惠安排等具体规定,可作为研究WTO规则、撰写论文或处理贸易实务的参考底本。目前已有429人学习下载,适合需要反复研读条文、对照案例分析的读者使用。

1. GATT1994 中文版 PDF 的获取与 Word 兼容处理:一份法律文本的工程化落地

做涉外合规、国际贸易法务或者关务系统的朋友,大概率都遇到过同一个场景:手头需要一份 GATT1994 的中文文本,要求能检索、能复制、能批注、能转成结构化数据入库,但拿到的往往是一个扫描件 PDF 或者排版混乱的网页存档。GATT1994 作为 WTO 协定附件一A 的核心组成部分,其条文被各国贸易救济调查、关税谈判、原产地规则制定反复引用,文本的准确性和可编辑性直接决定了后续工作的效率。这个标题指向的其实是一个很具体的工程问题:如何把一份「Word 兼容版」的 GATT1994 中文 PDF 变成真正能在本地跑通检索、比对、入库的可用资产。适合做合规系统、法律知识库、贸易政策研究的人,也适合需要把条约文本喂给检索或比对工具的技术同学。

2. GATT1994 文本的结构特征与 Word 兼容版 PDF 的选型逻辑

2.1 为什么 GATT1994 的文本处理比一般法律文件更麻烦

GATT1994 不是一份单一文件,它由四部分构成:GATT1947 的条文本身、WTO 成立前生效的各类法律文件(如关税减让议定书、加入议定书)、乌拉圭回合对 GATT1947 的谅解,以及 GATT1994 的马拉喀什议定书。中文版本在翻译和编排上存在多个来源,不同来源的条款编号、注释位置、附件归属都有差异。更麻烦的是,GATT1994 的条文里大量使用交叉引用,比如「按本协定第 XX 条」「依第 II 条第 2 款」,这些引用在纯文本里是字符串,在结构化数据里必须变成可跳转的锚点。如果 PDF 只是扫描图,OCR 出来的文本会把「第 II 条」识别成「第 II 条」还是「第 II 条」全看运气,条款编号一旦错位,后续所有引用链就断了。

Word 兼容版 PDF 的价值就在这里。所谓 Word 兼容,通常指 PDF 内部保留了文本层和段落标记,用 Adobe Acrobat、WPS 或者 LibreOffice 打开时能直接选中文字、复制段落,而不是整页一张图。这类 PDF 的来源一般是出版社排版文件导出,或者官方文本经过 Word 排版后另存为 PDF。它的文本流是连续的,段落边界清晰,标题层级虽然不一定带样式标签,但通过字号和加粗能推断出来。选型时优先看三个指标:文本层是否可选中、段落是否按条拆分、页眉页脚是否混入正文。如果 PDF 打开后选中一段文字,复制出来是乱序的,那说明文本流是碎的,处理成本会高很多。

2.2 判断一份 GATT1994 中文 PDF 是否值得动手的四个检查点

拿到文件后别急着写代码,先做四件事。第一,用pdftotext -layout跑一遍,看输出的文本里「第 X 条」是否独立成行,如果条款标题和正文黏在一起,说明排版时没有用段落分隔。第二,检查页码和页眉是否出现在文本流中间,很多 PDF 的页眉是「GATT1994 中文版」加页码,这些内容会污染正文。第三,搜索「附件」和「注释」,看它们是否被正确放在对应条款之后,而不是全部堆在文件末尾。第四,用pdfinfo看 PDF 的 Producer 字段,如果是「Microsoft Word」或者「WPS」导出的,文本层质量通常较好;如果是「ScanSnap」或者「ABBYY」之类的扫描仪软件,那基本就是 OCR 结果,需要额外校对。

# 检查 PDF 文本层质量和元数据 pdfinfo "GATT1994中文整理版.pdf" | grep -E "Producer|Pages|Encrypted" # 提取纯文本,保留布局,观察条款标题是否独立成行 pdftotext -layout "GATT1994中文整理版.pdf" gatt_raw.txt # 统计「第.*条」出现次数,和目录对比 grep -cE "^第[一二三四五六七八九十百]+条" gatt_raw.txt

pdfinfo的 Producer 字段能告诉你这份 PDF 是原生导出还是扫描后 OCR。pdftotext -layout保留原始空格布局,适合判断段落结构。grep统计条款数量是为了和官方目录做交叉验证,如果数量对不上,说明提取过程中丢了内容或者把注释里的「第 X 条」也算进去了。注意-layout模式有时会在行尾加多余空格,后续清洗时用sed 's/[[:space:]]*$//'去掉。

2.3 Word 兼容版 PDF 转结构化文本的两种路径选择

路径一:PDF → 文本 → 正则解析 → JSON/Markdown。适合条款结构规整、编号连续的版本。核心是用正则把「第 X 条」「第 X 款」「第 X 项」切成层级,再按标题层级重建树。路径二:PDF → Word(docx)→ 样式解析 → 结构化。适合 PDF 本身是从 Word 导出的情况,用pdf2docx或者 LibreOffice 转成 docx 后,标题样式(Heading 1/2/3)能直接映射条款层级,比正则更稳。我一般优先走路径二,因为 GATT1994 的条款层级深,正则写多了容易在「第 II 条」和「第 II 条第 2 款」之间翻车。但路径二依赖 PDF 的文本层质量,如果转出来的 docx 里标题样式全丢了,就得退回路径一。

from pdf2docx import Converter # 将 Word 兼容版 PDF 转为 docx,保留段落和样式 cv = Converter("GATT1994中文整理版.pdf") cv.convert("gatt1994.docx", start=0, end=None) cv.close() # 用 python-docx 读取标题层级 from docx import Document doc = Document("gatt1994.docx") for para in doc.paragraphs: if para.style.name.startswith("Heading"): print(para.style.name, "|", para.text[:60])

pdf2docx的convert方法会尽量还原 PDF 的段落和样式,但 GATT1994 里有些条款标题用的是加粗正文而不是 Heading 样式,转出来可能变成 Normal。这时候需要看para.style.name和para.runs[0].bold组合判断。start和end参数可以只转特定页,适合先拿几页试效果。如果转出来的 docx 里 Heading 数量远少于预期,说明 PDF 的样式信息在导出时丢了,路径二不可行。

3. 从 PDF 到可检索文本:提取、清洗与条款切分的完整操作

3.1 用 pdfplumber 做精细提取:表格、脚注和跨页条款的处理

pdftotext快但粗,遇到 GATT1994 里的表格(比如关税减让表引用)和脚注(比如条款的补充注释)容易丢内容。pdfplumber能按字符坐标提取,适合处理跨页条款和表格。GATT1994 的正文里有一些条款是跨页的,比如第 XX 条可能从第 5 页底部延续到第 6 页顶部,pdftotext会在页边界处断行,pdfplumber可以通过extract_text()的layout参数控制是否保留换行,再结合页码信息把跨页段落拼回去。

import pdfplumber with pdfplumber.open("GATT1994中文整理版.pdf") as pdf: full_text = [] for i, page in enumerate(pdf.pages): # 提取文本,x_tolerance 控制字符间距,y_tolerance 控制行间距 text = page.extract_text(x_tolerance=2, y_tolerance=3) # 去掉页眉页脚:通常页眉在顶部 50pt 以内,页脚在底部 50pt 以内 words = page.extract_words() body_words = [w for w in words if 50 < w['top'] < page.height - 50] body_text = " ".join(w['text'] for w in body_words) full_text.append(f"--- PAGE {i+1} ---\n{body_text}") with open("gatt_clean.txt", "w", encoding="utf-8") as f: f.write("\n".join(full_text))

x_tolerance和y_tolerance是pdfplumber的关键参数,值越小越严格,适合字符间距均匀的排版;值太大会把不同列的文本混在一起。extract_words()返回每个词的坐标,用top过滤页眉页脚比用正则匹配「第 X 页」更可靠,因为页码格式可能不统一。注意 GATT1994 的注释有时用小字号排在页面底部,如果页脚过滤范围设得太大,会把注释一起删掉,建议先打印几页的words坐标分布再定阈值。

3.2 条款切分的正则设计:处理「第 X 条」「第 X 款」「第 X 项」的嵌套

GATT1994 的层级是:部分(Part)→ 条(Article)→ 款(Paragraph)→ 项(Subparagraph)。中文版里「条」用「第 X 条」,「款」用「第 X 款」或者直接用阿拉伯数字加括号,「项」用「(a)(b)(c)」或者「(甲)(乙)」。正则要能区分这些层级,同时不能把正文里的交叉引用(比如「按第 II 条第 2 款」)误判为标题。我的做法是:标题行必须满足「行首 + 第 X 条 + 行尾无句号」或者「行首 + 数字 + 顿号 + 标题文字」,交叉引用通常出现在句子中间,用^锚定行首就能过滤掉大部分。

import re with open("gatt_clean.txt", "r", encoding="utf-8") as f: text = f.read() # 匹配「第 X 条」作为条款标题,要求行首且后面不跟「规定」「所指」等词 article_pattern = re.compile( r'^第([一二三四五六七八九十百]+)条\s*(.*?)$', re.MULTILINE ) # 匹配「第 X 款」,通常以数字开头,如「1.」「2.」 paragraph_pattern = re.compile( r'^(\d+)\.\s+(.*?)$', re.MULTILINE ) articles = [] current_article = None for line in text.split("\n"): line = line.strip() m = article_pattern.match(line) if m: if current_article: articles.append(current_article) current_article = {"article": m.group(1), "title": m.group(2), "content": []} elif current_article is not None: current_article["content"].append(line) if current_article: articles.append(current_article) print(f"共切分出 {len(articles)} 条") for a in articles[:3]: print(a["article"], a["title"], len(a["content"]))

article_pattern里的^和re.MULTILINE保证只匹配行首的「第 X 条」,避免把正文里的引用切出来。(.*?)捕获条款标题,GATT1994 里有些条款标题是空的(比如第 I 条就是「普遍最惠国待遇」,标题直接跟在条号后面),有些是换行写的,需要根据实际文本调整。paragraph_pattern匹配阿拉伯数字开头的款,但要注意 GATT1994 里有些条款的款是用「(a)(b)」标记的,这种需要单独处理。切分完后用len(articles)和官方目录对比,GATT1994 正文共 38 条,加上附件和注释,总数应该在 40 左右。

3.3 清洗规则:去掉页码残留、合并断行、统一标点

提取出来的文本里常见的脏数据有三类:页码残留(比如「- 5 -」或者「5」单独成行)、断行(一个句子被硬回车切成两行)、标点不统一(中文引号和英文引号混用)。页码残留用正则^\s*-?\s*\d+\s*-?\s*$匹配整行只有数字或数字加横线的行,直接删掉。断行合并要看上下文:如果一行以逗号、顿号、分号结尾,下一行开头是小写字母或者中文,大概率是断行,合并时去掉换行符;如果一行以句号结尾,下一行是「第 X 条」,那可能是新段落,不能合并。标点统一用str.maketrans把英文引号、括号转成中文,但要注意 GATT1994 里的条款编号「第 II 条」用的是罗马数字,不能把「II」转成中文「二」。

import re def clean_gatt_text(raw: str) -> str: lines = raw.split("\n") cleaned = [] for line in lines: # 去掉纯页码行 if re.match(r'^\s*-?\s*\d+\s*-?\s*$', line): continue # 去掉页眉标记 if "--- PAGE" in line: continue cleaned.append(line.strip()) # 合并断行:如果上一行不以句号、问号、感叹号结尾,且下一行不以「第」开头 merged = [] for i, line in enumerate(cleaned): if not line: continue if merged and not re.search(r'[。?!;]$', merged[-1]) and not re.match(r'^第[一二三四五六七八九十百]+条', line): merged[-1] += line else: merged.append(line) text = "\n".join(merged) # 统一标点:英文引号转中文 trans = str.maketrans({'"': '“', '"': '”', "'": '‘', "'": '’'}) text = text.translate(trans) return text with open("gatt_clean.txt", "r", encoding="utf-8") as f: raw = f.read() result = clean_gatt_text(raw) with open("gatt_final.txt", "w", encoding="utf-8") as f: f.write(result)

合并断行的逻辑是:如果上一行末尾没有句号、问号、感叹号、分号,且下一行不是以「第 X 条」开头,就把下一行拼到上一行。这个规则能处理大部分正文断行,但 GATT1994 里有些条款的款标题(比如「1.」)后面直接跟正文,如果上一行是「1.」且没有标点,会被误合并,所以加一个判断:如果上一行匹配^\d+\.$,不合并。标点转换只处理引号,括号和破折号保持原样,因为 GATT1994 里有些括号是条款编号的一部分,转成中文括号反而会破坏结构。

4. 避坑与排查:GATT1994 文本处理中五个真实翻车记录

4.1 现象:提取出的条款数量比官方目录少了好几条

原因:pdftotext默认不处理 PDF 里的文本框和浮动元素,GATT1994 中文版里有些条款的标题被放在文本框里,或者用了分栏排版,pdftotext按阅读顺序提取时会跳过这些内容。另外,如果 PDF 有加密权限限制,pdftotext可能只输出部分页面。

解决:先用pdfinfo确认Encrypted字段,如果是yes,用qpdf --decrypt解密后再提取。对于文本框,改用pdfplumber的extract_text()并设置layout=True,或者用page.extract_words()按坐标排序后手动拼接。提取完后用grep -cE "^第[一二三四五六七八九十百]+条"统计条款数,和官方目录逐条核对,缺哪条就单独去 PDF 里定位那一页,看是不是排版问题。

4.2 现象:条款编号里的罗马数字被错误转成了阿拉伯数字或中文数字

原因:清洗时用了全局的标点转换或者数字转换,把「第 II 条」里的「II」当成英文引号或者普通字母处理了。GATT1994 里罗马数字用于条款编号(第 I 条到第 XXXVIII 条),也用于款项编号(比如「第 II 条第 2 款」),这些罗马数字必须保持原样。

解决:在清洗规则里加白名单,用正则第\s*[IVXLCDM]+\s*条匹配罗马数字条款,匹配到的行不做数字转换。如果已经转错了,用re.sub把「第 2 条」这种误转的改回「第 II 条」,但前提是你有一份正确的条款编号映射表。更稳妥的做法是:清洗阶段只处理标点和空白,不做任何数字转换,罗马数字和阿拉伯数字的转换留到结构化阶段按字段类型处理。

4.3 现象:跨页条款被切成两段,合并后语义错乱

原因:GATT1994 里有些长条款跨页,pdftotext在页边界处插入换行,清洗时如果无脑合并,会把上一页末尾的句子和下一页开头的句子拼在一起,但下一页开头可能是另一个条款的标题,或者页眉残留。

解决:在合并前先标记页边界。用pdfplumber提取时记录每页的起止行号,合并断行时只在同一页内合并,跨页的行先检查下一页第一行是否以「第 X 条」开头,如果是,不合并;如果不是,再检查上一页最后一行是否以句号结尾,不是才合并。这个逻辑写起来有点绕,但能避免大部分跨页错乱。我一般会在清洗后的文本里保留--- PAGE N ---标记,结构化阶段再决定是否去掉。

4.4 现象:注释和附件被混入正文,导致条款内容多出无关段落

原因:GATT1994 的注释(Ad Note)和附件(Annex)在 PDF 里可能和正文用同样的字号排版,pdftotext不区分层级,提取出来就是连续文本。如果正则切分时只按「第 X 条」切,注释会被归到上一条的content里。

解决:在切分前先识别注释和附件的起始标记。GATT1994 的注释通常以「注释」或「对第 X 条的注释」开头,附件以「附件」或「ANNEX」开头。用正则^(注释|对第.*条的注释|附件|ANNEX)匹配到这些行时,把后续内容归入独立的notes或annexes列表,不混入articles。如果 PDF 里注释是用小字号排的,可以用pdfplumber的chars属性按字号过滤,但这种方法依赖 PDF 的字体信息,不是所有版本都可靠。

4.5 现象:转成 docx 后标题样式全丢,Heading 数量为零

原因:PDF 导出时没有保留 Word 的样式标签,或者pdf2docx的版本对中文 PDF 的样式还原支持不好。有些 GATT1994 中文版是扫描后 OCR 再排版的,PDF 里根本没有样式信息。

解决:先检查 PDF 的 Producer 字段,如果是扫描仪软件,直接放弃路径二,走正则解析。如果是 Word 导出但样式丢了,用pdf2docx时加multi_processing=True和cpu_count参数提高还原率,或者改用 LibreOffice 的命令行转换:libreoffice --headless --convert-to docx "GATT1994中文整理版.pdf"。LibreOffice 对中文 PDF 的段落还原有时比pdf2docx好。如果都不行,就回到pdfplumber按字号和加粗判断标题层级:字号大于正文且加粗的行,大概率是条款标题。

5. 把 GATT1994 文本变成可检索知识库:索引、比对与版本管理

5.1 用 SQLite FTS5 建全文索引,支持条款级检索

结构化后的 GATT1994 文本最适合放进 SQLite 的 FTS5 虚拟表,按条款建索引,支持中文分词和短语查询。FTS5 默认的分词器对中文是按字切分,检索「最惠国待遇」会匹配到包含这些字的条款,但也会匹配到无关内容。更好的做法是用jieba分词后把词序列存进 FTS 表,或者用 SQLite 的unicode61分词器加tokenchars参数。下面是一个可复现的建库脚本,把前面切分出的articles列表写入数据库。

import sqlite3 import json conn = sqlite3.connect("gatt1994.db") cur = conn.cursor() # 创建主表和 FTS5 索引表 cur.execute(""" CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY, article_num TEXT, title TEXT, content TEXT, part TEXT ) """) cur.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5( article_num, title, content, content='articles', content_rowid='id', tokenize='unicode61' ) """) # 假设 articles 是前面切分出的列表 for i, a in enumerate(articles): content = "\n".join(a["content"]) cur.execute( "INSERT INTO articles (article_num, title, content, part) VALUES (?, ?, ?, ?)", (a["article"], a["title"], content, a.get("part", "")) ) cur.execute( "INSERT INTO articles_fts (rowid, article_num, title, content) VALUES (?, ?, ?, ?)", (i+1, a["article"], a["title"], content) ) conn.commit() # 检索示例:查找包含「最惠国待遇」的条款 cur.execute(""" SELECT article_num, title FROM articles_fts WHERE articles_fts MATCH '最惠国待遇' ORDER BY rank LIMIT 5 """) for row in cur.fetchall(): print(row) conn.close()

FTS5 的content='articles'表示外部内容表,索引和主表分离,更新主表后需要手动同步 FTS 表,或者用触发器自动同步。tokenize='unicode61'对中文是按字切分,检索「最惠国」会匹配到「最惠国待遇」和「最惠国关税」等,召回率高但精度一般。如果要做精确短语检索,用MATCH '"最惠国待遇"'加双引号。ORDER BY rank按相关性排序,FTS5 的rank是内置的 BM25 变体,适合快速排序。

5.2 版本比对:用 difflib 找出不同来源 GATT1994 文本的差异

GATT1994 中文版有多个来源,条款措辞可能有细微差异。做合规系统时,需要知道当前用的版本和官方版本差在哪。用 Python 的difflib按条款比对,输出差异行,再人工判断是排版差异还是实质修改。下面是一个按条款比对的脚本,输入是两个版本的articles列表,输出差异报告。

import difflib def compare_articles(articles_a, articles_b): # 按条款号建字典 dict_a = {a["article"]: a for a in articles_a} dict_b = {a["article"]: a for a in articles_b} all_articles = sorted(set(dict_a.keys()) | set(dict_b.keys())) report = [] for art in all_articles: a = dict_a.get(art) b = dict_b.get(art) if not a: report.append(f"条款 {art}: 仅在版本 B 中存在") continue if not b: report.append(f"条款 {art}: 仅在版本 A 中存在") continue text_a = "\n".join(a["content"]) text_b = "\n".join(b["content"]) if text_a == text_b: continue diff = list(difflib.unified_diff( text_a.splitlines(), text_b.splitlines(), lineterm="", n=1 )) report.append(f"条款 {art} 差异:\n" + "\n".join(diff[:20])) return report # 假设 articles_v1 和 articles_v2 是两个版本的切分结果 # report = compare_articles(articles_v1, articles_v2) # print("\n".join(report))

difflib.unified_diff的n=1表示只显示差异行前后各 1 行上下文,适合快速定位。text_a.splitlines()按行比对,如果两个版本的断行方式不同,会产生大量伪差异,所以比对前要确保两个版本都经过了同样的清洗和断行合并。差异报告里如果出现「第 II 条」变成「第 2 条」这种,说明其中一个版本的罗马数字被转错了,需要先修正再比对。这个脚本只做文本级比对,不做语义比对,实质修改需要人工判断。

5.3 用 Markdown 重建条款层级,方便版本管理和协作

结构化后的 GATT1994 文本导出成 Markdown,用#表示部分,##表示条,###表示款,这样在 Git 里做版本管理时,diff 会按层级显示,比纯文本 diff 清晰得多。导出脚本用articles列表生成 Markdown,条款标题作为##,款内容作为段落,注释和附件单独放在文件末尾。

def export_markdown(articles, notes=None, annexes=None): lines = ["# GATT1994 中文版\n"] for a in articles: lines.append(f"## 第 {a['article']} 条 {a['title']}\n") for para in a["content"]: if para.strip(): lines.append(para.strip() + "\n") if notes: lines.append("# 注释\n") for n in notes: lines.append(n + "\n") if annexes: lines.append("# 附件\n") for an in annexes: lines.append(an + "\n") return "\n".join(lines) # with open("gatt1994.md", "w", encoding="utf-8") as f: # f.write(export_markdown(articles))

Markdown 的层级用##和###,不要用####,因为 GATT1994 的条款层级最多到项,###足够。条款标题里的罗马数字保持原样,不要转成阿拉伯数字。导出后在 Git 里提交,后续如果官方文本更新,重新跑一遍提取和导出,git diff就能看到哪些条款变了。这个做法比维护一个二进制 PDF 靠谱得多,也方便多人协作时合并修改。

5.4 一个我常用的验证习惯:用条款交叉引用做一致性检查

GATT1994 里大量交叉引用,比如「按本协定第 XX 条」「依第 II 条第 2 款」。结构化后,用正则提取所有交叉引用,检查被引用的条款是否存在、编号是否匹配。如果引用指向一个不存在的条款号,说明切分或清洗过程中丢了内容。这个检查能抓住大部分提取错误,比人工核对快得多。

import re def check_cross_references(articles): article_nums = {a["article"] for a in articles} ref_pattern = re.compile(r'第([一二三四五六七八九十百]+)条') missing = [] for a in articles: content = "\n".join(a["content"]) refs = ref_pattern.findall(content) for ref in refs: if ref not in article_nums: missing.append((a["article"], ref)) return missing # missing = check_cross_references(articles) # for src, dst in missing: # print(f"条款 {src} 引用了不存在的第 {dst} 条")

这个检查的误报率取决于正则的精度,如果正文里出现「第 X 条」但不是交叉引用(比如「本条」),会被误报。改进方法是只匹配「第 X 条」前后有「按」「依」「根据」「见」等动词的上下文。我一般会先跑一遍全量,把误报的条款号加入白名单,再跑第二遍。这个习惯帮我抓过好几次提取丢页的问题,尤其是 PDF 中间某页是图片的情况。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询