Как я узнал, что банки до сих пор кодят как в 1975-м
Путешествие в ад фиксированных ASCII-файлов, COBOL и того, почему вашему современному веб-приложению нужно говорить на языке мейнфреймов

Получил сегодня странное письмо. У клиента крутится какая-то “local soft”, которая должна забирать последние финансовые данные с нашего сервера. В письме сказано, что интеграция с их накладными сломана. Система не может прочитать наши данные. “Файл должен быть в ASCII с фиксированными отступами”.
ASCII. Фиксированные отступы. Смотрю в экран. На дворе 2026 год. У нас есть ИИ, который пишет стихи и водит машины. А где-то там банковская система требует, чтобы я форматировал данные так, будто их набивают на перфокартах и скармливают компьютеру размером с комнату.
Что вообще такое файл с фиксированной шириной?
Знаете, как обычные люди используют CSV? Или JSON? Форматы, где поля разделены запятыми или данные структурированы скобками? Где “John Doe” может занимать 9 символов в одной строке, а “Ковалев Александр Сергеевич” — 28 в следующей?
Файлы с фиксированной шириной так не работают. Каждое поле начинается в строго определённой позиции символа и имеет строго определённую длину. Всегда.
[127 пробелов] 12345000000000000002000000000000050Это не ошибка. Код товара живёт в символах 128-147, ширина 20 символов, ни больше ни меньше. Если код товара всего из 5 цифр — дополняете его 15 пробелами. Если он из 21 цифры? Не повезло, обрезаете.
Спецификация приходит в виде двух PDF на иврите. Один описывает структуру заголовка:
- Позиция 1: тип файла (1 символ)
- Позиция 19-43: имя клиента (25 символов)
- Позиция 45-69: адрес (25 символов)
- Позиция 112-126: итоговая сумма (15 символов)
Другой описывает строки товаров:
- Позиция 128-147: код товара (20 символов)
- Позиция 149-163: количество (15 символов)
- Позиция 165-179: цена (15 символов)
Заметили что-то? Поля заголовка начинаются с позиции 19. Поля строк товаров начинаются с позиции 128. Они и не должны делить одну строку, просто так устроен формат. А значит, 127 символов ничего идут до (или после) ваших настоящих данных.
10 20 30 40 50 60 70 80 ----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8 0·················Kovalev Alexander Sergeev·רחוב הרצל 5···············תל אביב···
25/25 колонок · отрезано:ich
С чего бы кому-то так делать?
Мне пришлось погуглить, потому что я искренне не мог в это поверить. Этот формат родом из мейнфреймов IBM 1960-х–1980-х. Банки, страховые компании, госсистемы — они строили всё на этих машинах. Системы, работающие на COBOL, обрабатывающие данные в форматах фиксированной ширины, потому что:
- Скорость — компьютер знает, что символ 128 — это всегда код товара. Парсинг не нужен. Прыгай сразу на позицию.
- Бинарная безопасность — нельзя использовать разделители, когда ваши данные сами могут содержать эти разделители. Фиксированные позиции устраняют проблему.
- COBOL этого ожидает — миллионы строк бизнес-логики написаны на COBOL, переписать которые стоило бы миллиарды.
Когда данных много (гига/терабайты), файлы с фиксированной шириной могут быть намного эффективнее.
Поскольку у каждой записи и поля фиксированный размер, можно просто перейти (seek) сразу к, скажем, n-миллионной строке и прочитать оттуда пару записей. Можно также замапить весь файл в память и получить довольно эффективный и лёгкий произвольный доступ ко всему.
Они никогда не обновятся. А зачем? Система работает. Она обработала триллионы долларов транзакций. Она стабильна. Она протестирована. Заменить её — это многомиллиардный проект с огромным риском.
Так что вместо этого каждому современному веб-разработчику, которому нужно интегрироваться с такими системами, приходится телепортироваться назад в 1975 и учиться говорить на языке мейнфреймов.
Код, который я унаследовал
$data .= $year . str_pad($order_id, 6, "0", STR_PAD_LEFT) . '|' . $order->get_date_created()->format('d/m/y') . '|' . mb_str_pad($order->get_formatted_billing_full_name(), 25, " ", STR_PAD_LEFT) . '|' . mb_str_pad($order->get_billing_address_1(), 25, " ", STR_PAD_LEFT) // ... продолжается ещё для 12 полейВидите эти символы вертикальной черты? Вот в чём проблема. Это создаёт файл с разделителями:
26240001|19/01/26|יוסי כהן|רחוב הרצל 5|...А мейнфрейму нужно вот так:
0יוסי כהן רחוב הרצל 5 תל אביב 1234567...Никаких вертикальных черт. Никаких разделителей. Только точные позиции. Всегда.
Сообщение об ошибке
Система прислала обратно файл ошибок. На иврите. В кодировке Windows-1255 (ещё одна реликвия прошлого). После декодирования:
תקלה ASCII במספר שורה בקריאת: 1שדה מספר 64 אינו תקין: 26 |00000000039Перевод: “Поле 64 повреждено. Вы отправили: 26 |00000000039”
Поле 64 должно быть 15-значным числом: 000000000000026
А мы отправили: 26 |00000000039 (неверная длина, есть символ вертикальной черты, не дополнено нулями)
Решение
Убрать все разделители. Дополнить всё до точных длин. Расставить поля по точным позициям символов.
function formatHeaderRecord($order) { return str_pad('0', 1) // поз 1: тип файла . str_repeat(' ', 18) // поз 2-18: пусто . mb_str_pad($order->billing_name, 25, ' ') // поз 19-43: имя . mb_str_pad($order->billing_address, 25, ' ') // поз 45-69: адрес . mb_str_pad($order->billing_city, 15, ' ') // поз 71-85: город . str_repeat(' ', 26) // поз 86-111: пусто . str_pad(formatNumber($order->total), 15, '0', STR_PAD_LEFT); // поз 112-126: сумма}
function formatLineRecord($item) { return str_repeat(' ', 127) // поз 1-127: пусто . str_pad($item->sku, 20, ' ') // поз 128-147: код товара . str_pad(formatNumber($item->quantity), 15, '0', STR_PAD_LEFT) // поз 149-163: кол-во . str_pad(formatNumber($item->price), 15, '0', STR_PAD_LEFT); // поз 165-179: цена}127 пробелов. Потом ваши данные. Потому что какой-то программист в 1975-м решил, что именно там должен быть код товара, и с тех пор никому не разрешили это поменять.
Что я понял
Ваш код живёт долго. Та хреновая функция, которую я пишу сегодня? Завтра я о ней забуду. Через год её, может, перепишут на rust. Но если я напишу её достаточно хорошо, она вполне может пережить меня. Какой-нибудь разработчик в 2077-м наткнётся на мою интеграцию с WooCommerce, будет проклинать моё имя, гадая, почему поле 64 начинается с позиции 112.
Программист COBOL, который написал эту банковскую систему в 1975-м, наверное, думал, что просто решает сегодняшнюю задачу. Написать спецификацию, обработать пару накладных, пойти домой. Пятьдесят лет спустя его код обрабатывает триллионы долларов. Банки сливались, поглощались, банкротились — код продолжал работать. Программисты уходили на пенсию, умирали — код продолжал работать.
Вспомните об этом, когда в следующий раз кто-то предложит “переписать легаси”. Особенно сейчас, когда каждая новая GPT-обёртка делает код дешевле, чем когда-либо, и переписывание выглядит как работа на выходные. Это соблазнительно. Просто помните:
большая часть кода живёт долго не просто так.
И раз уж я залез на трибуну, единственное, что стоит вынести из всего этого бардака: вот что такое хороший код. Не умный, не гибкий, не современный. Просто ясный, задокументированный и достаточно конкретный, чтобы кто-то в 2075-м всё ещё мог понять, какого чёрта должно содержать поле 64.
Так что да, я напишу свой форматтер с фиксированной шириной. Дополню свои строки. Буду считать позиции символов, как монах, переписывающий манускрипты. И оставлю короткий readme (да, Claude, короткий), потому что, может быть, если я напишу это достаточно хорошо, какой-нибудь бедолага-разработчик в 2077-м скажет мне спасибо.
А теперь простите, мне нужно дополнить 127 пробелов.
Полезные ссылки
Расхождение дат в Excel и ошибки на единицу, ещё один устаревший формат файлов, который меня укусил
https://www.easydatatransform.com/help/latest/windows/html/index.html?fixed_width_format.html https://stackoverflow.com/questions/7666780/why-are-fixed-width-file-formats-still-in-use
Комментарии
Комментарии живут в GitHub Discussions (через giscus), так что для участия нужен аккаунт GitHub.