首页 > 解决方案 > 使用正则表达式进行 Python 分块

问题描述

在 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, $1;  # 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()或类似的)。

欢迎提出建议和讨论。

标签: pythonregextokenize

解决方案


概括

正如您所描述的那样,没有直接等效的re.matchiter() 。

想到了两种选择:

  1. 创建一个不匹配的令牌。
  2. 编写具有所需行为的自己的生成器。

不匹配令牌

Python 中的常用技术是定义一个MISMATCH包罗万象的标记,并在遇到该标记时引发异常。

这是一个工作示例(我编写并放入Python 文档中以便每个人都可以找到它):

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()

推荐阅读