在大写字母处分割字符串

145

什么是Pythonic方法在给定字符出现之前拆分字符串的方式?

例如,我想在任何大写字母(可能除了第一个)出现的地方拆分 'TheLongAndWindingRoad' 并获得 ['The', 'Long', 'And', 'Winding', 'Road']

编辑:它还应该拆分单个出现,即从'ABC'中,我想获得 ['A','B','C']

22个回答

202

很遗憾,在Python中不可能按零宽度匹配分割。但是你可以使用re.findall代替:

>>> import re
>>> re.findall('[A-Z][^A-Z]*', 'TheLongAndWindingRoad')
['The', 'Long', 'And', 'Winding', 'Road']
>>> re.findall('[A-Z][^A-Z]*', 'ABC')
['A', 'B', 'C']

17
请注意,这将删除第一个大写字母之前的所有字符。'theLongAndWindingRoad' 的结果将是 ['Long', 'And', 'Winding', 'Road']。 - Marc Schulder
27
如果你需要这个案例,只需使用'[a-zA-Z][^A-Z]*'作为正则表达式即可。 - knub
5
为了拆分小驼峰式单词,可以使用以下代码:print(re.findall('^[a-z]+|[A-Z][^A-Z]*', 'theLongAndWindingRoad')) - Ulysses
2
'ThatLeadsToYourDooooor' <3 - Ulf Gjerdingen
从3.7版本开始,可以在零宽匹配上进行拆分。 - Bharel
显示剩余2条评论

50

这里有一个替代正则表达式的解决方案。问题可以重新描述为“在执行拆分之前,在每个大写字母前面插入一个空格”的方法:

>>> s = "TheLongAndWindingRoad ABC A123B45"
>>> re.sub( r"([A-Z])", r" \1", s).split()
['The', 'Long', 'And', 'Winding', 'Road', 'A', 'B', 'C', 'A123', 'B45']

这种方法的优点是保留了所有非空白字符,而大多数其他解决方案则没有这样做。


请问您能解释一下为什么在\1前面加上空格会起作用吗?这是因为split方法还是与正则表达式有关? - Lax_Sam
分隔符默认为任何空格字符串。 - CIsForCookies
@Lax_Sam 正则表达式替换只是在任何大写字母前添加一个空格,而 split() 函数会将其拆分。 - vitaly
当一个棘手的问题在重新表述后变得非常简单时,我总是倍感启发。 - Tony

28

使用前后断言:

在Python 3.7中,您可以这样做:

re.split('(?<=.)(?=[A-Z])', 'TheLongAndWindingRoad')

然后它产生:

['The', 'Long', 'And', 'Winding', 'Road']
你需要使用回顾表达式来避免在开头出现空字符串。

1
它将产生一个空字符串。re.split('(?=[A-Z])', 'ABC') 得到 ['', 'A', 'B', 'C'] - Ben
@Ben: 是的,你说得对。我已经更新了我的答案,避免了那个问题。 - Endlisnis

21
>>> import re
>>> re.findall('[A-Z][a-z]*', 'TheLongAndWindingRoad')
['The', 'Long', 'And', 'Winding', 'Road']

>>> re.findall('[A-Z][a-z]*', 'SplitAString')
['Split', 'A', 'String']

>>> re.findall('[A-Z][a-z]*', 'ABC')
['A', 'B', 'C']
如果你想将"It'sATest"拆分为["It's", 'A', 'Test'],则需要将正则表达式更改为"[A-Z][a-z']*"

+1:为了第一个让ABC工作的人。我现在也更新了我的答案。 - Mark Byers
re.findall('[A-Z][a-z]*', "It's about 70% of the Economy") -----> ['It', 'Economy']
- ChristopheD
@ChristopheD。OP没有说明非字母字符应如何处理。 - John La Rooy
1
正确,但是这种正则表达式的方法也会“删除”所有不以大写字母开头的常规(仅纯字母)单词。我怀疑这不是OP的本意。 - ChristopheD

16

@ChristopheD的解决方案的一种变化

s = 'TheLongAndWindingRoad'

pos = [i for i,e in enumerate(s+'A') if e.isupper()]
parts = [s[pos[j]:pos[j+1]] for j in xrange(len(pos)-1)]

print parts

2
不错 - 这也适用于非拉丁字符。而这里展示的正则表达式解决方案则不行。 - AlexVhr
这也返回了一个列表,这正是我所需要的! - JMVDA
请注意,应该使用range而不是xrange - raspiduino
我认为xrange还可以,@raspiduino,不是因为我在Python 2中测试过它,而是因为它在Python 3中与range一起工作,而rangexrange显然是一样的。Python 3中没有xrange:https://stackoverflow.com/questions/94935/what-is-the-difference-between-range-and-xrange-functions-in-python-2-x - undefined

