Створити

Як зробити UTF8 безпечним для відображення

Нещодавно @javascript показав мені що окрім банального екранування данних потрібно думати ще і про UTF8 символи, якими можно надати тексту кастомний вигляд.


Не зміг знайти точної назви для такої техніки, але на сайтах-генераторах це звесться “текст символами”, або “генерація надписів із символів”, або “ASCII art”.

Для прикладу наведу один з генераторів:

До певного моменту ці символи виглядають прикольно, але коли ними зловживати то це ломає зовнішній вид інтерфейсу та створює враження що сайт зламаний.

Приклад:

Як боротися з ASCII-артом, якщо символи ломають зовнішний вигляд?

Правильна відповідь – треба обмежити використання UTF-8 символів. Залишити лише символи, які входять до алфафіту мови, цифри, пунктуацію та емоджі.

Можна використати регулярні вирази – ідеальний інструмент для роботи з текстом.

Задача виглядає простою. Але коли я почав експерементувати з текстом, виявилося що треба додати в “білий список” абсалютно всі символи котрими я користуюся, а ще потенційно багато симіолів про які я й не здогадуюсь, але вони все одно потрібні. Це забагацько роботи.

На допомогу приходять спеціальні Regex-групи UTF-8 символів. Для зручності симфоли згрупували та надали їм назви, тож їх тепер можно не описувати в Regex-виразі посимвольно, а просто вказати назву групи.

Які є групи Regex-символів?

Є основні групи UTF-8 символів котрі ще діляться на підгрупи. Далі йде список, який складається з двох рівнів. Перший рівень – велика група символів, підрівень – підгрупа символів.

  • \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} або \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}: будь-яка кодова точка, якій не призначено жодного символу.

Ще є додаткові групи, які спецефічні до конкретних мов, повний список можно переглянути на сторінці:


А як бути з Emoji? Як написати regex який буде відловлювати смайлики?

Нажаль, спеціальної regex-групи для Emoji поки не створено. В окремих реалізаціях Regex такі групи існують, але в PHP нажаль ні.

Тому нам потрібно самостійно описати номери UTF-8 символів які потім перетворяться на Emoji.

Я загуглив ці послідовності і просто залишу їх тут:

\x{1F600}-\x{1F64F}\x{2700}-\x{27BF}\x{1F680}-\x{1F6FF}\x{24C2}-\x{1F251}\x{1F30D}-\x{1F567}\x{1F900}-\x{1F9FF}\x{1F300}-\x{1F5FF}

Повний Regex для очистки тексту від небажаних UTF-8 символів виглядає так (код на PHP):

function sanitizeString(string $string) {
    return preg_replace('/[^\p{L}\p{Z}\p{S}\p{N}\p{P}\p{C}\x{1F600}-\x{1F64F}\x{2700}-\x{27BF}\x{1F680}-\x{1F6FF}\x{24C2}-\x{1F251}\x{1F30D}-\x{1F567}\x{1F900}-\x{1F9FF}\x{1F300}-\x{1F5FF}]/u', '', $string);
}

Цей регулярний вираз залишає наступні символи:

  • будь-які літери з будь-якої мови,
  • будь-які пробіли або невидимі роздільники,
  • математичні символи, грошові знаки, динбати, символи для малювання коробок тощо,
  • будь-які цифрові символи у будь-якому шрифті,
  • невидимі контрольні символи і невикористані кодові точки,
  • будь-які знаки пунктуації,
  • емоджі.

Інші символи вирізаються.

Висновок

Я розказав про актуальну проблему з UTF-8 символами, які ломають відображення тексту. Ми розібрали які групи UTF-8 символів існують, а на прикінці я поділився регулірним виразом для очистки UTF-8 текста від небажаних символів.

Сподіваюся, статя була інформативною і у вас є запитання – з ними у коментарі.

15 коментарів

Увійдіть, щоб коментувати.
2 бали
Поскаржитись

Вау, дуже прикольно

1 бал
Поскаржитись

тож буквально хвилину тому у шапці було якесь непунятне "privet", а зараз вже виправили, не?

2 бали
Поскаржитись

@lemmy там багато всього, для прикладу bezpecnim можна ж було написати через ch, а vidobrazennia через zh, це давно вже використовується, то дійсно якийсь дивний конвертор

2 бали
Поскаржитись

@lemmy брешу, наведу, раніше призвища, яки закінчувалися на "ий" писалися як "yy" типу "gorobetskyy", але за новими правилами пишуть "yi" як "gorobetskyi", що технічно (посимвольно) має читатися як "горобецькиі"

1 бал
Поскаржитись

@triqnot Якщо брати трансліт який транслітерує паспортні дані, то такий трансліт певно має зворотнью конвертацію. Звідти я і дізнався про те як траслітирувати "привіт". Але той же самий трансліт, який використовується на цьому сайті (генерує Slug з заголовку) – транслітерує як "privit" (перевірив). Можливо, треба його замінити на більш точний.

1 бал
Поскаржитись

@lemmy прикладів не наведу, але не виключаю можливих проблем у складних випадках, але ж тут +- усе однозначно

1 бал
Поскаржитись

@triqnot Якщо не помиляюся, трансліти не завжди мають властивість зворотньої конвертації. Часто бачив трансліти які зʼїдали символи, або використовували одні й ті самі символи для споріднених звуків. Тож логіка тут не спрацювала.

1 бал
Поскаржитись

@himarstime я шось не вкурюю, ніби хештег "Kyiv not Kiev" пройшов повз усю комьюніті

1 бал
Поскаржитись

@lemmy але ж в цьому випадку мала би спрацювати програмістська логіка, бо якщо "і" передається як "і" і "и" також як "і", то як тоді розрізняти при читанні транслітерації?

1 бал
Поскаржитись

@triqnot Я подумав, що "и" англійською записується як "i". Але пошукав трансліти – дійсно для "и" є інша транслітирація. Правильно буде "pryvit". Не знав, запам'ятаю.

1 бал
Поскаржитись

@triqnot дай англійську літеру и

1 бал
Поскаржитись

@lemmy та я тікі хтів почати скаржитись. Але ж ми не кажимо "прівіт", чи то якись Український Творчий Формат, скорочено УТФ?

1 бал
Поскаржитись

@triqnot Спіймали мене! Так, здалося що можно зробити кращу картинку. :)

1 бал
Поскаржитись

@javascript Для тебе є інформація. :)