Черновики
quittance.ru
документы
прочее
Авторские дневники
   / черновики
   / заметки о типографике
   / SEO FAQ для неспециалистов

Юникод в Perl-совместимых регулярных выражениях php5

Ста­тья име­ет це­лью по­зна­ко­мить чи­та­те­ля с осо­бен­но­стя­ми, ко­то­рые на­кла­ды­ва­ет ис­поль­зо­ва­ние Юни­ко­да (англ. Unicode) на ра­бо­ту ме­ха­низ­ма Perl-сов­ме­сти­мых ре­гу­ляр­ных вы­ра­же­ний (сокр. PCRE) в php5. Пред­по­ла­га­ет­ся, что с ра­бо­той PCRE в од­но­бай­то­вых ко­ди­ров­ках чи­та­тель уже зна­ком, тем бо­лее, что на этот счет име­ет­ся ис­чер­пы­ва­ю­щая до­ку­мен­та­ция.

Исполь­зо­ва­ния Юни­ко­да в Perl-сов­ме­сти­мых ре­гу­ляр­ных вы­ра­же­ни­ях, на­про­тив, опи­са­на в до­ку­мен­та­ции php че­рес­чур ла­ко­нично.

Вслед­ствие это­го, при ис­поль­зо­ва­нии PCRE с Юни­ко­дом при­хо­дит­ся стал­ки­вать­ся с неко­то­ры­ми осо­бен­но­стя­ми и да­же стран­но­стя­ми. После осмыс­ле­ния этих стран­но­стей ста­но­вит­ся яс­на ло­ги­ка и при­хо­дит по­ни­ма­ние, по­че­му это ре­а­ли­зо­ва­но имен­но так, а не ина­че. Но оста­вим чи­та­те­лю осмыс­ле­ние осо­бен­но­стей ре­а­ли­за­ции Юни­ко­да в PCRE и зай­мем­ся фак­тами.

Документированные особенности

Под­держ­ка Unicode в Perl-сов­ме­сти­мых ре­гу­ляр­ных вы­ра­же­ни­ях php5 огра­ни­чи­ва­ет­ся ко­ди­ров­кой UTF-8. Дру­гие ко­ди­ров­ки, та­кие как UTF-16, UTF-32, UCS-2, не под­дер­жи­ва­ют­ся. Шаб­лон, пред­на­зна­чен­ный для об­ра­бот­ки тек­ста в ко­ди­ров­ке UTF-8 дол­жен иметь мо­ди­фи­ка­тор «u». Ина­че счи­та­ет­ся, что шаб­лон пред­на­зна­чен для об­ра­бот­ки тек­ста в од­но­бай­то­вой ко­ди­ровке.

До по­след­не­го вре­ме­ни до­ку­мен­ти­ро­ван­ные осо­бен­но­сти ра­бо­ты с Юни­ко­дом в PCRE на этом за­кан­чи­ва­лись. Но недав­но в со­став офи­ци­аль­ной до­ку­мен­та­ции php во­шла неболь­шая ста­тья, по­свя­щен­ная ис­поль­зо­ва­нию сим­воль­ных при­зна­ков Юни­ко­да в шаб­ло­нах PCRE.

Общие типы символов и символьные признаки Unicode

Шаб­ло­ны ти­пов сим­во­лов \d, \w, \s, и им об­рат­ные \D, \W, \S в ре­жи­ме Unicode кор­рект­но ра­бо­та­ют толь­ко для сим­во­лов на­бо­ра ASCII, то есть для ко­дов не бо­лее 0x7F. То же от­но­сит­ся к утвер­жде­ни­ям \b и \B. Все шаб­ло­ны об­щих ти­пов долж­ны быть за­ме­не­ны на со­от­вет­ству­ю­щие сим­воль­ные при­зна­ки, ко­то­рые обо­зна­ча­ют­ся в ре­гу­ляр­ных вы­ра­же­ни­ях при по­мо­щи кон­струк­ции:

\p{при­знак}

Для от­ри­ца­ния при­зна­ка ис­поль­зу­ет­ся за­глав­ная бук­ва «P»:

\P{при­знак}

Обра­бот­ка сим­воль­ных при­зна­ков адек­ват­но под­дер­жи­ва­ют­ся PCRE в ре­жи­ме Юни­ко­да. Таким об­ра­зом, обо­зна­че­ния ти­пов сим­во­лов в шаб­ло­нах долж­ны быть пре­об­ра­зо­ва­ны, на­при­мер:

1.  \d => \p{Nd}
\D => \P{Nd}
2. \w => \p{L}
\W => \P{L}
3. \s => \p{Zs}
\S => \P{Zs}

