Unicode – это набор символов, целью которого является определение всех символов и глифов всех человеческих языков, живых и мертвых. Поскольку всё больше и больше программ должны поддерживать несколько языков или просто любой язык, юникод в последние годы приобретает всё большую популярность. Использование различных наборов символов для разных языков может быть слишком обременительным для программистов и пользователей. К сожалению, юникод привносит свои требования и подводные камни, когда речь заходит о регулярных выражениях.  Из всех движков регулярных выражений, обсуждаемых здесь, Java, XML и .NET используют движки поддерживающие юникод. Perl поддерживает юникод, начиная с версии 5.6. PCRE может быть опционально скомпилирован с поддержкой юникод. Обратите внимание, что PCRE гораздо менее гибок в том, что он позволяет делать для \p, несмотря на своё название "Perl-совместимый". Функции PHP preg, основанные на PCRE, поддерживают юникод, если к регулярному выражению добавлена опция /u. Ruby поддерживает категории юникода в регулярных выражениях начиная с версии 1.9. XRegExp привносит поддержку категорий юникода в JavaScript. Regex-движок RegexBuddy полностью совместим с юникодом, начиная с версии 2.0.0. RegexBuddy 1.x.x не поддерживал юникод вообще. PowerGREP использует regex-движок с поддержкой юникода, начиная с версии 3.0.0. Более ранние версии конвертировали Unicode файлы в ANSI перед выполнением поиска с помощью 8-битного (т.е. не-Unicode) regex движка. EditPad Pro поддерживает юникод, начиная с версии 6.0.0.  Большинство людей считают à одним символом. К сожалению, это не так, в зависимости от значения слова "символ". Все regex-движки с поддержкой юникода, обсуждаемые здесь, рассматривают любой отдельный Unicode код как отдельный символ. Когда говорится, что . соответствует любому отдельному символу, это переводится на язык Unicode как ". соответствует любому отдельному коду Unicode". В юникоде символ à может быть представлен через два кода: U+0061 (a), за которым следует U+0300 (гравис). В этой ситуации ., примененная к à, будет соответствовать a без грависа.^.$ не будет соответствовать данной строке, так как она состоит из двух кодов, в то время как ^..$ соответствует. Код U+0300 (гравис) является комбинируемым символом. За любым кодом, который не является комбинируемым символом, может следовать любое количество комбинируемых. Эта последовательность, как и U+0061 U+0300 выше, отображается на экране как одна графема. à также может быть представлен единственным кодом U+00E0 (a с грависом). Причина такой двойственности в том, что многие исторические наборы символов кодируют "a с грависом" как один символ. Разработчики юникода решили, что будет полезно иметь сопоставление один к одному с популярными историческими наборами символов, в дополнение к способу юникода разделять знаки и базовые буквы (что делает возможными произвольные комбинации, не поддерживаемые историческими наборами символов). В Perl, PCRE, PHP, Boost, Ruby 2.0, Java 9 и приложениях Just Great Software сопоставление одной графемы, будь она закодирована как одним кодом или несколькими с помощью комбинируемых знаков, очень просто: просто используйте \X. Вы можете считать \X юникод-версией .. Но есть одно отличие: \X всегда соответствует символам перевода строки, в то время как . не соответствует им, если только вы не включите режим соответствия . новой строке. В .NET, Java 8 и ниже, а также Ruby 1.9 вы можете использовать \P{M}\p{M}*+ или (?>\P{M}\p{M}*) в качестве достаточно близкой замены. Чтобы подобрать любое количество графем, используйте (?>\P{M}\p{M}*)+ как замену \X+. Чтобы сопоставить конкретный код, используйте \uFFFF, где FFFF – шестнадцатеричный код, который вы хотите сопоставить. Вы всегда должны указывать все 4 шестнадцатеричных разряда. Например, \u00E0 соответствует à, при представлении одним кодом U+00E0. Perl, PCRE, Boost и std::regex не поддерживают синтаксис \uFFFF. Вместо него они используют \x{FFFF}. Вы можете опустить ведущие нули в фигурных скобках. Поскольку \x сам по себе не является допустимым символом регулярного выражения, \x{1234} никогда не может быть перепутано с \x 1234 раза. Это всегда соответствует коду U+1234. \x{1234}{5678} будет сопоставляться с кодом U+1234 ровно 5678 раз. В Java \uFFFF соответствует только указанному коду, даже если вы включили каноническую эквивалентность. Однако тот же синтаксис \uFFFF также используется для вставки символов юникода в литеральные строки в исходном коде Java. Pattern.compile("\u00E0") будет соответствовать как однокодовому, так и двухкодовому варианту à, в то время как Pattern.compile("\\u00E0") соответствует только однокодовой версии. Помните, что при записи regex как строкового литерала Java, обратные слэши должны быть экранированы. Первый код Java компилирует regex à, а второй компилирует \u00E0. В зависимости от того, что вы делаете, разница может быть существенной. JavaScript, который не предлагает никакой поддержки Unicode через свой класс RegExp, поддерживает \uFFFF для соответствия одному коду. XML Schema и XPath не имеют regex токенов для сопоставления кодов Unicode. Однако вы можете легко использовать такие XML-сущности, как &#xFFFF; для вставки явных кодов в ваше регулярное выражение.  В дополнение к сложностям, 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 всегда будет соответствовать à, независимо от того, как это закодировано. Possessive квантификатор гарантирует, что поиск с возвратом не приведет к тому, что \P{M}\p{M}*+ будет соответствовать не-знаку без комбинируемых символов, которые следуют за ним, чего \X никогда не сделает. PCRE, PHP и .NET чувствительны к регистру при проверке части между фигурными скобками лексемы \p. \p{Zs} будет соответствовать любому символу пробела, в то время как \p{zs} выдаст ошибку. Все другие regex-движки, описанные здесь, будут соответствовать пробелу в обоих случаях, игнорируя регистр категории между фигурными скобками. Тем не менее, рекомендуется взять за привычку использовать ту же комбинацию прописных и строчных букв, как в списке категорий ниже. Это позволит вашим регулярным выражениям работать со всеми regex-движками поддерживающими Unicode. В дополнение к стандартной нотации \p{L}, Java, Perl, PCRE, движок JGsoft и XRegExp 3 позволяют использовать сокращение \pL. Это сокращение работает только с однобуквенными категориями Unicode. \pLl не является эквивалентом \p{Ll}. Это эквивалент \p{L}l, который соответствует Al или àl или любой букве юникода, за которой следует буква l. Perl, XRegExp и движок JGsoft также поддерживают длиннобуквенный \p{Letter}. Полный список всех категорий Unicode приведен ниже. Вы можете опустить подчёркивания или использовать вместо них дефисы или пробелы.  \p{L} или \p{Letter} – любой вид любой буквы на любом языке. \p{Ll} или \p{Lowercase_Letter} – буква в нижнем регистре, которая имеет вариант в верхнем. * \p{Lu} или \p{Uppercase_Letter} – буква в верхнем регистре, которая имеет вариант в нижнем. * \p{Lt} или \p{Titlecase_Letter} – диграф единым символом (кодом) в начале слова, когда заглавной является только его первая часть. * \p{L&} или \p{Cased_Letter} – буква, которая существует в строчном и прописном вариантах (сочетание Ll, Lu и Lt). \p{Lm} или \p{Modifier_Letter} – специальный символ, который используется как буква. * \p{Lo} или \p{Other_Letter} – буква или идеограмма, не имеющая строчных и прописных вариантов. * \p{M} или \p{Mark} – символ, предназначенный для сочетания с другим символом. \p{Mn} или \p{Non_Spacing_Mark} – символ, предназначенный для сочетания с другим символом, не занимая дополнительного места (например, ударения, умляуты и т.д.). * \p{Mc} или \p{Spacing_Combining_Mark} – символ, предназначенный для сочетания с другим символом, занимающий дополнительное место (как знаки гласных во многих восточных языках). * \p{Me} или \p{Enclosing_Mark} – символ, который окружает символ, с которым он сочетается (круг, квадрат и т.д.). * \p{Z} или \p{Separator} – любые пробельные символы или невидимые разделители. \p{Zs} или \p{Space_Separator} – символ пробела, который невидим, но занимает место. * \p{Zl} или \p{Line_Separator} – разделитель строк U+2028. \p{Zp} или \p{Paragraph_Separator} – разделитель параграфов U+2029. \p{S} или \p{Symbol} – математические символы, знаки валют, дингбаты, символы для рисования и т.д. \p{Sm} или \p{Math_Symbol} – любой математический символ. * \p{Sc} или \p{Currency_Symbol} – любой символ валюты. * \p{Sk} или \p{Modifier_Symbol} – объединяющий символ (знак) как самостоятельный полный символ. * \p{So} или \p{Other_Symbol} – различные символы, не относящиеся к прошлым подкатегориям. * \p{N} или \p{Number} – любой числовой символ любой письменности. \p{Nd} или \p{Decimal_Digit_Number} – цифра от нуля до девяти в любой письменности, кроме идеографической. * \p{Nl} или \p{Letter_Number} – число, которое выглядит как буква, например, римская цифра. * \p{No} или \p{Other_Number} – надстрочная или подстрочная цифра, или число, не являющееся цифрой 0-9 (исключая числа из идеографических письменностей). * \p{P} or \p{Punctuation} – любой символ пунктуации. \p{Pd} или \p{Dash_Punctuation} – любой вид дефиса или тире. * \p{Ps} или \p{Open_Punctuation} – любой вид открывающей скобки. * \p{Pe} или \p{Close_Punctuation} – любой вид закрывающей скобки. * \p{Pi} или \p{Initial_Punctuation} – любой вид открывающей кавычки. * \p{Pf} или \p{Final_Punctuation} – любой вид закрывающей кавычки. * \p{Pc} или \p{Connector_Punctuation} – символ пунктуации, такой как подчёркивание, который соединяет слова. * \p{Po} или \p{Other_Punctuation} – любой знак препинания, не относящийся к прошлым подкатегориям. * \p{C} или \p{Other} – невидимые управляющие символы и неиспользуемые коды. \p{Cc} или \p{Control} – управляющие ASCII или Latin-1 символы: 0x00–0x1F и 0x7F–0x9F. * \p{Cf} или \p{Format} – невидимый индикатор форматирования. * \p{Co} или \p{Private_Use} – любой код, зарезервированный для частного использования. \p{Cs} или \p{Surrogate} – одна половина суррогатной пары в кодировке UTF-16. \p{Cn} или \p{Unassigned} – любой код, которому не присвоен ни один символ. Стандарт Unicode помещает каждый назначенный код (символ) в одну категорию письменности. Некоторые категории, например Thai, соответствуют одному человеческому языку. Другие категории, например Latin, относятся к нескольким языкам. Некоторые языки состоят из нескольких письменностей. Японской письменности в юникоде не существует. Вместо этого юникод предлагает хира́гану, ката́кану, китайское письмо и латиницу, из которых обычно состоят японские документы. Особой категорией является Common script. Здесь содержатся всевозможные символы, которые являются общими для широкого числа письменностей. Категория включает в себя всевозможные знаки препинания, пробелы и различные прочие символы. Все назначенные коды являются частью ровно одной категории письменности. Все неназначенные коды (те, которые соответствуют \p{Cn}) не являются частью какой-либо категории письменности вообще. Движки JGsoft, Perl, PCRE, PHP, Ruby 1.9, Delphi и XRegExp могут сопоставлять категории письменности юникода. \p{Common} \p{Arabic} \p{Armenian} \p{Bengali} \p{Bopomofo} \p{Braille} \p{Buhid} \p{Canadian_Aboriginal} \p{Cherokee} \p{Cyrillic} \p{Devanagari} \p{Ethiopic} \p{Georgian} \p{Greek} \p{Gujarati} \p{Gurmukhi} \p{Han} \p{Hangul} \p{Hanunoo} \p{Hebrew} \p{Hiragana} \p{Inherited} \p{Kannada} \p{Katakana} \p{Khmer} \p{Lao} \p{Latin} \p{Limbu} \p{Malayalam} \p{Mongolian} \p{Myanmar} \p{Ogham} \p{Oriya} \p{Runic} \p{Sinhala} \p{Syriac} \p{Tagalog} \p{Tagbanwa} \p{TaiLe} \p{Tamil} \p{Telugu} \p{Thaana} \p{Thai} \p{Tibetan} \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}, когда вы пытаетесь найти все символы валюты. \p{InBasic_Latin}: U+0000–U+007F  \p{InLatin-1_Supplement}: U+0080–U+00FF  \p{InLatin_Extended-A}: U+0100–U+017F  \p{InLatin_Extended-B}: U+0180–U+024F  \p{InIPA_Extensions}: U+0250–U+02AF  \p{InSpacing_Modifier_Letters}: U+02B0–U+02FF  \p{InCombining_Diacritical_Marks}: U+0300–U+036F  \p{InGreek_and_Coptic}: U+0370–U+03FF  \p{InCyrillic}: U+0400–U+04FF  \p{InCyrillic_Supplementary}: U+0500–U+052F  \p{InArmenian}: U+0530–U+058F  \p{InHebrew}: U+0590–U+05FF  \p{InArabic}: U+0600–U+06FF  \p{InSyriac}: U+0700–U+074F  \p{InThaana}: U+0780–U+07BF  \p{InDevanagari}: U+0900–U+097F  \p{InBengali}: U+0980–U+09FF  \p{InGurmukhi}: U+0A00–U+0A7F  \p{InGujarati}: U+0A80–U+0AFF  \p{InOriya}: U+0B00–U+0B7F  \p{InTamil}: U+0B80–U+0BFF  \p{InTelugu}: U+0C00–U+0C7F  \p{InKannada}: U+0C80–U+0CFF  \p{InMalayalam}: U+0D00–U+0D7F  \p{InSinhala}: U+0D80–U+0DFF  \p{InThai}: U+0E00–U+0E7F  \p{InLao}: U+0E80–U+0EFF  \p{InTibetan}: U+0F00–U+0FFF  \p{InMyanmar}: U+1000–U+109F  \p{InGeorgian}: U+10A0–U+10FF  \p{InHangul_Jamo}: U+1100–U+11FF  \p{InEthiopic}: U+1200–U+137F  \p{InCherokee}: U+13A0–U+13FF  \p{InUnified_Canadian_Aboriginal_Syllabics}: U+1400–U+167F  \p{InOgham}: U+1680–U+169F  \p{InRunic}: U+16A0–U+16FF  \p{InTagalog}: U+1700–U+171F  \p{InHanunoo}: U+1720–U+173F  \p{InBuhid}: U+1740–U+175F  \p{InTagbanwa}: U+1760–U+177F  \p{InKhmer}: U+1780–U+17FF  \p{InMongolian}: U+1800–U+18AF  \p{InLimbu}: U+1900–U+194F  \p{InTai_Le}: U+1950–U+197F  \p{InKhmer_Symbols}: U+19E0–U+19FF  \p{InPhonetic_Extensions}: U+1D00–U+1D7F  \p{InLatin_Extended_Additional}: U+1E00–U+1EFF  \p{InGreek_Extended}: U+1F00–U+1FFF  \p{InGeneral_Punctuation}: U+2000–U+206F  \p{InSuperscripts_and_Subscripts}: U+2070–U+209F  \p{InCurrency_Symbols}: U+20A0–U+20CF  \p{InCombining_Diacritical_Marks_for_Symbols}: U+20D0–U+20FF  \p{InLetterlike_Symbols}: U+2100–U+214F  \p{InNumber_Forms}: U+2150–U+218F  \p{InArrows}: U+2190–U+21FF  \p{InMathematical_Operators}: U+2200–U+22FF  \p{InMiscellaneous_Technical}: U+2300–U+23FF  \p{InControl_Pictures}: U+2400–U+243F  \p{InOptical_Character_Recognition}: U+2440–U+245F  \p{InEnclosed_Alphanumerics}: U+2460–U+24FF  \p{InBox_Drawing}: U+2500–U+257F  \p{InBlock_Elements}: U+2580–U+259F  \p{InGeometric_Shapes}: U+25A0–U+25FF  \p{InMiscellaneous_Symbols}: U+2600–U+26FF  \p{InDingbats}: U+2700–U+27BF  \p{InMiscellaneous_Mathematical_Symbols-A}: U+27C0–U+27EF  \p{InSupplemental_Arrows-A}: U+27F0–U+27FF  \p{InBraille_Patterns}: U+2800–U+28FF  \p{InSupplemental_Arrows-B}: U+2900–U+297F  \p{InMiscellaneous_Mathematical_Symbols-B}: U+2980–U+29FF  \p{InSupplemental_Mathematical_Operators}: U+2A00–U+2AFF  \p{InMiscellaneous_Symbols_and_Arrows}: U+2B00–U+2BFF  \p{InCJK_Radicals_Supplement}: U+2E80–U+2EFF  \p{InKangxi_Radicals}: U+2F00–U+2FDF  \p{InIdeographic_Description_Characters}: U+2FF0–U+2FFF  \p{InCJK_Symbols_and_Punctuation}: U+3000–U+303F  \p{InHiragana}: U+3040–U+309F  \p{InKatakana}: U+30A0–U+30FF  \p{InBopomofo}: U+3100–U+312F  \p{InHangul_Compatibility_Jamo}: U+3130–U+318F  \p{InKanbun}: U+3190–U+319F  \p{InBopomofo_Extended}: U+31A0–U+31BF  \p{InKatakana_Phonetic_Extensions}: U+31F0–U+31FF  \p{InEnclosed_CJK_Letters_and_Months}: U+3200–U+32FF  \p{InCJK_Compatibility}: U+3300–U+33FF  \p{InCJK_Unified_Ideographs_Extension_A}: U+3400–U+4DBF  \p{InYijing_Hexagram_Symbols}: U+4DC0–U+4DFF  \p{InCJK_Unified_Ideographs}: U+4E00–U+9FFF  \p{InYi_Syllables}: U+A000–U+A48F  \p{InYi_Radicals}: U+A490–U+A4CF  \p{InHangul_Syllables}: U+AC00–U+D7AF  \p{InHigh_Surrogates}: U+D800–U+DB7F  \p{InHigh_Private_Use_Surrogates}: U+DB80–U+DBFF  \p{InLow_Surrogates}: U+DC00–U+DFFF  \p{InPrivate_Use_Area}: U+E000–U+F8FF  \p{InCJK_Compatibility_Ideographs}: U+F900–U+FAFF  \p{InAlphabetic_Presentation_Forms}: U+FB00–U+FB4F  \p{InArabic_Presentation_Forms-A}: U+FB50–U+FDFF  \p{InVariation_Selectors}: U+FE00–U+FE0F  \p{InCombining_Half_Marks}: U+FE20–U+FE2F  \p{InCJK_Compatibility_Forms}: U+FE30–U+FE4F  \p{InSmall_Form_Variants}: U+FE50–U+FE6F  \p{InArabic_Presentation_Forms-B}: U+FE70–U+FEFF  \p{InHalfwidth_and_Fullwidth_Forms}: U+FF00–U+FFEF  \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 🟢 Краткая история календаря и фантазии о шестидневной неделе 🟢 Пройти LeetCode за год: экскурсия по сайту и roadmap 🟢 Абсолютная мультиязычность и типографика на любой раскладке 🟢 Фонетическая раскладка на основе анализов частотности 🔵 Частотный биграммный анализ на Python   Python developer