Перевод проекта на 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.
Шаги
- Снять полную резервную копию файлов и базы и убедиться, что она разворачивается на запасном стенде.
- Выгрузить дамп базы, перекодировать его текст и залить результат в чистую базу с новой кодировкой.
- Перевести файлы проекта пакетно, а затем найти остатки, которые пакетная перекодировка пропустила.
- Сменить кодировку в региональных настройках сайта и привести настройки PHP к требованиям платформы.
- Прогнать обмен с учётной системой и все выгрузки, сверяя русские названия в полученных результатах.
Код
Снимаем точку возврата до первой правки:
# от пользователя веб-сервера, архив ляжет в /bitrix/backup/sudo -u bitrix php -f /home/bitrix/www/bitrix/modules/main/tools/backup.phpmysqldump --opt --default-character-set=cp1251 -u root sitedb > /tmp/dump_1251.sql# ключ кодировки обязателен: без него дамп уедет в кодировке клиентаКопия платформы разворачивается штатным restore.php на запасном стенде, а дамп
уходит дальше в перекодировку и назад уже не вернётся.
Смотрим, что имеем на входе:
SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATAWHERE SCHEMA_NAME = DATABASE(); -- кодировка базы по умолчаниюSHOW VARIABLES LIKE 'character_set_%'; -- кодировка клиента и соединенияSELECT TABLE_COLLATION, COUNT(*) FROM information_schema.TABLESWHERE 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.sqlsed -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-8mbstring.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. Существующий сайт в однобайтовой кодировке работает, но свежие обновления на него уже не встанут.
Смежное
- Ошибки сервера и базы - оглавление подтемы
- Кракозябры вместо текста - поиск виновника, когда переводить весь проект не нужно
- Illegal mix of collations - ошибка запроса после половинчатой конвертации
- Восстановление из резервной копии - как разворачивают точку возврата
- Перенос сайта на другой сервер - репетиция перевода на запасном стенде
- Языковые файлы - куда переносят зашитые в код строки
- Стандарты кода - требование держать проект в UTF-8 и LF
- Протокол обмена с 1С - что проверяют в обмене после перевода
- Три контура проекта - где прогоняют перевод до боевого сайта
- Сервер и поиск - устройство сервера и окружения