在Python中用正则表达式替换时排除特定字符串区间
我遇到了一个问题。
假设有一个比较长且复杂的文本文件,其中包含一些用特定符号(可以是空的)包围的特殊块:
some text
----
text inside special block
----
some text
----
----
some text
----
text inside special block
----
我的任务是对整个文本进行一些替换,但要排除这些边界(----
)内的文本。
举个例子,我们需要把所有的 text
替换成 TEXT
,但不包括特殊块内的内容。最终的结果应该是:
some TEXT
----
text inside special block
----
some TEXT
----
----
some TEXT
----
text inside special block
----
在这里我们不能使用前瞻或后顾,因为在特定位置我们不知道自己是否在特殊块内(边界没有方向)。
所以我解决这个问题的方法是,首先解析整个文本,找到特殊块的边界,然后获取“坏”行的索引,接着逐行应用我的正则替换,检查这一行是否不是“坏”行。但是如果我的正则需要应用于多行,那就变得更复杂了。我相信有一些聪明又简单的方法可以处理这个问题。
基本上,我需要的是能够在对整个文本应用 re.sub
时,排除一些文本片段(通过它们的范围)。即使正则表达式只与范围相交(不一定包含它)。这样我就可以先应用第一个正则,获取特殊块的起始和结束索引,然后将这些范围从第二个正则中排除。这个怎么实现呢?
现在我有这个解决方案(上面的例子是简化过的,抱歉):
def find_code_lines(data):
# Search for blocks by regex They can be empty!
r = re.compile(r'(\n----(?=\n)(?P<group1>[\s\S]*?\n)----\n)')
# Delete all '\n' which are not line breaks (there are some of them in formulas etc.)
data_edited = data.replace('\\n', '')
# Save spans by symbol indexes
char_spans = []
for m in r.finditer(data_edited):
#print(m.span(1))
#print(m.span[1])
char_spans.append(m.span(1))
# Calculate spans by line indexes
line_spans = []
for span in char_spans:
begin = data_edited[:span[0]].count("\n") + 2
end = data_edited[:span[1]].count("\n") - 1
line_spans.append((begin, end))
return line_spans
# Check if index is inside one of spans
def in_spans(spans, line_index):
res = False
for span in spans:
if line_index >= span[0] and line_index < span[1]:
res = True
return res
# Parse text by blocks
code_lines = find_code_lines(data)
lines_edited = []
data_lines = data.splitlines()
replace_count = 0
for i in range(len(data_lines)):
if in_spans(code_lines, i):
lines_edited.append(data_lines[i])
#print('line in spans:', i)
else:
data_tuple = re.subn(r'(?P<group1>\s|^|\s\()\$(?P<group2>[^\$`\r\n]{1,1000}?)\$',
r'\1stem:[\2]',
data_lines[i])
if data_tuple[1] == 0:
lines_edited.append(data_lines[i])
else:
lines_edited.append(data_tuple[0])
replace_count += data_tuple[1]
lines_edited.append('')
data = '\n'.join(lines_edited)
log_it('Replaced Math blocks', replace_count)
更新
我在输入示例中添加了更多文本,因为下面的一些解决方案只能处理特定版本的输入(这些输入比较简单)。到目前为止,最复杂的情况是这样的:
some text
----
text inside special block
----
some text
----
----
some text
----
text inside special block
----
some text
期望的输出:
some TEXT
----
text inside special block
----
some TEXT
----
----
some TEXT
----
text inside special block
----
some TEXT
2 个回答
一种可能的方法是使用分隔符的正则表达式创建一个位置数组,然后在这些位置之间进行不同的替换,像这样:
import re
def replace(data, separator_re, replace_outside_special_func):
poss = [0]
for match in re.finditer(separator_re, data):
poss.extend((match.start(), match.end()))
poss.append(len(data))
return ''.join(
data[poss[i] : poss[i + 1]] if i & 3
else replace_outside_special_func(data[poss[i] : poss[i + 1]])
for i in range(0, len(poss) - 1))
data = '''some text
----
text inside special block
----
some text
'''
print(replace(
data, r'(?m)^----$',
lambda data: data.replace('text', 'TEXT')))
它会输出:
some TEXT
----
text inside special block
----
some TEXT
一些解释:
- 我们使用
re.finditer
和match.start()
以及match.end()
来找到每个分隔符的开始和结束位置(在字符串中的索引)。我们把这些位置按顺序保存到数组poss
中。 - 我们还在
poss
的开头加上0
,在结尾加上len(data)
,这样可以确保不会漏掉输入字符串开头和结尾附近的任何块。 - 我们遍历
poss
中相邻位置标记的每个子字符串,比如data[poss[0] : poss[1]]
、data[poss[1] : poss[2]]
、data[poss[2] : poss[3]]
等等。 data[poss[i] : poss[i + 1]]
的内容是:- 如果
i % 4 == 0
:普通块,比如'some text\n'
。 - 如果
i % 4 == 1
:普通块后的分隔符,比如'----'
。 - 如果
i % 4 == 2
:特殊块,比如'\ntext inside special block\n'
。 - 如果
i % 4 == 3
:特殊块后的分隔符,比如'----'
。
- 如果
- 我们只在普通块中进行替换。
这里有一个更简短的解决方案:
import re
data = '''some text
----
text inside special block
----
some text
'''
print(re.sub(
r'(?s)(.*?)(\Z|\n(?:\Z|----(?:\Z|\n.*?(?:\Z|\n(?:\Z|----(?:\Z|\n))))))',
lambda match: match.group(1).replace('text', 'TEXT') + match.group(2),
data))
我觉得这个更简短的解决方案可能会慢一些。你可能想在长输入上运行一些基准测试。
一些解释:
(?s)
设置了s
标志,这样.
就可以匹配任何字符,包括\n
。(.*?)
匹配普通块,并将其保存到match.group(1)
。(\Z|\n(?:\Z|----(?:\Z|\n.*?(?:\Z|\n(?:\Z|----(?:\Z|\n))))))
匹配分隔符 + 特殊块 + 分隔符,并将其保存到match.group(2)
。.*?
类似于.*
,但会优先尝试较短的匹配。如果没有?
,这个正则表达式的部分会匹配整个输入。TEXT
替换只应用于普通块(match.group(1)
)。- 这个正则表达式包含了很多
\Z|\n...
的实例,以确保即使输入字符串提前结束(例如在特殊块内),也能正确匹配。
在使用正则表达式进行字符串处理时,这些想法很有用:
用一个形式为
(...)|(...)|...
的正则表达式匹配所有内容,然后在替换函数中用if match.group(1) is not None:
等进行分类。用多个短的正则表达式进行匹配(例如
^----$
和text
),并编写代码将匹配和替换连接在一起。用一个长的正则表达式进行匹配,在字符串末尾用
\Z|...
进行部分匹配。
上面的长解决方案做了第2种,而短的解决方案做了第3种。对于标记化(比如从C源代码中去除注释),第1种方法很有用。
你可以使用 re.sub
这个函数,配合一个替换函数来实现你的需求。这里有个链接可以帮助你理解这个正则表达式的用法:regex101。
import re
text = """\
some text
----
text inside special block
----
some text
----
----
some text
----
text inside special block
----
some text"""
pat = r"(.*?)(-+\n.*?-+(?:\n|\Z))(.*?)(?=-+|\Z)"
def repl_fn(g):
return (
g.group(1).replace("text", "TEXT")
+ g.group(2)
+ g.group(3).replace("text", "TEXT")
)
text = re.sub(pat, repl_fn, text, flags=re.S)
print(text)
运行后会输出:
some TEXT
----
text inside special block
----
some TEXT
----
----
some TEXT
----
text inside special block
----
some TEXT