Перейти к содержимому

Перевод проекта на UTF-8 - файлы, база, настройки, обмен

Разбираем перевод старого проекта с однобайтовой кодировки на UTF-8 целиком. Смотрим, что меняется в файлах, базе, настройках платформы и обмене с учётной системой, и где лежит единственная точка возврата.

Механика

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

Порядок пролога объясняет, почему одной сменой настройки языка обойтись не выйдет. Ядро читает dbconn.php, соединяется с базой, выполняет after_connect.php и только после этого определяет сайт. Константы SITE_CHARSET и LANG_CHARSET появляются на шаге определения сайта, когда строки из базы уже прочитаны соединением.

Кодировка сайта живёт в региональных настройках, а те хранятся в таблице b_culture. Потерянная на середине перевода региональная настройка гасит публичную часть целиком и без единой строки в журнале ошибок.

Настройка mbstring.func_overload - это и есть перекодирование строковых функций на лету. Со значением 2 расширение подменяет strlen, substr и соседей многобайтовыми версиями, и старые установки в UTF-8 требовали именно такого значения. Параметр устарел с PHP 7.2, платформа не поддерживает его с главного модуля 20.100.0, а ненулевое значение блокирует загрузку обновлений.

База переводится не запросом, а дампом, и это расходится с ожиданиями. Запрос ALTER DATABASE меняет кодировку по умолчанию для будущих таблиц, но не трогает байты уже записанных строк. Настоящий перевод - это выгрузка дампа, перекодировка его текста и заливка в базу с новой кодировкой.

Несовпадение кодировок при заливке дампа даёт не кракозябры, а ошибку ERROR 1062 Duplicate entry. Строки, различавшиеся регистром или одной буквой, в новой сортировке становятся одинаковыми и упираются в уникальный ключ. Лечится это строкой SET NAMES в самом начале SQL-архива перед импортом.

Файлы проекта переводят пакетно, и на этом шаге всплывают остатки. Файлы в ANSI скрипты пакетной перекодировки регулярно пропускают, потому что определение кодировки по содержимому на них не срабатывает. Каталог ядра /bitrix/ не трогают вовсе: его меняют переустановкой дистрибутива нужной кодировки.

Русский текст, зашитый прямо в PHP-код, автоматика платформы не переводит. Конвертация кодировки при установке модуля или тиражного решения применяется только к файлам в папках /<код языка>/. Всё остальное - это ручной обход по шаблонам, компонентам и обработчикам событий.

Обмен и выгрузки живут по своим правилам и переводятся отдельным шагом. У модуля обмена на стороне 1С есть собственный реквизит кодировки, в которой она пишет файлы CommerceML. Выгрузка YML в /bitrix/catalog_export/ тоже допускает и UTF-8, и windows-1251.

Шаги

  1. Снять полную резервную копию файлов и базы и убедиться, что она разворачивается на запасном стенде.
  2. Выгрузить дамп базы, перекодировать его текст и залить результат в чистую базу с новой кодировкой.
  3. Перевести файлы проекта пакетно, а затем найти остатки, которые пакетная перекодировка пропустила.
  4. Сменить кодировку в региональных настройках сайта и привести настройки PHP к требованиям платформы.
  5. Прогнать обмен с учётной системой и все выгрузки, сверяя русские названия в полученных результатах.

Код

Снимаем точку возврата до первой правки:

Окно терминала
# от пользователя веб-сервера, архив ляжет в /bitrix/backup/
sudo -u bitrix php -f /home/bitrix/www/bitrix/modules/main/tools/backup.php
mysqldump --opt --default-character-set=cp1251 -u root sitedb > /tmp/dump_1251.sql
# ключ кодировки обязателен: без него дамп уедет в кодировке клиента

Копия платформы разворачивается штатным restore.php на запасном стенде, а дамп уходит дальше в перекодировку и назад уже не вернётся.

Смотрим, что имеем на входе:

SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = DATABASE(); -- кодировка базы по умолчанию
SHOW VARIABLES LIKE 'character_set_%'; -- кодировка клиента и соединения
SELECT TABLE_COLLATION, COUNT(*) FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() GROUP BY TABLE_COLLATION;
SELECT ID, CHARSET FROM b_culture; -- кодировка региональной настройки

Четыре величины из этого набора обязаны совпасть после перевода, а несколько разных сравнений в списке таблиц означают частично переведённую базу.

Перекодируем дамп и заливаем его:

Окно терминала
iconv -f CP1251 -t UTF-8 /tmp/dump_1251.sql > /tmp/dump_utf8.sql
sed -i 's/CHARSET=cp1251/CHARSET=utf8mb4/g' /tmp/dump_utf8.sql # объявления таблиц
sed -i "1i SET NAMES 'utf8mb4';" /tmp/dump_utf8.sql # иначе ERROR 1062 на импорте
# заливаем в свежую базу: старая остаётся нетронутой точкой возврата
mysql --default-character-set=utf8mb4 -u root sitedb_new < /tmp/dump_utf8.sql

Строка SET NAMES в начале архива снимает ошибку Duplicate entry, которая на импорте выглядит как порча самих данных, а не как расхождение кодировок.

Переводим файлы проекта пакетно:

Окно терминала
# каталог ядра исключён: его меняют переустановкой дистрибутива
find /home/bitrix/www -path '*/bitrix/*' -prune -o -name '*.php' -print0 \
| xargs -0 -I{} sh -c 'iconv -f CP1251 -t UTF-8 "{}" -o "{}.new" && mv "{}.new" "{}"'
# остатки, до которых пакетный проход не добрался
file -I $(find /home/bitrix/www/local -name '*.php') | grep -v 'charset=utf-8'

Последняя команда показывает файлы, которые перекодировка пропустила; файлы без русских букв попадут в тот же список как ascii, и это не ошибка перевода.

Приводим настройки PHP к требованиям платформы:

; /etc/php.d/z_bx_custom.ini - этот файл переживает обновление окружения
default_charset = UTF-8
mbstring.func_overload = 0 ; параметр должен быть обнулён или удалён совсем
; проверка: Настройки > Инструменты > Диагностика > Настройки PHP

Значение 2 требовалось старым установкам в UTF-8 и работало как перекодирование строковых функций на лету, а сейчас платформа не даст с ним поставить обновление.

Читаем кодировку из кода правильно:

use Bitrix\Main\Context;
$charset = Context::getCurrent()->getCulture()->getCharset(); // кодировка текущего сайта
// в шаблоне: <meta http-equiv="Content-Type" content="text/html; charset=<?=LANG_CHARSET?>">
// константы BX_UTF и SITE_CHARSET оставлены только для совместимости со старым кодом

Подстановка константы вместо строки с именем кодировки доводит смену региональной настройки до заголовка страницы без правки шаблона.

Выносим зашитые строки в языковые файлы:

// было: echo 'Товар добавлен в корзину';
use Bitrix\Main\Localization\Loc;
echo Loc::getMessage('SHOP_BASKET_ADDED'); // строка лежит в /lang/ru/component.php
// автоконвертация при установке трогает только файлы внутри папок /<код языка>/

Пакетная перекодировка чинит зашитые строки один раз, а следующая установка решения в другой кодировке ломает их снова: языковой файл переживает и это.

Приводим кодировку входящего файла обмена:

use Bitrix\Main\Text\Encoding;
$data = Encoding::convertEncoding($raw, 'windows-1251', 'UTF-8'); // строка или массив
// при совпадении кодировок метод возвращает исходные данные без изменений
// кодировку выгрузки CommerceML задают на стороне 1С в реквизите обмена

Учётная система продолжит слать файлы в своей кодировке и после перевода сайта, поэтому входящий файл приводят к кодировке проекта прямо на чтении.

