- 教程
- 文档
【免费下载链接】30-seconds-of-python
Short Python code snippets for all your development needs
本篇文章以 30-seconds-of-python 仓库中的 words 片段 为讲解主体,深入剖析这个短小精悍的字符串分词函数:它如何借助re.findall()与一个可定制正则模式,把任意字符串转换为单词列表。读完本文,你将掌握re.findall()的匹配行为与返回值细节、[a-zA-Z-]+字符类与\b词边界的实际差异、如何传入自定义模式解决带连字符单词的分词问题,以及该片段与仓库内 slugify、camel、palindrome 等字符串类片段的协同用法。
一、片段概览:一段代码解决分词需求
在 30-seconds-of-python 仓库中,words.md 是一个类型为snippet、标签为string、regexp的短代码片段,其元信息(dateModified: 2020-11-02)表明它属于该集合的早期版本。片段的定位非常明确——"Converts a given string into a list of words"(把给定字符串转换为单词列表),核心实现只有区区几行:
import re def words(s, pattern = '[a-zA-Z-]+'): return re.findall(pattern, s)函数签名只有两个参数:
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
s | str | 必填 | 待拆分的输入字符串 |
pattern | str(正则) | '[a-zA-Z-]+' | 用来匹配"单词"的正则模式,可省略以使用默认行为 |
整个函数的逻辑只有一句话:把pattern交给re.findall(),从s中找出所有匹配的子串并作为列表返回。下面的示例展示了它在不同输入下的表现(代码取自 words.md):
words('I love Python!!') # ['I', 'love', 'Python'] words('python, javaScript & coffee') # ['python', 'javaScript', 'coffee'] words('build -q --out one-item', r'\b[a-zA-Z-]+\b') # ['build', 'q', 'out', 'one-item']二、核心机制:re.findall()的匹配行为
要真正理解words(),必须先弄清re.findall()的三条底层行为,它们是整个函数正确性的来源:
- 从左到右扫描,返回所有非重叠匹配:
re.findall(pattern, s)从字符串起始位置开始,每次尝试在当前位置匹配pattern,成功则记录该匹配并紧接着从匹配结束的位置继续,绝不回头重叠。所以结果列表中的顺序就是单词在原文中出现的顺序。 - 匹配不到任何内容时返回空列表:例如
words('!!!')会返回[],而不是None或报错。这使words()的结果可以安全地直接用于len()、for循环或列表拼接。 - 若模式中包含捕获组,返回值会改变形状:这是最容易踩的坑。当
pattern里出现(...)捕获组时,re.findall()返回的不再是"匹配到的整个字符串列表",而是每个匹配中各组内容的元组列表(组数大于 1 时)或各组内容的字符串列表(恰有 1 组时)。例如re.findall(r'([a-z]+)-([0-9]+)', 'abc-123')返回[('abc', '123')]。因此在使用自定义pattern参数时,若模式含捕获组,words()返回的将不再是单词字符串,需要格外留意。
对默认模式'[a-zA-Z-]+'而言,它不含任何捕获组,re.findall()会规规矩矩地返回所有匹配子串的列表——这正是分词场景想要的形态。
三、默认模式'[a-zA-Z-]+'逐字符拆解
默认模式是理解整个片段行为的钥匙,我们把它拆成三块来看:
[a-zA-Z]:字符类,匹配任意一个英文字母(大写A-Z或小写a-z)。注意它是纯 ASCII 的,不包含数字、下划线,也不包含é、ö这类带变音符号的字母,更不包含中文等非拉丁字符。-:字符类中的连字符。在 Python 正则中,-位于字符类的末尾([a-zA-Z-])时被当作字面量字符处理,因此该字符类额外允许匹配一个普通的连字符-。+:量词,表示前面的字符类至少出现一次,贪婪地连续匹配尽可能多的字母与连字符。
把三者组合起来,默认模式的含义是:匹配由英文字母与连字符组成的一个或多个连续子串。这正好解释了示例一与示例二:
words('I love Python!!') # ['I', 'love', 'Python'] # '!!' 不是字母也不是连字符,被当作分隔符丢弃 words('python, javaScript & coffee') # ['python', 'javaScript', 'coffee'] # 逗号、空格、'&' 均为非匹配字符,被跳过,剩余字母串按出现顺序收集可以看到,分词的过程本质上是"把不匹配的字符全部当作分隔符丢弃",这正是re.findall加字符类的组合优势:无需手工编写复杂的切分逻辑。
四、实战用例:默认模式的一个"坑"与\b词边界的解法
片段文档给出的第三个示例特意传入了一个自定义模式,这是有原因的。先看默认模式在带命令行风格参数输入下的结果:
words('build -q --out one-item') # ['build', '-q', '--out', 'one-item']由于默认字符类把-也视为"单词的一部分",-q、--out会被完整匹配进来,这显然不是我们想要的"单词"。片段文档因此引入了带词边界的自定义模式:
words('build -q --out one-item', r'\b[a-zA-Z-]+\b') # ['build', 'q', 'out', 'one-item']\b是词边界断言:它匹配"单词字符与非单词字符之间"的位置。这里的"单词字符"指\w(字母、数字、下划线),而-不是单词字符。于是:
-q的开头-之前紧邻字符串起点(非单词字符),-本身也是非单词字符,二者之间没有边界,\b无法在此成立,-q整体无法被匹配;- 从
q开始,其前一位是-(非单词字符)、自身是单词字符,边界成立,于是匹配到独立的q; one-item中的连字符两侧都是单词字符,\b约束的是整个匹配的起止位置,one-item两端与空格相邻处边界成立,因此整个带连字符的词组仍被保留。
这就是该示例想要展示的"精细分词"能力:在不需要连字符单独成词、却又希望保留 one-item 这类复合词时,用\b[a-zA-Z-]+\b替换默认模式即可。你可以根据业务需要,自由更换pattern参数(例如改成r'[a-z]+'或r'\w+')来调整分词规则,这正是words()把模式做成参数而非写死的原因。
五、边界与局限:什么情况下不适合用它
基于对源码实现的上述分析,可以明确指出words()的适用边界:
- 只认 ASCII 字母:默认模式匹配不了中文、日文、带重音符号的字母等非 ASCII 字符,含这些字符的文本会被当作分隔符丢弃。若要分词包含中文的文本,需要改用支持 Unicode 的模式(如
r'\w+'配合re.UNICODE,Python 3 默认对\w开启 Unicode 匹配)。 - 数字与下划线被排除:默认字符类不含
[0-9]与_,因此'version 2.0'中的2、snake_case中的下划线都不会被当作词的一部分。 - 连字符的一体两面:默认模式会把
-吸收进单词(如-q、--out),这是设计使然;若要排除孤立连字符,必须像片段示例那样显式传入\b[a-zA-Z-]+\b模式。 - 大小写不归一:函数不做任何大小写折叠或清洗,
'Python'与'python'会作为两个不同元素返回。若需要统一词形,可配合str.lower()预处理。
六、拓展应用:分词之后的常见落点
words()返回的单词列表可以直接接入更长的处理管线,这也是它在仓库中与其它字符串片段自然衔接的地方:
- 词频统计:
len(words(s))得到词数,配合 frequencies、count-occurrences 可统计关键词出现次数,是朴素 NLP 文本预处理的第一步。 - 命名风格转换:把分词结果交给 camel(camelCase)、kebab(kebab-case)、snake(snake_case)做标识符风格转换——例如
words('some mixed_string', r'\w+')得到词列表后按需拼接。 - URL 友好化:与 slugify(把字符串转成
hello-world形式的 slug)搭配,先用re.sub()清洗、再拼接连字符,实现标题到 URL 的转换。 - 文本清洗:palindrome 片段同样使用
re.sub('[\W_]', '', s.lower())去除非字母数字字符来判回文,与words()的"以非匹配字符为分隔符"思路异曲同工;capitalize 可对每个词做首字母大写。 - 行级拆分:若输入是多行文本,可先参考 split-lines(
s.split('\n'),Python 标准库str.splitlines()提供类似能力)按行切分,再对每行调用words()做词级切分。
七、小结
30-seconds-of-python 的 words 片段 用一行return re.findall(pattern, s)优雅地封装了正则分词的核心逻辑:默认模式'[a-zA-Z-]+'覆盖了绝大多数英文分词场景,可选的pattern参数则把模式定制的自由留给调用方,示例中的\b[a-zA-Z-]+\b更是演示了如何解决"孤立连字符混入单词"的经典问题。理解这个片段,等于同时掌握了re.findall()的返回值约定、字符类与量词的使用技巧,以及词边界断言\b的实际效果——这些正是日常文本处理中高频复用的正则知识。若想深入实践,可以直接在本地 Python 环境运行片段中的代码与示例,并尝试修改pattern观察输出变化(本仓库为只读镜像,建议复制代码到自己的工程中实验)。
- 教程
- 文档
【免费下载链接】30-seconds-of-python
Short Python code snippets for all your development needs
相关推荐
30-seconds-of-python 的 digitize 代码片段:一行代码把数字拆成数字列表
30 seconds of python 的 digitize 代码片段:一行代码把数字拆成数字列表 digitize 是 30 seconds of pyth
教程文档30-seconds-of-python 字符串技巧:用 `capitalize_every_word` 与 `str.title()` 实现单词首字母大写
30 seconds of python 字符串技巧:用 capitalize_every_word 与 str.title 实现单词首字母大写 本篇技术指南围
教程文档30 seconds of code:使用 Intl.Segmenter 将 JavaScript 字符串拆分为单词的完整指南
30 seconds of code:使用 Intl.Segmenter 将 JavaScript 字符串拆分为单词的完整指南 本文是 30 seconds o
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考