View source for Транслитерация URL-адреса: задачи и решения

Эта статья о том, что такое «транслитерация URL-адреса», зачем она нужна и какие её виды нами используются. В статье рассказывается про «взаимно-однозначный транслит», а также приводятся другие интересные наборы правил для транслитерации адресов формата URL. 

{{toc numerate=1}}

===О чём речь===

Почти всем из нас, кто решал проблему автоматической или полу-автоматической генерации «удобных для человека URL-адресов» (их ещё называют "((http://spectator.ru/technology/php/user_friendly_urls ЧПУ))" или pretty-urls), приходилось сталкиваться с задачей так называемой «транслитерации URL-адреса».

Эта задача имеет следующие ограничения и стартовые условия:
  1. URL-адреса страниц могут состоять из ограниченного набора символов: цифр ##0–9##, латинских букв ##a-z## и ##A-Z##, слэша ##/##, тильды ##~## и следующих символов, указанных в ((http://www.ietf.org/rfc/rfc2396.txt RFC 2396)): ##-_.!*'()##
  1. То, из чего мы должны программным путём сгенерировать URL — как правило, заголовок новости. Или документа. Словом, может включать и русские буквы, и знаки препинания, и почти всё, что угодно, включая HTML-таги.

Если разобраться, то перед нами встаёт не одна задача, а целых три:
  1. **Получение красивого читаемого адреса URL** — задача, в которой мы жертвуем дотошностью в угоду красивости.
  1. **Формирование короткого однозначного идентификатора** (мы его привыкли называть «супертагом») — задача, в которой мы жертвуем красивостью ради борьбы с опечатками.
  1. **Передача англо-русскоязычного слова (или фразы) через URL без потерь** — для последующего восстановления исходного «текста» (слова или фразы). Именно эта задача и получила название «взаимно-однозначный транслит», и жертвует красивостью ради однозначности преобразования.

Для решения этих трёх разных задач мы разработали три разных способа транслитерации. Кроме того, мы разработали пару классов на языках PHP и ~JavaScript, осуществляющих все эти три способа. Ссылку на этот проект с открытым исходным кодом вы найдёте в самом низу документа.

===URL Translit: Красивый читаемый URL-адрес===

Красивый читаемый адрес нам нужно формировать автоматически для новостей, документов, статей, — как правило, на основе заголовка, формат которого мы условно называем «что-угодно». Т.е. там нам могут встретиться не только недопустимые в URL русские буквы, но ещё и знаки препинания.

Нам нужно добиться от автоматики такого преобразования, чтобы результат легко читался, не вызывая затруднений и недоумений. Такое преобразование мы называем «урл-транслит», а его результат, обычно, ##pretty_url##.

====Постановка задачи====

Из чего-угодно™ получать валидный урл, URL, который легко «прочитать» — понять содержимое, глядя на него в адресной строке или в строке, теле страницы или статус-баре.

