【问题标题】:Python chunking with regular expressions使用正则表达式进行 Python 分块
【发布时间】:2020-07-06 23:03:46
【问题描述】:

在 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() 或类似的)。

欢迎提出建议和讨论。

【问题讨论】:

    标签: python regex tokenize


    【解决方案1】:

    总结

    没有你描述的 re.matchiter() 的直接等价物。

    我想到了两种选择:

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

    不匹配令牌

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

    【讨论】:

    • 哦,是您将其添加到文档中的!这确实很有帮助,尽管在这种情况下需要应用很多机制。似乎自定义生成器可能是最好的解决方案,并且可以很好地重复使用。
    • 我最终使用了生成器的修改版本,它允许调用者负责错误处理,我可以将其编辑到您的响应中或发布我自己的答案,任何偏好?
    • @KenWilliams 我建议保持原样。我们编写库代码并不是为了满足所有用户的所有需求。 SO 答案的目的是尽可能简单地提出解决问题的可能方法。定制会妨碍实现这一目标。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-01
    • 2013-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多