Проверьте, находится ли позиция в строке в паре определенных символов

В Python, что было бы наиболее эффективным способом выяснить, если позиция в строке находится в паре определенных последовательностей символов?

       0--------------16-------------------37---------48--------57
       |               |                    |          |        |
cost=r"a) This costs \$1 but price goes as $x^2$ for \(x\) item(s)."

В строке costЯ хочу выяснить, если определенная позиция заключена в пару $ или в пределах \( а также \),

Для строки cost функция is_maths(cost,x) вернется True за x в [37,38,39,48] и оценить False для всего остального.

Мотивация состоит в том, чтобы выяснить действительные позиции по латексной математике, также приветствуются любые альтернативные эффективные способы использования Python.

1 ответ

Решение

Вам нужно будет проанализировать строку до запрошенной позиции и, если она находится в допустимой паре разделителей среды LaTeX, до закрывающего разделителя, чтобы иметь возможность ответить с True или же False, Это потому, что вы должны обработать каждый соответствующий метасимвол (обратную косую черту, доллары и скобки), чтобы определить их влияние.

Я поняла что латекс $...$ а также \(...\) Разделители среды не могут быть вложенными, поэтому вам не нужно беспокоиться о вложенных выражениях здесь; вам нужно только найти ближайший полный $...$ или же \(...\) пара.

Вы не можете просто соответствовать буквальному $ или же \( или же \) символы, однако, потому что каждому из них может предшествовать произвольное число \ обратные косые. Вместо этого токенизируйте входную строку по обратным слешам, долларам или скобкам, итерируйте по токенам, чтобы отследить, что было найдено в последний раз, чтобы определить их эффект (экранирование следующего символа, а также открывающая и закрывающая математические среды).

Вам не нужно продолжать синтаксический анализ, если вы находитесь за запрошенной позицией и вне раздела математической среды; у вас уже есть ответ, и вы можете вернуться False рано.

Вот моя реализация такого парсера:

import re

_maths_pairs = {
    # keys are opening characters, values matching closing characters
    # each is a tuple of char (string), escaped (boolean)
    ('$', False): ('$', False),
    ('(', True): (')', True),
}
_tokens = re.compile(r'[\\$()]')

def _tokenize(s):
    """Generator that produces token, pos, prev_pos tuples for s

    * token is a single character: a backslash, dollar or parethesis
    * pos is the index into s for that token
    * prev_pos is te position of the preceding token, or -1 if there
      was no preceding token

    """
    prev_pos = -1
    for match in _tokens.finditer(s):
        token, pos = match[0], match.start()
        yield token, pos, prev_pos
        prev_pos = pos

def is_maths(s, pos):
    """Determines if pos in s is within a LaTeX maths environment"""
    expected_closer = None  # (char, escaped) if within $...$ or \(...\)
    opener_pos = None  # position of last opener character
    escaped = False  # True if the most recent token was an escaping backslash

    for token, token_pos, prev_pos in _tokenize(s):
        if expected_closer is None and token_pos > pos:
            # we are past the desired position, it'll never be within a
            # maths environment.
            return False

        # if there was more text between the current token and the last
        # backslash, then that backslash applied to something else.
        if escaped and token_pos > prev_pos + 1:
            escaped = False

        if token == '\\':
            # toggle the escaped flag; doubled escapes negate
            escaped = not escaped
        elif (token, escaped) == expected_closer:
            if opener_pos < pos < token_pos:
                # position is after the opener, before the closer
                # so within a maths environment.
                return True
            expected_closer = None
        elif expected_closer is None and (token, escaped) in _maths_pairs:
            expected_closer = _maths_pairs[(token, escaped)]
            opener_pos = token_pos

        prev_pos = token_pos

    return False

Демо-версия:

>>> cost = r'a) This costs \$1 but price goes as $x^2$ for \(x\) item(s).'
>>> is_maths(cost, 0)  # should be False
False
>>> is_maths(cost, 16)  # should be False, preceding $ is escaped
False
>>> is_maths(cost, 37)  # should be True, within $...$
True
>>> is_maths(cost, 48)  # should be True, within \(...\)
True
>>> is_maths(cost, 57)  # should be False, within unescaped (...)
False

и дополнительные тесты, чтобы показать, что экранирование обрабатывается правильно:

>>> is_maths(r'Doubled escapes negate: \\$x^2$', 27)  # should be true
True
>>> is_maths(r'Doubled escapes negate: \\(x\\)', 27)  # no longer escaped, so false
False

Моя реализация старательно игнорирует искаженные проблемы LaTeX; неэкранированный $ символы внутри \(...\) или сбежал \( а также \) символы внутри $...$ игнорируются, как и дальше \( открывашки внутри \(...\) последовательности или \) доводчики без соответствия \( открывалка предшествующая. Это гарантирует, что функция продолжает работать, даже если задан ввод, который сам LaTeX не будет отображать. Парсер может быть изменен для выдачи исключения или возврата False в этих случаях, однако. В этом случае вам нужно добавить глобальный набор, созданный из _math_pairs.keys() | _math_pairs.values() и проверить (char, escaped) против этого набора, когда expected_closer is not None and (token, escaped) != expected_closer false (обнаружение разделителей вложенной среды) и проверка на char == ')' and escaped and expected_closer is None обнаружить \) ближе без проблемы сошника.

Другие вопросы по тегам