使用多个单词边界分隔符将字符串拆分为单词

829

我认为我想做的是一个相当常见的任务,但我在网上找不到任何参考资料。我有带标点符号的文本,我想要一个单词列表。

"Hey, you - what are you doing here!?"
应该是。
['hey', 'you', 'what', 'are', 'you', 'doing', 'here']

但是 Python 的 str.split() 只能使用一个参数,所以在使用空格分割后会将标点符号与单词分开。有什么解决办法吗?


7
好的,我会尽力以最准确和易懂的方式来翻译这个网页。http://docs.python.org/library/re.html这是Python编程语言中re模块的官方文档。re模块提供了正则表达式的支持,可以用于在文本中搜索、匹配和替换字符串。该文档包含了re模块的所有功能和方法的详细说明,以及使用示例和常见问题解答。如果你需要在Python中使用正则表达式,这个文档将是一个非常有用的参考资料。 - mtasic85
13
Python的str.split()方法也可以不传任何参数来使用。 - Ivan Vinogradov
31个回答

706

re.split()

通过模式匹配来分割字符串。如果在模式中使用了捕获括号,则模式中所有组的文本也会作为结果列表的一部分返回。如果maxsplit不为零,则最多发生maxsplit次拆分,并且字符串的其余部分将作为列表的最后一个元素返回。(不兼容说明:在原始Python 1.5版中,maxsplit被忽略。这已经在后续版本中得到修复。)

>>> re.split('\W+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('(\W+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('\W+', 'Words, words, words.', 1)
['Words', 'words, words.']

22
这个解决方案的优点是可以轻松地适应下划线分隔的情况,而findall解决方案则不行:print re.split("\W+|_", "Testing this_thing")输出结果为:['Testing', 'this', 'thing']。 - Emil Stenström
3
字符串拆分常见的应用场景是从最终结果中移除空字符串条目。使用这种方法能否达到这个效果? re.split('\W+', ' a b c ') 的结果为 ['', 'a', 'b', 'c', ''],需要将其中的空字符串条目移除。 - Scott Morken
5
通常使用“shift”键来执行相反的操作,例如:ctrl+z 撤消与 ctrl+shift+z 重做。因此,“shift w”或“W”将是“w”的相反操作。 - Frank Vel
2
这个答案应该放在最顶部——它是唯一精确回答问题标题的答案。 - Kranach
2
这应该是 r'\W+'(原始字符串)吗? - nyanpasu64
显示剩余3条评论

542

正则表达式被证明是合理的情况:

import re
DATA = "Hey, you - what are you doing here!?"
print re.findall(r"[\w']+", DATA)
# Prints ['Hey', 'you', 'what', 'are', 'you', 'doing', 'here']

3
仍然有兴趣,但是我该如何实现此模块中使用的算法?为什么它不出现在字符串模块中?谢谢。 - ooboo
33
正则表达式初学者可能感到有些棘手,但它们非常强大。正则表达式'\w+'的意思是“一个单词字符(a-z等)重复一次或多次”。这里有一篇关于Python正则表达式的HOWTO:http://www.amk.ca/python/howto/regex/ - RichieHindle
372
这不是问题的答案。这是对另一个问题的回答,恰好适用于这种特定情况。这就像有人问“如何左转”,而得到的最佳回答是“接下来右转三次”。它适用于某些交叉口,但不提供所需的答案。具有讽刺意味的是,答案确实re中,只是不在findall中。下面给出的使用re.split()的答案更好。 - Jesse Dhillon
4
“取由单词字符序列组成的所有子字符串”和“在由非单词字符序列组成的所有子字符串上进行分割”实质上只是表达同一操作的不同方式;我不确定为什么您会说其中任何一个答案优越。 - Mark Amery
5
@TMWP:撇号的作用是使像“don't”这样的单词被视为一个整体,而不是被分成“don”和“t”两个部分。 - RichieHindle
显示剩余13条评论

504

另一个不使用正则表达式的快速方法是首先替换字符,代码如下:

>>> 'a;bcd,ef g'.replace(';',' ').replace(',',' ').split()
['a', 'bcd', 'ef', 'g']

91
快速粗略但对我的情况非常完美(我的分隔符是一个小的、已知的集合)。 - Andy Baker
10
非常适合在没有RE库的情况下使用,比如某些小型微控制器。 :-) - tudor -Reinstate Monica-
18
我认为这比正则表达式更明确易懂,所以对新手更友好。有时候,并不需要针对所有情况都使用通用解决方案。 - Adam Hughes
1
太棒了。我在多个输入的情况下使用了.split(),并且需要捕获用户(也就是我)用空格而不是逗号分隔输入的情况。我快要放弃并重新使用re,但是你的.replace()解决方案正中要害。谢谢。 - JayJay123
1
非常聪明和不错的解决方案。可能不是最“优雅”的方法,但它不需要额外的导入,并且适用于大多数类似情况,因此在某种程度上,它实际上也相当优雅和美丽。 - kushy
显示剩余3条评论

398
很多答案,但我找不到任何能够高效地执行问题标题所要求的操作(即在多个可能的分隔符上拆分——相反,许多答案会在任何非单词字符上进行拆分,这是不同的)。因此,这里有一个关于问题标题的答案,它依赖于Python标准且高效的re模块:
>>> import re  # Will be splitting on: , <space> - ! ? :
>>> filter(None, re.split("[, \-!?:]+", "Hey, you - what are you doing here!?"))
['Hey', 'you', 'what', 'are', 'you', 'doing', 'here']

其中:

  • [...] 匹配括号内的任意一个分隔符,
  • 正则表达式中的 \- 是为了避免将 - 解释为字符范围指示符(如 A-Z),
  • + 用于跳过一个或多个分隔符(通过使用 filter() 可以省略,但这样会在匹配单个字符分隔符之间产生不必要的空字符串),以及
  • filter(None, ...) 去除由前导和尾随分隔符创建的空字符串(因为空字符串具有 false 布尔值)。

这个 re.split() 函数可以精确地“按多个分隔符拆分”,正如问题标题所要求的那样。

此解决方案还免疫某些其他解决方案中出现的单词中的非 ASCII 字符的问题(请参见对 ghostdog74 的答案的第一条评论)。

与手动编写 Python 循环和测试相比,re 模块效率更高(速度更快、更简洁)!


3
我无法找到任何有效地做到问题标题所要求的事情的解决方案-第二个回答在5年前发布:https://dev59.com/onNA5IYBdhLWcg3wL6sc#1059601。 - BartoszKP
26
这个答案不会根据多个分隔符来进行拆分,而是会根据任何非字母数字字符来进行拆分。尽管如此,我认为原帖作者的意图可能只是想保留单词,而不是去除一些标点符号。 - Eric O. Lebigot
3
我刚刚意识到你的评论“此答案不分割……”让我感到困惑。一开始我以为“此”指的是你的 re.split 答案,但现在我明白你指的是 gimel 的答案。我认为这个答案(我正在发表评论的答案)是最好的答案 :) - GravityWell
  • 用于展示如何将多个连续的分隔符视为一个。谢谢!
