用正则表达式替换为XML节点

1 投票
6 回答
2136 浏览
提问于 2025-04-15 16:39

我正在用Python写一个正则表达式,用来把字符串中的某些部分替换成XML节点。

源字符串看起来是这样的:

Hello
REPLACE(str1) this is to replace
REPLACE(str2) this is to replace

而结果字符串应该是这样的:

Hello
<replace name="str1"> this is to replace </replace>
<replace name="str2"> this is to replace </replace>

有没有人能帮我一下?

6 个回答

1

这里有一个使用pyparsing的解决方案。我知道你特别想要一个正则表达式的解决方案,但如果你的需求发生变化,使用pyparsing的解析器可能会更容易扩展。或者,pyparsing的原型解决方案可能会让你对问题有更多的理解,从而帮助你找到正则表达式或其他最终实现的方法。

src = """\
Hello
REPLACE(str1) this is to replace
REPLACE(str2) this is to replace
"""

from pyparsing import Suppress, Word, alphas, alphanums, restOfLine

LPAR,RPAR = map(Suppress,"()")
ident = Word(alphas, alphanums)
replExpr = "REPLACE" + LPAR + ident("name") + RPAR + restOfLine("body")
replExpr.setParseAction(
    lambda toks : '<replace name="%(name)s">%(body)s </replace>' % toks
    )

print replExpr.transformString(src)

在这个例子中,你用pyparsing创建了一个要匹配的表达式,定义了一个解析动作来进行文本转换,然后调用transformString来扫描输入源,找到所有匹配的内容,对每个匹配应用解析动作,并返回结果。这个解析动作的功能类似于@steveha解决方案中的mksub。

除了解析动作,pyparsing还支持给表达式中的各个元素命名——我用了“name”和“body”来标记两个感兴趣的部分,在正则表达式的解决方案中,它们分别被表示为组1和组2。你可以在正则表达式中命名组,对应的正则表达式看起来是这样的:

s_pat = "^\s*REPLACE\((?P<name>[^)]+)\)(?P<body>.*)$"

不幸的是,要通过名称访问这些组,你必须在正则匹配对象上调用group()方法,不能像我在lambda解析动作中那样直接进行命名字符串插值。但这就是Python,对吧?我们可以用一个类来包装这个可调用对象,这样就能通过名称像访问字典一样访问这些组:

class CallableDict(object):
    def __init__(self,fn):
        self.fn = fn
    def __getitem__(self,name):
        return self.fn(name)

def mksub(m):    
    return '<replace name="%(name)s">%(body)s</replace>' %  CallableDict(m.group)

s_output = re.sub(pat, mksub, s_input)

使用CallableDict,mksub中的字符串插值现在可以通过调用m.group来获取每个字段,就像我们在检索字典中的['name']和['body']元素一样。

4

这里有一个很棒的教程,教你如何在Python中写正则表达式。

5

你遇到的问题有点棘手,因为你想在多行字符串中进行匹配。为了实现这一点,你需要使用 re.MULTILINE 这个标志。

接下来,你需要在源字符串中匹配一些组,并在最终输出中使用这些组。下面是可以解决你问题的代码:

import re


s_pat = "^\s*REPLACE\(([^)]+)\)(.*)$"
pat = re.compile(s_pat, re.MULTILINE)

s_input = """\
Hello
REPLACE(str1) this is to replace
REPLACE(str2) this is to replace"""


def mksub(m):
    return '<replace name="%s">%s</replace>' % m.groups()


s_output = re.sub(pat, mksub, s_input)

唯一棘手的部分是正则表达式的模式。我们来详细看看。

^ 匹配字符串的开始。使用 re.MULTILINE 时,这个符号会匹配多行字符串中每一行的开始;换句话说,它会在字符串中的换行符后面匹配。

\s* 匹配可选的空白字符。

REPLACE 匹配字面意思的字符串 "REPLACE"。

\( 匹配字面意思的字符 "("。

( 开始一个“匹配组”。

[^)] 意思是“匹配任何不是 ')' 的字符”。

+ 表示“匹配一个或多个前面的模式”。

) 结束一个“匹配组”。

\) 匹配字面意思的字符 ")"。

(.*) 是另一个匹配组,包含 ".*"。

$ 匹配字符串的结束。使用 re.MULTILINE 时,这个符号会匹配多行字符串中每一行的结束;换句话说,它会匹配字符串中的换行符。

. 匹配任何字符,而 * 表示匹配零个或多个前面的模式。因此 .* 可以匹配任何内容,直到行的结束。

所以,我们的模式有两个“匹配组”。当你运行 re.sub() 时,它会生成一个“匹配对象”,这个对象会被传递给 mksub()。匹配对象有一个方法 .groups(),它会返回匹配的子字符串作为一个元组,然后这些内容会被替换到最终的文本中。

编辑:其实你不需要使用替换函数。你可以在替换文本中放入特殊字符串 \1,它会被替换为匹配组 1 的内容。(匹配组从 1 开始计数;特殊的匹配组 0 对应整个被模式匹配的字符串。)\1 字符串唯一棘手的地方是 \ 在字符串中是特殊的。要在普通字符串中得到一个 \,你需要连续写两个反斜杠,比如 "\\1"。不过你可以使用 Python 的“原始字符串”方便地写出替换模式。这样你就可以得到:

import re

s_pat = "^\s*REPLACE\(([^)]+)\)(.*)$"
pat = re.compile(s_pat, re.MULTILINE)

s_repl = r'<replace name="\1">\2</replace>'

s_input = """\
Hello
REPLACE(str1) this is to replace
REPLACE(str2) this is to replace"""


s_output = re.sub(pat, s_repl, s_input)

撰写回答