PDF词典结构化实战:英语姓名数据提取、建索引与检索
2026/9/18 16:43:18 网站建设 项目流程

简介:这份资源是外研社出版、李慎廉等编著的《英语姓名词典》PDF电子版,面向语言学者、翻译人员、英语教师及中高级英语学习者,用于查证英语姓名的来源、含义、变体与历史文化背景,解决姓名释义零散、缺乏系统参考的问题。压缩包内仅含1个PDF文件,约53KB,以字母为序编排,从A到Z逐条解释姓名,涵盖字面意义、语源、性别区分及在英美等国的使用差异。内容预览可见R部条目,如Rab、Rabb、Rabbit、Rabbitt、Raby、Rackham、Radford、Rafferty、Raine等,逐一标注男子名或姓氏属性、昵称关系、父名派生、住所名称来源及古英语、盖尔语、希伯来语等语源,并说明“忠告+信息”“红+津渡”等构词含义。目前已有393人学习下载,适合作为英语姓名研究、跨文化交际与翻译查证的工具书,也可为语言教学提供词源与文化素材。

1. 从一份 PDF 词典到可检索的姓名数据:为什么值得拆

手头这份《英语姓名词典》PDF 是外研社出版、李慎廉等编著的,按 A 到 Z 排列,逐条解释英语姓名的来源、含义、变体和所属语种。翻到 R 部,Rab、Rabb、Rabbitt、Raby、Rackham 一路排下来,每条都带着词源标注,比如 Rabbitt 同时挂着三条来源:Rabb 的昵称、诺曼底人名加日耳曼语“忠告+信息”、盖尔语人名被误译成“兔”。这种密度,纸质翻阅效率极低,但把它当成一份结构化语料来用,价值就完全不一样了。

适合三类人:做姓名实体识别或数据清洗的工程师,需要一份可查询的英文姓氏词源表;做翻译或本地化的从业者,遇到人名要快速判断性别、语种和变体关系;以及做语言数据处理的开发者,想拿真实词典数据练手解析、建索引、做检索。这份 PDF 不是扫描图片,文字层可提取,意味着可以走完整的“提取—清洗—结构化—查询”链路。下面按这条链路拆开讲。

2. PDF 文本提取与词条边界识别

2.1 为什么不能直接复制粘贴

从 PDF 里直接选中复制,R 部这段文本会变成一大坨:词条之间没有换行分隔,Robert??Rupert??这种带问号的交叉引用混在正文里,[英格兰人姓氏][男子名]这类标签和释义黏在一起。直接拿去做检索,搜“Rabbitt”会命中一大段无关内容。所以第一步不是写查询,而是把连续文本切成一条条独立词条。

2.2 用 pdfplumber 提取并观察文本特征

常见做法是用pdfplumber逐页提取,它比PyPDF2对版面保留更好,能拿到每个字符的坐标,方便后续按位置切分。

import pdfplumber def extract_pages(pdf_path): pages_text = [] with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): # extract_text 默认按阅读顺序拼接,layout=False 保留原始换行 text = page.extract_text(x_tolerance=2, y_tolerance=2) pages_text.append({"page": i + 1, "text": text}) return pages_text if __name__ == "__main__": pages = extract_pages("english_names_R.pdf") # 先看第一页前 800 字符,确认词条分隔符长什么样 print(pages[0]["text"][:800])

x_tolerancey_tolerance控制字符合并的容差,值越小越贴近原始排版。提取后重点观察:词条开头是不是“英文名+中文译名”的模式,比如Rab[ 男子名 ] 拉布,词条之间靠什么分隔——通常是下一个英文名首字母大写开头。这一步不写解析逻辑,先把样本看清楚。

2.3 用正则切分词条并处理交叉引用

观察清楚后,用正则按“英文词+可选标签+中文”的模式切。R 部词条有个特点:英文名后常跟[英格兰人姓氏][男子名][女子名][苏格兰人姓氏]等标签,中文译名紧跟其后。

import re # 匹配词条起始:英文名(允许 . 和空格)+ 可选方括号标签 + 中文译名 ENTRY_START = re.compile( r'(?<=[\s。.])([A-Z][a-zA-Z\.\s]{1,20}?)' # 英文名,非贪婪 r'(\[[^\]]{1,30}\])?' # 可选标签,如 [男子名] r'\s*([\u4e00-\u9fa5·]{1,15})' # 中文译名 ) def split_entries(raw_text): # 先把交叉引用里的 ?? 和 ↑ 统一成占位符,避免干扰切分 cleaned = raw_text.replace("??", "").replace("↑", "->") matches = list(ENTRY_START.finditer(cleaned)) entries = [] for idx, m in enumerate(matches): start = m.start() end = matches[idx + 1].start() if idx + 1 < len(matches) else len(cleaned) entries.append({ "name": m.group(1).strip(), "tag": (m.group(2) or "").strip("[]"), "cn": m.group(3).strip(), "body": cleaned[start:end].strip() }) return entries

