Я пытаюсь автоматически перевести простой код Perl с регулярным выражением на Python, и у меня возникла проблема. Вот код Perl:
$stamp='[stamp]';
$message = "message\n";
$message =~ s/^/$stamp/gm;
print "$message";
[stamp]message
Вот мой эквивалент Python:
>>> import re
>>> re.sub(re.compile("^", re.M), "[stamp]", "message\n", count=0)
'[stamp]message\n[stamp]'
Обратите внимание, что ответ другой (в конце есть лишний [stamp]). Как сгенерировать код, который имеет такое же поведение для регулярного выражения?
Без re.M пример был бы правильным, но я не знаю, как Perl ведет себя на других примерах.
Похоже, это может быть несовместимость между «многострочными» режимами PCRE и Python RE. С re.M в Python «символ шаблона '^' совпадает в начале строки и в начале каждой строки (сразу после каждой новой строки)». Похоже, что PCRE делает еще один шаг вперед и требует, чтобы ^ находился в начале действительной строки POSIX (т. е. завершался символом новой строки).
Кстати, если вы просто хотите добавить к строке префикс [start], использование регулярного выражения кажется излишним.
Он добавляет префикс к каждой строке потенциально многострочной строки, и снова я автоматически перевожу код perl на python.
Примечание: re.sub(re.compile(...), ...) — это странный/многословный/избыточный способ написания re.compile(...).sub(...). А count=0 стоит по умолчанию, поэтому передавать его не нужно. Вы можете упростить re.sub(re.compile("^", re.M), "[stamp]", "message\n", count=0) до re.compile("^", re.M).sub("[stamp]", "message\n").
Re: Примечание: Pythonizer — это автоматический переводчик с perl на python = он генерирует код в определенном порядке для обработки вещей, например, сначала он обрабатывает все флаги, кроме флага G, а затем обрабатывает последний, устанавливая count=0 vs count =1. Я понимаю, что код может выглядеть немного забавно, но он работает.
Похоже, что regex101.com/r/uyT1i2/1 и regex101.com/r/mNTTeP/1 может быть то, что EOS \Z переопределяет ^ в отношении строк, заканчивающихся новой строкой. Вы можете исключить двусмысленность с помощью (?m)^(?!\a|\Z) или (?m)(?!\a|\Z)^
Многострочный режим Perl не считает пустую строку после последней новой строки отдельной строкой. То есть он рассматривает A\nB и A\nB\n как две строки, а A\nB\nC как три строки. Это отличается от многострочного режима Python, который рассматривает каждую новую строку как начало новой строки:
re.M: если указано, символ шаблона '^' соответствует началу строки и началу каждой строки (сразу после каждой новой строки).
Вы можете сымитировать поведение многострочного режима Perl, добавив предварительное утверждение по крайней мере для одного символа в начале строки:
(?=.|\n)
Обратите внимание, что нам нужно явно разрешить \n с альтернативным шаблоном |, поскольку по умолчанию . не соответствует \n. Без этого шаблон не сможет соответствовать началу строк, за которыми сразу следует \n.
Вот как этот шаблон ведет себя в вашем примере:
>>> re.sub(re.compile(r"^(?=.|\n)", re.M), "[stamp]", "message\n", count=0)
'[stamp]message\n'
>>> re.sub(re.compile(r"^(?=.|\n)", re.M), "[stamp]", "message\nmessage", count=0)
'[stamp]message\n[stamp]message'
>>> re.sub(re.compile(r"^(?=.|\n)", re.M), "[stamp]", "message\nmessage\n", count=0)
'[stamp]message\n[stamp]message\n'
>>> re.sub(re.compile(r"^(?=.|\n)", re.M), "[stamp]", "message\n\n", count=0)
'message\n[stamp]\n'
Это не требует строк POSIX; Perl будет работать с любой непустой последней строкой, например "message\na", который имеет символ, но не завершает новую строку в последней (не POSIX) строке. Похоже, что Perl просто имеет немного другое определение строки (похожее на то, которое используется в методе Python str.splitlines); Perl не рассматривает строку, оканчивающуюся на новую строку, как оканчивающуюся совершенно пустой строкой, в отличие от Python re.
@ShadowRanger, так можем ли мы каким-то образом изменить положительное опережающее утверждение, просто требуя, чтобы там был хотя бы 1 символ, например, с использованием '.'?
@snoopyjc: Этого было бы недостаточно, потому что . по умолчанию не соответствует новой строке, поэтому "message\n\n" (где вторая строка — это просто новая строка и ничего больше) добавит [stamp] только один раз, а не дважды. Добавление re.S (он же re.DOTALL) к флагам работает (re.M | re.S), но неясно, может ли это привести к странному поведению чего-то еще.
Спасибо, а это вообще работает? Например. $STRING =~ s/$PATTERN/$REPLACEMENT/gm => re.sub(re.compile(r'PATTERN(?=.)", re.M), REPLACEMENT, STRING), всякий раз, когда используется флаг M Я могу изменить их регулярное выражение следующим образом?
@ShadowRanger Да, добавление флага S в регулярное выражение пользователя небезопасно. Как насчет того, чтобы использовать (?=[.\n]) для просмотра вперед?
@ShadowRanger Хотели бы вы опубликовать ответ, в котором говорится об этом? Я рад интерпретировать ваш отзыв и обновить этот, но на данный момент это действительно ваш ответ, а не мой.
@snoopyjc: На первый взгляд это кажется разумным, но кажется, что это не работает с классом персонажей. r"^(?=.|\n)" работает. пожимает плечами
@Брайан: Если ты настаиваешь. :-)
Я только что проверил ваше решение с помощью $message = "message\n\nmes\nmmm"; и теперь он делает то же самое в perl и python. Но мой вопрос остается - как вы думаете, будет ли это работать с любым регулярным выражением, предоставленным пользователем, если у них установлен флаг M?
@snoopyjc: Вы предлагаете заменить ведущий ^ на ^(?=.|\n) в любом пользовательском шаблоне? Он должен работать; ?= имеет нулевую ширину, поэтому остальная часть их регулярного выражения будет совпадать как обычно, но регулярное выражение достаточно странное, поэтому я никогда не даю 100% гарантию для полностью произвольного регулярного выражения.
@snoopyjc: И да, я придумал способ сломать простой подход «заменить ведущий ^»: r"abc|^". ^ скрыт чередованием, поэтому вы не увидите его в начале узора. Как я уже сказал, регулярное выражение — это странно.
Для /abc|^/ Считаете ли вы, что r"abc|^(?=.|\n)" является правильным ответом, другими словами, замените все '^' на '^(?=.|\n)', не только ведущий?
Проблема в том, что Perl не считает пустую строку после \n строкой текста, в отличие от Python. Таким образом, код RegEx Perl видит "message\n" как одну строку текста, а код RegEx Python — как две.
Вы можете устранить эту разницу, проверив код Python для окончательного \n. Если он обнаружит его, удалите этот \n перед запуском регулярного выражения, а затем добавьте его обратно после.
Вероятно, вы также захотите проверить все пограничные случаи. Например, как ведет себя код Perl и Python, если само сообщение представляет собой пустую строку? (Будет ли код Perl делать что-нибудь в этом случае? Как насчет кода Python?)
Если все ваши сообщения гарантированно представляют собой текст ненулевой длины, заканчивающийся новой строкой, то вы, вероятно, можете просто удалить последний \n, применить код регулярного выражения Python, а затем добавить этот \n обратно. Но это все равно будет быть хорошим тоном, чтобы рассмотреть все крайние случаи.
ДОБАВЛЕНИЕ:
Хотя заманчиво придумать регулярное выражение в Python, которое точно имитирует выражение в Perl, я бы не обязательно рекомендовал его, особенно если регулярное выражение Python настолько сложное, что трудно сказать, что оно делает в первый или второй раз. взглянуть мельком.
Регулярные выражения не всегда корректно обрабатывают алгоритмическую логику. Поэтому, если вы можете устранить сложность регулярных выражений, введя простую алгоритмическую логику, я бы рекомендовал сделать это вместо этого.
Другими словами, вы не получите никаких наград, используя слишком сложные регулярные выражения, так почему бы вместо этого не использовать простое регулярное выражение в сочетании с простой алгоритмической логикой, не связанной с регулярными выражениями? (Будущие сопровождающие вашего кода будут вам благодарны!)
Вот рекомендация:
import re
message = "message\n"
message = re.sub(r"(?m:^)", "[stamp]", message)
# Remove the final "[stamp]" if it appears alone at the end:
message = message.removesuffix("[stamp]")
Это просто и легко понять, и единственная часть, которая может сбить с толку, — это если вы никогда раньше не видели флаг m, используемый внутри регулярного выражения, такого как (?m:...).
Просто не забудьте проверить это на крайних случаях (например, на пустых сообщениях), прежде чем вы решите его использовать. Вы не хотите оставлять логику своей программы для поведения, о существовании которого вы не подозревали.
См. комментарий @Shadowranger к другому ответу.
@snoopyjc: я не думаю, что мой комментарий имеет отношение к этому ответу; удаление одной завершающей новой строки перед заменой и добавление ее обратно (только если для начала была завершающая новая строка) должно привести к согласию двух языков, это намного более многословно, чем небольшие изменения в регулярном выражении.
Я имел в виду ваш другой комментарий :-)
Механизмы регулярных выражений Perl и Python немного отличаются в определении «строки»; Perl не считает пустую строку, следующую за символом новой строки в конце входной строки, строкой, в отличие от Python.
Лучшее решение, которое я могу придумать, это изменить "^" на r"^(?=.|\n)" (обратите внимание на префикс r в строке, чтобы сделать ее необработанным литералом; все регулярные выражения должны использовать необработанные литералы). Вы также можете немного упростить, просто вызывая методы в скомпилированном регулярном выражении или вызывая re.sub с нескомпилированным шаблоном, а поскольку count=0 уже используется по умолчанию, вы можете его опустить. Таким образом, окончательный код будет либо:
re.compile(r"^(?=.|\n)", re.M).sub("[stamp]", "message\n")
Или:
re.sub(r"^(?=.|\n)", "[stamp]", "message\n", flags=re.M)
Еще лучше было бы:
start_of_line = re.compile(r"^(?=.|\n)", re.M) # Done once up front
start_of_line.sub("[stamp]", "message\n") # Done on demand
Избегать повторной компиляции/перепроверки кэша скомпилированных регулярных выражений каждый раз, создавая скомпилированное регулярное выражение только один раз и повторно используя его.
Альтернативные решения:
Разделите строки таким образом, чтобы они соответствовали определению строки в Perl, затем используйте не-re.MULTILINE версию регулярного выражения для каждой строки, а затем соедините их вместе, например:
start_of_line = re.compile(r"^") # Compile once up front without re.M
# Split lines, keeping ends, in a way that matches Perl's definition of a line
# then substitute on line-by-line basis
''.join([start_of_line.sub("[stamp]", line) for line in "message\n".splitlines(keepends=True)])
Удалите одну завершающую новую строку, если она существует, заранее, выполните замену регулярных выражений, добавьте новую строку (если применимо):
message = '...'
if message.endswith('\n'):
result = start_of_line.sub("[stamp]", message[:-1]) + '\n'
else:
result = start_of_line.sub("[stamp]", message)
Ни один из вариантов не является таким кратким/эффективным, как попытка настроить регулярное выражение, но если необходимо обрабатывать произвольное пользовательское регулярное выражение, всегда будет крайний случай, и предварительная обработка чего-то, что устраняет несовместимость Perl/Python, — это много. безопаснее.
Спасибо. Как вы думаете, это решение будет работать с любым регулярным выражением, предоставленным пользователем, с флагом M или просто '^'?
@snoopyjc: он справится с большинством случаев, когда начальный ^ в шаблоне заменяется на ^(?=.|\n). Но если пользователь использовал чередование, чтобы поставить ^ позже в шаблоне, например. r"abc|^", ой, это ^ действительно означало начало строки, и мы его пропустили. Regex сумасшедший.
@snoopyjc: я только что опубликовал альтернативу, в которой используется str.splitlines, чтобы Python фактически разделялся на отдельные строки, поэтому вы вообще не используете re.M, и проблема исчезнет. Очевидно, что это будет медленнее, но я думаю, что это безопасно для шаблонов регулярных выражений, ориентированных на строки. Однако при наличии регулярных выражений я не могу поклясться ни в чем, это немного безумно.
О, я просто собирался добавить (?=.|\n) к регулярному выражению пользователя всякий раз, когда он использовал флаг M. Например, если бы они сказали ^m, я бы поставил ^m(?=.|\n). Должен ли я вместо этого изменить это на ^(?=.|\n)m ?? Похоже, мне нужна куча тестовых случаев.
@snoopyjc: ^m(?=.|\n) определенно неверен для такого ввода, как "abc\nm", где ^m будет соответствовать и заменять этот окончательный m (как на Perl, так и на Python), но ^m(?=.|\n) не будет (на любом), потому что после символа нет никакого символа. m. ^(?=.|\n)m отлично сработает в этом случае, но вам придется написать анализатор шаблонов регулярных выражений, чтобы надежно определить правильное место для вставки (?=.|\n), если вы хотите обрабатывать полностью произвольное регулярное выражение, которое в противном случае было бы совместимо с синтаксисом регулярных выражений Perl и Python. . Пограничные случаи в конце концов убьют вас.
Понял - спасибо! Начну писать эти тесты и посмотрю, что работает.
^
сre.M
соответствует началу каждой строки, а\n
делает пример Python двумя строками, следовательно, двумя заменами.