Разные результаты регулярных выражений в онлайн песочнице и в re.findall в PyCharm

Дана задача:

Текстовый файл состоит из символов X, Y и Z. Определите максимальную длину цепочки вида XYZXYZXYZ... (составленной из фрагментов XYZ, последний фрагмент может быть неполным).

При написании шаблона возникло несоответствие: шаблон "(XYZ)+(XY|X)" в песочницах https://regexr.com/ и https://pythex.org/ работает как и ожидалось подсвечивая строки максимальной длины, в то время как findall в PyCharm выводит строки минимальной длины, например:

[('XYZ', 'XY'), ('XYZ', 'XY'), ('XYZ', 'X')]

Почему такое несоответствие и если я не понимаю логику findall, как поменять её чтобы findall искал и добавлял в список из пересекающихся подстрок подстроки максимальной длины


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

Документация по re.findall, ваш случай выделен:

The result depends on the number of capturing groups in the pattern. If there are no groups, return a list of strings matching the whole pattern. If there is exactly one group, return a list of strings matching that group. If multiple groups are present, return a list of tuples of strings matching the groups. Non-capturing groups do not affect the form of the result.

Перевод:

Результат зависит от количества захватывающих групп в образце. Если групп нет, возвращается список полных совпадений с образцом. Есть группа ровно одна, возвращается список совпадений с этой группой. Если групп несколько, возвращается список кортежей с совпадениями для всех групп. Не захватывающие группы не влияют на результат.

В вашем случае в образце есть две группы и вы получаете результаты парами. Первая группа повторяется метасимволом +, она сопоставляется с несколькими фрагментами строки. В результат попадает первое совпадение. То есть в ответ попадёт пара ('XYZ', 'XY'):

#              это первый элемент пары из ответа
#                            |
#                            |         это второй элемент
#                            |             |
#                            v             v
#                           ---            --
re.findall('(XYZ)+(XY|X)', 'XYZXYZXYZXYZXYZXY'))

Совпадение было полным, но в ответ помещены лишь его части, первая и последняя. Замените группы на не захватывающие, результатом станет полная строка, а не её фрагменты:

@>>> re.findall('(?:XYZ)+(?:XY|X)', 'XYZXYZXYZXYZXYZXY')
['XYZXYZXYZXYZXYZXY']

P.S. В комментариях уже написали что ваше выражение теряет последний символ Z в совпадении. Поправить можно добавив знак вопроса в конце:
'(?:XYZ)+(?:XY|X)?'.

P.P.S. Кроме того вы не захватываете совсем короткие строки X и XY. В тестах эта ошибка не будет видна. Но лучше не требовать обязательного вхождения первой группы:
'(?:XYZ)*(?:XY|X)?'.

P.P.P.S. Причина недоразумения – сложная логика re.findall. Посмотрите re.finditer. Он делает ту же работу, расходует меньше памяти и вы получаете целиком совпадение, а не его кусочки. С ним '(XYZ)*(XY|X)?' и '(?:XYZ)*(?:XY|X)?' будут работать одинаково.

→ Ссылка