ENTRY_START(?<=[\s。.])是后顾断言,保证英文名前面是空白或句号,避免把释义中间的英文单词误判成新词条。body保留整条原文,后面提取词源时还要用。切完先统计条数,R 部正常应该在几百条量级,如果只有几十条,说明正则太严,需要放宽英文名长度或标签匹配。

注意:PDF 里存在Rabbitts拉比茨这种英文名和中文之间没有空格的写法,正则里\s*用星号而非加号就是为了兼容这种情况。

3. 词源字段的结构化抽取

3.1 词源信息的几种固定句式

切出词条后,body里混着好几类信息:性别/语种标签、中文译名、词源解释、变体关系、交叉引用。词源解释的句式其实很有限,R 部高频出现这几种:

句式模式示例可抽取字段
来源于X语,含义是“A+B”来源于古英语,含义是“红+津渡”语种、含义A、含义B
取自父名,来源于X,含义是“X之子”取自父名,来源于Rabb,含义“拉布之子”构词类型、父名、含义
X的变体/异体Ratcliffe 的变体关系类型、关联词条
住所名称,来源于X语,含义是“A+B”住所名称,来源于古英语,含义是“芦苇+溪流”命名类型、语种、含义

把这四类做成规则,比上模型更可控,因为词典句式高度规整。

3.2 用规则抽取语种、含义和构词类型

ORIGIN_PATTERNS = [ # 语种 + 含义 (re.compile(r'来源于([\u4e00-\u9fa5]{2,8}语)[,,]\s*含义[是为]?[“"]([^”"]+)[”"]'), lambda m: {"origin_lang": m.group(1), "meaning": m.group(2)}), # 取自父名 (re.compile(r'取自父名[,,]\s*来源于([A-Za-z]+)[,,]\s*含义[“"]([^”"]+)[”"]'), lambda m: {"patronymic": m.group(1), "meaning": m.group(2)}), # 变体关系 (re.compile(r'([A-Z][a-zA-Z]+)\s*的(变体|异体)'), lambda m: {"relation": m.group(2), "related_to": m.group(1)}), # 住所名称 (re.compile(r'住所名称[,,]\s*来源于([\u4e00-\u9fa5]{2,8}语)'), lambda m: {"place_name": True, "origin_lang": m.group(1)}), ] def parse_body(body): result = {} for pattern, handler in ORIGIN_PATTERNS: m = pattern.search(body) if m: result.update(handler(m)) # 含义里的 A+B 拆成两个语义成分 if "meaning" in result and "+" in result["meaning"]: parts = result["meaning"].split("+") result["meaning_parts"] = [p.strip() for p in parts] return result

每个 pattern 对应一种句式,handler负责把匹配组映射成字段。meaning_parts把“红+津渡”拆成["红", "津渡"],后面做语义检索时,搜“红”能命中所有含“红”这一构词成分的姓氏。跑完对 R 部做一次覆盖率统计,正常能覆盖七成以上词条,剩下的多是语源不详或句式变体,人工补规则即可。

3.3 交叉引用与变体关系的归一化

R 部里Raby标注为Roby ↑ 2 的变体RaeRachel 的昵称,这些关系散落在正文里。归一化的做法是建一张关系表,把“变体”“异体”“昵称”统一成variant_ofnickname_of两类边。

RELATION_MAP = {"变体": "variant_of", "异体": "variant_of", "昵称": "nickname_of"} def extract_relations(entries): edges = [] for e in entries: for m in re.finditer(r'([A-Z][a-zA-Z]+)\s*的(变体|异体|昵称)', e["body"]): edges.append({ "from": e["name"], "to": m.group(1), "type": RELATION_MAP[m.group(2)] }) return edges

这样Raby -> RobyRae -> Rachel都变成有向边,后续可以顺着边做变体链查询,比如输入Raby反查它所有上游词条。

4. 建索引与多条件查询实战

4.1 用 SQLite 建一张姓名表

结构化数据落到 SQLite 最省事,单文件、无需服务、支持全文检索扩展。表结构按前面抽出的字段设计:

CREATE TABLE names ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, cn TEXT, tag TEXT, -- 男子名/女子名/英格兰人姓氏等 origin_lang TEXT, -- 语种 meaning TEXT, -- 完整含义 meaning_parts TEXT, -- 逗号分隔的语义成分 patronymic TEXT, -- 父名 place_name INTEGER, -- 是否住所名称 body TEXT -- 原始释义 ); CREATE TABLE relations ( from_name TEXT, to_name TEXT, rel_type TEXT -- variant_of / nickname_of ); CREATE INDEX idx_names_name ON names(name); CREATE INDEX idx_names_lang ON names(origin_lang);

