Конвертация сайта из Windows-1251 в UTF-8

Преимущества кодировки UTF-8

Забегая на перед можно с уверенностью сказать, что на сегодняшний день следует использовать исключительно кодировку UTF-8. Это, конечно, не означает, что нужно конвертировать из win-1251 в UTF-8 все, особенно не поддерживаемые более сайты, но задуматься о переводе активно развиваемых проектов нужно было еще, как говорят, вчера. Потребность в конвертации сайта из win-1251 в UTF-8 возникает по многим причинам, но для начала следует разобраться, в чем же принципиальная разница этих кодировок? Самое главное отличие - количество символов, которые могут содержаться в каждой из них.

Кодировка Windows-1251 может содержать не более 255 символов, так как все символы этой кодировки кодируются одним байтом. Из-за этой особенности, такие кодировки, как win-1251 еще называют "однобайтными". В состав Windows-1251 входят символы кириллического, латинского алфавитов, знаки препинания и некоторые другие символы. Из-за столь ограниченного набора символов, вывести в Windows 1251 символы китайского иероглифа (供) или, например, немецкого умляута (ö) невозможно.

Можно предположить, как делают множество создателей кириллических сайтов, что для проектов, на которых будут использоваться только кириллица или латинские символы кодировка utf-8 ни к чему. Еще больше, в приоритете использования Windows-1251 может убедить тот факт, что символы этой кодировки занимают меньше места, и в следствие чего немного быстрее обрабатываются строковыми функциями PHP.

Кодировка UTF-8 имеет настолько больше преимуществ, что увеличенный размер её символов перестает играть значимую роль и превращается в одни плюсы. В utf-8 хранится огромный набор знаков, иероглифов и букв различных алфавитов, включая и кириллические, в связи с чем русскоязычный сайт в кодировке utf-8 корректно отобразиться на компьютерах в Японии, арабских и других стран, которые даже не подозревают о существовании кодировки win-1251 и других однобайтных кодировок (KOI8-R, CP866, ISO 8859-5 и др.) с поддержкой кириллических символов.

Подробнее про кодировку UTF-8 можно узнать из материалов Википедии.

Перекодирование сайта в UTF-8

Перекодирование скриптов сайта из win-1251 в UTF-8 - это далеко не простое занятие, как может показаться на первый взгляд. Необходимо проделать большой объем работы, вкратце рассмотренный ниже:

  1. Конвертировать данные MySQL.
  2. Конвертировать файлы скриптов и некоторые другие.
  3. Заменить строковые функции на их мультибайтные аналоги.
  4. Изменить паттерны во многих функциях регулярных выражений.
  5. Заменить функции отправки почты.
  6. Исправить все внутренние перекодировки iconv() и др.
  7. Протестировать.

Большинство сложностей с конвертацией сайта из win-1251 (или любой другой однобайтовой кодировки) в UTF-8, в первую очередь вызваны тем, что PHP, даже самая новая версия (на момент написания статьи 5.4), не полностью поддерживает кодировку UTF-8. Поскольку большинство символов в UTF-8 закодированы двумя (до четырех) байт - обычные строковые функции не правильно работают с такими символами. Пример ниже следует набирать в UTF-8:

header('Content-type: text/html; charset=utf-8');
$str = 'Проверка кодировки';
echo substr($str, 0, 7);

Вместо ожидаемого вывода "Проверк" получаем "Про" и "крокозябл" в конце - "�". Обычные строковые функции PHP предназначены для работы с однобайтными кодировками, символы которых состоят из одного байта, вот функция substr() и вырезает первые семь байт соответствующие симовлам "П,р,о" (кириллические буквы в UTF-8 кодируются 2-мя байтами) и первому байту буквы "в", вследствие чего "в" и превращается в "крокозябл �".

Далее будут рассмотрены способы, как подружить строковые функции PHP с кодировкой UTF-8.

Редактирование скриптов в кодировке UTF-8

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

В качестве редактора PHP-кода можно посоветовать бесплатный NetBeans или платные редакторы PhpStorm (99$), Zend Studio (299$). Поскольку это не обычные редакторы, а так называемые IDE (интегрированная среда разработки) - дополнительный функционал этих программ будет весьма кстати при рефакторинге кода для UTF-8.

