在Python中反转正则表达式

50

我想翻转一个正则表达式。即,给定一个正则表达式,我想生成任何匹配该正则表达式的字符串。

我知道如何从理论计算机科学背景下使用有限状态机来完成此操作,但我只想知道是否有人已经编写了可执行此操作的库。:)

我正在使用Python,所以我希望有一个Python库。

再次强调,我只想要一个可以匹配正则表达式的字符串。像"." 或 ".*" 这样的东西会使无数个字符串满足正则表达式,但是我不关心所有选项。

我愿意这个库只在某些正则表达式子集上工作。


1
我假设你只想要一个任意字符串,而不是所有的字符串... 对吧? 否则,一旦你包含了一个*,你就会得到一个无限列表。你想用它做什么?以这种方式回答可能更容易。 - user54650
参见:https://dev59.com/iW445IYBdhLWcg3w7ug0(重复问题) - dreftymac
8个回答

29

有人曾经有过类似(重复?)的问题,在此处,我想提供一个小型Python辅助库用于生成随机字符串,我一直在这上面工作。

它包括一个名为xeger()的方法,可以让你根据正则表达式创建一个字符串:

>>> import rstr
>>> rstr.xeger(r'[A-Z]\d[A-Z] \d[A-Z]\d')
u'M5R 2W4'

现在,它可以处理大多数基本正则表达式,但我相信它可以得到改进。


1
你有计划打包这个项目,以便可以通过pip安装吗? - Andy Hayden
现在已经在PyPI上发布了,所以您应该能够使用pip安装它。如果您不介意使用pull request提交您的Py3更改(它们是否向后兼容?),我很乐意将它们包含在内。 - bjmc
Andy,如果你还没有看过的话,你应该去看看 python-faker 这个库,它与你的库类似(尽管据我所知它已经不再进行活跃开发)。 - bjmc
1
谢谢,我认为这个链接是最好的选择,但似乎没有太多的开发。更改是向后兼容的,我不确定如何在Bitbucket上创建pr,但我稍后可以尝试一下(可以在这里看到,实际上你可以继续使用xrange并在if块中添加xrange=range)。 - Andy Hayden
另一个选择是intxeger,它速度更快,并提供一些额外的功能,如类似数组的索引。 - Kevin Alex Zhang
显示剩余2条评论

19

虽然我觉得这没有太多意义,但还是来试试:

import re
import string

def traverse(tree):
    retval = ''
    for node in tree:
        if node[0] == 'any':
            retval += 'x'
        elif node[0] == 'at':
            pass
        elif node[0] in ['min_repeat', 'max_repeat']:
            retval += traverse(node[1][2]) * node[1][0]
        elif node[0] == 'in':
            if node[1][0][0] == 'negate':
                letters = list(string.ascii_letters)
                for part in node[1][1:]:
                    if part[0] == 'literal':
                        letters.remove(chr(part[1]))
                    else:
                        for letter in range(part[1][0], part[1][1]+1):
                            letters.remove(chr(letter))
                retval += letters[0]
            else:
                if node[1][0][0] == 'range':
                    retval += chr(node[1][0][1][0])
                else:
                    retval += chr(node[1][0][1])
        elif node[0] == 'not_literal':
            if node[1] == 120:
                retval += 'y'
            else:
                retval += 'x'
        elif node[0] == 'branch':
            retval += traverse(node[1][1][0])
        elif node[0] == 'subpattern':
            retval += traverse(node[1][1])
        elif node[0] == 'literal':
            retval += chr(node[1])
    return retval

print traverse(re.sre_parse.parse(regex).data)

我从正则表达式语法一直到分组的所有内容都进行了整理-- 这似乎是一个合理的子集 -- 我忽略了一些细节,比如行尾。 错误处理等等留给读者自己来实现。

在正则表达式中的12个特殊字符中,我们完全可以忽略其中的6个(其中两个甚至连它们应用的原子都可以忽略),有4.5个会导致简单替换,另外1.5个需要我们认真思考。

我认为,这样做出来的结果并不是太有趣。


1
'assert'节点怎么样?例如:r'(?=[a-z])(?=[x-z])' - Vegard
我知道重点不是提供完整的解决方案。我只是想问一下,你是否也能勾勒出“assert”节点的解决方案。如果你仍然觉得我没有理解重点,能否请你解释一下为什么我的评论如此离谱? - Vegard
@Vegard:你没有理解这个练习是完全没有意义的。例如:将(?=[a-z])替换为a。完成了,毫无意思。 - user3850
我认为这很有趣,因为使用一个或多个 (?=...),你现在需要同时找到与两个或更多的正则表达式匹配的内容。对于我给出的示例,您只能选择范围 [x-z] 中的字母,因为那是重叠范围。所以这不再是那么直接了。 - Vegard
@Vegard:给我一个现实生活中的例子,说明你的正则表达式有意义。 - user3850
显示剩余2条评论

13

我不知道是否有任何模块能够完成这个任务。如果在Cookbook或PyPI中没有找到类似的内容,你可以尝试自己编写,使用(re.sre_parse)未公开文档的模块。这可能会帮助你入门:

In [1]: import re

In [2]: a = re.sre_parse.parse("[abc]+[def]*\d?z")

In [3]: a
Out[3]: [('max_repeat', (1, 65535, [('in', [('literal', 97), ('literal', 98), ('literal', 99)])])), ('max_repeat', (0, 65535, [('in', [('literal', 100), ('literal', 101), ('literal', 102)])])), ('max_repeat', (0, 1, [('in', [('category', 'category_digit')])])), ('literal', 122)]

