Python 中的正则表达式:仅当不在列表中时将单词与数字分开

Regex in Python: Separate words from numbers JUST when not in list

我有一个列表,其中包含我需要保留的一些替代品。例如,替换列表:['1st', '2nd', '10th', '100th', '1st nation', 'xlr8', '5pin', 'h20'].

一般情况下,包含字母数字字符的字符串需要按如下方式拆分数字和字母:

text = re.sub(r'(?<=\d)(?=[^\d\s])|(?<=[^\d\s])(?=\d)', ' ', text, 0, re.IGNORECASE)

之前的正则表达式模式通过在以下之间添加 space 成功地将所有数字与字符分开:

Original       Regex
ABC10 DEF  --> ABC 10 DEF
ABC DEF10  --> ABC DEF 10
ABC 10DEF  --> ABC 10 DEF
10ABC DEF  --> 10 ABC DEF

但是,替换列表中有一些字母数字单词无法分开。例如,以下包含替换列表一部分的 1ST 的字符串不应被分隔,它们应该被省略而不是添加 space:

Original            Regex                Expected
1ST DEF 100CD  -->  1 ST DEF 100 CD  --> 1ST DEF 100 CD
ABC 1ST 100CD  -->  ABC 1 ST 100 CD  --> ABC 1ST 100 CD
100TH DEF 100CD ->  100 TH DEF 100 CD -> 100TH DEF 100 CD
10TH DEF 100CD  ->  10 TH DEF 100 CD  -> 10TH DEF 100 CD 

为了获得上例中的预期列,我尝试在正则表达式中使用 IF THEN ELSE 方法,但我在 Python:

中的语法中遇到错误
(?(?=condition)(then1|then2|then3)|(else1|else2|else3))

根据语法,我应该有如下内容:

?(?!1ST)((?<=\d)(?=[^\d\s])|(?<=[^\d\s])(?=\d)))

其中 (?!...) 将包括匹配正则表达式模式时要避免的可能替换,在本例中为词 1ST 10TH 100TH.

如何避免匹配字符串中的单词替换?

您可以使用 lambda 函数执行此操作,以检查匹配的字符串是否在您的排除列表中:

import re

subs = ['1st','2nd','1st nation','xlr8','5pin','h20']
text = """
ABC10 DEF
1ST DEF 100CD
ABC 1ST 100CD
AN XLR8 45X
NO H20 DEF
A4B PLUS
"""

def add_spaces(m):
    if m.group().lower() in subs:
        return m.group()
    res = m.group(1)
    if len(res):
        res += ' '
    res += m.group(2)
    if len(m.group(3)):
        res += ' '
    res += m.group(3)
    return res

text = re.sub(r'\b([^\d\s]*)(\d+)([^\d\s]*)\b', lambda m: add_spaces(m), text)
print(text)

输出:

ABC 10 DEF
1ST DEF 100 CD
ABC 1ST 100 CD
AN XLR8 45 X
NO H20 DEF
A 4 B PLUS

您可以将 lambda 函数简化为

def add_spaces(m):
    if m.group().lower() in subs:
        return m.group()
    return m.group(1) + ' ' + m.group(2) + ' ' + m.group(3)

但这可能会导致输出字符串中出现额外的空格。然后可以用

删除
text = re.sub(r' +', ' ', text)

使用regex(*SKIP)(*FAIL)f-strings的另一种方式:

import regex as re

lst = ['1st','2nd','1st nation','xlr8','5pin','h20']

data = """
ABC10 DEF
ABC DEF10
ABC 10DEF
10ABC DEF
1ST DEF 100CD
ABC 1ST 100CD"""

rx = re.compile(
    rf"""
    (?:{"|".join(item.upper() for item in lst)})(*SKIP)(*FAIL)
    |
    (?<=\d)(?=[^\d\s])|(?<=[^\d\s])(?=\d)
    """, re.X)

data = rx.sub(' ', data)
print(data)

这会产生

ABC 10 DEF
ABC DEF 10
ABC 10 DEF
10 ABC DEF
1ST DEF 100 CD
ABC 1ST 100 CD

处理异常时,最简单、最安全的方法是使用“best trick ever”方法。替换时,这个技巧意味着:保留捕获的内容,删除匹配的内容,反之亦然。在正则表达式中,您必须使用 alternation 并使用 capturing group 围绕其中一个(或一些复杂场景)才能分析遇到匹配后的匹配结构

因此,首先,使用异常列表构建交替的第一部分:

exception_rx = "|".join(map(re.escape, exceptions))

注意 re.escape 在需要的地方添加反斜杠以支持异常中的任何特殊字符。如果您的例外都是字母数字,则不需要,您可以只使用 exception_rx = "|".join(exceptions)。甚至 exception_rx = rf'\b(?:{"|".join(exceptions)})\b' 只将它们作为整个单词进行匹配。

Next,您需要一种模式,无论上下文如何都能找到所有匹配项,我 :

generic_rx = r'(?<=\d)(?=[^\d\s])|(?<=[^\d\s])(?=\d)'

最后,使用(exceptions_rx)|generic_rx方案加入他们:

rx = re.compile(rf'({exception_rx})|{generic_rx}', re.I)   

并使用 .sub():

替换
s = rx.sub(lambda x: x.group(1) or " ", s)

此处,lambda x: x.group(1) or " "表示return组1值,如果第1组匹配,否则,替换为space.

参见 Python demo:

import re

exceptions = ['1st','2nd','10th','100th','1st nation','xlr8','5pin','h20', '12th'] # '12th' added
exception_rx = '|'.join(map(re.escape, exceptions))
generic_rx = r'(?<=\d)(?=[^\d\s])|(?<=[^\d\s])(?=\d)'
rx = re.compile(rf'({exception_rx})|{generic_rx}', re.I)

string_lst = ['1ST DEF 100CD','ABC 1ST 100CD','WEST 12TH APARTMENT']
for s in string_lst:
    print(rx.sub(lambda x: x.group(1) or " ", s))

输出:

1ST DEF 100 CD
ABC 1ST 100 CD
WEST 12TH APARTMENT