python 正则表达式,其中一组选项在列表中最多出现一次,顺序不限

python regex where a set of options can occur at most once in a list, in any order

我想知道在 python 或 perl 中是否有任何方法可以构建一个正则表达式,您可以在其中定义一组选项,这些选项最多可以按任何顺序出现一次。因此,例如我想要 foo(?: [abc])* 的导数,其中 abc 只能出现一次。所以:

foo a b c
foo b c a
foo a b
foo b

都有效,但是

foo b b

不会

您可以将此正则表达式与捕获组和否定前瞻结合使用:

对于 Perl,您可以将此变体与 forward referencing 一起使用:

^foo((?!.*) [abc])+$

RegEx Demo

正则表达式详细信息:

  • ^: 开始
  • foo:匹配foo
  • (: 启动捕获组#1
    • (?!.*):否定前瞻断言我们在输入的任何地方都不匹配捕获组 #1 中的内容
    • [abc]:匹配space后跟[=​​19=]或bc
  • )+: 结束捕获组#1。重复此组 1+ 次
  • $:结束

如前所述,此正则表达式使用了一个名为 前向引用 的功能,它是对 稍后出现在正则表达式模式中的组的反向引用。 JGsoft、.NET、Java、Perl、PCRE、PHP、Delphi 和 Ruby 允许前向引用,但 Python 不允许。


这是 Python 的相同正则表达式的 解决方法,它不使用前向引用:

^foo(?!.* ([abc]).*)(?: [abc])+$

在这里,我们在重复组之前使用否定前瞻来检查并使匹配失败,如果允许的子字符串有任何重复,即 [abc].

RegEx Demo 2

您可以使用对之前捕获的组的引用来完成。

foo(?: ([abc]))?(?: (?!)([abc]))?(?: (?!|)([abc]))?$

这很长,有很多选项。如有必要,可以动态生成这样的正则表达式。

def match_sequence_without_repeats(options, seperator):
    def prevent_previous(n):
        if n == 0:
            return ""
        groups = "".join(rf"\{i}" for i in range(1, n + 1))
        return f"(?!{groups})"

    return "".join(
        f"(?:{seperator}{prevent_previous(i)}([{options}]))?"
        for i in range(len(options))
    )


print(f"foo{match_sequence_without_repeats('abc', ' ')}$")

您可以断言 space 和右侧字母的第二个匹配项没有匹配项:

foo(?!(?: [abc])*( [abc])(?: [abc])*)(?: [abc])*
  • foo字面匹配
  • (?! 否定前瞻
    • (?: [abc])* 匹配 a space 和 b 或 c
    • 的可选重复
    • ( [abc]) 捕获组,用于与相同的反向引用进行比较
    • (?: [abc])* 再次匹配 a space 和 b 或 c
    • </code> 向后引用组 1</li> </ul> </li> <li><code>) 关闭前瞻
    • (?: [abc])* 匹配可选重复或 a space 和 b 或 c

    Regex demo

    如果不想只匹配foo,可以将量词改为1个或多个(?: [abc])+


    perl 中的一个变体使用 (?1) 重用第一个子模式,它指的是捕获组 ([abc])

    ^foo ([abc])(?: (?!)((?1))(?: (?!|)(?1))?)?$
    

    Regex demo

如果字符串的顺序无关紧要,并且你想确保每个字符串只出现一次,你可以将列表变成一个集合 Python:

my_lst = ['a', 'a', 'b', 'c']
my_set = set(lst)

print(my_set)
# {'a', 'c', 'b'}

我假设字符串的元素可以以任意顺序出现并且出现任意次数。例如,'a foo' 应该匹配而 'a foo b foo' 不应该匹配。

您可以通过一系列使用前瞻的交替来做到这一点,每个感兴趣的子字符串一个,但是当有很多字符串需要考虑时,它就变得有些吃力了。假设您想要匹配零个或一个 "foo" 的 and/or 零个或一个 "a" 的。您可以使用以下正则表达式:

^(?:(?!.*\bfoo\b)|(?=(?:(?!\bfoo\b).)*\bfoo\b(?!(.*\bfoo\b))))(?:(?!.*\ba\b)|(?=(?:(?!\ba\b).)*\ba\b(?!(.*\ba\b))))

