ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

英文单词分割实战:用正则处理撇号连字符与缩写

英文单词分割实战:用正则处理撇号连字符与缩写 简介在自然语言处理中文本预处理阶段经常遇到所有字母连在一起的情况需要将连续英文序列正确拆分为独立单词。这份PDF教程以Python的symspellpy库为主线讲解如何使用对称删除拼写纠正算法完成无空格文本的单词分割从环境安装、词典准备到SymSpell对象参数配置再到调用分割函数输出结果步骤完整且带示例代码适合NLP初学者、数据清洗人员和需要批量处理畸形英文文本的开发者参考。资源包含1个PDF文档资源包仅29KB内容紧凑便于离线阅读。目前已有4805人参与学习下载。除了单词分割教程还结合词频和编辑距离介绍基础拼写纠错并解释分割结果字符串、距离总和、对数概率总和等返回参数的含义有助于读者理解分词原理直接用于文本清洗、用户输入纠错等真实场景。1. 为什么说英文单词分割不是英文分词拿到一段英文原始文本第一反应是text.split( )但对做过一次真实语料处理的人来说这个直觉立刻会撞上三种碎片句尾的world,、所有格的Neils、连字符组成的state-of-the-art。英文单词分割English Word Tokenization要解决的是把连续字符串切成“后续分析里最小的有意义单元”它和中文分词的区别在于英文天然有空格问题不在“哪里是词”而在“哪些字符应该被拼回一个词、哪些标点应该被剥离”。这个任务贯穿几乎所有 Python 文本处理场景词频统计、文本分类前的特征提取、聊天机器人的意图识别、搜索引擎倒排索引的构建。判定的核心围绕三个问题撇号怎么拆、连字符怎么处理、缩写和数字混合串要不要完整保留。文中会给出可直接落地的正则方案、边界策略、批处理管道和验证手段适合正在写爬虫清洗逻辑、NLP 预处理脚本或日志分析的工程师。2. 用 re.findall 做英文单词分割的基础实现2.1 为什么 replace 掉标点再 split 不是单词分割很多初学者会把分割做成两步先用replace删掉所有标点再split()。这个方案的直接后果是信息丢失——C变成了两个 C3.14被截断state-of-the-art这类携带语义的信息被强行拆散。而更隐蔽的问题在于replace依赖你枚举所有标点符号漏掉一个特殊符号整个统计结果就带上污染。单词分割本质上是逐个字符做“词内/词外”的二元判断而不是做替换。词内字符包括字母、数字、撇号、连字符和部分 Unicode 扩展字母词外字符是空白、标点、符号。用一遍遍历完成这个判断最省事的工具就是正则表达式。2.2 一条能跑的最小正则代码import re text Hello, world! This is a test: its not that hard. words re.findall(r[\w], text) print(words) # 输出: [Hello, world, This, is, a, test, its, not, that, hard][\w]的逐段含义是\w匹配 Unicode 字母、数字和下划线将撇号显式放入字符类把相邻字符拼成一个完整的单词单元。re.findall只返回匹配到的内容不返回分隔符正好满足“只提取词”的需求。这个模式能覆盖约八成常见英文语料。但它有两个明显缺口连字符state-of-the-art会被拆成三段缩写U.S.会被点号截断。需要考虑到底层逻辑再决定是否用更复杂的模式。2.3 正则模式中 \w 和边界 \b 的实际行为\b是单词边界它匹配一个位置而不是字符。直接使用re.findall(r\b\w\b, text)同样是有效的分割方法它依赖边界的零宽匹配来切分词。模式匹配示例适用场景[\w]its,hello保留撇号的基础场景速度快\b\w\bit,s会把 its 拆开只需要纯字母词时[a-zA-Z](?:[-][a-zA-Z])*state-of-the-art,dont需要保留连字符和撇号作为词内字符时\b的边界判断基于\w的字符集\b的一侧是\w字符而另一侧不是。dont中间的撇号不是\w所以\b\w\b会以撇号为界拆出don和t这是很多人初次尝试时踩到的第一道坑。不带\b、直接用字符类拼接的模式更适合做英文单词分割因为它把“什么字符属于词内部”的决定权完全握在自己手里。3. 英文单词分割的边界情况撇号、连字符和缩写3.1 撇号的三种处理策略撇号是英文单词分割里最值得细抠的字符。它同时承担所有格Johns、缩写dont、its、复数缩写90s三种角色没有统一的语法信号。策略模式对dont的结果适用任务保留撇号[a-zA-Z](?:[a-zA-Z])*dont词频统计、模糊匹配拆开撇号[a-zA-Z]再做切分don,t拼写纠错、语音合成丢弃撇号[a-zA-Z]dont搜索引擎索引我的默认选择是保留。原因在于后续做单词归一化时保留下来的dont能通过规则还原成do not而拆开的t很难拼回原意。如果下游是向量化或词嵌入拆开的碎片基本是噪声。3.2 缩写后处理U.S. 不该被切成 U 和 SU.S.、U.K.、e.g.这类带点缩写在一段正常英文里出现频率不低任何基于字符类的正则都会把它们拆成单字母词。简单做法是分两步走先用正则切再按白名单合并。我常用的合并逻辑如下。import re PATTERN re.compile(r[a-zA-Z](?:[-][a-zA-Z])*) ABBR_MAP { (u, s): u.s., (u, k): u.k., (e, g): e.g., (i, e): i.e., } def split_with_abbreviations(text): tokens PATTERN.findall(text.lower()) merged [] i 0 while i len(tokens): if i 1 len(tokens) and (tokens[i], tokens[i1]) in ABBR_MAP: merged.append(ABBR_MAP[(tokens[i], tokens[i1])]) i 2 else: merged.append(tokens[i]) i 1 return merged这个实现的关键是合并逻辑永远在分割之后进行避免正则本身把e.g.当成整体去匹配而干扰其他单词。注意白名单是有限的落到具体业务时应该从语料里统计高频单字母对再决定要不要纳入映射。3.3 连字符拆开是三个词留着是一个词state-of-the-art在技术文档里大量出现。拆开得到state、of、the、art四个高频词会稀释统计的区分度留着作为整体则能保留复合词含义。两者没有绝对对错取决于任务。做词频统计或 TF-IDF 时我倾向于保留因为state-of-the-art作为一个整体比四个独立词更能代表文档主题。做机器翻译或语法分析时反而应该拆开因为句法树需要独立的state和of。保留连字符的正则写法是# 匹配字母开头 中间允许出现连字符或撇号 字母结尾 re.findall(r[a-zA-Z](?:[-][a-zA-Z])*, text)这里有个细节连字符若出现在单词末尾如well-这种断行标记上述模式不会吞入结尾的-从而避免尾部噪声。这是我喜欢用这个模式而不是[a-zA-Z-]的原因后者会把word--next里的双连符一起吞进去。3.4 数字和 Unicode 字符的取舍英文语料里常混入1984、3.14、COVID-19这类数字相关词。\w默认包含数字但纯数字串对词频统计没有语义价值。如果任务是文本分类通常只保留字母开头的 token。遇到café、naïve这类带重音符号的单词时[a-zA-Z]会直接丢弃重音字母\w在 Python 3 的默认 Unicode 模式下能完整保留。取舍的原则是语料面向搜索引擎或统计任务时应显式加入re.ASCII标志让\w只匹配 ASCII避免带重音的变体被拆成两个不完整词。# 只保留 ASCII 字母和内部连字符/撇号 re.findall(r[a-zA-Z](?:[-][a-zA-Z])*, text) # 保留 Unicode 字母适合多语言混合语料 re.findall(r[^\W\d_](?:[-][^\W\d_])*, text, flagsre.UNICODE)[^\W\d_]是\w减去数字和下划线的写法匹配所有的 Unicode 字母。在英文为主、偶尔夹杂法文或德文的语料里这个模式比[a-zA-Z]更稳。4. 实战英文单词分割在词频统计里的完整管道4.1 从文件读取到分割的最小管道把分割逻辑接到真实文件处理上我通常一次性完成读取、分割和统计而不是中间保存中间结果。下面是一个完整的词频统计管道。import re from collections import Counter PATTERN re.compile(r[a-zA-Z](?:[-][a-zA-Z])*) def tokenize_file(filepath, min_len2): with open(filepath, encodingutf-8) as f: text f.read() tokens PATTERN.findall(text.lower()) return [t for t in tokens if len(t) min_len] filepath article.txt tokens tokenize_file(filepath, min_len2) counter Counter(tokens) print(counter.most_common(20))这段代码维持了单向数据流读入原始文本 → 小写归一化 → 正则分割 → 长度过滤 → 频次统计。min_len2能过滤掉单字母词但也会误伤I和a所以这个参数必须结合停用词表使用不能单独依赖长度过滤。4.2 大小写归一化与词形归并的边界Python和python在统计时应算同一个词所以lower()是分割后必做的一步。但 lower 不能彻底解决问题running、runs、ran属于同一个词根词频统计里被当成三个独立词。正则只能做表层切割做不了词形归并Lemmaization。如果下游做的是文档相似度或主题聚类建议引入词干化工具常见做法是加一层 SnowballStemmer。但对于单词分割本身的职责边界分词只负责把 token 切对词形归并不是它的工作。把职责分开后续如果换用深度学习模型做向量化只要重写归并层不需要动分割层。4.3 停用词表与最小词长过滤的组合只按词频排序结果前列永远是the、and、of。停用词表能把这些常见词从结果中剔除但要避免一个隐患停用词过滤必须放在大小写归一化之后否则The和the会被当成两个词分别判断。STOP_WORDS { the, and, of, to, in, for, on, with, at, by, is, are, was, were, be, been, have, has, had, } def filter_stopwords(tokens): return [t for t in tokens if t not in STOP_WORDS]对于没有现成停用词表的场景可以先用 Counter 统计全量词频把出现次数超过语料 1% 的高频虚词提取出来作为停用词初稿。这样生成的停用词表与业务语料强相关比直接套用通用表更贴合场景。min_len和停用词表是互补关系前者过滤结构上的短噪声后者过滤语义上的噪声。4.4 三个必调参数及调参路径参数位置推荐初始值调整依据正则模式findall第一个参数[a-zA-Z](?:[-][a-zA-Z])*语料含法语/德语词时改[^\W\d_]min_len筛选 token2统计到单字母术语如C语言时降为 1是否纳入缩写合并分割后处理关语料缩写密度高时开启白名单调参的顺序是先看分割质量再做长度过滤。输出里出现don和t这种碎片时先修正则出现整段的https或文件名时再考虑加前缀过滤。一次性调整多个参数会让结果难以定位真正原因。5. 性能对比与结果验证的进阶技巧5.1 先 split 后清洗 vs 直接 findall一个常被提起的老问题re.findall要逐字符扫描文本会不会比先split再用str.translate清洗标点慢得多我做一个约 20 万词的新闻语料测试结论是findall与splittranslate耗时基本持平差异在 10% 以内。原因在于split虽然快但后续还要用translate遍历每个 token 做第二次扫描findall是在一次遍历中完成字符分类和输出。后者多花的成本仅在于正则引擎的状态机切换而 Python 层省下了列表二次遍历的开销。因此性能不该成为选择 split 的理由正确性才是。5.2 用 re.compile 缓存模式和进程池加速批处理处理上百个文件时反复调用re.findall(r..., text)会导致正则模式被反复编译。把模式提前用re.compile编译findall方法直接在编译后的模式对象上调用能减少这一层开销。配合进程池能达到接近线性加速。import re from concurrent.futures import ProcessPoolExecutor PATTERN re.compile(r[a-zA-Z](?:[-][a-zA-Z])*) def tokenize_file(filepath): with open(filepath, encodingutf-8) as f: text f.read() return PATTERN.findall(text.lower()) file_list [a.txt, b.txt, c.txt, d.txt] with ProcessPoolExecutor(max_workers4) as ex: results list(ex.map(tokenize_file, file_list))注意进程池的回调函数里不需要再写一次re.compile因为全局变量PATTERN会被每个 worker 进程继承。这比每次调用tokenize_file时临时编译要快。5.3 一个验证方法把分割结果重新拼回原文分割逻辑改过几次后最怕的问题不是切得不准而是切丢了字符。一个有效的自查方式是把分割后的单词重新连接成字符串与原始文本去掉所有空白后的字符串做比较。如果两者完全相等说明没有字符丢失。def verify_no_char_loss(text, words): joined .join(words) reference re.sub(r\s, , text) return joined reference, len(joined), len(reference) text Dont stop believing, hold on to that feelin. words PATTERN.findall(text.lower()) ok, joined_len, ref_len verify_no_char_loss(text, words) print(ok, joined_len, ref_len)这个验证的局限在于它只保证“没丢字符”不保证“切分正确”比如把dont拆成don和t再拼回去长度仍然相等。所以把它作为冒烟测试跑真正判断切分质量要把输出目测一遍或者和权威分词器的结果抽样对比。两种手段结合才能放心把分割结果送进下游统计。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表