Lemmy Lemmy 25 листопада 2023, 00:15

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

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


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

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

( ͡❛ ͜ʖ ͡❛) Text Art Generator ✅ Ⓒⓞⓟⓨ 𝒜𝓃𝒹 𝓟𝓪𝓼𝓽𝓮 ASCII Art textartgenerator.net

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

При­клад:

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

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

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

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

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

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

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

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

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

Regex Tutorial - Unicode Characters and Properties regular-expressions.info

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

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

Тому нам потрі­бно само­стій­но опи­са­ти номе­ри UTF8 сим­во­лів які потім пере­тво­ря­ться на 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 для очис­тки текс­ту від неба­жа­них UTF8 сим­во­лів вигля­дає так (код на 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);
}

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

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

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

Висно­вок

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

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

  • 6
  • 15
  • 658

15 коментарів

  • Triqnot Triqnot 2 роки тому

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

    • 1
    • Triqnot Triqnot 2 роки тому

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

      • 1
    • Lemmy Lemmy 2 роки тому · Автор

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

      • 1
    • Triqnot Triqnot 2 роки тому

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

      • 1
    • Triqnot Triqnot 2 роки тому

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

      • 1
    • Lemmy Lemmy 2 роки тому · Автор

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

      • 1
    • Triqnot Triqnot 2 роки тому

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

      • 1
    • Triqnot Triqnot 2 роки тому

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

      • 2
    • Lemmy Lemmy 2 роки тому · Автор

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

      • 1
    • Triqnot Triqnot 2 роки тому

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

      • 2