Пре­об­ра­зо­ва­ние 1 мо­жет по­ка­зать­ся из­лиш­ним, по­сколь­ку араб­ские циф­ры на­хо­дят­ся в пре­де­лах ко­до­вой таб­ли­цы ASCII и в лю­бом слу­чае со­от­вет­ству­ют клас­си­че­ско­му шаб­ло­ну \d. Но в дей­стви­тель­но­сти, кро­ме араб­ских цифр су­ще­ству­ет мно­же­ство дру­гих сим­во­лов, обо­зна­ча­ю­щих де­ся­тич­ные зна­ки, и при ра­бо­те в Юни­ко­де име­ет смысл с ни­ми счи­тать­ся.

Пре­об­ра­зо­ва­ния 2 и 3 стро­го го­во­ря не яв­ля­ют­ся эк­ви­ва­лент­ны­ми, но на­хо­дят­ся в пре­де­лах здра­во­го смыс­ла и в боль­шин­стве слу­ча­ев вполне при­ме­ни­мы на прак­ти­ке.

Кро­ме неудобств, свя­зан­ных с необ­хо­ди­мо­стью пре­об­ра­зо­вы­вать про­стые ти­пы сим­во­лов в при­зна­ки Unicode, есть и неоспо­ри­мые пре­иму­ще­ства. Не го­во­ря о том, что ме­ха­низм при­зна­ков Unicode зна­чи­тель­но гиб­че клас­си­че­ских ти­пов сим­во­лов, он, в от­ли­чие от по­след­них, еще и неза­ви­сим от уста­но­вок ло­ка­ли (англ. locale).

Подроб­ную ин­фор­ма­цию о при­зна­ках сим­во­лов в Юни­ко­де мож­но по­черп­нуть из до­ку­мен­та Unicode Technical Standard #18  Unicode Regular Expressions в раз­де­ле 1.2 Properties.

Позиция символа от начала строки

Неко­то­рые функ­ции php5 из се­мей­ства PCRE име­ют де­ло с по­зи­ци­ей сим­во­ла от на­ча­ла стро­ки. В част­но­сти, функ­ции preg_match() и preg_match_all() в слу­чае вы­зо­ва с фла­гом PREG_OFFSET_CAPTURE поз­во­ля­ют по­лу­чить по­зи­ции вхож­де­ния все­го шаб­ло­на и всех его под­ма­сок.

Здесь име­ет ме­сто еще од­на недо­ку­мен­ти­ро­ван­ная осо­бен­ность: по­зи­ция воз­вра­ща­ет­ся в бай­тах, а не в сим­во­лах, как это мож­но бы­ло бы пред­по­ло­жить.
// UTF-8
preg_match('/г/u', 'абвгд',
$matches, PREG_OFFSET_CAPTURE);
echo $matches[0][1];
// выведет 6, а не 3
Точ­но так же  в бай­тах, а не в сим­во­лах!  сле­ду­ет за­да­вать и на­чаль­ную по­зи­цию для по­ис­ка  необя­за­тель­ный пя­тый па­ра­метр $offset двух упо­мя­ну­тых функ­ций. При­чем, ес­ли на­чаль­ная по­зи­ция для по­ис­ка не по­па­да­ет на пер­вый байт пред­став­ле­ния сим­во­ла, то со­по­став­ле­ние тер­пит неудачу.

Опи­сан­ная осо­бен­ность не пред­став­ля­ет боль­ших неудобств. Пре­об­ра­зо­ва­ние сим­воль­ной по­зи­ции в бай­то­вую и об­рат­но  за­да­ча неслож­ная и од­но­знач­ная. Сму­ща­ет лишь то, что эта осо­бен­ность не до­ку­мен­ти­ро­ва­на и есть опа­се­ние, что в бу­ду­щих вер­си­ях php по­ве­де­ние мо­жет из­ме­нить­ся.

Задание символа кодом

В клас­си­че­ских Perl-сов­ме­сти­мых ре­гу­ляр­ных вы­ра­же­ни­ях для за­да­ния сим­во­ла его од­но­бай­то­вым шест­на­дца­те­рич­ным ко­дом ис­поль­зу­ет­ся кон­струк­ция \xHH. Такая за­пись при­ме­ни­ма толь­ко для од­но­бай­то­во­го ко­да сим­во­ла, в то вре­мя как в Юни­ко­де его раз­мер мо­жет до­сти­гать че­ты­рех байт.

В Юни­ко­де при­ня­то ис­поль­зо­вать кон­струк­цию \uHHHH, но PCRE та­кой фор­мат не под­дер­жи­ва­ют. Вза­мен пред­ла­га­ет­ся за­клю­чать мно­го­бай­то­вый код сим­во­ла в фигур­ные скоб­ки: \x{HHHH}. Если код сим­во­ла со­сто­ит из од­но­го бай­та, мож­но оста­вить клас­си­че­скую фор­му­лу \xHH.