Конвертация данных в MySQL

Перед конвертацией данных в базе MySQL из Windows-1251 в UTF-8 необходимо обязательно выполнить резервное копирование конвертируемой БД, в противном случае можно безвозвратно потерять все или часть данных! Следует обратить внимание, что в MySQL кодировка Windows-1251 называется cp1251, а UTF-8 - utf8.

Первым шагом необходимо обязательно проверить - соответствует ли кодировка БД, таблиц и всех её колонок соединению по умолчанию, во избежание необратимого перекодирования неизвестных новой кодировке символов в вопросительные знаки. Точнее, следует проверить, правильно ли "общался" скрипт с БД и нет ли во всех таблицах самой БД колонок с разными кодировками. Соединение с MySQL по умолчанию для всех запросов должно быть установлено в cp1251 - этот параметр задается либо в настройках MySQL в главном конфигурационном файле (MySQL - установка и настройка default-character-set=cp1251), либо в скриптах сайта сразу после подключения к БД: mysql_query('SET NAMES cp1251'). Для самой БД, таблиц и её колонок кодировка также, должна быть cp1251. Выполнить проверку поможет скрипт, код которого опубликован ниже (если таблиц не много можно посмотреть данные о кодировках при помощи PHPMyAdmin):

<?php
$mysqlhost = '';
$mysqlusers = '';
$mysqlpass = '';
$dbname = '';

$db = @mysql_connect($mysqlhost, $mysqlusers, $mysqlpass);
mysql_select_db($dbname);
//mysql_query('SET NAMES cp1251');

// Скрипт проверки кодировок
// http://petrenco.com/php.php?txt=142

// Кодировка соединения
$result = mysql_query('show variables like "character_set_connection";');
$row = mysql_fetch_assoc($result);
$mysql_charset_conn = $row['Value'];

// Кодировка БД
$result = mysql_query('SHOW VARIABLES LIKE "character_set_database";');
$row = mysql_fetch_assoc($result);
$mysql_charset_db = $row['Value'];


$result = mysql_query('SHOW TABLES');
while ($row = mysql_fetch_row($result))
  $tables_arr[] = $row[0];

$counter_tables = 0;
foreach ($tables_arr AS $table_name)
  {
  $result = mysql_query('SHOW CREATE TABLE  `'.$table_name.'`');
  while ($row = mysql_fetch_assoc($result))
    {
    preg_match_all('~\`(.*?)\` ([a-z]{1,10}).*?(?:character set|collate) ([a-z0-9-_]+) ~i', $row['Create Table'], $matches);
    preg_match('~DEFAULT CHARSET\=([a-z0-9-_]+) {0,1}~i', $row['Create Table'], $match);
    if (is_array($matches[1]))
      {
      foreach ($matches[0] as $key => $found_cols)
        {
        $res_arr[$table_name]['columns'][$matches[1][$key]]['type'] = $matches[2][$key];
        $res_arr[$table_name]['columns'][$matches[1][$key]]['charset'] = $matches[3][$key];
        }
      }
    if ($match[1] !== $mysql_charset_conn)
      {
      $res_arr[$table_name]['default_charset'] = $match[1];
      }
    $counter_tables++;
    }
  }
//print_r($res_arr);

$table_txt = '';
foreach ($res_arr AS $table => $vals_arr)
  {
  if (is_array($vals_arr['columns']))
    {
    foreach ($vals_arr['columns'] AS $column => $column_val)
      {
      $columns .= '<div style="color: red; font-weight: bold;">Колонка: '.$column.'; тип: '.$column_val['type'].'; кодировка: '.$column_val['charset'].'</div>';  
      }
    }
  else
    {
    $columns = '<span style="color: green;">Кодировка столбцов равна кодировке таблицы</span>';
    }
   
  if (!empty($vals_arr['default_charset']))  
    $default_charset_txt = '<span style="color: red; font-weight: bold;">'.$vals_arr['default_charset'].'</span>';
  else
    $default_charset_txt = '<span style="color: green;">'.$mysql_charset_conn.'</span>';
  $table_txt .= '
      <tr style="background: #fff;">
        <td>'
.$table.'</td>
        <td>'
.$default_charset_txt.'</td>
        <td>'
.$columns.'</td>
      </tr>'
;  
  }
 

