Čištění textu, který přišel odjinud
Text, který přišel z PDF, z buňky tabulky, z pole v CMS, z e-mailového klienta nebo z chatovací aplikace, s sebou nese formátovací rozhodnutí toho, co ho vyrobilo, a téměř žádné z těch rozhodnutí není na obrazovce vidět. Výsledkem je řetězec, který vypadá správně, správně se vytiskne a pak bez zjevného důvodu selže při porovnání, hledání, parsování JSON nebo vyhledání v databázi.
Znaky, které nevidíte
Pro Unicode jsou to plnohodnotné znaky. Jen nemají viditelný tvar, nebo mají stejný tvar jako něco jiného.
| Znak | Kódový bod | Odkud obvykle přichází | Co rozbije |
|---|---|---|---|
| Nezlomitelná mezera | U+00A0 | v HTML, Word, sazba PDF | dělení podle mezery, přesné shody |
| Úzká nezlomitelná mezera | U+202F | francouzská typografie, data z Wordu | totéž, jen méně nápadně |
| Ideografická mezera | U+3000 | vstupní metody CJK | vypadá jako široká mezera |
| Mezera nulové šířky | U+200B | nápovědy pro zalomení v CMS, kopírovaný text z webu | neviditelná a není to bílý znak |
| Nespojovník / spojovník nulové šířky | U+200C, U+200D | perština, indická písma, sekvence emoji | hranice slov, počty znaků |
| Spojovač slov | U+2060 | sázecí nástroje | nic viditelného, všechno textové |
| Měkký spojovník | U+00AD | dělení slov ve Wordu, exporty do PDF | slovo přestane odpovídat samo sobě |
| Byte order mark | U+FEFF | první bajty souboru v UTF-8 | první pole prvního řádku |
| Značka zleva doprava / zprava doleva | U+200E, U+200F | obousměrný text | zatoulané značky kolem čísel |
| Oddělovač řádku a odstavce | U+2028, U+2029 | některé aplikace na Macu a sázecí programy | dělení na řádky, starší parsery JavaScriptu |
Hledání selže proto, že porovnává kódové body, ne tvary. Když vám PDF dalo New U+00A0 York a vy napíšete New York s obyčejnou mezerou U+0020, jsou ty dva řetězce různé a nic vám neřekne proč.
"New York".includes("New York") false, the gap is U+00A0
" text".trim().length 5, the zero-width space survives
Nezlomitelná mezera se pro většinu funkcí trim a pro \s ve většině regexových enginů počítá jako bílý znak, takže na okrajích řetězce zmizí a uprostřed přežije, přesně tam, kde dělení očekává obyčejnou mezeru. Mezera nulové šířky není bílým znakem nikde, takže ji ořezání ani slučování nechají být. Právě proto může Email Extractor z textu zkopírovaného z poštovního klienta nevrátit nic: jediný znak nulové šířky uvnitř adresy zabrání tomu, aby vzor odpovídal.
Hidden Character Detector ukáže, co tam doopravdy je, a Unicode Escape vypíše přesné kódové body jedné hodnoty. Nemažte ale všechno, na co narazíte: spojovník nulové šířky uvnitř sekvence emoji a nespojovník nulové šířky v perštině nebo dévanágarí jsou obsah, ne šum.
Interpunkce, kterou vám změnil textový editor
Automatické opravy nahrazují znaky typografickými už při psaní a tato náhrada odejde s textem i z dokumentu.
| Napsali jste | Teď máte | Kódový bod |
|---|---|---|
' | pravou jednoduchou uvozovku | U+2019 |
" a " | levou a pravou dvojitou uvozovku | U+201C, U+201D |
- mezi slovy | pomlčku nebo dlouhou pomlčku | U+2013, U+2014 |
... | výpustku | U+2026 |
- před číslem | znak minus nebo nezlomitelný spojovník | U+2212, U+2011 |
V próze je to v pořádku, ve všem, co parsuje stroj, je to smrtelné.
{ “name”: “Ada” } unexpected token, only U+0022 is a JSON string quote
git commit -m “fix” the shell sees three words, not one quoted argument
WHERE name = ‘Ada’ SQL syntax error near ‘
10\u201320 not a number range, U+2013 is not a hyphen
V CSV je selhání tišší. Parser uznává jako oddělovač pole jen rovnou dvojitou uvozovku, takže hodnota, kterou textový editor obalil kudrnatými uvozovkami, se považuje za neuvozenou; každá čárka uvnitř ní pak řádek rozdělí a všechny sloupce za ní se posunou. Sloupec, jehož záporná čísla používají U+2212, se naimportuje jako text a součty ho tiše vynechají.
Řešením je Find and Replace s krátkým seznamem náhrad, ale používejte ho jen na text, který míří do kódu, CSV nebo klíče. Když ho pustíte na prózu, kterou publikujete, zplošťí interpunkci, která tam byla záměrně.
Konce řádků a bílé znaky na konci
Nástroje na Windows ukončují řádek dvojicí CR LF (U+000D U+000A), unixové nástroje samotným LF a pár velmi starých exportů z Macu stále používá samotné CR. Většina parserů si poradí. Naivní dělení ne.
"UK\r\n" split on "\n" -> "UK\r"
"UK\r" === "UK" false
"UK\r".length 3
Dva řetězce, které se v tabulce, logu nebo v zobrazení rozdílů vykreslí stejně, se pořád mohou lišit o návrat vozíku, mezeru na konci nebo tabulátor. Když Text Diff označí řádek jako změněný a žádná změna není vidět, je to právě tohle. Je to také to, co vloží zatoulanou mezeru dovnitř uvozovek, když vložený sloupec projde nástrojem Quote Lines nebo Join Lines.
Normalizujte jedním průchodem, kdy se CR LF i osamocené CR zachytí najednou (\r\n? nahrazeno za \n), jinak dvoukroková náhrada zdvojí prázdné řádky.
Jedno písmeno, dva způsoby zápisu
Unicode dovoluje, aby písmeno s diakritikou bylo jediný kódový bod, nebo základní písmeno následované kombinační značkou. Obojí je správně a nerovnají se.
| Forma | é je uloženo jako | Kódových jednotek v JavaScriptu |
|---|---|---|
| NFC (složená) | U+00E9 | 1 |
| NFD (rozložená) | U+0065 U+0301 | 2 |
macOS historicky vydává rozložené názvy souborů a několik generátorů PDF i vstupních metod produkuje rozložený text, takže hodnota z jednoho zdroje se neshoduje s toutéž hodnotou napsanou na klávesnici.
"é" === "é" false
"é".normalize("NFC") === "é" true
Následky sahají za pouhou rovnost. Řazení, které porovnává kódové body, umístí rozložené tvary vedle prostého e a složené tvary daleko od nich, takže se seznam jmen vrátí ve dvou shlucích. Počty znaků se mezi oběma tvary liší, což hraje roli u limitu 280 znaků nebo u sloupce VARCHAR(50), a Text Statistics hlásí u zdánlivě stejného textu různá čísla. Zkracování je horší: rozříznutí rozloženého řetězce na pevné délce může vést mezi základním písmenem a jeho diakritikou, takže se ta diakritika připojí k tomu, co následuje, a Truncate Text tak na nenormalizovaném vstupu může vyrobit viditelně chybný poslední znak.
Kompatibilní tvary NFKC a NFKD jdou dál a slučují znaky, které se jiným jen podobají: ligatura U+FB01 se stane fi, A v plné šířce U+FF21 se stane A, znak mikro U+00B5 se stane řeckým mí U+03BC a horní index ² se stane obyčejnou 2. To je skvělé pro klíč k hledání nebo k odstranění duplicit a ničivé pro cokoli, co zobrazujete, protože x² se tiše promění v x2.
Praktické pravidlo: NFC pro ukládání a zobrazení, NFKC jen pro klíče, které nikdo nikdy nevidí.
Převod velikosti písmen není jedna operace
Převod na velká písmena není mapování znak po znaku a není nezávislý na lokalizaci.
- Německé
ßU+00DF se na velká písmena převede jakoSS, takže se řetězec prodlouží a cesta zpět na malá písmena původní hodnotu nevrátí. - Řecká sigma se na konci slova zmenší na
ςa jinde naσ, což opět rozbije cestu tam a zpět. - Turečtina a ázerbájdžánština mají
ıbez tečky U+0131 a velkéİs tečkou U+0130. V těchto lokalizacích seIzmenší naıaizvětší naİ.
Ten poslední případ je klasická produkční chyba. Kód, který převádí na malá písmena název hlavičky, příponu souboru nebo řetězec protokolu, funguje všude, dokud neběží na stroji s turečtinou jako výchozí lokalizací, kde se "FILE" zmenší na fıle a přestane odpovídat file. V Javě a .NET používají bezparametrové metody výchozí lokalizaci stroje, takže toUpperCase() a ToUpper() jsou past; u čehokoli strojově čitelného uveďte invariantní lokalizaci. Pro porovnávání je case folding (casefold() v Pythonu) lepší než převod na malá písmena, protože zachází s ß jako se ss.
Title case nemá jedinou definici, a proto „velké písmeno u každého slova“ vyrobí „The Lord Of The Rings“ a „IPhone“. Většina redakčních příruček píše velké písmeno u prvního a posledního slova a u všeho kromě členů, souřadicích spojek a krátkých předložek, u složenin se spojovníkem nechává velké obě části a nikdy nesahá na zkratky ani na jména s velkým písmenem uvnitř, jako McDonald, O'Brien nebo iPhone.
Programátorské konvence zápisu mají vlastní problém s hranicemi. Převod HTTPResponseCode na snake case zcela závisí na tom, jak dělič naloží se sérií velkých písmen; Case Converter dá http_response_code, naivní dělič dá h_t_t_p_response_code.
Pořadí, které funguje
Každý krok předpokládá, že proběhl ten předchozí. Ve špatném pořadí si navzájem překážejí.
- Vyřešte kódování. Mojibake jako
caféznamená, že se bajty dekódovaly ve špatném kódování, takže původní bajty dekódujte znovu, místo abyste látali příznaky. BOM odstraňte jen na úplném začátku textu. - Sjednoťte konce řádků na LF jedním průchodem.
- Odstraňte formátovací znaky, které jste vyhodnotili jako šum: měkké spojovníky, mezery nulové šířky, spojovače slov, obousměrné značky. Udělejte to před normalizací, protože NFC nedokáže složit základní písmeno s jeho diakritikou přes neviditelný znak, který sedí mezi nimi.
- Nahraďte zaměnitelnou interpunkci, pokud je cílem kód, CSV, JSON nebo klíč.
- Aplikujte normalizaci Unicode, ve výchozím stavu NFC.
- Teď se pusťte do bílých znaků. Zbylé exotické mezery převeďte na U+0020, sloučte jejich série a pak ořežte okraje. Když to uděláte před krokem 3, zůstanou dvojité mezery všude tam, kde se nezlomitelná mezera potkala s obyčejnou, a ořezání před krokem 2 nechá návrat vozíku přilepený k poslední hodnotě na každém řádku.
- Velikost písmen převádějte až nakonec, s výslovně uvedenou lokalizací.
Operace na úrovni slov patří rovněž až za krok 3. Dělení textu nástrojem Split Text, dokud v něm zůstávají měkké spojovníky, dá nafouknuté počty a půlky slov, a totéž platí pro cokoli, co prochází text a hledá výrazy, včetně Censor Text.
Když se hodnota po tom všem pořád odmítá shodovat, přestaňte se na ni dívat. Vypište její délku, převeďte ji na kódové body a porovnejte obě escapované podoby přímo; tam je rozdíl zřejmý způsobem, jakým na obrazovce nikdy nebude.