如何将Perl中的正则表达式翻译成Python?

48

大约一年前,我从Perl转换到了Python,并且没有回头。在使用Python的过程中,我只发现了一个用Perl比Python更容易实现的习语:

if ($var =~ /foo(.+)/) {
  # do something with $1
} elsif ($var =~ /bar(.+)/) {
  # do something with $1
} elsif ($var =~ /baz(.+)/) {
  # do something with $1
}

由于if语句不断嵌套,因此相应的Python代码不太优雅:

m = re.search(r'foo(.+)', var)
if m:
  # do something with m.group(1)
else:
  m = re.search(r'bar(.+)', var)
  if m:
    # do something with m.group(1)
  else:
    m = re.search(r'baz(.+)', var)
    if m:
      # do something with m.group(2)

有没有一种优雅的方法在Python中重现这个模式?我看到过使用匿名函数分派表,但对于少量正则表达式来说,它们似乎有些笨重...


1
就我个人而言,我认为这里答案的临时性质表明Python的正则表达式功能仍有未填补的空白。 - Ken Williams
1
我认为它仍然不像Perl原始版本那样自然或流畅,首先你必须创建一个新的实用类,并且为了不失去功能,你必须包装大量额外的方法。如果这个类或类似的东西在核心中可用,或者作为常用的PyPI附加组件,我同意它会非常好。 - Ken Williams
1
此外,我必须说,在自己写下这个问题并学会更加习惯地使用Python的8.5年里,我几乎从未需要这种功能。 - Dan Lenski
1
我想祝贺你 - 经过约9年的时间,你接受了这个问题;你的问题是整个StackExchange的记录保持者。 - peterh
1
Stack Exchange非常棒,他们允许我们在他们的大部分数据库上执行SQL查询。这里可以看到我的查询结果,希望对你有所帮助。 :-) - peterh
显示剩余3条评论
15个回答

17

使用命名组和调度表:

r = re.compile(r'(?P<cmd>foo|bar|baz)(?P<data>.+)')

def do_foo(data):
    ...

def do_bar(data):
    ...

def do_baz(data):
    ...

dispatch = {
    'foo': do_foo,
    'bar': do_bar,
    'baz': do_baz,
}


m = r.match(var)
if m:
    dispatch[m.group('cmd')](m.group('data'))

通过简单的内省,您可以自动生成正则表达式和分派表。


2
如果这三个正则表达式不相似呢?比如 /^foo(.)/, /(.)bar$/, 以及 /^(.)baz(.)$/ ? - raldi
然后你需要更复杂的代码。构建一个将正则表达式映射到函数的字典,或者如果您想按特定顺序应用它们,则构建一个(正则表达式,函数)对的列表。应用每个正则表达式并调用匹配的函数。例如。 - Thomas Wouters
1
您正在将特定于上下文的代码定义移得太远,远离其使用的地方。 - jfs

10
r"""
This is an extension of the re module. It stores the last successful
match object and lets you access it's methods and attributes via
this module.

This module exports the following additional functions:
    expand  Return the string obtained by doing backslash substitution on a
            template string.
    group   Returns one or more subgroups of the match.
    groups  Return a tuple containing all the subgroups of the match.
    start   Return the indices of the start of the substring matched by
            group.
    end     Return the indices of the end of the substring matched by group.
    span    Returns a 2-tuple of (start(), end()) of the substring matched
            by group.

This module defines the following additional public attributes:
    pos         The value of pos which was passed to the search() or match()
                method.
    endpos      The value of endpos which was passed to the search() or
                match() method.
    lastindex   The integer index of the last matched capturing group.
    lastgroup   The name of the last matched capturing group.
    re          The regular expression object which as passed to search() or
                match().
    string      The string passed to match() or search().
"""

import re as re_

from re import *
from functools import wraps

__all__ = re_.__all__ + [ "expand", "group", "groups", "start", "end", "span",
        "last_match", "pos", "endpos", "lastindex", "lastgroup", "re", "string" ]

last_match = pos = endpos = lastindex = lastgroup = re = string = None

def _set_match(match=None):
    global last_match, pos, endpos, lastindex, lastgroup, re, string
    if match is not None:
        last_match = match
        pos = match.pos
        endpos = match.endpos
        lastindex = match.lastindex
        lastgroup = match.lastgroup
        re = match.re
        string = match.string
    return match

@wraps(re_.match)
def match(pattern, string, flags=0):
    return _set_match(re_.match(pattern, string, flags))


@wraps(re_.search)
def search(pattern, string, flags=0):
    return _set_match(re_.search(pattern, string, flags))

@wraps(re_.findall)
def findall(pattern, string, flags=0):
    matches = re_.findall(pattern, string, flags)
    if matches:
        _set_match(matches[-1])
    return matches

@wraps(re_.finditer)
def finditer(pattern, string, flags=0):
    for match in re_.finditer(pattern, string, flags):
        yield _set_match(match)

def expand(template):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.expand(template)

def group(*indices):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.group(*indices)

def groups(default=None):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.groups(default)

def groupdict(default=None):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.groupdict(default)

def start(group=0):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.start(group)

def end(group=0):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.end(group)

def span(group=0):
    if last_match is None:
        raise TypeError, "No successful match yet."
    return last_match.span(group)

