Несоответствие между регулярными выражениями sed и python
Прошу прощения, если это где-то опубликовано, но мой беглый поиск ничего не нашел.
При выполнении некоторого программирования на Python я заметил, что следующая команда:
re.sub("a*((ab)*)b", r"\1", "aabb")
возвращает пустую строку. Но эквивалентная команда в sed:
echo "aabb" | sed "s/a*\(\(ab\)*\)b/\1/"
возвращает ab
.
Мне кажется, что директива "a *" в начале регулярного выражения python будет соответствовать как a
, так и "(ab) *", чтобы соответствовать нулю, но я понятия не имею, как sed появляется ab
. Кто-нибудь знает, какая разница между двумя двигателями регулярных выражений, которые это порождают? Я считаю, что они по-умолчанию соответствуют звездам по умолчанию, но мне пришло в голову, что sed может совпадать с правильным, а не с левым. Любое понимание было бы оценено.
Ответы
Ответ 1
Интересная головоломка, которую вы создали. Из того, что я читал, regexp-двигатели как python, так и sed основаны на библиотеке регулярных выражений Генри Спенсера (как и perl's), которая полагается на обратное отслеживание. (К сожалению, я не могу найти статью, на которой я основываюсь).
Во всяком случае, это не то, что должно быть детализация реализации: поведение Python идет против стандарта POSIX, что требует, чтобы REs соответствовали (a) как можно скорее, и (b) сопоставить самую длинную возможную строку, которая начинается с этой точки. (См. man 7 regex
(для Linux) для этого и многое другое.)
Чтобы найти самое длинное совпадение, механизм регулярного выражения backtracking ( "NFA-type" ) должен продолжить изучение альтернатив после того, как он найдет одно совпадение. Поэтому неудивительно, что исполнители срезают углы. Очевидно, что поведение python является несоответствующим, поскольку не удается найти самое длинное совпадение. Согласно странице руководства sed, sed также не всегда соответствует "по соображениям производительности". Но, очевидно, это правильно.
Кстати, ваши команды не полностью эквивалентны: re.sub
будет выполнять подстановку столько раз, сколько возможно, тогда как sed s/a/b/
выполнит ее только один раз. Версия sed должна была быть:
echo "aabb" | sed "s/a*\(\(ab\)*\)b/\1/g"
Это объясняет, почему мы получаем пустую строку в python: RE соответствует aab
в первый раз, а оставшийся b
- второй раз, удаляя каждую часть (поскольку все они соответствуют a*
и окончательному b
регулярного выражения). Вы можете увидеть это по следующему варианту:
>>> re.sub("a*((ab)*)b", r"X\1Y", "aabb")
'XYXY'
Ответ 2
Оба Python и sed по умолчанию жадные, но...
Регулярное выражение Python пытается оценить слева направо при любых обстоятельствах, несмотря на то, что в конечном итоге оно должно быть обратным для предыдущего состояния, если ветвь, которую судили, не может продолжить путем сопоставления.
Опция Sed regex наоборот оптимизирована перед оценкой, чтобы предотвратить ненужную обратную трассировку, переписывая регулярное выражение в более детерминированную форму. Поэтому объединенный необязательный шаблон "aab" , вероятно, проверяется перед простым "a", потому что сначала запрашивается самая конкретная возможная строка.
Шаблон Python совпадает с строкой "aabb" дважды "aab" + "b" (помечен между " > " )
>>> re.sub("a*((ab)*)b", r"<\1>", "aabb")
'<><>'
в то время как sed соответствует целому "aabb" одной заменой:
$ echo "aabb" | sed "s/a*\(\(ab\)*\)b/<\1>/"
<ab>
Алгоритм backtrace python regex хорошо объясняется в regex howto - Повторяющиеся вещи в двух абзацах, введенных словами "Пошаговый пример...". ИОО именно то, что описано regex docs:" Когда проверяется целевая строка, RE разделяются символом '|' проверяются слева направо.
Демонстрация
Порядок "(| a | aa)" btw. "(aa | a |)" соблюдается Python
>>> re.sub("(?:|a|aa)((ab)*)b", r"<\1>", "aabb")
'<ab>'
>>> re.sub("(?:aa|a|)((ab)*)b", r"<\1>", "aabb")
'<><>'
но этот порядок игнорируется sed, потому что sed оптимизирует регулярные выражения. Согласование "aab" + "b" можно воспроизвести, удалив опцию "a" из шаблона.
$ echo "aabb" | sed "s/\(\|a\|aa\)\(\(ab\)*\)b/<\2>/g"
<ab>
$ echo "aabb" | sed "s/\(aa\|a\|\)\(\(ab\)*\)b/<\2>/g"
<ab>
$ echo "aabb" | sed "s/\(aa\|\)\(\(ab\)*\)b/<\2>/g"
<><>
Изменить. Я удалил все о DFA/NFA, потому что не могу доказать это из текущих текстов.