2025, Oct 04 17:16

Как уменьшать каждую серию переводов строки ровно на один

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

Нормализация текста часто кажется простой, пока небольшое правило не меняет саму суть задачи. Распространённое требование — уменьшать любую последовательность символов перевода строки ровно на один, а не схлопывать её до одного. Этот тонкий принцип «x переводов строки превращаются в x−1» легко упустить, если автоматически применять стандартное “свести к одному”.

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

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

"Anna lives in Latin America.\n\nShe loves the vibes from the cities\n and the good weather.\n\n\nAnna is great"

Ожидаемый результат:

"Anna lives in Latin America.\nShe loves the vibes from the cities and the good weather.\n\nAnna is great"

Наивная попытка, которая меняет смысл

Легко захотеть упростить: свести несколько переводов строки к одному. Но это не равнозначно точному уменьшению каждой серии на один. Рассмотрим такой вариант:

import re

def shrink_lines(blob):
    blob = re.sub(r'\n{2,}', '\n', blob)
    return blob

sample = "Anna lives in Latin America.\n\nShe loves the vibes from the cities\n and the good weather.\n\n\nAnna is great"
result = shrink_lines(sample)

Он даёт:

"Anna lives in Latin America.\nShe loves the vibes from the cities\n and the good weather.\nAnna is great"

что не то, что нам нужно. Тройной перевод строки перед “Anna is great” схлопывается до одного, фактически убирая два перевода строки вместо одного.

Почему возникает несоответствие

Шаблон \n{2,} находит любую последовательность из двух и более переводов строки и заменяет её одним переводом. Чем длиннее серия, тем больше символов удаляется. Иначе говоря, он нормализует, но не уменьшает. Требование же — убирать ровно один перевод строки из каждой непрерывной серии, включая одиночные, чтобы любая серия становилась на один короче.

Решение: удалять только последний перевод строки в каждой серии

Точный способ вычесть по одному из каждой непрерывной последовательности — найти перевод строки, за которым не следует ещё один перевод строки, и удалить его. Для этого подходит просмотр вперёд (lookahead):

import re

def trim_one_break(segment):
    return re.sub(r'\n(?!\n)', '', segment)

text_in = "Anna lives in Latin America.\n\nShe loves the vibes from the cities\n and the good weather.\n\n\nAnna is great"
text_out = trim_one_break(text_in)

Так мы находим последний перевод строки в каждом непрерывном блоке и убираем его. Одиночные переводы строки сводятся к нулю, двойные становятся одиночными, тройные — двойными: именно поведение x → x−1.

Важная оговорка

Уменьшение на один работает только для непрерывных серий переводов строки. Если между ними есть пробелы, это уже не одна серия. Например, последовательность "This is line 1\n \nThis is line 2" после применения \n(?!\n) превращается в "This is line 1 This is line 2". Каждый перевод строки обрабатывается отдельно, потому что пробел нарушает смежность; в итоге удаляются оба и суммарное уменьшение равно двум.

Почему это важно

Текстовые конвейеры часто зависят от предсказуемой семантики пробелов. Правило вида «каждую серию переводов строки сделать на один короче» сохраняет относительную структуру абзацев, одновременно слегка уплотняя верстку по всему документу. Схлопывание “до одного” незаметно меняет эту структуру и может уничтожить значимые интервалы.

Итоги

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

Статья основана на вопросе на StackOverflow от Alexis и ответе Chris Maurer.