Start your engine!

这匹配,例如,'foofoo''aa'afooa。如果它们不匹配,请删除分词符 (\b)。

请注意,此表达式首先断言字符串的开头 (^),然后是两个正前瞻,一个用于 'foo',一个用于 'a'。还要检查,比如说,'c' 一个人会加入

(?:(?!.*\bc\b)|(?=(?:(?!\bc\b).)*\bc\b(?!(.*\bc\b))))

相同
(?:(?!.*\ba\b)|(?=(?:(?!\ba\b).)*\ba\b(?!(.*\ba\b))))

\ba\b 更改为 \bc\b

如果能够使用反向引用就好了,但我不知道该怎么做。

通过将鼠标悬停在 link 中的正则表达式上,将为表达式的每个元素提供解释。 (如果不清楚,我指的是光标。)

注意

(?!\bfoo\b).

匹配不以单词 'foo' 开头的字符。因此

(?:(?!\bfoo\b).)*

匹配不包含 'foo' 且不以 'f' 后跟 'oo'.

结尾的子字符串

我会在实践中提倡这种方法,而不是使用简单的字符串方法吗?让我思考一下。

这是 anubhava 答案的修改版本,使用 backreference(在 Python 中有效,至少对我来说更容易理解)而不是前向引用。

在捕获组中使用 [abc] 进行匹配,然后检查捕获组匹配的文本不会再次出现在它之后的任何地方:

^foo(?:( [abc])(?!.*))+$

regex demo

  • ^: 开始
  • foo:匹配foo
  • (?:: 启动非捕获组(?:( [abc])(?!.*))
    • ( [abc]):捕获第 1 组,匹配 space 后跟 abc
    • (?!.*):否定前瞻,如果第一个捕获组匹配的文本出现在.
    • 匹配的零个或多个字符之后,则匹配失败
  • )+:结束非捕获组并匹配1次或多次
  • $:结束

如果不必是正则表达式:

import collections

# python >=3.10
def is_a_match(sentence):
    words = sentence.split()
    return (
      (len(words) > 0)
      and (words[0] == 'foo')
      and (collections.Counter(words) <= collections.Counter(['foo', 'a', 'b', 'c']))
    )

# python <3.10
def is_a_match(sentence):
    words = sentence.split()
    return (
      (len(words) > 0)
      and (words[0] == 'foo')
      and not (collections.Counter(words) - collections.Counter(['foo', 'a', 'b', 'c']))
    )

# TESTING
#foo a b c True
#foo b c a True
#foo a b True
#foo b True
#foo b b False

或者用集合和海象运算符:

def is_a_match(sentence):
    words = sentence.split()
    return (
      (len(words) > 0)
      and (words[0] == 'foo')
      and (
        (s := set(words[1:])) <= set(['a', 'b', 'c'])
        and len(s) == len(words) - 1
      )
    )

除了这里有一个不使用后向或前向引用的正则表达式外,上面的答案没什么可补充的。相反,它使用 3 个单独的否定先行断言来确保输入不包含 2 次出现的 abc。正则表达式还允许自由使用 spaces.

^foo(?![^a]*a[^a]*a)(?![^b]*b[^b]*b)(?![^c]*c[^c]*c)( +[abc])* *$

See Regex Demo

  1. ^ - 匹配字符串的开头
  2. (?![^a]*a[^a]*a) - 否定前瞻性断言,接下来的内容不包含两次出现的 a
  3. (?![^b]*b[^b]*b) - 否定前瞻性断言,随后的内容不包含两次出现的 b
  4. (?![^c]*c[^c]*c) - 否定前瞻断言,随后的内容不包含两次出现的 c
  5. ( +[abc])* - 匹配 0 次或多次出现:1 次或多次 space 后跟 abc
  6. * - 匹配 0 次或多次出现的 space 7 $ - 匹配字符串的结尾

正则表达式看起来“笨拙”,但非常简单明了。输入 foo a b c 时,成功的匹配需要 35 步,输入 foo b b 时,不成功的匹配需要 13 步。 Thich 与其他答案相比有优势。