if ($mysql_charset_db !== $mysql_charset_conn)
  $db_charset = '<h2 style="color: red;">В БД '.$dbname.' установлена кодировка по умолчанию: '.$mysql_charset_db.'</h2>';
else
  $db_charset = '<h2 style="color: green;">В БД '.$dbname.' установлена кодировка по умолчанию: '.$mysql_charset_db.'</h2>';

$counter_tables;


if (empty($table_txt))
  $table_found = '<h2 style="color: green;">Найдено таблиц '.$counter_tables.' шт., кодировка соединения соответствует табличным кодировкам по умолчанию</h2>';
else
  {
  $table_found = '
    <h2 style="color: red;">Таблицы не соответствующие кодировке соединения</h2>
    <table cellpadding="5" cellspacing="1" style="background: grey;">
      <tr style="background: red;">
        <th>Название таблицы</th>
        <th>Кодировка таблицы<br>
            (DEFAULT CHARSET)
        </th>
        <th>Кодировка колонок</th>
      </tr>
    '
.$table_txt.'
    </table>'
;
  }
 
 
echo '<h1>Кодировка соединения с БД: <span style="color: orange; font-size: 70px;">'.$mysql_charset_conn.'</span></h1>
    '
.$db_charset.'
    '
.$table_found.'';
?>

Скрипт находит все несоответствующие соединению кодировки в БД. О наличии таких кодировок можно узнать из сообщений красного цвета. Как подготовить такие данные к конвертации можно прочитать тут: "Проблемы с кодировкой в MySQL версий 4.1+", "Исправление и преобразование кодировок таблиц MySQL". Если на странице только черный, зеленый и оранжевый (кодировка соединения) цвета - с кодировками в БД все впорядке и можно приступать к конвертированию данных: "Конвертация БД из Win-1251 в UTF8".

После преобразования данных в таблицах MySQL из cp1251 в utf8 необходимо изменить кодировку по умолчанию для скриптов (изменение настройки в my.ini / my.conf или SQL запросом SET NAMES utf8 каждый раз после подключения к БД). Также следует заменить заголовок с указанием кодировки, отправляемый сервером браузеру клиента. Это можно сделать изменив в HTML-коде строку, расположенную между дескрипторами <head>...</head>

...
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
...

или выведя заголовок непосредственно средствами PHP:

header('Content-type: text/html; charset=utf-8');

Эти два способа указания кодировки браузеру можно совместить.

Теперь, вся информация из БД будет выводиться на страницах сайта как положено, а вот все тексты не латинскими буквами, расположенные в скриптах, отобразятся "кракозяблами". Для превращения "кракозяблов" в нормальный текст необходимо перекодировать уже сами скрипты, о чем речь пойдет в следующем разделе.

Перекодировка скриптов из Windows-1251 в UTF-8

Поскольку латинские буквы и знаки препинания имеют одинаковые коды символов как в кодировке Windows-1251 и так и в UTF-8 - проблем с их отображением не возникнет. В связи с этим ни код PHP, ни HTML-разметка, заданная латинскими буквами и знаками препинания практически не пострадают. Другое дело - кириллический алфавит.

Чтобы кириллические символы отображались как положено в кодировке UTF-8 необходимо их перекодировать. Один из простейших способов организовать этот процесс - использование мощностей библиотеки ICONV (PHP-функция iconv()). Конвертировать все подряд не нужно - необходимо исключить бинарные файлы (файлы изображений, pdf, архивов и др.) - если случайно конвертировать и их - данные этих файлов будут потеряны.