15

我认为一个更好的答案可能是将字符串分割成不以大写字母结尾的单词。这样就可以处理字符串不以大写字母开头的情况。

 re.findall('.[^A-Z]*', 'aboutTheLongAndWindingRoad')

例子:

>>> import re
>>> re.findall('.[^A-Z]*', 'aboutTheLongAndWindingRoadABC')
['about', 'The', 'Long', 'And', 'Winding', 'Road', 'A', 'B', 'C']

7
import re
filter(None, re.split("([A-Z][^A-Z]*)", "TheLongAndWindingRoad"))

或者

[s for s in re.split("([A-Z][^A-Z]*)", "TheLongAndWindingRoad") if s]

1
这个过滤器完全没有必要,与直接使用带有捕获组的正则表达式分割相比并没有任何优势:[s for s in re.compile(r"([A-Z][^A-Z]*)").split( "TheLongAndWindingRoad") if s] 可以得到 ['The', 'Long', 'And', 'Winding', 'Road'] - smci
1
@smci:这个 filter 的用法和带条件的列表推导式是一样的。你对此有什么意见吗? - Gabe
1
我知道它可以被替换为带有条件的列表推导式,因为我刚刚发布了那段代码,然后你复制了它。以下是列表推导式更可取的三个原因:a)易读的习惯用法:列表推导式是一种更Pythonic的习惯用法,从左到右阅读比filter(lambdaconditionfunc, ...)更清晰。 b)在Python 3中,filter()返回一个迭代器。所以它们不完全等价。 c)我认为filter()也更慢。 - smci

7

Pythonic方式可能是:

"".join([(" "+i if i.isupper() else i) for i in 'TheLongAndWindingRoad']).strip().split()
['The', 'Long', 'And', 'Winding', 'Road']

适用于Unicode,避免使用re/re2。

"".join([(" "+i if i.isupper() else i) for i in 'СуперМаркетыПродажаКлиент']).strip().split()
['Супер', 'Маркеты', 'Продажа', 'Клиент']

不使用正则表达式的好方法 - callmeanythingyouwant
感觉有点违反 Python 的禅意。 - rearThing
我更喜欢这个方法,因为它不需要使用正则表达式。我不喜欢加载库和进行所有的解析工作,只是为了找到大写字母。但是在这两者之间没有必要构建一个列表。这个方法更符合"Pythonic"的风格:"".join(" " + c if c.isupper() else c for c in s).strip()我没有看到任何违规之处,@rearThing。我很想知道你对这个方法有何不满意之处。毕竟它使用了生成器表达式:https://peps.python.org/pep-0289/当你去掉不必要的括号后,它看起来更好。 - undefined

5

这是一种不需要使用正则表达式的方法,同时如果需要的话可以保留连续的大写字母。

def split_on_uppercase(s, keep_contiguous=False):
    """

    Args:
        s (str): string
        keep_contiguous (bool): flag to indicate we want to 
                                keep contiguous uppercase chars together

    Returns:

    """

    string_length = len(s)
    is_lower_around = (lambda: s[i-1].islower() or 
                       string_length > (i + 1) and s[i + 1].islower())

    start = 0
    parts = []
    for i in range(1, string_length):
        if s[i].isupper() and (not keep_contiguous or is_lower_around()):
            parts.append(s[start: i])
            start = i
    parts.append(s[start:])

    return parts

>>> split_on_uppercase('theLongWindingRoad')
['the', 'Long', 'Winding', 'Road']
>>> split_on_uppercase('TheLongWindingRoad')
['The', 'Long', 'Winding', 'Road']
>>> split_on_uppercase('TheLongWINDINGRoadT', True)
['The', 'Long', 'WINDING', 'Road', 'T']
>>> split_on_uppercase('ABC')
['A', 'B', 'C']
>>> split_on_uppercase('ABCD', True)
['ABCD']
>>> split_on_uppercase('')
['']
>>> split_on_uppercase('hello world')
['hello world']

5
src = 'TheLongAndWindingRoad'
glue = ' '

result = ''.join(glue + x if x.isupper() else x for x in src).strip(glue).split(glue)

1
请您能否解释一下为什么这是解决问题的好方法。 - Matas Vaitkevicius
对不起,我忘记了最后一步。 - user3726655
对我来说,它看起来简洁、Pythonic且自说明。 - user8554766

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