- szeta
2
这里的讽刺是,这个答案没有得到最多的投票的原因是...有技术上正确的答案,还有原始请求者要找的东西(他们的意思而不是他们所说的)。这是一个很好的答案,我已经复制下来以备将来需要。然而,对我来说,评价最高的答案解决了一个非常类似于帖子中作者正在处理的问题,而且代码简洁清晰。如果一个答案同时提供了两种解决方案,我会给它投4票。哪个更好取决于您实际要做什么(而不是提问“如何”)。 :-) - TMWP
显示剩余8条评论

61

另一种方式,无需使用正则表达式

import string
punc = string.punctuation
thestring = "Hey, you - what are you doing here!?"
s = list(thestring)
''.join([o for o in s if not o in punc]).split()

8
这个解决方案实际上比被接受的那个更好。它可以不使用ASCII字符,试试“嘿,你-玛丽亚,你在这里干什么?!”。被接受的解决方案将无法处理前面的例子。 - Christopher Ramírez
4
我认为这里有一个小问题...您的代码将追加与标点符号分隔的字符,因此不会将它们拆分...如果我没错的话,您的最后一行应该是: ''.join([o if not o in string.punctuation else ' ' for o in s]).split() - cedbeu
如果需要,正则表达式库可以接受Unicode字符约定。此外,它与已接受的解决方案存在相同的问题:现在它会在撇号上分割。您可能希望使用o for o in s if (o in not string.punctuation or o == "'"),但是如果我们还加入了cedbeu的补丁,这将变得过于复杂,无法成为一行代码。 - Daniel H
这里还有另一个问题。即使我们考虑了@cedbeu的更改,如果字符串是像“First Name,Last Name,Street Address,City,State,Zip Code”这样的,并且我们只想在逗号“,”上拆分,那么这段代码也无法正常工作。期望的输出应该是:['First Name','Last Name','Street Address','City','State','Zip Code'],但实际得到的却是:['First','Name','Last','Name','Street','Address','City','State','Zip','Code']。 - Stefan van den Akker
6
这个解决方案非常低效:首先将列表分解成单个字符,然后对原始字符串中的每个单个字符遍历整个标点符号集合,然后将字符重新组合,最后再次拆分。相比基于正则表达式的解决方案,所有这些“移动”都非常复杂:即使在特定应用程序中速度不重要,也没有必要使用复杂的解决方案。由于re模块是标准的,可以提供可读性和速度,我不明白为什么要避免使用它。 - Eric O. Lebigot

