Перенос строки: LF, CRLF и CR и чем они отличаются

Перенос строки: LF, CRLF и CR и чем они отличаются Полезное

Перенос строки — это управляющий символ или пара символов, которые отмечают конец одной строки текста и начало следующей. На практике встречаются три разных представления — LF, CRLF и CR, и они не взаимозаменяемы на уровне байтов.

Ниже — чем они отличаются, почему путаница между ними ломает git-диффы и текстовые файлы, и как перенос строки ведет себя в языках программирования.

Три формата переноса строки

Исторически оба символа пришли из телетайпов: CR (carriage return, возврат каретки) возвращал печатающую головку к началу строки, а LF (line feed, подача строки) сдвигал бумагу на строку вниз без возврата к началу.

Это упрощенная историческая модель — в компьютерах эти операции давно не связаны с физическим движением, но названия и коды символов остались от нее.

Формат Символы Код Где принят
LF \n 0x0A Unix, Linux, macOS после перехода на Mac OS X (2001)
CRLF \r\n 0x0D 0x0A Windows; текстовый режим ряда сетевых протоколов
CR \r 0x0D Классическая Mac OS, до перехода на Mac OS X

CR как отдельный формат переноса строки сегодня практически не встречается — это факт про историю платформы, а не про то, что символ \r вышел из употребления вообще: он используется, например, для перерисовки строки на месте (прогресс-бар в консоли).

Почему различие важно на практике

Git сравнивает файлы побайтово. Если один разработчик сохранил файл с LF, а другой — с CRLF, git увидит «измененными» все строки файла, хотя текст не менялся по смыслу. Диффы становятся нечитаемыми, а слияние веток — конфликтным на пустом месте.

Решение — нормализация: настройка core.autocrlf на стороне git и файл .gitattributes с правилом * text=auto, который приводит перенос строки в репозитории к единому формату (обычно LF) независимо от ОС разработчика.

Похожая проблема встречается при открытии Windows-файла в редакторе, который ждет только LF: символ \r остается «повисшим» в конце каждой строки и виден как посторонний знак вместо перевода строки.

В сетевых текстовых протоколах (HTTP, SMTP) спецификации исторически требуют CRLF как разделитель строк заголовков. Многие серверы и клиенты дополнительно принимают одиночный LF ради совместимости, но в собственном коде правильнее явно формировать именно CRLF, не полагаясь на снисходительность конкретной реализации.

Перенос строки в языках программирования

Здесь важно различать два уровня: что хранится внутри строки в памяти программы и что записывается на диск или уходит в сеть. Их легко перепутать, потому что для программиста оба уровня выглядят как «перенос строки».

В Python внутри программы перенос строки всегда хранится как один символ \n — это гарантия языка, не зависящая от ОС. При чтении файлов в текстовом режиме питон делает трансляцию: любые из \r\n, \r, \n превращаются в один \n (режим называется universal newlines).

При записи \n заменяется на os.linesep — значение, зависящее от платформы, на которой запущен интерпретатор.

В Java аналогичную роль платформенной переменной играет System.lineSeparator() — если в коде явно написать "\n", трансляции не будет и в файл попадет ровно LF на любой ОС.

В JavaScript автоматической трансляции нет вообще: "\n" в строке остается одним байтом 0x0A независимо от системы, а нужный формат при записи файла (например, через Node.js fs) программист выбирает сам.

Как проверить, что реально в файле

Полагаться на предположение «наверное, LF» опасно — платформозависимое поведение нужно смотреть, а не угадывать. Ниже — воспроизводимый способ через repr() и os.linesep, а не через фиксированный hex-дамп, который отличался бы на Windows и Unix.

import os

# внутри python перенос строки - всегда один символ \n,
# это гарантия языка, а не платформы
text = "line1\nline2"
print(repr(text))
# 'line1\nline2'

# os.linesep - то, что ОС использует "снаружи", для текстовых файлов;
# значение платформозависимое, поэтому смотрим через repr(), а не пишем руками
print(repr(os.linesep))
# Linux/macOS: '\n'
# Windows:     '\r\n'

# запись в текстовом режиме без newline='' транслирует \n в os.linesep
with open("demo.txt", "w") as f:
    f.write("line1\nline2\n")

with open("demo.txt", "rb") as f:
    print(repr(f.read()))
# Linux/macOS: b'line1\nline2\n'
# Windows:     b'line1\r\nline2\r\n'

# newline='' отключает трансляцию - на диске окажется ровно то, что написано
with open("demo_lf.txt", "w", newline="") as f:
    f.write("line1\nline2\n")

with open("demo_lf.txt", "rb") as f:
    print(repr(f.read()))
# на любой ОС: b'line1\nline2\n'

Типичная ошибка — резать текст жестко по "\r\n", предполагая один формат:

lines = data.split("\r\n")

На LF-файле результат — список из одного элемента: искомой подпоследовательности \r\n в байтах нет, деления не происходит вообще. Исправление — использовать splitlines(), который одинаково распознает LF, CRLF и CR:

lines = data.decode().splitlines()

Выводы

  • LF, CRLF и CR — три разных байтовых представления переноса строки, не взаимозаменяемых напрямую.
  • Unix и современный macOS используют LF (\n), Windows — CRLF (\r\n); CR отдельно как формат переноса строки остался в истории классической Mac OS.
  • Смешение форматов в одном git-репозитории делает диффы нечитаемыми — решается нормализацией через .gitattributes и core.autocrlf.
  • В Python внутри программы перенос строки — всегда \n; конкретные байты на диске зависят от режима открытия файла и os.linesep.
  • Реальный байтовый состав файла нужно проверять через repr()/бинарное чтение, а не предполагать «по умолчанию».

Где применяется / связь с практикой

Корректная работа с текстовыми файлами на разных платформах — базовый навык при обработке логов, парсинге CSV, настройке git и написании кода, который потом запускают и на Linux-сервере, и на Windows-машине коллеги.

Освойте тему на практике

Разбираться в этом стоит не по отдельным статьям, а системно — с этого и других базовых механизмов Python начинается курс Python для начинающих в Otus.

Освойте тему на практике

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

FAQ

Почему при открытии Windows-файла в Unix-редакторе видны лишние символы ^M?
Это символ CR (\r), оставшийся в конце каждой строки CRLF-файла. Редактор, который ожидает только LF, показывает \r как отдельный видимый знак вместо перевода строки.

Можно ли встретить LF и CRLF одновременно в одном файле?
Да, это называется mixed line endings — обычно результат правки файла в разных ОС или редакторах без нормализации. Git и большинство линтеров помечают такие файлы предупреждением; правильнее привести весь файл к одному формату.

Как перенос строки представлен внутри JSON?
Внутри строкового значения JSON перенос кодируется экранированной последовательностью \n (или \r\n, если нужны оба байта). Сам JSON-документ как файл на диске может использовать любой формат переноса строк между токенами — на разбор значений парсером это не влияет.

OTUS Журнал