In [4]: eval(str(a))
Out[4]: 
[('max_repeat',
  (1, 65535, [('in', [('literal', 97), ('literal', 98), ('literal', 99)])])),
 ('max_repeat',
  (0,
   65535,
   [('in', [('literal', 100), ('literal', 101), ('literal', 102)])])),
 ('max_repeat', (0, 1, [('in', [('category', 'category_digit')])])),
 ('literal', 122)]

In [5]: a.dump()
max_repeat 1 65535
  in
    literal 97
    literal 98
    literal 99
max_repeat 0 65535
  in
    literal 100
    literal 101
    literal 102
max_repeat 0 1
  in
    category category_digit
literal 122

8

Exrex 可以从正则表达式生成字符串。

Exrex 是一个命令行工具和 Python 模块,可根据给定的正则表达式生成所有匹配的字符串或随机匹配的字符串等。

示例:

>>> exrex.getone('\d{4}-\d{4}-\d{4}-[0-9]{4}')
'3096-7886-2834-5671'

5

虽然其他答案使用了re引擎来解析元素,但我自己编写了一个解析re并返回最小匹配模式的代码(请注意,它无法处理 [^ads]、复杂的分组结构以及行首/行尾特殊字符)。如果您需要的话,我可以提供单元测试 :)

import re
class REParser(object):
"""Parses an RE an gives the least greedy value that would match it"""

 def parse(self, parseInput):
    re.compile(parseInput) #try to parse to see if it is a valid RE
    retval = ""
    stack = list(parseInput)
    lastelement = ""
    while stack:
        element = stack.pop(0) #Read from front
        if element == "\\":
            element = stack.pop(0)
            element = element.replace("d", "0").replace("D", "a").replace("w", "a").replace("W", " ")
        elif element in ["?", "*"]:
            lastelement = ""
            element = ""
        elif element == ".":
            element = "a"
        elif element == "+":
            element = ""
        elif element == "{":
            arg = self._consumeTo(stack, "}")
            arg = arg[:-1] #dump the }     
            arg = arg.split(",")[0] #dump the possible ,
            lastelement = lastelement * int(arg)
            element = ""
        elif element == "[":
            element = self._consumeTo(stack, "]")[0] # just use the first char in set
            if element == "]": #this is the odd case of []<something>]
                self._consumeTo(stack, "]") # throw rest away and use ] as first element
        elif element == "|":
            break # you get to an | an you have all you need to match
        elif element == "(":
            arg = self._consumeTo(stack, ")")
            element = self.parse( arg[:-1] )

        retval += lastelement
        lastelement = element
    retval += lastelement #Complete the string with the last char

    return retval

 def _consumeTo(self, stackToConsume, endElement ):
    retval = ""
    while not retval.endswith(endElement):
        retval += stackToConsume.pop(0)
    return retval

4
除非您的正则表达式非常简单(即没有星号或加号),否则将有无限多个字符串与之匹配。如果您的正则表达式只涉及连接和选择,那么可以将每个选择扩展为其所有可能性,例如(foo|bar)(baz|quux) 可以扩展为列表 ['foobaz', 'fooquux', 'barbaz', 'barquux']

6
我不想生成所有可能匹配的字符串,我只想要一个能匹配的字符串。 - Amandasaurus

3

hypothesis包有一个名为 from_regex 的策略,可以实现这一点:

>>> from hypothesis import strategies as st
>>> regex_strategy = st.from_regex(r'[0-9]{4}-[0-9]{8}-[0-9]{16}', fullmatch=True)
>>> regex_strategy.example()
'5000-00000000-0000000000000000'
>>> regex_strategy.example()
'8934-72779254-0542308893797380'
>>> regex_strategy.example()
'0000-00000000-0000000000000000'
>>> regex_strategy.example()
'9000-00000000-0000000000000000'
>>> regex_strategy.example()
'2791-03881838-5840296980736661'

请注意,hypothesis是一个用于模糊测试而不仅仅是一个简单数据生成器的库。因此需要谨慎:如果您例如修改上面示例中的模式为\d{4}-\d{8}-\d{16},则生成的示例可能会像'۵෫൮۹-๕꯱౦໓᠘௮᭒৮-꯳೩꘥៦६༣߃੮8༡႑۹᪒٩꧶'这样,看起来可能出乎意料,但与模式匹配。


2
我没有看到一个Python模块可以实现这一功能,但我在Perl中看到了一个(部分)实现:Regexp::Genex。从模块描述来看,它似乎依赖于Perl正则表达式引擎的内部细节,因此即使从理论上讲也可能没有用处(我没有调查实现,只是根据文档中的注释)。
我认为一般情况下做你提出的事情是一个难题,可能需要使用非确定性编程技术。首先需要解析正则表达式并构建解析树,然后遍历树并在遍历过程中构建示例字符串。具有挑战性的部分可能包括反向引用和避免在实现中出现无限循环等问题。

我在搜索中遇到了genex,这正是我想要做的事情。我很乐意将我的正则表达式限制在一小部分正则表达式中。 - Amandasaurus
构建一个解析树,然后遍历该树并在遍历过程中构建示例字符串。这是一个有趣的观点。我的想法是:这真的只是“遍历”,还是更像“进入,然后尝试逃脱”? - Wolf

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