Работа с Unicode в регулярных выражениях: графемы, категории, письменности

от автора

Unicode – это набор символов, целью которого является определение всех символов и глифов всех человеческих языков, живых и мертвых. Поскольку всё больше и больше программ должны поддерживать несколько языков или просто любой язык, юникод в последние годы приобретает всё большую популярность. Использование различных наборов символов для разных языков может быть слишком обременительным для программистов и пользователей.

К сожалению, юникод привносит свои требования и подводные камни, когда речь заходит о регулярных выражениях. Из всех движков регулярных выражений, обсуждаемых здесь, Java, XML и .NET используют движки поддерживающие юникод. Perl поддерживает юникод, начиная с версии 5.6. будет соответствовать как однокодовому, так и двухкодовому варианту à, в то время как Pattern.compile("\\u00E0") соответствует только однокодовой версии. Помните, что при записи regex как строкового литерала Java, обратные слэши должны быть экранированы. Первый код Java компилирует regex à, а второй компилирует \u00E0. В зависимости от того, что вы делаете, разница может быть существенной.

JavaScript, который не предлагает никакой поддержки Unicode через свой класс RegExp, поддерживает \uFFFF для соответствия одному коду.

XML Schema и XPath не имеют regex токенов для сопоставления кодов Unicode. Однако вы можете легко использовать такие XML-сущности, как  для вставки явных кодов в ваше регулярное выражение.

Категории в Юникоде

В дополнение к сложностям, Unicode также приносит и новые возможности. Одна из них заключается в том, что каждый символ юникода принадлежит к определенной категории. Вы можете сопоставить один символ, принадлежащий к категории «буква», с \p{L}, или не принадлежащий к ней, с \P{L}.

Опять же, «символ» на самом деле означает «код юникод-символа». \p{L} соответствует одному коду в категории «буква». Если входная строка à закодирована как U+0061 U+0300, она соответствует a без грависа. Если входная строка à закодирована как U+00E0, она соответствует à с грависом. Причина в том, что коды U+0061 (a) и U+00E0 (à) относятся к категории «буква», а U+0300 – к категории «знак».

Теперь вы должны понять, почему \P{M}\p{M}*+ эквивалентно \X. \P{M} соответствует коду, который не является комбинируемым знаком, в то время как \p{M}*+ соответствует нулю или более кодов, которые являются комбинируемыми. Чтобы найти букву, включая все диакритические знаки, используйте \p{L}\p{M}*+. Последний regex всегда будет соответствовать à, независимо от того, как это закодировано.

Список письменностей
  1. \p{Common}

  2. \p{Arabic}

  3. \p{Armenian}

  4. \p{Bengali}

  5. \p{Bopomofo}

  6. \p{Braille}

  7. \p{Buhid}

  8. \p{Canadian_Aboriginal}

  9. \p{Cherokee}

  10. \p{Cyrillic}

  11. \p{Devanagari}

  12. \p{Ethiopic}

  13. \p{Georgian}

  14. \p{Greek}

  15. \p{Gujarati}

  16. \p{Gurmukhi}

  17. \p{Han}

  18. \p{Hangul}

  19. \p{Hanunoo}

  20. \p{Hebrew}

  21. \p{Hiragana}

  22. \p{Inherited}

  23. \p{Kannada}

  24. \p{Katakana}

  25. \p{Khmer}

  26. \p{Lao}

  27. \p{Latin}

  28. \p{Limbu}

  29. \p{Malayalam}

  30. \p{Mongolian}

  31. \p{Myanmar}

  32. \p{Ogham}

  33. \p{Oriya}

  34. \p{Runic}

  35. \p{Sinhala}

  36. \p{Syriac}

  37. \p{Tagalog}

  38. \p{Tagbanwa}

  39. \p{TaiLe}

  40. \p{Tamil}

  41. \p{Telugu}

  42. \p{Thaana}

  43. \p{Thai}

  44. \p{Tibetan}

  45. \p{Yi}

Perl и JGsoft позволяют использовать \p{IsLatin} вместо \p{Latin}. Синтаксис Is полезен для различения письменностей и блоков, как объясняется в следующем разделе. PCRE, PHP и XRegExp не поддерживают префикс Is.