Скрипт, представленный ниже, помогает автоматизировать процесс конвертирования скриптов в UTF-8 из Windows 1251. Функция win1251_to_utf8() обрабатывает все вложенные директории и файлы, позволяя задать исключения - файлы и директории, данные в которых конвертировать в UTF-8 не нужно. Также необходимо задать расширения файлов - только файлы с заданными расширениями будут конвертированы. Перед запуском скрипта - обязательно сделайте резервную копию конвертируемых файлов! Ведь, например, если случайно запустить этот скрипт повторно, и конвертировать файлы, которые уже находятся в кодировке UTF-8 из Win1251 снова в UTF-8 - возможны проблемы с данными.

Перед запуском функции win1251_to_utf8(), следует ОБЯЗАТЕЛЬНО убедиться, что все файлы, подлежащие конвертации доступны на запись, в противном случае может получиться "каша" из файлов с кодировкой Windows-1251 и UTF-8, разобраться в которой будет уже совсем не легко. Для этих целей следует изначально задать переменной $convert значение 0. Если ошибок не найдено, то тогда нужно присвоить $convert значение 1.

<?php

ini_set('display_errors', 1);

// Конвертер модуля ссылок

// Входные данные
// $source ....... - путь к директории, например my_script_w1251/scripts (в конце слеш не нужен)
// $ext_convert ... - массив с разрешенными расширениями файлов - только файлы с указанными разрешениями будут конвертироваться. Например: array('php', 'inc');
// $exclude ...... - массив с файлами и директориями, данные которых конвертировать запрещено. Например: array('dir' => array('mod', 'lib/scripts'), 'file' => array('lib/index.php'))
// $convert ...... - флаг (1 перезаписывать файлы, 0 - только прочитать/проверить доступ на запись)
// $result ....... - стоит оставить пустым, используется для передачи данных при рекурсивном проходе директорий

// Результат выполнения - $result = win1251_to_utf8()
// $result['output_txt'] ... - текстовый вывод в HTML результатов выполнения функции: черный цвет - найденный файл подлежит конвертации (расширение есть в массиве), серый цвет - не подлежит
function win1251_to_utf8($source, $ext_convert = array(), $exclude = array('file' => array(), 'dir' => array()), $convert = 0, $result = array())
  {
  // http://petrenco.com/php.php?txt=142
  if (!isset($result['source']['dir']))
    {
    $result['source']['dir'] = $source;
    $result['source']['strlen'] = mb_strlen($source);
    }
  if ($paths = array_diff(scandir($source), array('..', '.')))
    {
    foreach($paths as $path)
      {
      if (is_dir($source.'/'.$path))
        {
        // Обработка исключений
        if (!in_array($source.'/'.$path, $exclude['dir']))
          {
          $result = win1251_to_utf8($source.'/'.$path, $ext_convert, $exclude, $convert, $result);
          }
        }
      else
        {        
        if (!in_array($source.'/'.$path, $exclude['file']))
          {
          // Только файлы с определенными расширениями
          foreach ($ext_convert AS $key => $ext)
            $tmp .= '(?:\.'.$ext.')|';
          $tmp = substr($tmp, 0, -1);
          $pattern = '~'.$tmp.'$~i';
          $result['counter_files']++;
          if (preg_match($pattern, $source.'/'.$path))
            {
            $file_content = file_get_contents($source.'/'.$path);

            // Перекодировка
            $fs1 = filesize($source.'/'.$path);
            if ($convert == 1)
              {
              // Проверка на доступность файла для записи
              if (!is_writable($source.'/'.$path))
                {
                $result['output_txt'] .= '<div style="color: red;">'.$result['counter_files'].'. Ошибка: '.$source.'/'.$path.' не доступен для записи, конвертация остановлена.</div>';
                return $result;
                }
              $file_content = iconv('windows-1251', 'utf-8', $file_content);
              file_put_contents($source.'/'.$path, $file_content);
              $fs2 = filesize($source.'/'.$path);
              if ($fs1 != $fs2)
                $result['output_txt'] .= '<div style="color: red;">'.$result['counter_files'].'. Ошибка: '.$source.'/'.$path.' ('.$fs1.' != '.$fs2.')</div>';
              else
                $result['output_txt'] .= '<div style="color: green;">'.$result['counter_files'].'. '.$source.'/'.$path.'</div>';
              }
            else
              {
              if (!is_writable($source.'/'.$path))
                $result['output_txt'] .= '<div style="color: red;">'.$result['counter_files'].'. Ошибка: '.$source.'/'.$path.' не доступен для записи.</div>';
              else
                $result['output_txt'] .= '<div style="">'.$result['counter_files'].'. '.$source.'/'.$path.'</div>';
              }
            $result['path']['conv_file_path'][] = $source.'/'.$path;
            }
          else
            {
            $result['output_txt'] .= '<div style="color: #a0a0a0;">'.$result['counter_files'].'. '.$source.'/'.$path.'</div>';
            }
          }
        }
      }
    }
  return $result;
  }

$source = 'my_project/lib'; // Начинать с той же директории, из которой запущен файл с функцией
$convert = 0;
$ext_convert = array('php', 'inc', 'html', 'htm');
$exclude_dir = array($source.'/img', $source.'/scripts'); // Директории, файлы в которых конвертировать не нужно
$exclude_file = array($source.'/!capcha.php'); // файлы, которые конвертировать не нужно
$exclude = array('file' => $exclude_file, 'dir' => $exclude_dir);

$result_func = win1251_to_utf8($source, $ext_convert, $exclude, $convert);

echo '<html>

<head>
  <title>Конвертация файлов</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>

<body>
'
.$result_func['output_txt'].'
</body>
</html>'
;

?>

После изменения кодировки файлов необходимо прописать или изменить локаль (предварительно, стоит ознакомиться с документацией по функции setlocale(), чтобы избежать проблем из-за отличий в операционных системах):

setlocale (LC_ALL, "ru_RU.UTF-8");

Эта строчка должна присутствовать вначале каждого скрипта.

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

Строковые функции PHP и UTF-8

Как было написано ранее, обычные строковые функции PHP (strlen(), substr(), strtolower() и др.) корректно работают только с однобайтными кодировками. Для мультибайтных кодировок, одной из которых и является UTF-8, необходимо применять специальные функции, например, из расширения PHP Mbstring.

Исключением является функция strlen(). В однобайтной кодировке, функция, возвращая количество символов, возвращает и размер строки в байтах, поскольку, как уже говорилось ранее, каждый символ в таких кодировках равен одному байту, и, соответственно, количество символов строки равно размеру в байтах. Заменять такую функцию, например на mb_strlen() нужно только тогда, когда задача strlen() была в подсчете количества символов. Если же strlen() использовалась для подсчета размера строки в байтах с последующей передачей этих данных, например в заголовок HTTP отправляемый клиенту для указания размера передаваемых данных - её следует оставить без изменений. Например, необходимо узнать размер и кол-во символов строки "строка, передаваемая клиенту":

<?php

mb_internal_encoding('UTF-8');
setlocale (LC_ALL, "ru_RU.UTF-8");
header('Content-type: text/html; charset=utf-8');

$str = 'строка, передаваемая клиенту';
$strlen = strlen($str);
$mb_strlen = mb_strlen($str);

echo 'Строка состоит из: '.$strlen.' байт и '.$mb_strlen.' символов.';
// 53 байта, 28 символов
?>

(В кодировке UTF-8 кириллические символы занимают 2 байта, пробел и запятая - 1 байт)

Есть несколько решений этой проблемы: перезагрузка функций, замена обычных строковых функций в коде на функции с приставкой "mb_" и использование функций написанных сторонними разработчиками или самостоятельно.

Перезагрузка функций

Перезагрузка функций - это указание интерпретатору PHP воспринимать часть обычных функций, работающих с однобайтовыми кодировками как функции MBString - со списком перегружаемых функций можно ознакомиться в официальной документации PHP). Чтобы выполнить перезагрузку функций необходимо в главном конфигурационном файле PHP (php.ini) установить параметр:

mbstring.func_overload 2

Ранее, до версии PHP 5.2.7, изменение настроек перезагрузки функций можно было осуществить в файле .htaccess (php_value mbstring.func_overload 2), но в более поздних версиях эта возможность, к сожалению, не доступна.