Важ­но раз­ли­чать код сим­во­ла (англ. Code Point) и его бай­то­вое пред­став­ле­ние (англ. Code Units). В од­но­бай­то­вых ко­ди­ров­ках эти два по­ня­тия суть од­но и то же, но в Юни­ко­де не так. Рас­про­стра­нен­ная ошиб­ка  по­пыт­ка ис­поль­зо­вать внут­ри фигур­ных ско­бок бай­то­вое пред­став­ле­ние сим­во­ла вме­сто его ко­да. Раз­ни­цу меж­ду Code Point и Code Units на­гляд­но де­мон­стри­ру­ет таб­ли­ца:

Code     Code Units    Code Units    Code Units
Point UTF-8  UTF-16  UTF-32 
7F 7F 007F 0000007F
80  C2 80  0080  00000080
7FF DF BF 07FF 000007FF
800  E0 A0 80  0800  00000800
FFFF EF BF BF FFFF 0000FFFF
10000  F0 90 80 80  D800 DC00  00010000

Хоро­шо вид­но, что код сим­во­ла пол­но­стью со­от­вет­ству­ет его пред­став­ле­нию толь­ко для ко­ди­ров­ки UTF-32.

Миграция от классических PCRE к Юникоду

И в за­клю­че­ние пред­став­ля­ем про­стую функ­цию для пре­об­ра­зо­ва­ния клас­си­че­ских шаб­ло­нов в Unicode. Функ­ция пред­на­зна­ча­лась для ав­то­ма­ти­че­ско­го пре­об­ра­зо­ва­ния шаб­ло­нов в про­грам­мах на php5, от ко­то­рых тре­бу­ет­ся од­новре­мен­но под­держ­ка и клас­си­че­ских ко­ди­ро­вок и Юни­ко­да. Может быть с успе­хом ис­поль­зо­ва­на для об­лег­че­ния ми­гра­ции Perl-сов­ме­сти­мых ре­гу­ляр­ных вы­ра­же­ний на Unicode.

Выпол­ня­ют­ся сле­ду­ю­щие пре­об­ра­зо­ва­ния:

  • пе­ре­ко­ди­ров­ка все­го шаб­ло­на в UTF-8;
  • за­ме­на об­щих ти­пов /s, /d, и /w на со­от­вет­ству­ю­щие сим­воль­ные при­зна­ки Юни­ко­да, как опи­са­но выше;
  • в ко­нец шаб­ло­на до­бав­ля­ет­ся мо­ди­фи­ка­тор «u».

Каж­дое из пе­ре­чис­лен­ных пре­об­ра­зо­ва­ний мо­жет быть вклю­че­но или от­клю­че­но от­дель­но при по­мо­щи фла­гов. По умол­ча­нию вы­пол­ня­ют­ся все три.

Пре­об­ра­зо­ва­ние сим­во­лов, за­дан­ных шест­на­дца­те­рич­ным ко­дом не про­из­во­дит­ся. Сим­во­лы из на­бо­ра Latin-1 (ISO 8859–1) в та­ком пре­об­ра­зо­ва­нии не нуж­да­ют­ся, а за­да­ние ко­дом сим­во­лов дру­гих од­но­бай­то­вых ко­ди­ро­вок яв­ля­ет­ся пло­хой прак­ти­кой, от ко­то­рой необ­хо­ди­мо из­бав­ляться.

<?php
 
define('P2U_RECODE', 0x01); // recode pattern
define('P2U_PROPERTIES', 0x02); // convert types to properties
define('P2U_MODIFIER', 0x03); // add pattern modifier
define('P2U_ALL', P2U_RECODE | P2U_PROPERTIES | P2U_MODIFIER);
 
// converts classic PCRE pattern to Unicode one
// $pattern can be the single string or array of strings
function sk_pattern2unicode($pattern, $from_enc = 'ISO-8859-1', $flags = P2U_ALL) {
 
// pattern is array: recursive call
if (is_array($pattern))
foreach ($pattern as $key => $val)
$ret[$key] = sk_pattern2unicode($val, $from_enc, $flags);
 
// pattern is string: process it
elseif (is_string($pattern)) {
 
// recode pattern
$ret = ($flags & P2U_RECODE) ? @iconv($from_enc, 'UTF-8', $pattern) : $pattern;
 
// convert types to properties
if ($flags & P2U_PROPERTIES) {
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5Cd/'; $repl[] = '\p{Nd}';
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5CD/'; $repl[] = '\P{Nd}';
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5Cw/'; $repl[] = '\p{L}';
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5CW/'; $repl[] = '\P{L}';
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5Cs/'; $repl[] = '\p{Zs}';
$patt[] = '/(?<!(?<!(?<!\x5C)\x5C)\x5C)\x5CS/'; $repl[] = '\P{Zs}';
$ret = preg_replace($patt, $repl, $ret);
}
 
// add pattern modifier
$ret .= ($flags & P2U_MODIFIER) ? 'u' : '';
 
// pattern is not a string nor array: return as is
} else $ret = $pattern;
 
return $ret;
}
?>

комментировать 08/12/2010
Copyright 2009–2010 Sergey Kurakin