del wraps  # Not needed past module compilation
例如:
if gre.match("foo(.+)", var):
  # do something with gre.group(1)
elif gre.match("bar(.+)", var):
  # do something with gre.group(1)
elif gre.match("baz(.+)", var):
  # do something with gre.group(1)

11
这种方法的问题在于你只有一个全局的“最后匹配”。如果从多个线程使用该模块,它将会被破坏;如果在信号处理程序或从上面的“if”主体调用的代码中使用“gre”模块,也会出现问题。如果你一定要使用它,请格外小心。 - Thomas Wouters

9

是的,这有点烦人。也许以下方法适用于您的情况。


import re

class ReCheck(object):
    def __init__(self):
        self.result = None
    def check(self, pattern, text):
        self.result = re.search(pattern, text)
        return self.result

var = 'bar stuff'
m = ReCheck()
if m.check(r'foo(.+)',var):
    print m.result.group(1)
elif m.check(r'bar(.+)',var):
    print m.result.group(1)
elif m.check(r'baz(.+)',var):
    print m.result.group(1)

编辑:Brian正确指出我的第一次尝试没有成功。不幸的是,这个尝试更长了。


这样做行不通 - Python 是按值调用的,因此函数不会改变结果。您可以通过传递可变变量(例如对象或列表)来实现它,或者将最后的结果存储在全局或函数属性中。 - Brian

8

Python 3.8 开始,引入了 赋值表达式(PEP 572):= 操作符),我们现在可以将条件值 re.search(pattern, text) 赋给变量 match,以便在条件的主体中既检查它是否不是 None,又重复使用它:

if match := re.search(r'foo(.+)', text):
  # do something with match.group(1)
elif match := re.search(r'bar(.+)', text):
  # do something with match.group(1)
elif match := re.search(r'baz(.+)', text)
  # do something with match.group(1)

2
10年后,将其替换为被接受的答案——太棒了!实际上,这个“完全相同的例子”(在elif中重复使用正则表达式搜索)在PEP中被引用:https://www.python.org/dev/peps/pep-0572/#capturing-condition-values - Dan Lenski

8
我建议使用这种方法,因为它使用最少的正则表达式来实现您的目标。这仍然是可用的代码,但不比您以前的Perl差。
import re
var = "barbazfoo"

m = re.search(r'(foo|bar|baz)(.+)', var)
if m.group(1) == 'foo':
    print m.group(1)
    # do something with m.group(1)
elif m.group(1) == "bar":
    print m.group(1)
    # do something with m.group(1)
elif m.group(1) == "baz":
    print m.group(2)
    # do something with m.group(2)

6

感谢这个其他的SO问题:

import re

class DataHolder:
    def __init__(self, value=None, attr_name='value'):
        self._attr_name = attr_name
        self.set(value)
    def __call__(self, value):
        return self.set(value)
    def set(self, value):
        setattr(self, self._attr_name, value)
        return value
    def get(self):
        return getattr(self, self._attr_name)

string = u'test bar 123'
save_match = DataHolder(attr_name='match')
if save_match(re.search('foo (\d+)', string)):
    print "Foo"
    print save_match.match.group(1)
elif save_match(re.search('bar (\d+)', string)):
    print "Bar"
    print save_match.match.group(1)
elif save_match(re.search('baz (\d+)', string)):
    print "Baz"
    print save_match.match.group(1)

4

或者,完全不使用正则表达式的替代方案:

prefix, data = var[:3], var[3:]
if prefix == 'foo':
    # do something with data
elif prefix == 'bar':
    # do something with data
elif prefix == 'baz':
    # do something with data
else:
    # do something with var

是否适用取决于您实际的问题。不要忘记,正则表达式在Python中并不像Perl中那样是瑞士军刀;Python有不同的结构来进行字符串操作。


3
我猜想问题中的正则表达式纯属假设,因此基于它们来回答你的问题是错误的,也无法真正回答你的问题。请修正答案以符合问题要求。 - Piotr Dobrogost

4
def find_first_match(string, *regexes):
    for regex, handler in regexes:
        m = re.search(regex, string):
        if m:
            handler(m)
            return
    else:
        raise ValueError

find_first_match(
    foo, 
    (r'foo(.+)', handle_foo), 
    (r'bar(.+)', handle_bar), 
    (r'baz(.+)', handle_baz))

为了加快速度,可以将所有的正则表达式在内部合并成一个,并动态创建分发器。理想情况下,这应该转化为一个类。

3
这是我解决这个问题的方法:
matched = False;

m = re.match("regex1");
if not matched and m:
    #do something
    matched = True;

m = re.match("regex2");
if not matched and m:
    #do something else
    matched = True;

m = re.match("regex3");
if not matched and m:
    #do yet something else
    matched = True;

这个模式并不像原始的那么简洁。但是,它很简单,直接,不需要额外的模块或更改原始的正则表达式。


1
一个极简的数据持有者:
class Holder(object):
    def __call__(self, *x):
        if x:
            self.x = x[0]
        return self.x

data = Holder()

if data(re.search('foo (\d+)', string)):
    print data().group(1)

或者作为单例函数:
def data(*x):
    if x:
        data.x = x[0]
    return data.x

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