Если необходимо установить/отключить перезагрузку функций только для одного/нескольких сайтов следует воспользоваться директивой php_admin_value mbstring.func_overload 2 в конфигурации сервера Apache для виртуальных хостов.

Перезагрузку функций стоит применять лишь для быстрого изменения поведения скриптов. Мало того, что перезагрузка изменяет поведение далеко не всех необходимых функций, так еще она не решает проблему с определением, в каком контексте используется функция strlen().

Замена строковых функций

Замена строковых функций, другими словами рефакторинг кода PHP, более трудоёмкое, но и более правильное решение. Заменять обычные строковые функции можно на функции из библиотеки MBString или другие функции. Лучший вариант - заменить все однобайтовые функции аналогами с приставкой "mb_", а вот функции, которые аналогов не имеют (str_split(), wordwrap(), ucfirst(), lcfirst(), ucwords() и др.) - создавать самостоятельно или искать готовые решения в сети, например в комментариях к каждой из функций в официальной документации PHP.

Для замены функций на MB-аналоги как раз и пригодится один из редакторов, о которых говорилось выше. Предлагаемый способ замены тестировался в NetBeans, но скорее всего он будет аналогичен и в других IDE.

Перед использованием функций из MBString необходимо установить внутреннюю кодировку скрипта в UTF-8 при помощи функции mb_internal_encoding(), которая должна выполнятся перед использованием всех функций MBString в скриптах

mb_internal_encoding('UTF-8')

Также, задать внутреннюю кодировку скрипта можно в конфигурационном файле PHP - тогда этот параметр распространится на все скрипты.

Для начала следует создать проект в NetBeans, в который должны быть включены все скрипты, обслуживающие сайт. Далее, для замены, например функции substr(), необходимо создать собственную функцию substr() в новом файле подключенном к проекту, иначе рефакторинг произвести не удастся:

<?php
function substr() {};
?>

Потом на функции subst() нужно нажать правой кнопкой мыши и из меню выбрать: Refactor -> Rename, в поле New Name изменить subst на mb_substr и нажать кнопку "Preview":

Теперь можно заменить все функции сразу во всех файлах проекта, либо предварительно просмотреть в каком контексте используется substr() в коде каждого файла скрипта - байты или символы.

На что еще следует обратить внимание

Регулярные выражения и UTF-8

Чтобы регулярные выражения в функциях preg_...() работали корректно, необходимо добавить модификатор шаблона u. Также, следует забыть о шаблоне \w, и заменить его на \pL, хотя лучше всего использовать такую конструкцию [а-я].

Функция iconv()

Необходимо найти все вызовы функций iconv() в коде скриптов сайта, и заменить кодировки, либо вообще убрать эту функцию там, где из UTF-8 символы перекодировались в Windows1251.

Sape

Если сайт работает с ситемой Сапе (на сайте размещаются ссылки), необходимо изменить код подключения Сапе и вместо $sape = new SAPE_client() нужно написать:

$o['charset'] = 'UTF-8';
$sape = new SAPE_client($o);
unset($o);

Подробнее тут.

Опубликовано: 2012/06/10
HTML-код ссылки на эту страницу:
<a href="http://petrenco.com/php.php?txt=142" target="_blank">Конвертация сайта из win-1251 в UTF-8</a>
34642
Комментарии
Классная статья! Благодарю, очень помогла.
Спасибо вам большое, от всей души!
У меня свой проект около 4х лет, по незнанию на тот момент написала его в win-1251. Пару лет назад решила перегнать его (и уже довольно большую базу) в utf, убила несколько дней, четкой схемы не было и какой-то пункт оказался упущен. Так что пришлось все откатить.
Прочитала вашу статью, вдохновилась и попробовала еще раз: всего-то ночь потратила и все получилось! Пригодились и скрипты, и описание процесса, и необходимость найти в недрах фреймворка SET NAMES :)
Еще раз спасибо.
Спасибо. Переводим старый проект с 1251 на юникод, статья помогла сохранить достаточно много времени :)
Добавить комментарий
Ваш e-mail: (не виден посетителям сайта)
Ваше имя:
Комментарий:
Символы с картинки:
Только выделенные поля формы добавления комментариев обязательны к заполнению.