Java 7 добавляет поддержку категорий письменностей. В отличие от прочих, Java 7 требует префикс Is.

Блоки Юникода

Стандарт Unicode делит карту символов на различные блоки или диапазоны кодов. Каждый блок используется для определения символов конкретной письменности, например, «тибетское письмо» или относящихся к определенной группе, например, «шрифт Брайля». Большинство блоков включают нераспределённые коды, зарезервированные для будущего расширения стандарта.

Обратите внимание, что блоки юникода не совпадают на 100% с категориями письменностей. Существенное различие между блоками и данными категориями заключается в том, что блок – это один непрерывный диапазон кодов, как указано ниже. Категории письменностей состоят из символов, взятых со всей карты символов юникода. Блоки могут включать неназначенные коды (т.е. коды, соответствующие \p{Cn}). Категории письменностей никогда не включают неназначенные коды. Обычно, если вы не уверены, использовать ли категорию письменности или блок юникода, используйте категорию письменности.

Например, блок Currency не включает символы доллара и йены. Вместо этого они находятся в блоках Basic_Latin и Latin-1_Supplement, несмотря на то, что оба символа являются валютными, а символ йены не является латинским символом. Это объясняется историческими причинами, поскольку стандарт ASCII включает знак доллара, а стандарт ISO-8859 – знак йены. Не следует слепо использовать любой из перечисленных ниже блоков, основываясь на их названиях. Вместо этого посмотрите на диапазоны символов, которым они фактически соответствуют. В этом может помочь такой инструмент, как RegexBuddy. Категория \p{Sc} или \p{Currency_Symbol} будет лучшим выбором, чем блок \p{InCurrency_Symbols}, когда вы пытаетесь найти все символы валюты.

