Невозможный lookbehind с обратной рецензией

С моей точки зрения,

(.)(?<!\1)

никогда не должно совпадать. На самом деле, php preg_replace даже отказывается компилировать это, а также ruby ​​gsub. У модуля python re, похоже, есть другое мнение:

import re
test = 'xAAAAAyBBBBz'
print (re.sub(r'(.)(?<!\1)', r'(\g<0>)', test))

Результат:

(x)AAAA(A)(y)BBB(B)(z)

Может ли кто-нибудь дать разумное объяснение этому поведению?

Update

Это поведение выглядит как ограничение в модуле re. Альтернативный regex, похоже, правильно обрабатывает группы в утверждениях:

import regex

test = 'xAAAAAyBBBBz'

print (regex.sub(r'(.)(?<!\1)', r'(\g<0>)', test))
## xAAAAAyBBBBz

print (regex.sub(r'(.)(.)(?<!\1)', r'(\g<0>)', test))
## (xA)AAA(Ay)BBB(Bz)

Обратите внимание, что в отличие от pcre, regex также позволяет искать переменные ширины:

print (regex.sub(r'(.)(?<![A-Z]+)', r'(\g<0>)', test))
## (x)AAAAA(y)BBBB(z)

В конце концов, regex будет включен в стандартную библиотеку, как указано в PEP 411.

Ответ 1

Это выглядит как ограничение (хороший способ сказать "ошибка", как я узнал из запроса поддержки с Microsoft) в модуле Python re.

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

Как ни странно:

>>> print (re.sub(r'.(?<!\0)', r'(\g<0>)', test))
(x)(A)(A)(A)(A)(A)(y)(B)(B)(B)(B)(z)
>>>
>>> re.compile(r'(.*)(?<!\1)') # This should trigger an error but doesn't!
<_sre.SRE_Pattern object at 0x00000000026A89C0>

Поэтому лучше не использовать обратные ссылки в утверждениях lookbehind в Python. Положительный lookbehind не намного лучше (он также соответствует здесь, как если бы он был положительным взглядом):

>>> print (re.sub(r'(.)(?<=\1)', r'(\g<0>)', test))
x(A)(A)(A)(A)Ay(B)(B)(B)Bz

И я даже не могу догадаться, что происходит здесь:

>>> print (re.sub(r'(.+)(?<=\1)', r'(\g<0>)', test))
x(AA)(A)(A)Ay(BB)(B)Bz