ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python正则表达式实现中英文混合文本自动分句工具

Python正则表达式实现中英文混合文本自动分句工具 你有没有遇到过这样的场景从网上爬取了一篇长文章或者从数据库里导出了一段用户评论想要按句子进行分析、处理或展示结果发现文本是一整段没有换行标点符号也混用手动拆分几百上千条数据根本不可能。用简单的split(.)遇到“比如Python 3.9.5 发布了。”这样的句子直接就被错误地切开了。这就是字符串分句的经典痛点。表面上看它只是一个简单的文本分割问题但实际开发中它直接关系到后续自然语言处理NLP的准确性、数据清洗的质量甚至是前端展示的用户体验。一个鲁棒的分句工具能帮你省去大量繁琐的数据预处理时间。网上有很多零散的代码片段但往往只处理英文句点或者无法应对中文混合标点、缩写、特殊符号等复杂情况。今天我们就来彻底解决这个问题。本文将带你从零构建一个健壮、可配置、支持中英文混合文本的自动分句工具。不止是给出代码更重要的是我会拆解其中的核心思路、常见陷阱并提供一个可以直接集成到你项目中的生产级解决方案。读完本文你将能理解为什么简单的split方法在真实场景中会失效。掌握使用正则表达式re模块进行复杂文本匹配与分割的核心技巧。获得一个封装好的SentenceSplitter类支持自定义分句标点、处理缩写词。学会如何处理边缘案例如URL、邮箱、小数点、省略号等。了解如何将分句功能无缝集成到你的数据流水线中。1. 为什么str.split(.)是远远不够的在开始动手之前我们必须先搞清楚一个“完美”的分句算法需要应对哪些挑战。直接使用split方法就像用一把斧头去做外科手术——太粗糙了。场景一英文缩写与小数点text1 Mr. Smith bought 3.14 apples from the U.S. market. Then he went home. # 错误分割[Mr, Smith bought 3, 14 apples from the U S, market, Then he went home, ] # 期望分割[Mr. Smith bought 3.14 apples from the U.S. market., Then he went home.]在这里Mr.,U.S.,3.14中的点号都不应该作为句子结束符。场景二中文混合标点与全半角text2 你好这是第一句。第二句呢第三句……第四句。 # 简单 split(。) 会丢失其他标点且无法处理省略号。中文的句子结束符可能包括。……等甚至还有全角半角混用的情况如!和。场景三引号、括号等成对符号text3 他说“你好吗”然后笑了。 # 理想分割[他说“你好吗”然后笑了。] # 错误分割可能将引号内的问句单独切出。句子结束符如果出现在引号、括号内部通常不代表整个句子的结束。场景四特殊格式内容text4 访问我们的网站 https://example.com/page.html。了解更多 # URL 中的点号不能作为分句点。看到这里你应该明白了分句不是一个简单的“找标点然后切分”的过程而是一个需要结合上下文进行模式识别的任务。这正是正则表达式大显身手的地方。2. 核心武器Pythonre模块与正则表达式正则表达式Regular Expression是处理文本模式的瑞士军刀。对于分句我们需要用它来定义一个“句子边界”的模式。核心思路是寻找后面跟着空白字符或字符串结尾且不在特定上下文中的句子结束标点。Python 的re模块提供了finditer或split函数可以基于复杂的模式进行查找或分割。我们将主要使用re.finditer来定位所有句子结束的位置然后进行切片。3. 环境准备只需要 Python本项目对环境要求极低任何安装了 Python 的环境均可运行。Python 版本 推荐 Python 3.6 及以上。本文代码在 Python 3.8 上测试通过。核心库 仅需 Python 标准库re正则表达式。IDE 任意你喜欢的编辑器或 IDE如 VS Code, PyCharm, Jupyter Notebook 等。无需安装任何第三方包确保最大的可移植性。4. 分句算法核心流程拆解我们将构建一个分句器其工作流程可以分解为以下几步定义句子结束符 明确哪些字符或字符组合表示一个句子的结束。例如。.?!…。定义保护模式 明确哪些情况下的结束符应该被“保护”起来不被视为句子边界。例如缩写词 如Mr.,Dr.,Inc.等。数字中的点 如3.14,192.168.1.1。URL/邮箱 如www.example.com,namecompany.co.uk。成对标点内 如引号“ ”、括号( )内的结束符这是一个高级特性本文基础版本暂不处理但会提供思路。构建正则表达式模式 将1和2的规则翻译成一个正则表达式。这个模式要能“匹配”到真正的句子边界位置。执行分割 使用re.finditer找到所有边界位置然后在边界处将长字符串切片。后处理 清理结果列表中的空字符串和首尾空格。5. 从基础到进阶完整代码实现我们将实现一个SentenceSplitter类它易于使用且可扩展。5.1 基础版本处理中英文结束符我们先实现一个能正确处理中英文常见句子结束符并避免切割缩写和数字的版本。# sentence_splitter.py import re class SentenceSplitter: 一个基于正则表达式的健壮句子分割器。 def __init__(self, abbreviationsNone): 初始化分割器。 Args: abbreviations (list): 自定义的缩写词列表例如 [Mr., Mrs., Dr., e.g., i.e.] # 默认的英文缩写列表 self.default_abbr [ Mr., Mrs., Ms., Dr., Prof., Sr., Jr., vs., etc., e.g., i.e., a.m., p.m., U.S., U.K., Inc., Ltd., Co., Corp., Jan., Feb., Mar., Apr., Jun., Jul., Aug., Sep., Oct., Nov., Dec., ] self.abbreviations set(abbreviations) if abbreviations else set(self.default_abbr) self._compile_pattern() def _compile_pattern(self): 编译核心的正则表达式模式。 # 1. 定义句子结束符中文句号、感叹号、问号、英文句号、感叹号、问号、省略号 # 使用 re.UNICODE 标志确保正确匹配Unicode字符如中文标点 sentence_endings r[。.?!…] # 2. 定义需要“保护”的模式防止错误分割 # a. 缩写词模式匹配已知的缩写词后面可能跟空格或字符串结束 # 我们将缩写词列表转换为一个正则表达式可选组 abbr_pattern r(?: |.join(re.escape(abbr) for abbr in self.abbreviations) r) # b. 数字模式匹配数字中间的点如 3.14, 192.168.1.1 # \d 代表数字\. 匹配点号? 表示前面的模式出现0次或1次用于匹配可能的小数点或IP地址中的点 number_pattern r\d(?:\.\d) # c. 基础URL/邮箱模式简单版本匹配常见的 www. 开头或包含 的字符串 # 这是一个简化版复杂的URL匹配需要更长的正则。 url_email_pattern r(?:www\.|\b\w)\S # 3. 组合成最终的正则表达式 # 核心思想匹配一个句子结束符但这个结束符前面不能是受保护的模式。 # (?!...) 是“否定回顾后发断言”表示匹配位置的前面不能是...模式。 # 我们匹配的是结束符本身然后通过查找其后的边界空白或字符串结尾来确定切割点。 # 模式解释 # (?:...|...|...) 是一个非捕获组表示“或”关系。 # \s 代表空白字符空格、换行、制表符等 $ 代表字符串结尾。 pattern rf (?!{abbr_pattern}) # 前面不是缩写词 (?!{number_pattern}) # 前面不是数字模式如 3.14 (?!{url_email_pattern}) # 前面不是URL/邮箱模式简化 ({sentence_endings}) # 匹配句子结束符 [\s\n\r]* # 匹配结束符后的任意空白字符包括换行 (?\s|$) # 正向预查后面必须是空白或字符串结尾 # 使用 re.VERBOSE 允许我们书写多行并添加注释使用 re.UNICODE 支持Unicode self.pattern re.compile(pattern, re.VERBOSE | re.UNICODE) def split(self, text): 将输入文本分割成句子列表。 Args: text (str): 输入的长字符串文本。 Returns: list: 分割后的句子列表。 if not text or not text.strip(): return [] sentences [] last_end 0 # 使用 finditer 找到所有匹配的句子结束位置 for match in self.pattern.finditer(text): # match.end() 是匹配到的结束符之后的位置 sentence_end match.end() sentence text[last_end:sentence_end].strip() if sentence: # 避免添加空句子 sentences.append(sentence) last_end sentence_end # 处理最后一句话如果没有以结束符结尾 if last_end len(text): final_sentence text[last_end:].strip() if final_sentence: sentences.append(final_sentence) return sentences # 示例用法 if __name__ __main__: splitter SentenceSplitter() test_text Hello world! This is Mr. Smith from the U.S. He bought 3.14 apples. 你好世界这是来自美国的史密斯先生。他买了3.14个苹果。访问网站 www.example.com。结束了吗 result splitter.split(test_text) for i, sent in enumerate(result, 1): print(f{i}: {sent})关键逻辑解释_compile_pattern方法是核心它构建了一个复杂的正则表达式。(?!...)确保结束符前面不是缩写、数字等受保护内容。split方法遍历所有匹配到的“边界”利用字符串切片提取句子并自动处理末尾文本。我们使用re.VERBOSE标志让正则表达式可读性更强便于维护。5.2 进阶版本处理成对标点与更复杂的缩写基础版本已经能解决80%的问题。但对于引号内的句子或者更复杂的缩写如Ph.D.我们需要升级。# sentence_splitter_advanced.py import re class AdvancedSentenceSplitter(SentenceSplitter): 进阶版句子分割器增加对成对标点内部分句的保护。 def __init__(self, abbreviationsNone, protect_pairsNone): 初始化进阶分割器。 Args: abbreviations (list): 同基类。 protect_pairs (list): 需要保护的成对标点列表例如 [(“, ”), ((, )), ([, ])]。 保护意味着在这些符号内部的句子结束符不会被用于分句。 super().__init__(abbreviations) # 默认保护中文引号和英文引号 self.protect_pairs protect_pairs if protect_pairs else [(“, ”), (, ), ((, )), ([, ]), (【, 】)] self._compile_advanced_pattern() def _compile_advanced_pattern(self): 编译包含成对标点保护的正则表达式模式。 # 继承基类的结束符定义 sentence_endings r[。.?!…] # 构建成对标点保护的正则部分这是一个简化方案 # 更严谨的方案需要用到递归正则或状态机这里我们用一种近似方法 # 匹配一个结束符但要确保它前面没有未闭合的开启标点。 # 注意这是一个启发式方法对于极度嵌套或混乱的标点可能不完美。 protect_start_pattern .join(re.escape(pair[0]) for pair in self.protect_pairs) protect_end_pattern .join(re.escape(pair[1]) for pair in self.protect_pairs) # 构建一个模式用于粗略判断当前位置是否在成对标点内部。 # 我们检查从文本开头到当前位置开启标点数量是否大于关闭标点数量。 # 这需要在 split 方法中动态计算而不是静态正则。因此我们调整策略。 # 我们先编译基础模式同基类成对标点保护在 split 逻辑中实现。 super()._compile_pattern() # 保存基础模式 self.base_pattern self.pattern # 我们不再覆盖 self.pattern而是在 split 方法中增加逻辑。 def split(self, text): 重写 split 方法加入成对标点保护逻辑。 使用一个栈来跟踪开启的标点。 if not text or not text.strip(): return [] sentences [] last_end 0 i 0 length len(text) stack [] # 栈用于跟踪未闭合的开启标点 while i length: # 1. 更新栈状态 char text[i] for start_char, end_char in self.protect_pairs: if char start_char: stack.append(start_char) break elif char end_char and stack and stack[-1] start_char: stack.pop() break # 2. 尝试匹配句子结束符仅当不在保护栈内时 match self.base_pattern.match(text, i) # 使用基类的模式从位置 i 开始匹配 if match and not stack: # 找到匹配且不在保护范围内 sentence_end match.end() sentence text[last_end:sentence_end].strip() if sentence: sentences.append(sentence) last_end sentence_end i sentence_end # 跳转到匹配结束的位置 else: i 1 # 未匹配到或处于保护中则向前移动一个字符 # 3. 处理最后一段 if last_end length: final_sentence text[last_end:].strip() if final_sentence: sentences.append(final_sentence) return sentences # 示例用法测试成对标点保护 if __name__ __main__: advanced_splitter AdvancedSentenceSplitter() test_text_advanced 他说“你好吗今天天气真好。”然后离开了。她又问“你呢” result_adv advanced_splitter.split(test_text_advanced) print(进阶分句结果保护引号内句子) for i, sent in enumerate(result_adv, 1): print(f{i}: {sent}) # 期望输出[他说“你好吗今天天气真好。”然后离开了。, 她又问“你呢”]进阶逻辑解释我们引入了一个stack栈来跟踪未闭合的开启标点如左引号、左括号。在遍历文本时遇到开启标点就入栈遇到对应的关闭标点就出栈。只有当栈为空即当前不在任何成对标点内部时才允许将匹配到的句子结束符作为边界。这种方法能有效防止将他说“你好吗”错误地分割成他说“你好吗和”。5.3 实用工具函数一行代码快速分句为了方便日常使用我们可以提供一个简单的函数接口。# sentence_splitter_util.py from sentence_splitter import SentenceSplitter # 假设基础类保存在此文件 _default_splitter SentenceSplitter() # 创建一个默认的全局分割器实例以提高效率 def split_sentences(text, use_advancedFalse, custom_abbrNone): 快速分句工具函数。 Args: text (str): 待分句的文本。 use_advanced (bool): 是否使用进阶版保护成对标点。默认为 False。 custom_abbr (list): 自定义缩写词列表。 Returns: list: 句子列表。 if use_advanced: # 按需导入避免循环依赖 from sentence_splitter_advanced import AdvancedSentenceSplitter splitter AdvancedSentenceSplitter(abbreviationscustom_abbr) else: if custom_abbr: splitter SentenceSplitter(abbreviationscustom_abbr) else: splitter _default_splitter return splitter.split(text) # 使用示例 if __name__ __main__: long_text 这是一个测试。Dr. Smith 今天不在。价格是 $19.99。你觉得呢好的。 sentences split_sentences(long_text) print(快速分句结果) print(sentences)6. 运行结果与效果验证让我们用几个典型的测试案例来验证我们的分句器。# test_sentence_splitter.py from sentence_splitter import SentenceSplitter from sentence_splitter_advanced import AdvancedSentenceSplitter from sentence_splitter_util import split_sentences def run_tests(): print( * 50) print(测试基础 SentenceSplitter) print( * 50) splitter SentenceSplitter() test_cases [ (英文缩写与数字, Mr. Johnson works at ABC Inc. He earned $500.50 today.), (中英文混合, Hello! 你好吗Im fine. 谢谢), (URL与邮箱, 请访问 http://example.com/page.php。联系 adminsite.co.uk。), (多空格与换行, 第一句。\n\n第二句 第三句), (连续结束符, 真的吗太棒了), ] for name, text in test_cases: print(f\n测试: {name}) print(f输入: {text}) result splitter.split(text) print(f输出 ({len(result)} 句):) for i, s in enumerate(result): print(f {i1}: {s}) print(\n * 50) print(测试进阶 AdvancedSentenceSplitter (成对标点保护)) print( * 50) adv_splitter AdvancedSentenceSplitter() advanced_test_cases [ (引号内问句, 老师问“你明白了吗”学生回答“明白了”), (括号内容, 会议将于下午2:00开始。你参加吗很重要。), (混合嵌套, 他说“这本书出版于2020年。非常有趣。”), ] for name, text in advanced_test_cases: print(f\n测试: {name}) print(f输入: {text}) result adv_splitter.split(text) print(f输出 ({len(result)} 句):) for i, s in enumerate(result): print(f {i1}: {s}) print(\n * 50) print(测试工具函数 split_sentences) print( * 50) quick_test 快速测试。使用 Dr. 等缩写。价格 99.99。完成 print(f输入: {quick_test}) result split_sentences(quick_test) print(f输出: {result}) if __name__ __main__: run_tests()运行上述测试代码你将会看到类似以下的输出 测试基础 SentenceSplitter 测试: 英文缩写与数字 输入: Mr. Johnson works at ABC Inc. He earned $500.50 today. 输出 (2 句): 1: Mr. Johnson works at ABC Inc. 2: He earned $500.50 today. 测试: 中英文混合 输入: Hello! 你好吗Im fine. 谢谢 输出 (4 句): 1: Hello! 2: 你好吗 3: Im fine. 4: 谢谢 ...通过对比输出和输入你可以清晰地验证分句器是否正确处理了缩写、数字、URL、混合标点以及成对标点等复杂情况。7. 常见问题与排查思路在实际使用中你可能会遇到一些问题。下表列出了常见问题及其解决方法问题现象可能原因排查方式解决方案缩写词被错误分割(如Mr.被切开)缩写词未包含在abbreviations列表中。检查SentenceSplitter初始化时传入的列表或默认列表。将遗漏的缩写词添加到abbreviations参数中。例如splitter SentenceSplitter(abbreviations[Ph.D., M.Sc.])数字中的点被当作句子结束(如3.14被切开)数字模式正则未能匹配该格式如科学计数法1.23e-4。检查_compile_pattern方法中的number_pattern。增强数字模式正则例如改为r\d(?:\.\d)*(?:[eE][-]?\d)?以支持科学计数法。URL或邮箱被错误分割URL/邮箱正则模式过于简单未能覆盖复杂情况。使用更全面的URL正则表达式。替换url_email_pattern为更健壮的正则例如使用urllib.parse库先进行验证或在正则中排除更复杂的域名格式。成对标点保护失效(引号内句子仍被分割)1. 标点对未在protect_pairs中定义。2. 文本中标点嵌套混乱或不成对。1. 检查protect_pairs列表。2. 打印遍历时的栈状态检查逻辑。1. 添加缺失的标点对。2. 对于不成对标点基础保护逻辑会失效。考虑使用更简单的规则或预处理文本。分句结果包含空字符串文本开头或结尾有空白或连续多个结束符产生空匹配。检查split方法中切片后的.strip()和空值判断。确保在添加句子到列表前使用if sentence:进行过滤。我们的代码已包含此逻辑。处理速度慢超长文本正则表达式过于复杂或文本极长10MB。对长文本进行分句测试使用性能分析工具。1. 考虑将文本预先按段落\n\n分割再对每个段落分句。2. 对于超长文本流式处理或使用专门的自然语言处理库如 spaCy可能更合适。无法处理某些特殊Unicode标点正则表达式中的sentence_endings字符集未包含该标点。确认该标点的Unicode编码并添加到字符集中。扩展sentence_endings变量例如添加r[。.?!…‥]包含更多中文和英文变体。8. 最佳实践与工程建议将分句功能集成到实际项目中时遵循以下建议可以避免很多坑预处理文本 在分句前建议先对文本进行基本的清洗如去除多余的空格、换行符统一全角/半角标点可以使用str.replace或unicodedata.normalize。import unicodedata def normalize_text(text): # 示例将全角标点转换为半角根据需求调整 text text.replace(, ,) # 全角逗号转半角 text text.replace(。, .) # 全角句号转半角谨慎中文句号通常保留 # 或者使用更通用的方式 # text unicodedata.normalize(NFKC, text) # 兼容性分解但可能改变符号 return text.strip()自定义缩写词库 根据你的领域医学、法律、科技建立专属的缩写词库。可以从专业词典或领域语料中抽取。medical_abbr [Dr., Pt., Rx., ICU, MRI, CT scan] legal_splitter SentenceSplitter(abbreviationsmedical_abbr SentenceSplitter().default_abbr)性能考量 对于需要处理海量文本的应用程序如新闻聚合、搜索引擎SentenceSplitter的实例应该被复用而不是每次调用都新建。因为编译正则表达式re.compile有一定开销。与 NLP 管道集成 分句通常是 NLP 任务如情感分析、命名实体识别、机器翻译的第一步。确保你的分句器输出与下游任务兼容。例如有些 NLP 工具期望句子是字符串列表。处理极端情况 对于来自社交媒体、聊天记录等非规范文本分句会非常困难如大量表情符号、非标准缩写、无标点。在这种情况下可能需要结合基于规则的方法和简单的统计模型如查找常见开头词或者直接使用更高级的 NLP 库。单元测试 为你的分句器编写单元测试覆盖各种边缘案例。这能确保代码修改不会破坏现有功能。import unittest class TestSentenceSplitter(unittest.TestCase): def test_basic_split(self): splitter SentenceSplitter() result splitter.split(Hello world. How are you?) self.assertEqual(result, [Hello world., How are you?]) # ... 更多测试日志与监控 在生产环境中记录分句失败或产生异常输出的情况例如分出的句子数量为0或单个句子过长这有助于你发现数据质量问题并持续优化分句规则。9. 总结本文深入探讨了在 Python 中实现健壮的自动分句功能。我们从最原始的split方法的问题出发逐步构建了一个基于正则表达式、支持中英文、能处理缩写和数字的SentenceSplitter类并进一步扩展了其对成对标点的保护能力。关键收获正则表达式是利器 对于复杂的文本模式匹配精心设计的正则表达式远比简单的字符串方法强大。上下文是关键 判断一个标点是否是句子结束符必须考虑其上下文前面是否是缩写、数字是否在引号内。没有银弹 我们实现的规则引擎对于规范文本新闻、文章效果很好但对于极度不规范的文本如网络聊天可能需要更复杂的方法或接受一定的不完美。可配置性很重要 通过允许用户自定义缩写词列表和成对标点我们的工具可以轻松适应不同的领域和语言风格。下一步你可以尝试集成spaCy或NLTK等成熟的 NLP 库它们内置了更复杂的分句模型适用于对精度要求极高的场景。探索机器学习方法如训练一个序列标注模型来预测句子边界这在处理新兴网络语言时可能更有优势。将分句功能封装成一个Web API或命令行工具方便其他系统调用。希望这个工具和其中蕴含的思路能切实提升你处理文本数据的效率。代码已准备好复制到你的项目中根据实际数据微调一下缩写词列表马上就能用起来。
返回列表