42

专业提示:使用 string.translate 可以让 Python 进行最快速的字符串操作。

证明如下...

首先,我们来看一下较慢的方法(抱歉 pprzemek):

>>> import timeit
>>> S = 'Hey, you - what are you doing here!?'
>>> def my_split(s, seps):
...     res = [s]
...     for sep in seps:
...         s, res = res, []
...         for seq in s:
...             res += seq.split(sep)
...     return res
... 
>>> timeit.Timer('my_split(S, punctuation)', 'from __main__ import S,my_split; from string import punctuation').timeit()
54.65477919578552

接下来,我们使用re.findall()(如建议答案所示)。速度要快得多:
>>> timeit.Timer('findall(r"\w+", S)', 'from __main__ import S; from re import findall').timeit()
4.194725036621094

最后,我们使用 translate

>>> from string import translate,maketrans,punctuation 
>>> T = maketrans(punctuation, ' '*len(punctuation))
>>> timeit.Timer('translate(S, T).split()', 'from __main__ import S,T,translate').timeit()
1.2835021018981934

解释:

string.translate 是用 C 编写的,并且与 Python 中许多字符串操作函数不同,string.translate 不会 生成一个新字符串。所以它是关于字符串替换的速度最快的。

然而有点笨拙,因为它需要一个翻译表才能完成这个魔术。你可以使用 maketrans() 方便函数创建一个翻译表。这里的目标是将所有不需要的字符都翻译为空格。一对一替换。同样,不会产生新数据。因此这很

接下来,我们使用老掉牙的 split()split() 默认会对所有空白字符进行操作,将它们组合在一起进行分割。结果将是您想要的单词列表。这种方法几乎比 re.findall()4倍


4
我在这里进行了测试,如果需要使用 Unicode,则使用 patt = re.compile(ur'\w+', re.UNICODE); patt.findall(S) 比使用 translate 更快,因为您必须在应用转换之前对字符串进行编码,并在拆分后解码列表中的每个项目以返回 Unicode。 - Rafael S. Calsaverini
你可以用一行代码实现翻译,并确保 S 不在 splitters 中,如下所示:s.translate(''.join([(chr(i) if chr(i) not in seps else seps[0]) for i in range(256)])).split(seps[0]) - hobs
没关系。你在比较不同的东西。 ;) 我在 Python 3 中的解决方案仍然有效 ;P 并且支持多字符分隔符。 :) 尝试以简单的方式做到这一点,而不需要分配新字符串。 :) 但是确实,我的解决方案仅限于解析命令行参数,而不是例如书籍之类的内容。 - pprzemek
你说“不生成新字符串”,是指它在原字符串上直接操作吗?我现在用Python 2.7测试了一下,它没有修改原始字符串并返回了一个新的字符串。 - Prokop Hapala
string.translatestring.maketrans在Python 3中不可用,只有在Python 2中才有。 - Futal

30

我面临类似的困境,不想使用're'模块。

def my_split(s, seps):
    res = [s]
    for sep in seps:
        s, res = res, []
        for seq in s:
            res += seq.split(sep)
    return res

print my_split('1111  2222 3333;4444,5555;6666', [' ', ';', ','])
['1111', '', '2222', '3333', '4444', '5555', '6666']

1
我喜欢这个。只是注意,分隔符的顺序很重要。如果这很明显,对不起。 - crizCraig
2
为什么不使用 re 模块呢?它既快速又清晰(虽然正则表达式本身并不特别清晰),但是因为更简短和直接。 - Eric O. Lebigot
有许多版本的Python,不仅仅是python.org上的那一个。并非所有版本都有re模块,特别是如果你进行嵌入式开发,那么你需要尽可能地精简。 - pprzemek

16

首先,我同意其他人的观点,即基于正则表达式或 str.translate(...) 的解决方案最为高效。对于我的用例,这个函数的性能并不重要,因此我想添加一些符合这个标准的想法。

我的主要目标是将其他答案中的思路概括成一个可以适用于包含除正则表达式单词外更多内容的字符串的解决方案(例如,将显式子集的标点符号列入黑名单与将单词字符列入白名单进行过滤)。

注意,在任何方法中,都可以考虑使用 string.punctuation 来代替手动定义列表。

选项 1 - re.sub

我很惊讶到目前为止没有答案使用re.sub(...)。我认为这是一个简单而自然的解决方法。

import re

my_str = "Hey, you - what are you doing here!?"

words = re.split(r'\s+', re.sub(r'[,\-!?]', ' ', my_str).strip())
在这个解决方案中,我将对re.sub(...)的调用嵌套在re.split(...)内部 - 但如果性能很关键,编译正则表达式可能会更有益 - 对于我的用例来说,差异不大,所以我更喜欢简单和可读性。
选项2- str.replace 这是多了几行代码,但它的好处是可以扩展而无需检查是否需要在正则表达式中转义某些字符。
my_str = "Hey, you - what are you doing here!?"