meaning_parts存成逗号分隔字符串,查询时用LIKE '%红%'就能命中,数据量不大时够用。relations单独成表,避免主表字段膨胀。

4.2 写入数据并做基础查询

import sqlite3 def save_to_db(entries, edges, db_path="names.db"): conn = sqlite3.connect(db_path) cur = conn.cursor() for e in entries: parsed = parse_body(e["body"]) cur.execute( "INSERT INTO names (name, cn, tag, origin_lang, meaning, meaning_parts, patronymic, place_name, body) " "VALUES (?,?,?,?,?,?,?,?,?)", (e["name"], e["cn"], e["tag"], parsed.get("origin_lang"), parsed.get("meaning"), ",".join(parsed.get("meaning_parts", [])), parsed.get("patronymic"), 1 if parsed.get("place_name") else 0, e["body"]) ) cur.executemany( "INSERT INTO relations (from_name, to_name, rel_type) VALUES (?,?,?)", [(x["from"], x["to"], x["type"]) for x in edges] ) conn.commit() conn.close()

写入后先验证条数,再跑几个查询确认字段没串。比如查所有含“忠告”语义的姓氏:

SELECT name, cn, origin_lang, meaning FROM names WHERE meaning LIKE '%忠告%' ORDER BY name;

R 部里RaineRainbirdRainbowRaynerRaymond都含“忠告”这一日耳曼语成分,这条查询能把它们一次捞出来,这是纸质翻阅做不到的。

4.3 变体链查询与性别筛选

变体关系建好后,可以写递归查询追一条变体链。比如从Raby出发,看它最终归到哪个主词条:

WITH RECURSIVE chain(name, depth) AS ( SELECT to_name, 1 FROM relations WHERE from_name = 'Raby' UNION ALL SELECT r.to_name, c.depth + 1 FROM relations r JOIN chain c ON r.from_name = c.name WHERE c.depth < 5 ) SELECT * FROM chain;

depth < 5是防止数据里出现环导致死循环。性别筛选更直接,tag字段里带“女子名”的:

SELECT name, cn FROM names WHERE tag LIKE '%女子名%';

R 部能筛出RachaelRaeRebaRebeccaReginaRenataRenee等,配合origin_lang还能进一步按语种过滤。

注意:tag字段可能同时含多个标签,比如[男子名][英格兰人姓氏],用LIKE而非等号匹配。

5. 释义文本的检索优化与一个实用技巧

5.1 用 FTS5 替代 LIKE 做全文检索

数据量再大一点,LIKE '%关键词%'会全表扫描。SQLite 的 FTS5 扩展能建全文索引,对body字段做分词检索,速度差一个量级。

CREATE VIRTUAL TABLE names_fts USING fts5( name, cn, body, content='names', content_rowid='id', tokenize='unicode61' ); INSERT INTO names_fts (rowid, name, cn, body) SELECT id, name, cn, body FROM names;

tokenize='unicode61'对中文按字切分,搜“忠告”能命中含该词的释义。查询时:

SELECT n.name, n.cn FROM names_fts f JOIN names n ON f.rowid = n.id WHERE names_fts MATCH '忠告 AND 狼';

这条能捞出同时含“忠告”和“狼”两个语义成分的词条,比如RalphRandolphRaoul,它们都是日耳曼语“忠告+狼”的构词。

5.2 一个容易踩的坑:中文分词与同义译名

FTS5 的unicode61按字切,搜“拉尔夫”能命中,但搜“拉夫”也会命中一堆无关词条,因为按字匹配。更稳的做法是查询前把中文译名也建一份拼音索引,或者直接用LIKE做精确子串匹配,全文检索只用于长释义。另一个坑是同一英文名在不同词条里译名不统一,比如Ralph有时译“拉尔夫”,Rafe标注为Ralph 的变体却译“雷夫”。处理办法是在relations表基础上,查询时把变体链上的所有译名一起返回,让使用者自己判断。

5.3 导出成可复用的 JSON 供其他系统调用

最后一步,把库里的数据导成 JSON,方便喂给其他程序或做前端检索。

import json, sqlite3 def export_json(db_path="names.db", out="names_r.json"): conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row rows = conn.execute("SELECT * FROM names").fetchall() data = [dict(r) for r in rows] with open(out, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) conn.close() print(f"exported {len(data)} entries") export_json()

导出的 JSON 每条含namecnorigin_langmeaning_parts等字段,可以直接被姓名识别模型当特征用,也可以挂到内部知识库做检索。R 部跑通后,把同一套正则和表结构套到 A 到 Q、S 到 Z,整本词典就变成一份可查询、可关联、可扩展的姓名数据源。真正费时间的不是写代码,而是把Rabbitt那种一条挂三种词源的复杂词条规则补全——这部分建议先跑覆盖率统计,把没匹配上的词条单独导出,逐条补正则,比一开始就追求全自动靠谱得多。

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

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

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

立即咨询