Блоки юникода
  1. \p{InBasic_Latin}: U+0000–U+007F

  2. \p{InLatin-1_Supplement}: U+0080–U+00FF

  3. \p{InLatin_Extended-A}: U+0100–U+017F

  4. \p{InLatin_Extended-B}: U+0180–U+024F

  5. \p{InIPA_Extensions}: U+0250–U+02AF

  6. \p{InSpacing_Modifier_Letters}: U+02B0–U+02FF

  7. \p{InCombining_Diacritical_Marks}: U+0300–U+036F

  8. \p{InGreek_and_Coptic}: U+0370–U+03FF

  9. \p{InCyrillic}: U+0400–U+04FF

  10. \p{InCyrillic_Supplementary}: U+0500–U+052F

  11. \p{InArmenian}: U+0530–U+058F

  12. \p{InHebrew}: U+0590–U+05FF

  13. \p{InArabic}: U+0600–U+06FF

  14. \p{InSyriac}: U+0700–U+074F

  15. \p{InThaana}: U+0780–U+07BF

  16. \p{InDevanagari}: U+0900–U+097F

  17. \p{InBengali}: U+0980–U+09FF

  18. \p{InGurmukhi}: U+0A00–U+0A7F

  19. \p{InGujarati}: U+0A80–U+0AFF

  20. \p{InOriya}: U+0B00–U+0B7F

  21. \p{InTamil}: U+0B80–U+0BFF

  22. \p{InTelugu}: U+0C00–U+0C7F

  23. \p{InKannada}: U+0C80–U+0CFF

  24. \p{InMalayalam}: U+0D00–U+0D7F

  25. \p{InSinhala}: U+0D80–U+0DFF

  26. \p{InThai}: U+0E00–U+0E7F

  27. \p{InLao}: U+0E80–U+0EFF

  28. \p{InTibetan}: U+0F00–U+0FFF

  29. \p{InMyanmar}: U+1000–U+109F

  30. \p{InGeorgian}: U+10A0–U+10FF

  31. \p{InHangul_Jamo}: U+1100–U+11FF

  32. \p{InEthiopic}: U+1200–U+137F

  33. \p{InCherokee}: U+13A0–U+13FF

  34. \p{InUnified_Canadian_Aboriginal_Syllabics}: U+1400–U+167F

  35. \p{InOgham}: U+1680–U+169F

  36. \p{InRunic}: U+16A0–U+16FF

  37. \p{InTagalog}: U+1700–U+171F

  38. \p{InHanunoo}: U+1720–U+173F

  39. \p{InBuhid}: U+1740–U+175F

  40. \p{InTagbanwa}: U+1760–U+177F

  41. \p{InKhmer}: U+1780–U+17FF

  42. \p{InMongolian}: U+1800–U+18AF

  43. \p{InLimbu}: U+1900–U+194F

  44. \p{InTai_Le}: U+1950–U+197F

  45. \p{InKhmer_Symbols}: U+19E0–U+19FF

  46. \p{InPhonetic_Extensions}: U+1D00–U+1D7F

  47. \p{InLatin_Extended_Additional}: U+1E00–U+1EFF

  48. \p{InGreek_Extended}: U+1F00–U+1FFF

  49. \p{InGeneral_Punctuation}: U+2000–U+206F

  50. \p{InSuperscripts_and_Subscripts}: U+2070–U+209F

  51. \p{InCurrency_Symbols}: U+20A0–U+20CF

  52. \p{InCombining_Diacritical_Marks_for_Symbols}: U+20D0–U+20FF

  53. \p{InLetterlike_Symbols}: U+2100–U+214F

  54. \p{InNumber_Forms}: U+2150–U+218F

  55. \p{InArrows}: U+2190–U+21FF

  56. \p{InMathematical_Operators}: U+2200–U+22FF

  57. \p{InMiscellaneous_Technical}: U+2300–U+23FF

  58. \p{InControl_Pictures}: U+2400–U+243F

  59. \p{InOptical_Character_Recognition}: U+2440–U+245F

  60. \p{InEnclosed_Alphanumerics}: U+2460–U+24FF

  61. \p{InBox_Drawing}: U+2500–U+257F

  62. \p{InBlock_Elements}: U+2580–U+259F

  63. \p{InGeometric_Shapes}: U+25A0–U+25FF

  64. \p{InMiscellaneous_Symbols}: U+2600–U+26FF

  65. \p{InDingbats}: U+2700–U+27BF

  66. \p{InMiscellaneous_Mathematical_Symbols-A}: U+27C0–U+27EF

  67. \p{InSupplemental_Arrows-A}: U+27F0–U+27FF

  68. \p{InBraille_Patterns}: U+2800–U+28FF

  69. \p{InSupplemental_Arrows-B}: U+2900–U+297F

  70. \p{InMiscellaneous_Mathematical_Symbols-B}: U+2980–U+29FF

  71. \p{InSupplemental_Mathematical_Operators}: U+2A00–U+2AFF

  72. \p{InMiscellaneous_Symbols_and_Arrows}: U+2B00–U+2BFF

  73. \p{InCJK_Radicals_Supplement}: U+2E80–U+2EFF

  74. \p{InKangxi_Radicals}: U+2F00–U+2FDF

  75. \p{InIdeographic_Description_Characters}: U+2FF0–U+2FFF

  76. \p{InCJK_Symbols_and_Punctuation}: U+3000–U+303F

  77. \p{InHiragana}: U+3040–U+309F

  78. \p{InKatakana}: U+30A0–U+30FF

  79. \p{InBopomofo}: U+3100–U+312F

  80. \p{InHangul_Compatibility_Jamo}: U+3130–U+318F

  81. \p{InKanbun}: U+3190–U+319F

  82. \p{InBopomofo_Extended}: U+31A0–U+31BF

  83. \p{InKatakana_Phonetic_Extensions}: U+31F0–U+31FF

  84. \p{InEnclosed_CJK_Letters_and_Months}: U+3200–U+32FF

  85. \p{InCJK_Compatibility}: U+3300–U+33FF

  86. \p{InCJK_Unified_Ideographs_Extension_A}: U+3400–U+4DBF

  87. \p{InYijing_Hexagram_Symbols}: U+4DC0–U+4DFF

  88. \p{InCJK_Unified_Ideographs}: U+4E00–U+9FFF

  89. \p{InYi_Syllables}: U+A000–U+A48F

  90. \p{InYi_Radicals}: U+A490–U+A4CF

  91. \p{InHangul_Syllables}: U+AC00–U+D7AF

  92. \p{InHigh_Surrogates}: U+D800–U+DB7F

  93. \p{InHigh_Private_Use_Surrogates}: U+DB80–U+DBFF

  94. \p{InLow_Surrogates}: U+DC00–U+DFFF

  95. \p{InPrivate_Use_Area}: U+E000–U+F8FF

  96. \p{InCJK_Compatibility_Ideographs}: U+F900–U+FAFF

  97. \p{InAlphabetic_Presentation_Forms}: U+FB00–U+FB4F

  98. \p{InArabic_Presentation_Forms-A}: U+FB50–U+FDFF

  99. \p{InVariation_Selectors}: U+FE00–U+FE0F

  100. \p{InCombining_Half_Marks}: U+FE20–U+FE2F

  101. \p{InCJK_Compatibility_Forms}: U+FE30–U+FE4F

  102. \p{InSmall_Form_Variants}: U+FE50–U+FE6F

  103. \p{InArabic_Presentation_Forms-B}: U+FE70–U+FEFF

  104. \p{InHalfwidth_and_Fullwidth_Forms}: U+FF00–U+FFEF

  105. \p{InSpecials}: U+FFF0–U+FFFF

