Python 使用正则表达式分块
Python chunking with regular expressions
在 Perl 中,遍历字符串以将其分块为标记很容易:
$key = ".foo[4][5].bar.baz";
@chunks = $key =~ m/\G\[\d+\]|\.[^][.]+/gc;
print "@chunks\n";
#>> output: .foo [4] [5] .bar .baz
# Optional error handling:
die "Malformed key at '" . substr($key, pos($key)) . "'"
if pos($key) != length($key);
如果需要更多控制,可以改为循环:
while ($key =~ m/(\G\[\d+\]|\.[^][.]+)/g) {
push @chunks, ; # Optionally process each one
}
我想在 Python 中找到一种简洁、惯用的方法来执行此操作。到目前为止我只有这个:
import re
key = ".foo[4][5].bar.baz"
rx = re.compile(r'\[\d+\]|\.[^][.]+')
chunks = []
while True:
m = re.match(rx, key)
if not m:
raise ValueError(f"Malformed key at '{key}'")
chunk = m.group(0)
chunks.append(chunk[1:] if chunk.startswith('.') else int(chunk[1:-1]))
key = key[m.end(0):]
if key == '':
break
print(chunks)
除了它更冗长之外,我不喜欢它,因为我需要在处理字符串时销毁它,因为似乎没有与 Perl 的 \G
锚点等效的东西(从上一场比赛结束的地方接起)。另一种方法是在每个循环中跟踪我自己在字符串中的匹配位置,但这似乎更加繁琐。
有没有我没找到的成语?我也尝试了一些使用 re.finditer()
的解决方案,但它似乎没有办法让每场比赛都在上一场比赛的确切结束处开始(例如 re.matchiter()
或类似的)。
欢迎提出建议和讨论。
总结
没有直接等同于您描述的 re.matchiter()。
我想到了两个备选方案:
- 创建一个不匹配令牌。
- 编写您自己的具有所需行为的生成器。
令牌不匹配
Python 中的常用技术是定义一个 MISMATCH 包罗万象的标记,并在遇到该标记时引发异常。
这是一个工作示例(我编写并放在 Python docs 中以便每个人都能找到它):
from typing import NamedTuple
import re
class Token(NamedTuple):
type: str
value: str
line: int
column: int
def tokenize(code):
keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
token_specification = [
('NUMBER', r'\d+(\.\d*)?'), # Integer or decimal number
('ASSIGN', r':='), # Assignment operator
('END', r';'), # Statement terminator
('ID', r'[A-Za-z]+'), # Identifiers
('OP', r'[+\-*/]'), # Arithmetic operators
('NEWLINE', r'\n'), # Line endings
('SKIP', r'[ \t]+'), # Skip over spaces and tabs
('MISMATCH', r'.'), # Any other character
]
tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
line_num = 1
line_start = 0
for mo in re.finditer(tok_regex, code):
kind = mo.lastgroup
value = mo.group()
column = mo.start() - line_start
if kind == 'NUMBER':
value = float(value) if '.' in value else int(value)
elif kind == 'ID' and value in keywords:
kind = value
elif kind == 'NEWLINE':
line_start = mo.end()
line_num += 1
continue
elif kind == 'SKIP':
continue
elif kind == 'MISMATCH':
raise RuntimeError(f'{value!r} unexpected on line {line_num}')
yield Token(kind, value, line_num, column)
statements = '''
IF quantity THEN
total := total + price * quantity;
tax := price * 0.05;
ENDIF;
'''
for token in tokenize(statements):
print(token)
自定义生成器
另一种选择是编写具有所需行为的自定义生成器。
用于已编译正则表达式的 match() 方法允许匹配操作的可选起始位置。使用该工具,不难编写将 match() 应用于连续起始位置的自定义生成器:
def itermatch(pattern, string):
p = re.compile(pattern)
pos = 0
while True:
mo = p.match(string, pos)
if mo is None:
break # Or raise exception
yield mo
pos = mo.end()
在 Perl 中,遍历字符串以将其分块为标记很容易:
$key = ".foo[4][5].bar.baz";
@chunks = $key =~ m/\G\[\d+\]|\.[^][.]+/gc;
print "@chunks\n";
#>> output: .foo [4] [5] .bar .baz
# Optional error handling:
die "Malformed key at '" . substr($key, pos($key)) . "'"
if pos($key) != length($key);
如果需要更多控制,可以改为循环:
while ($key =~ m/(\G\[\d+\]|\.[^][.]+)/g) {
push @chunks, ; # Optionally process each one
}
我想在 Python 中找到一种简洁、惯用的方法来执行此操作。到目前为止我只有这个:
import re
key = ".foo[4][5].bar.baz"
rx = re.compile(r'\[\d+\]|\.[^][.]+')
chunks = []
while True:
m = re.match(rx, key)
if not m:
raise ValueError(f"Malformed key at '{key}'")
chunk = m.group(0)
chunks.append(chunk[1:] if chunk.startswith('.') else int(chunk[1:-1]))
key = key[m.end(0):]
if key == '':
break
print(chunks)
除了它更冗长之外,我不喜欢它,因为我需要在处理字符串时销毁它,因为似乎没有与 Perl 的 \G
锚点等效的东西(从上一场比赛结束的地方接起)。另一种方法是在每个循环中跟踪我自己在字符串中的匹配位置,但这似乎更加繁琐。
有没有我没找到的成语?我也尝试了一些使用 re.finditer()
的解决方案,但它似乎没有办法让每场比赛都在上一场比赛的确切结束处开始(例如 re.matchiter()
或类似的)。
欢迎提出建议和讨论。
总结
没有直接等同于您描述的 re.matchiter()。
我想到了两个备选方案:
- 创建一个不匹配令牌。
- 编写您自己的具有所需行为的生成器。
令牌不匹配
Python 中的常用技术是定义一个 MISMATCH 包罗万象的标记,并在遇到该标记时引发异常。
这是一个工作示例(我编写并放在 Python docs 中以便每个人都能找到它):
from typing import NamedTuple
import re
class Token(NamedTuple):
type: str
value: str
line: int
column: int
def tokenize(code):
keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
token_specification = [
('NUMBER', r'\d+(\.\d*)?'), # Integer or decimal number
('ASSIGN', r':='), # Assignment operator
('END', r';'), # Statement terminator
('ID', r'[A-Za-z]+'), # Identifiers
('OP', r'[+\-*/]'), # Arithmetic operators
('NEWLINE', r'\n'), # Line endings
('SKIP', r'[ \t]+'), # Skip over spaces and tabs
('MISMATCH', r'.'), # Any other character
]
tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
line_num = 1
line_start = 0
for mo in re.finditer(tok_regex, code):
kind = mo.lastgroup
value = mo.group()
column = mo.start() - line_start
if kind == 'NUMBER':
value = float(value) if '.' in value else int(value)
elif kind == 'ID' and value in keywords:
kind = value
elif kind == 'NEWLINE':
line_start = mo.end()
line_num += 1
continue
elif kind == 'SKIP':
continue
elif kind == 'MISMATCH':
raise RuntimeError(f'{value!r} unexpected on line {line_num}')
yield Token(kind, value, line_num, column)
statements = '''
IF quantity THEN
total := total + price * quantity;
tax := price * 0.05;
ENDIF;
'''
for token in tokenize(statements):
print(token)
自定义生成器
另一种选择是编写具有所需行为的自定义生成器。
用于已编译正则表达式的 match() 方法允许匹配操作的可选起始位置。使用该工具,不难编写将 match() 应用于连续起始位置的自定义生成器:
def itermatch(pattern, string):
p = re.compile(pattern)
pos = 0
while True:
mo = p.match(string, pos)
if mo is None:
break # Or raise exception
yield mo
pos = mo.end()