Проверяем результат после переключения:

Окно терминала
curl -sI https://example.com/ | grep -i 'content-type' # кодировка в заголовке
mysql -u root sitedb_new -e 'SELECT NAME FROM b_iblock_element LIMIT 3;'
php -r 'var_dump(ini_get("mbstring.func_overload"));' # ожидаем ноль или пусто
file -I /home/bitrix/www/local/templates/main/header.php # ожидаем charset=utf-8

Четыре проверки закрывают четыре слоя кодировки, и расхождение хотя бы в одной из них означает, что перевод не завершён именно на этом слое.

Ограничения

Перекодировать дважды нельзя, и это главное ограничение всей операции. Файл, прогнанный через iconv второй раз, теряет русские буквы уже безвозвратно. Поэтому пакетную перекодировку запускают ровно один раз и всегда на копии.

Ядро платформы вручную не переводится и в пакетную обработку не попадает. Каталог /bitrix/ меняют переустановкой дистрибутива нужной кодировки, а свой код держат в каталоге /local/.

Обратной дороги у переведённой базы практически нет. Дамп в новой кодировке разворачивается назад тем же способом и с теми же потерями на границах строк.

Требования к кодировке базы расходятся между версиями продукта, и это сбивает. Актуальные требования - MySQL 8.0 и выше с utf8mb4, а старые уроки требовали однобайтовый utf8 и прямо запрещали utf8mb4.

С версии продукта 24.0.0 поставка идёт только в UTF-8, а cp1251 не поддерживается. Проект в однобайтовой кодировке продолжает работать, но обновление до свежих версий упирается ровно в это ограничение.

Типичные проблемы

После смены кодировки сайт отдаёт пустую страницу.

На середине перевода потеряна региональная настройка сайта, а хранится она в таблице b_culture. Настройку восстанавливают из резервной копии базы либо заводят заново в разделе региональных настроек.

Файлы перевелись, а база остаётся в старой кодировке.

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

При заливке дампа приходит ERROR 1062 Duplicate entry.

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

Часть файлов осталась нечитаемой после пакетной перекодировки.

Файлы в ANSI скрипт перекодировки не распознал: определение кодировки по содержимому на них не сработало. Такие файлы находят отдельной проверкой типа по содержимому и переводят поштучно вручную.

Установка обновлений останавливается на проверке настроек PHP.

В конфигурации остался ненулевой mbstring.func_overload, который платформа не поддерживает с главного модуля 20.100.0. Параметр обнуляют или удаляют, а результат смотрят в диагностике настроек PHP.

После перевода из 1С приезжают испорченные названия товаров.

Учётная система пишет файлы CommerceML в прежней кодировке, заданной у неё в реквизите обмена. Кодировку меняют на стороне 1С либо приводят входящий файл к кодировке проекта на чтении.

Частые вопросы

Можно ли перевести старый сайт на UTF-8 без потерь?

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

Почему база не хочет переводиться, а файлы перевелись?

Потому что кодировка базы и байты в её строках - разные вещи. Запрос смены кодировки меняет умолчание для новых таблиц, а старые строки переводит только перекодированный дамп.

Зачем раньше требовался mbstring.func_overload = 2?

Он подменял строковые функции PHP многобайтовыми версиями, и на этом держалась работа старых установок в UTF-8. Сейчас параметр устарел, платформа его не поддерживает и требует нулевого значения.

Что делать с файлами в ANSI, которые скрипт не видит?

Находить их отдельной проверкой кодировки по содержимому и переводить вручную по одному. Автоматическое определение на таких файлах не срабатывает, поэтому пакетный проход их молча пропускает.

Обязательно ли переходить на UTF-8?

Для развития проекта - да: с версии продукта 24.0.0 поставка идёт только в UTF-8. Существующий сайт в однобайтовой кодировке работает, но свежие обновления на него уже не встанут.

Смежное

Первоисточник