Не все regex-движки с поддержкой юникода используют одинаковый синтаксис для сопоставления блоков. Java, Ruby 2.0 и XRegExp используют синтаксис \p{InBlock}, как указано выше. В .NET и XML вместо этого используется \p{IsBlock}. Perl и JGsoft поддерживают обе нотации. Рекомендуется использовать нотацию In, если ваш regex-движок поддерживает её. In можно использовать только для блоков Unicode, в то время как Is можно использовать для общих категорий и категорий письменностей, в зависимости от используемого вами шаблона регулярных выражений. При использовании In очевидно, что вы сопоставляете блок, а не общую категорию или письменность с аналогичным названием.

В .NET и XML вы должны опустить подчёркивание, но сохранить дефисы в именах блоков. Например, используйте \p{IsLatinExtended-A} вместо \p{InLatin_Extended-A}. В Java вы должны опустить дефисы. .NET и XML также сравнивают имена с учетом регистра, в то время как Perl, Ruby и JGsoft сравнивают их без учета регистра. Java 4 чувствительна к регистру. Java 5 и более поздние версии учитывают регистр для префикса Is, но не для самих имен блоков.

Фактические имена блоков одинаковы во всех движках регулярных выражений. Имена блоков определены в стандарте Unicode. PCRE и PHP не поддерживают блоки, хотя они поддерживают категории письменностей.

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

Хотя вы всегда должны помнить о подводных камнях, создаваемых различными способами кодирования комбинированных символов, вам не всегда нужно беспокоиться об этом. Если вы знаете, что ваша входная строка и ваше регулярное выражение используют один и тот же подход, то вам вообще не нужно беспокоиться об этом. Этот процесс называется нормализацией Unicode. Все языки программирования со встроенной поддержкой Unicode, такие как Java, C# и VB.NET, имеют функции библиотек для нормализации строк. Если вы нормализуете и объект, и регулярное выражение перед попыткой сопоставления, никаких несоответствий не возникнет.

Если вы используете Java, вы можете передать флаг CANON_EQ в качестве второго параметра в Pattern.compile(). Это указывает движку регулярных выражений Java считать канонически эквивалентные символы идентичными. Символ à, закодированный как U+00E0, соответствует à, закодированному как U+0061 U+0300, и наоборот. Ни один из других regex-движков в настоящее время не поддерживает каноническую эквивалентность при сопоставлении.

Если набрать на клавиатуре клавишу à, все известные нам текстовые процессоры вставят в файл код U+00E0. Таким образом, если вы работаете с текстом, который вы набрали сами, любое регулярное выражение, который вы набрали сами, будет соответствовать таким же образом.

Наконец, если вы используете PowerGREP для поиска в текстовых файлах, закодированных с помощью традиционной кодовой страницы Windows (часто называемой «ANSI») или ISO-8859, PowerGREP всегда использует подстановку один к одному. Поскольку все кодовые страницы Windows или ISO-8859 представляют комбинированные символы одним кодом, почти все программы используют один код для каждого символа при преобразовании файла в юникод.

Нашли опечатку или неточность в переводе? Выделите и нажмите CTRL/⌘+Enter


ссылка на оригинал статьи https://habr.com/ru/post/713256/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *