Как стать автором
Обновить

Работа с кодировками в Perl

Время на прочтение10 мин
Количество просмотров55K
На хабре уже есть хорошая статья об использовании UTF-8 в Perl — habrahabr.ru/post/53578. Я все же немного по своему
хотел бы рассказать о кодировках.

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

Определение кодировки исходного файла. Определение кодировки исходного документа, задача которая довольно часто встречается на практике. Возьмем как пример браузер. Кроме html файла он также может получать в HTTP ответе заголовок, который задает кодировку документа и этот заголовок может быть не правильным, поэтому ориентироваться только на него нельзя, как следствие, браузеры поддерживают возможность автоматической определения кодировки.

В Perl для этого вы можете использовать Encode::Guess, однако более «продвинутым» промышленным вариантом является Encode::Detect::Detector. Как написано в документации к нему, он предоставляет интерфейс к Мозиловскому универсальному определителю кодировки.

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

nsresult nsUniversalDetector::HandleData(const char* aBuf, PRUint32 aLen)

Из этого метода начинается вся работа по определению кодировки. Вначале определяется, есть ли BOM заголовок, если да то дальнейшее определении кодировки выполняется простым сравнением начальных байтов данных:
  • EF BB BF UTF-8 encoded BOM
  • FE FF 00 00 UCS-4, unusual octet order BOM (3412)
  • FE FF UTF-16, big endian BOM
  • 00 00 FE FF UTF-32, big-endian BOM
  • 00 00 FF FE UCS-4, unusual octet order BOM (2143)
  • FF FE 00 00 UTF-32, little-endian BOM
  • FF FE UTF-16, little endian BOM


Далее анализируется каждый байт данных и анализируется относится ли символ к не US-ASCII (коды от 128 до 255) если да то создаются объекты классов:
  • nsMBCSGroupProber;
  • nsSBCSGroupProber;
  • nsLatin1Prober;


каждый из которых отвечает за анализ групп кодировок (MB – мультибайтовые, SB – однобайтовые).

Если же это US-ASCII то здесь 2-а варианта, либо это обыкновенный ASCII (pure ascii) либо файл содержащий escape последовательности и относится к таким кодировкам как ISO-2022-KR и т.п. (более подробно — en.wikipedia.org/wiki/ISO/IEC_2022). В этом случае используется детектор реализованный классом nsEscCharSetProber.

nsMBCSGroupProber поддерживает такие кодировки как: «UTF8», «SJIS», «EUCJP», «GB18030», «EUCKR», «Big5», «EUCTW».

nsSBCSGroupProber – такие как Win1251,koi8r,ibm866 и другие.

Определение однобайтовой кодировки базируется на анализе частоты вхождения 2-ух символьных последовательностей в текст.

Следует сказать, что все эти методы носят вероятностный характер. Например, если будет недостаточное количество слов для определения, никакой алгоритм не сможет автоматически определить кодировку. Поэтому, в различных среда программирования вопрос с кодировками решается по своему, но нет такого, чтобы все определялось само.

Unicode и Perl. Исторический ракурс. Согласно www.unicode.org/glossary в Unicode есть 7 возможных схем кодирования: UTF-8, UTF-16, UTF-16BE, UTF-16LE, UTF-32, UTF-32BE, UTF-32LE. Для самого термина Unicode дано следующее определение «…стандарт цифрового представления символов, которые используются в письме всеми языками мира…». Кроме этого также существует UTF-7, которая не является частью стандарта, но поддерживается Perl — Encode::Unicode::UTF7 (см.также RFC 2152).

UTF-7 практически не используется. Вот что написано в Encode::Unicode::UTF7 – «…Впрочем, если вы хотите использовать UTF-7 для документов в почте и веб страниц, не используйте ее, пока не удостоверетесь что получатели и читатели (в смысле этих документов) могут обрабатывать эту кодировку…».

Разработчики Perl следуя прогрессу в части повсеместной реализации кодировок Unicode в приложениях, также реализовали поддержку Unicode в Perl. Кроме того модуль Encode поддерживает также другие кодировки как однобайтовые так и многобайтовые, список которых можно просмотреть в пакете Encode::Config. Для работы с письмами, поддерживаются «MIME кодировки»: MIME-Header, MIME-B, MIME-Q, MIME-Header-ISO_2022_JP.

Следует сказать, что UTF-8 очень широко распространена в качестве кодировки для веб документов. UTF-16 используется в Java и Windows, UTF-8 и UTF-32 используется Linux и другими Unix-подобными системами.

Начиная с версии Perl 5.6.0 была изначально реализована возможность работы с Unicode. Тем не менее, для более серьезной работы с Unicode был рекомендован Perl 5.8.0. Perl 5.14.0 – первая версия в которой поддержка Unicode легко (почти) интегрируемая без нескольких подводных камней (исключения составляют некоторые различия в quotemeta). Версия 5.14 также исправляет ряд ошибок и отклонений от стандарта Unicode.

Visual Studio 2012 и кодировки (для сравнения с Perl). Когда мы пишем некоторое приложение на C# в Visual Studio мы не задумываемся о том, в какой кодировке все это хранится и обрабатывается. При создании документа в Vistual Studio она создаст его в UTF8 и еще добавит в заголовок BOM UTF8 — последовательность байтов 0xEF, 0xBB, 0xBF. Когда же мы конвертируем исходный файл (уже открытый в Visual Studio), например, с UTF8 в CP1251 то получим сообщение об ошибке
Some bytes have been replaced with the Unicode substitution character while loading … with Unicode (UTF-8) encoding. Saving the file will not preserve the original file contents.

Если открыть существующий файл в cp1251 – ToUpper(), например, будет отрабатывать корректно, а если конвертировать файл в KOI8-R а потом открыть в Visual Studio и выполнить, ни о какой корректной работе не может быть и речи, здесь среда не знает, что это KOI8-R, да и как она это может узнать?

“Unicode Bug в Perl”. Так же как и в Visual Studio, что-то похожое происходит и с программой на Perl, но разработчики Perl могут явно указывать кодировку исходного кода приложения. Вот почему когда начинающие программировать на perl открывают на русскоязычной Windows XP свой любимый редактор и в ANSI (тоесть cp1251) пишут что-то в духе

use strict;
use warnings;

my $a = "слово";
my $b = "СЛОВО";
my $c = “word”;

print "Words are equal" if uc($a) eq uc($b);


а на выходе получают, что строки в переменных не равны, им вначале сложно понять, что происходит. Аналогичные вещи происходят с регулярными выражениями, строковыми функциями (но uc($c) будет работать корректно).

Это так называемый «Unicode Bug» в Perl (более подробно смотрите в документаци), связанный с тем, что для разных однобайтовых кодировок, символы с кодами от 128 до 255 будут иметь разный смысл. Например, буква П в cp1251 – имеет код 0xCF, тогда как в CP866 – 0x8F, а в KOI8-R – 0xF0. Как в таком случае, отработать правильно таким строковым функциям как uc(), ucfirst(), lc(), lcfirst() или \L, \U в регулярных выражениях?

Достаточно «подсказать» интерпретатору, что кодировка исходного файла cp1251 и все будет работать правильно. Более точно в приведенном ниже коде, переменные $a и $b будут хранить строки во внутреннем формате Perl.

use strict;
use warnings;
use encoding 'cp1251';

my $a = "слово";
my $b = "СЛОВО";

print "equal" if uc($a) eq uc($b);



Внутренний формат строк в Perl. В не очень старых версиях Perl строки могут хранится в так называемом внутреннем формате (Perl's internal form). Обратите внимание, что также они могут хранится как просто набор байтов. В примере выше, там, где явно не задавалась кодировка исходного файла (с помощью use encoding 'cp1251';) переменные $a, $b, $c хранят просто набор байтов (еще в документации к Perl используется термин последовательность октетов — a sequence of octets).

Внутренний формат от набора байтов отличается тем, что используется кодировка UTF-8 и для переменной включен флаг UTF8. Приведу пример. Изменим немного исходный код программы на следующий

use strict;
use warnings;
use encoding 'cp1251';
use Devel::Peek;


my $a = "слово";
my $b = "СЛОВО";

print Dump ($a);



Вот, что мы получим в результате

SV = PV(0x199ee4) at 0x19bfb4
REFCNT = 1
FLAGS = (PADMY,POK,pPOK,UTF8)
PV = 0x19316c "\321\201\320\273\320\276\320\262\320\276"\0 [UTF8 "\x{441}\x{43b}\x{43e}\x{432}\x{43e}"]
CUR = 10
LEN = 12

Обратите внимание, что FLAGS = (PADMY,POK,pPOK,UTF8). Если мы уберем use encoding 'cp1251';
то получим

SV = PV(0x2d9ee4) at 0x2dbfc4
REFCNT = 1
FLAGS = (PADMY,POK,pPOK)
PV = 0x2d316c "\321\201\320\273\320\276\320\262\320\276"\0
CUR = 10
LEN = 12

Когда мы указываем, что исходный код файла в кодировке cp1251 или какой-либо другой то Perl знает, что нужно конвертировать строковые литерали в исходном коде из указанной кодировки во внутренний формат (в данном случае из cp1251 во внутренний формат UTF-8 )и делает это.

Аналогичная проблема определения кодировки возникает при работе с данными получаемыми «извне», например файлов или веб. Рассмотрим каждый из случаев.

Пусть у нас есть файл в кодировке cp866, который содержит слово «Когда» (в текстовом файле слово Когда с большой буквы). Нам необходимо открыть его и проанализировать все строки на предмет нахождения слова «когда». Вот как это сделать правильно (при этом сам исходный код должен быть в utf8).

use strict;
use warnings;
use encoding 'utf8';

open (my $tmp, "<:encoding(cp866)", $ARGV[0]) or die "Error open file - $!";


while (<$tmp>)
{
	if (/когда/i)
	{
		print "OK\n";
	}
}

close ($tmp);



Обратите внимание, что в случае если мы не будем использовать "<:encoding(cp866)", и укажем use encoding ‘cp866’ то регулярные выражения будут работать, но только с набором байт и /i работать не будет. Конструкция «<:encoding(cp866)» подсказывает Perl, что данные в текстовом файле в кодировке CP866, поэтому он правильно выполняет перекодировку из CP866 во внутренний формат (CP866 -> UTF8 + включает флаг UTF8).

Следующий пример, мы получаем страницу с помощью LWP::UserAgent. Вот правильний пример, как это нужно делать.

use strict;
use warnings;
use LWP::UserAgent;
use HTML::Entities;
use Data::Dumper;
use Encode;
use Devel::Peek;


my $ua = LWP::UserAgent->new();

my $res = $ua->get("http://wp.local");

my $content;

if (!$res->is_error)
{
	$content = $res->content;
}
else
{
	exit(1);
}

# Только если страница в UTF8, если в cp1251 - $content = decode('cp1251',$content);
# decode конвертирует из utf8 байтов (последовательности октетов) во внутренний формат Perl

$content = decode('utf8',$content);

# теперь переменная $content содержит текст во внутреннем формате, с которым можно работать другим модулям, таким как, например, HTML::Entities, а также строковым функциями, регулярными выражениями и т.д.

decode_entities($content);


Обратите внимание на вызов $content = decode('utf8',$content).

LWP::UserAgent работает с байтами, он не знает, и это не его забота, в какой кодировке страница в однобайтовой cp1251 или в UTF8, мы должны явно указывать это. К сожалению, много литературы содержит примеры на английском языке и для более старых версий Perl, как следствие, в этих примерах нет ничего о перекодировке.

Например, роботы поисковых систем (или другой код), должны не только правильно определять кодировку страниц, не используя заголовки ответов серверов или содержимое HTML тега meta, которые могут быть ошибочными, но и определять язык страницы. Поэтому не думайте, что все вышесказанное должны делать только программисты на Perl.

На примере получения внешних данных с веб сайта мы подошли к рассмотрению использования модуля Encode. Вот его основное API, очень важное в работе любого Perl программиста:

$string = decode(ENCODING, OCTETS[, CHECK]). Выполняет конвертацию набора байтов (октетов) из кодировки ENCODING во внутренний формат Perl;

$octets = encode(ENCODING, STRING[, CHECK]). Выполняет конвертацию из внутреннего формата Perl в набор байтов в кодировке ENCODING.

[$length =] from_to($octets, FROM_ENC, TO_ENC [, CHECK]). Выполняет конвертацию байтов из одной кодировки в другую.



В примере, в котором мы открывали текстовый файл в CP866 мы можем не указывать <:encoding(cp866). Тогда, при каждой операции чтения мы будем получать набор байтов в CP866. Мы можем сами конвертировать их во внутренний формат с помощью

$str = decode(‘cp866’,$str)


и дальше работать с переменной $str.

Кто-то может предположить, что можно в качестве исходного текста программы использовать utf8, а кроме того, перекодировать из cp866 в utf8 и все будет работать как нужно. Это не так, рассмотрим пример (в текстовом файле слово Когда с большой буквы).

use strict;
use warnings;
use encoding 'utf8';
use Encode;

#open (my $tmp, "<:encoding(cp866)", $ARGV[0]) or die "Error open file - $!";
open (my $tmp, "<", $ARGV[0]) or die "Error open file - $!";


while (<$tmp>)
{

	my $str = $_;


	Encode::from_to($str,'cp866','utf8');

	if ($str=~/когда/i)
	{
		print "OK\n";
	}
}

close ($tmp);



$str после выполнения Encode::from_to($str,'cp866','utf8') содержит данные в utf8 но как последовательность байтов (октетов) поэтому /i не работает. Чтобы все работало как нужно добавить вызов

$str = decode('utf8',$str)


Конечно же более простым вариантом является одна строка вместо двух

$str = decode(‘cp866’,$str)


Внутренний формат строк Perl, более подробно. Мы уже говорили о том, что регулярные выражения, часть модулей и строковые функции корректно работают со строками, которые хранятся не как набор байтов а во внутреннем представлении Perl. Также было сказано, что в качестве внутреннего формата хранения строк в Perl используется UTF-8. Эта кодировка выбрана не просто так. Часть кодов символов в этой кодировке от 0-127 совпадает с ASCII (US-ASCII), которые как раз отвечают за английский алфавит, вот почему вызов uc для строки с кодами от 0 до 127 отрабатывает правильно и это будет работать в независимости от однобайтовой кодировки в которой сохранен исходный код. Для UTF8 все так же работает корректно.

Однако это еще не все, что нужно знать.

UTF-8 vs utf8 vs UTF8. Кодировка UTF-8 со временем стала более «строгой» (например, наличие определенных символов было запрещено). Поэтому реализация UTF-8 в Perl устарала. Начиная с Perl 5.8.7 “UTF-8” означает современный «диалент» более «строгий», тогда как “utf8” означает более «либеральный старый диалект». Вот небольшой пример

use strict;
use warnings;
use Encode;

# символ который не используется в UTF-8 
my $str = "\x{FDD0}";

$str = encode("UTF-8",$str,1); # Ошибка
$str = encode("utf8",$str,1); # OK



Таким образом дефис между “UTF” и “8” важен, без него Encode становится более либеральной и возможно чрезмерно разрешительной. Если выполнить

use strict;
use warnings;
use Encode;

my $str = sprintf ("%s | %s | %s | %s | %s\n",
   find_encoding("UTF-8")->name ,
   find_encoding("utf-8")->name ,
   find_encoding("utf_8")->name ,
  	find_encoding("UTF8")->name ,
	find_encoding("utf8")->name 

	);

print $str;


Мы получим следующий результат — utf-8-strict | utf-8-strict | utf-8-strict | utf8 | utf8.

Работа с консолью. Рассмотрим консоль ОС семейства Windows. Как все знают в Windows есть понятие кодировки Unicode, ANSI, OEM. API самой ОС поддерживает 2-а типа функций, которые работают с ANSI и Unicode (UTF-16). ANSI зависит от локализации ОС, для русской версии используется кодировка CP1251. OEM – это кодировка, которая используется для операций ввода/вывода консоли, для русскоязычной Windows – это CP866. Эта та кодировка, которая была предложена в русскоязычной MS-DOS, а позже перекочевала и в Windows для обратной совместимости со старым ПО. Вот почему, следующая программа в utf-8

use strict;
use warnings;
use Encode;

use encoding 'utf8';


my $str = 'Привет мир';

print $str;



не выведет заветной строки, мы же выводим UTF8, когда нужно CP866. Здесь нужно использовать модуль Encode::Locale. Если просмотреть его исходный код то можно увидеть, что для ОС Windows он определяет кодировку ANSI и консоли и создает алиасы console_in, console_out, locale, locale_fs. Все что остается сделать это немного изменить нашу программу.

use strict;
use warnings;
use Encode::Locale;
use Encode;

use encoding 'utf8';

my $str = 'Привет мир';

if (-t) 
{
	binmode(STDIN, ":encoding(console_in)");
	binmode(STDOUT, ":encoding(console_out)");
	binmode(STDERR, ":encoding(console_out)");
}

print $str;


P.S. Эта статья для тех, кто начинает работать с Perl и может быть она немного шереховата. Готов выслушать и реализовать пожелания относительно расширения статьи.
Теги:
Хабы:
+16
Комментарии6

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн