2025, Dec 26 00:02

Надёжное извлечение пар имя — значение через 3+ точки (Python)

Показываем, как с помощью регулярного выражения в Python надёжно извлекать пары «имя — значение» через 3+ точки, игнорируя мусор в конце строки. Шаблон и код.

Разбирать полуструктурированный текст интересно до тех пор, пока разделители не начинают вести себя непредсказуемо. Типичный сценарий: имена и значения идут парами, между ними — переменное число точек; в одной строке может встречаться несколько пар, а в конце нередко пристаёт случайный мусор. Цель — стабильно извлекать чистые пары (имя, значение), не подгоняя решение под единственный формат строки.

Постановка задачи

Рассмотрим ввод, где после каждого имени следует 3 и более точки, а затем числовое значение. В строках может быть несколько пар, какие-то строки вовсе не содержат полезных данных, а любой нежелательный текст появляется только после последнего значения в строке.

raw_blob = 'apples, red .... 0.15 apples, green ... 0.99\nbananas (bunch).......... 0.111\nfruit salad, small........1.35 [unwanted stuff #1.11 here]\nunwanted line here\nfruit salad, large .... 1.77 strawberry ........ 0.66 unwanted 00-11info here'

Делить по переводам строки и по последовательностям из 3+ точек кажется заманчиво, но такая стратегия разрывает связь между именем и его значением.

import re
raw_blob = 'apples, red .... 0.15 apples, green ... 0.99\nbananas (bunch).......... 0.111\nfruit salad, small........1.35 [unwanted stuff #1.11 here]\nunwanted line here\nfruit salad, large .... 1.77 strawberry ........ 0.66 unwanted 00-11info here'
pieces = re.split(r"\.{3,}|\n", raw_blob)
print(pieces)
['apples, red ', ' 0.15 apples, green ', ' 0.99', 'bananas (bunch)', ' 0.111', 'fruit salad, small', '1.35 [unwanted stuff #1.11 here]', 'unwanted line here', 'fruit salad, large ', ' 1.77 strawberry ', ' 0.66 unwanted 00-11info here']

Результат близок, но пользоваться им нельзя: разделение произошло между именем и значением, а после некоторых значений остался мусор.

Что на самом деле задаёт структуру

Полезный шаблон остаётся стабильным, даже если окружение «шумит». Каждая пара — это имя, в котором нет цифр, точек и переводов строки, затем необязательные пробелы, далее 3 и более точек, снова необязательные пробелы и число. Всё, что идёт за этим числом, нас не интересует и должно игнорироваться.

Регулярное выражение, которое захватывает только нужное

Вместо разбиения сразу сопоставляйте пары и захватывайте обе части:

([^\d.\n]+)[^\S\n]*\.{3,}[^\S\n]*(\d+.\d+)

Первая группа находит имя, исключая цифры, точки и переводы строк, и «съедает» всё остальное до зоны разделителей. Средняя часть допускает любое количество горизонтальных пробелов по обе стороны от цепочки точек и требует как минимум три точки. Вторая группа захватывает число, идущее после точек. Поскольку нежелательный текст всегда следует за значением и расположен в конце строки, он не попадает в захват.

Решение в коде

import re
raw_blob = 'apples, red .... 0.15 apples, green ... 0.99\nbananas (bunch).......... 0.111\nfruit salad, small........1.35 [unwanted stuff #1.11 here]\nunwanted line here\nfruit salad, large .... 1.77 strawberry ........ 0.66 unwanted 00-11info here'
rx = r"([^\d.\n]+)[^\S\n]*\.{3,}[^\S\n]*(\d+.\d+)"
pairs = re.findall(rx, raw_blob)
"\n".join(" | ".join(z.strip() for z in grp) for grp in pairs)

Ожидаемое форматирование вывода для дальнейшей обработки в R или Excel может выглядеть так:

apples, red | 0.15
apples, green | 0.99
bananas (bunch) | 0.111
fruit salad, small | 1.35
fruit salad, large | 1.77
strawberry | 0.66

Почему это работает

Этот подход избегает главной ловушки — разбиения по разделителям, которые находятся между двумя половинами пары. Сопоставляя полные пары и захватывая обе части, вы сохраняете связь между именем и значением, допускаете несколько пар в одной строке и игнорируете всё, что идёт после последнего значения. Использование findall возвращает ровно те пары, которые нужны, так что последующая обработка минимальна и предсказуема.

Практические заметки

Если вы рассматриваете альтернативные техники, например lookahead-утверждение, смело экспериментируйте. Представленный шаблон уже опирается на понятную структуру данных и показывает, как findall может сразу выдать чистый набор. Удобно сначала проверить подход на небольшом срезе текста, а затем запускать его на всём корпусе.

Итоги

Когда разделители «плавают», а шум неизбежен, лучше сопоставлять нужную структуру, чем резать строки и потом сшивать куски. Здесь компактное регулярное выражение, ориентированное на «имя — точки — число», надёжно извлекает пары даже при множественных записях в строке и хвостовом мусоре. Явно задавайте зону разделителя, захватывайте только необходимое и завершайте лёгким объединением — так вы получите аккуратный формат, готовый к экспорту.