replacements = (',', '-', '!', '?')
for r in replacements:
    my_str = my_str.replace(r, ' ')

words = my_str.split()

str.replace 映射到字符串本身会很好,但我认为在不可变字符串上无法实现。虽然映射到字符列表可以工作,但对每个字符运行每次替换听起来有些过度了。(编辑:请参阅下一个选项以获取功能示例。)

选项3 - functools.reduce

(在Python 2中,reduce 在全局命名空间中可用而无需从functools导入。)

import functools

my_str = "Hey, you - what are you doing here!?"

replacements = (',', '-', '!', '?')
my_str = functools.reduce(lambda s, sep: s.replace(sep, ' '), replacements, my_str)
words = my_str.split()

嗯,另一种方法是使用str.translate - 它不支持Unicode,但很可能比其他方法更快,因此在某些情况下可能很好: replacements = ',-!?';import string;my_str = my_str.translate(string.maketrans(replacements,' ' * len(replacements))) 此外,这里必须将替换字符作为一个字符串,而不是元组或列表。 - MarSoft
@MarSoft 谢谢!我在回答的顶部提到了它,但决定不添加它,因为现有的答案已经很好地讨论了它。 - Taylor D. Edmiston

10
join = lambda x: sum(x,[])  # a.k.a. flatten1([[1],[2,3],[4]]) -> [1,2,3,4]
# ...alternatively...
join = lambda lists: [x for l in lists for x in l]

那么这就变成了三行:

fragments = [text]
for token in tokens:
    fragments = join(f.split(token) for f in fragments)
解释 这在 Haskell 中被称为列表单子(List monad)。单子背后的理念是一旦「进入单子」,只要没有特殊操作将你带出来,你就一直处于单子中。例如,在 Haskell 中,如果你将 python 的 range(n) -> [1,2,.. .,n] 函数映射到列表上,若结果也是列表,则会将其原地附加到列表中,你会得到类似这样的东西:map(range, [3,4,1]) -> [0,1,2,0,1,2,3,0]。这被称为地图-append(或 mappend,或可能是这样的东西)。这里的意思是,你有一个正在应用的操作(按令牌拆分),每当你执行该操作时,就将结果连接到列表中。
你可以将其抽象成一个函数,通过默认方式使 tokens=string.punctuation
这种方法的优点:
  • 这种方法(不像朴素的基于 regex 的方法),可以使用任意长度的标记(regex 可以使用更高级的语法实现此目的)。
  • 你不受限于简单的标记;你可以有任意逻辑来代替每个标记,例如其中之一的「标记」可以是根据嵌套括号来拆分的函数。

Haskell的解决方案很整洁,但我认为在Python中可以更清晰地写出来,而不需要使用mappend。 - Vlad the Impala
@Goose:重点是这个两行函数map_then_append可以用来将一个问题变成两行代码,同时也可以让许多其他问题更容易编写。大多数其他解决方案使用正则表达式re模块,这不是Python的特性。但我对我的答案看起来不够优雅和臃肿感到不满,实际上它非常简洁...我要进行编辑... - ninjagecko
这个代码是否应该原封不动地在 Python 中运行?我的“fragments”结果只是字符串中的字符列表(包括令牌)。 - Rick
@RickTeachey:在我的Python2和Python3中都可以运行。 - ninjagecko
可能这个例子有点模糊。我已经尝试了答案中的代码以各种不同的方式 - 包括将 fragments = ['the,string']fragments = 'the,string'fragments = list('the,string'),但它们都没有产生正确的输出。 - Rick

9

我喜欢使用 re,但这里是我没有使用它的解决方案:

from itertools import groupby
sep = ' ,-!?'
s = "Hey, you - what are you doing here!?"
print [''.join(g) for k, g in groupby(s, sep.__contains__) if not k]

sep.__contains__是被'in'运算符使用的方法。基本上它与

lambda ch: ch in sep

但在这里更方便。

groupby使用我们的字符串和函数。它使用该函数将字符串分成组:每当函数的值发生变化时,就会生成一个新组。因此,sep.__contains__正是我们所需要的。

groupby返回一系列成对出现的内容,其中pair [0]是我们函数的结果, pair [1]是一个组。通过'if not k'过滤掉带有分隔符的组(因为分隔符上的sep.__contains__结果为True)。好了,现在我们有了一个序列,其中每个序列都是一个单词(实际上,组是可迭代的,因此我们使用join将其转换为字符串)。

这种解决方案非常通用,因为它使用函数来分隔字符串(您可以按任何需要拆分)。此外,它不会创建中间字符串/列表(您可以删除join,而表达式将变得惰性,因为每个组都是迭代器)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接