Не требуется восстановление исходного чего-угодно™.
====Критерии, которые актуальны для данной задачи====

  простота восприятия букв (например, ##Ц## проще воспринимается как ##ts##, чем как ##c##);
  «похожесть» на ГОСТ (но отступления там, где простота важнее);
  спецсимволы не имеют значения, а разделители (пробелы, например) — имеют;
  разницы между ##Е## и ##Ё## в таких адресах быть не должно;
  все буквы в нижнем регистре, потому что это не вызывает неоднозначности (и, как показал наш опрос — больше нравится людям);
  буквы латинского алфавита не должны пострадать.

====Решение====

Решение реализовано на двух языках программирования (PHP и ~JavaScript), содержится в открытом проекте ((../Translit Translit)).

Метод называется ##~UrlTranslit()##.

===Supertag Translit: Короткий однозначный идентификатор===

Если то, о чём мы говорили раньше, своей первой и наиглавнейшей задачей ставило «понятность» адреса для читателя-человека, то задача номер два — уметь формировать адреса-идентификаторы (т.е. такие, по которым мы будем находить, скажем, в БД ту новость, которую следует показать).

Адрес-идентификатор в виде числа часто недостаточен для читателя-человека (например: http://somesite.ru/page/34722 ), поэтому его можно сопровождать «красивым и понятным» дополнением, которое наш программный механизм будет игнорировать (например, страница с адресом http://www.npj.ru/kuso/259318_dve_trenirovki_podrjad показывает то же, что и http://www.npj.ru/kuso/259318 ).

Однако, часто такое решение недостаточно, — ставится дополнительное условие «никаких цифр». Тогда нам ничего не остаётся, как использовать «красивое и понятное» дополнение в качестве идентификатора. В таком случае нам хочется обезопаситься хотя бы от того, что при переписывании забудут написать подчёркивание ##_## или напишут два подчерка вместо одного. Длину ключа тоже хочется сделать поменьше — дабы хоть немного ускорить поиск/сравнение строк. Такой ключ мы привыкли называть **супертаг** — ##supertag##.

====Постановка задачи====

Из чего-угодно™ получить максимально короткий идентификатор, который не не теряет смысла/однозначности и вместе с тем некритичен к лишним разделителям и большим/малым буквам.

Не требуется восстановление исходного чего-угодно™. 

====Критерии, которые актуальны для данной задачи====

  ##Supertag(~UrlTranslit(x))## должен быть равен ##Supertag(x)##,
  ++иными словами, наше «укорачивающее» преобразование должно выдавать одинаковый результат, как на исходной строке, так и на «красивом URL», полученном из исходной строки;++
  преобразование должно давать как можно меньше случайных совпадений результата (ошибок 1 рода);
  буквы латинского алфавита не должны пострадать.

====Решение====

Решение реализовано на двух языках программирования (PHP и ~JavaScript), содержится в открытом проекте Translit.

Метод называется ##Supertag()##.

===~BiDi Translit: Взаимно-однозначный транслит===

В качестве исторического экскурса можно заметить, что вся эта «эпопея» с транслитерацией началась в своё время с постановки задачи «взаимно-однозначного транслита» — когда возникла необходимость передавать в URL-совместимом формате информацию таким образом, чтобы её можно было однозначно восстановить обратным преобразованием.

Передаваемая информация включала в себя только буквы, цифры, дефис и «слэш» ##/##. Это — именно та функциональность, которая используется в НПЖ при формировании ссылок **на создание новых страниц**. Для формирования «красивых» ссылок на уже существующие страницы используется «урл-транслит», а в БД хранятся «супертаги».

====Постановка задачи====

Из строки, содержащей русские и английские буквы, получать валидный URL (без использования ##%AB##-комбинаций), таким образом, чтобы затем было возможно восстановить оригинальную строку. Также уметь производить и обратную операцию.

В отличие от предыдущих рассмотренных способов здесь требуется восстановление исходной строки (допустима потеря запятых и прочих «слабозначащих» символов).

====Критерии, которые актуальны для данной задачи====

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

====Идея решения====

В отличие от предыдущих способов здесь, кроме банальной таблицы транслитерации (приведённой чуть ниже) использовано некоторое алгоритмически-изобретательское решение, идею которого мы сейчас осветим.

Идея решения заключается в подходе к преобразуемой строке как к чередующейся последовательности кириллических и английских «подстрок»:
#|
|| ##!!(green)~ThisIs!!!!Русский!!!!(green)Name/!!!!~ДляВас!!!!(green)/!!!!Демонстрация!!!!(green)~OfSwitching!!## ||
|#

  !!(green)зелёным!! выделены «английские строки»;
  !!красным!! — кириллические.

Очевидно, что каждая чётная подстрока в этом случае — точно «кириллическая», а каждая нечётная — «англоязычная». Из способа деления на подстроки следует, что в «кириллической» не встречаются буквы ##a-zA-Z##.

Если теперь мы вставим в строку «разграничители» подстрок, то смело можем перевести все «кириллические» строки в алфавит ##a-z## с помощью любого взаимно-однозначного преобразования, например, ГОСТ.

Наш пример тогда будет выглядеть так:
#|
|| ##!!(green)~ThisIs!!+Russkijj+!!(green)Name/!!+!!~DljaVas!!+!!(green)/!!+!!Demonstracija!!+!!(green)~OfSwitching!!## ||
|#

  в качестве «разграничителя» (своеобразного «переключателя языка») мы используем «плюс»;
  обратите внимание, что ##/## считается «англоязычным» символом.

Осталось решить вопрос с теми заглавными русскими буквами, что после транслитерации занимают более одного символа — мы их переводим как ##Ж## — ##Zh##, потому что так «читабельнее». Кроме того, есть некоторый нюанс с твёрдыми и мягкими знаками, пробелом и «экранированием» плюса. Все эти нюансы освещены в сводных таблицах, приведённых ниже.

====Решение====

Решение реализовано на двух языках программирования (PHP и ~JavaScript), содержится в открытом проекте Translit.

Метод называется ##~BiDiTranslit()##.

===Важное примечание===

Все функции/способы транслитерации должны уметь (и умеют) работать в двух режимах:

  «уничтожать» все слэши ##/##, которые встретятся во входной строке;
  пропускать сквозь себя все эти слэши неизменными.

Это необходимо для двух разных способов их использования: а) генерирования «имён папок» в виртуальном «дереве сайта», б) генерирования полных адресов/путей (как это делает WackoWiki или НПЖ).

===Сводные таблицы===

Все правила, по которым производится транслитерация URL-адресов, вынесены нами в отдельный документ ConversionTables, чтобы не затруднять чтение статьи.

===Авторы и благодарности===

Материал для этой статьи подготовлен Романом Ивановым и Кусо Мендокуси:.

Библиотеки ##translit.php## и ##translit.js## написаны Романом Ивановым и Кусо Мендокуси: соответственно и отлажены коллективным разумом. Test suite для обеих библиотек подобран коллективным разумом, а организован Романом Ивановым.

Текст этой статьи написан Кусо Мендокуси:, стёрт Кусо Мендокуси:, заботливо сохранён Романом Ивановым, восстановлен и переписан заново Кусо Мендокуси: и бережно вычитан Романом Ивановым.

Мы очень благодарны нам за весь этот цирк.
Ещё мы благодарны всем, спрашивавшим нас о «взаимно-однозначном транслите».

===Ссылки===
  * RFC 2396. http://www.ietf.org/rfc/rfc2396.txt
  * ГОСТ 16876–71. http://spelling.spb.ru/spell057.htm
  * Опрос стиля ссылок в ваке. http://web.archive.org/web/20060208071822/http://wackowiki.com/WackoIdeas/OprosStiljaSsylok
  * ЧПУ. http://spectator.ru/technology/php/user_friendly_urls
  * Translit PHP+JS Classes. http://pixel-apes.com/translit
  * Таблицы транслитерации. ((ConversionTables ConversionTables))