Ripulire il testo che arriva da qualche altra parte
Il testo arrivato da un PDF, da una cella di un foglio di calcolo, da un campo di un CMS, da un client di posta o da un'app di chat si porta dietro le scelte di formattazione di ciò che lo ha prodotto, e quasi nessuna di quelle scelte è visibile a schermo. Il risultato è una stringa che sembra corretta, che si stampa correttamente e che poi fallisce un confronto, una ricerca, il parsing di un JSON o una ricerca su database senza un motivo apparente.
Caratteri che non puoi vedere
Per Unicode sono caratteri veri e propri. Semplicemente non hanno una forma visibile, oppure hanno la stessa forma di qualcos'altro.
| Carattere | Code point | Di solito arriva da | Che cosa rompe |
|---|---|---|---|
| Spazio insecabile | U+00A0 | in HTML, Word, impaginazione PDF | la divisione sugli spazi, le corrispondenze esatte |
| Spazio insecabile stretto | U+202F | Tipografia francese, date provenienti da Word | lo stesso, in modo meno evidente |
| Spazio ideografico | U+3000 | Metodi di input CJK | sembra uno spazio largo |
| Spazio a larghezza zero | U+200B | Suggerimenti di a capo dei CMS, testo web copiato | invisibile, e non è spazio bianco |
| Non-joiner / joiner a larghezza zero | U+200C, U+200D | Persiano, testi indiani, sequenze di emoji | i confini di parola, i conteggi dei caratteri |
| Word joiner | U+2060 | Strumenti di composizione tipografica | niente di visibile, tutto ciò che è testuale |
| Trattino morbido | U+00AD | Sillabazione di Word, esportazioni PDF | una parola smette di corrispondere a sé stessa |
| Byte order mark | U+FEFF | I primi byte di un file UTF-8 | il primo campo della prima riga |
| Marcatore da sinistra a destra / da destra a sinistra | U+200E, U+200F | Contenuti bidirezionali | segni vaganti attorno ai numeri |
| Separatore di riga e di paragrafo | U+2028, U+2029 | Alcune app Mac e di impaginazione | la divisione in righe, i vecchi parser JavaScript |
Il motivo per cui una ricerca fallisce è che una ricerca confronta code point, non forme. Se un PDF ti ha dato New U+00A0 York e tu digiti New York con uno spazio normale U+0020, le due stringhe sono diverse e niente ti dirà perché.
"New York".includes("New York") false, the gap is U+00A0
" text".trim().length 5, the zero-width space survives
Uno spazio insecabile conta come spazio bianco per la maggior parte delle funzioni di trim e per \s nella maggior parte dei motori di espressioni regolari, quindi sparisce ai bordi di una stringa e sopravvive nel mezzo, esattamente dove una divisione si aspetta uno spazio normale. Uno spazio a larghezza zero non è spazio bianco da nessuna parte, quindi tagliare i bordi e comprimere le sequenze lo lasciano intatto. È anche il motivo per cui l'Estrattore di email può non restituire nulla da un testo copiato da un client di posta: un solo carattere a larghezza zero dentro l'indirizzo impedisce al pattern di corrispondere.
Rileva caratteri nascosti mostra che cosa c'è davvero, ed Escape Unicode dà i code point esatti di un singolo valore. Non eliminare però tutto a prima vista: un joiner a larghezza zero dentro una sequenza di emoji e un non-joiner a larghezza zero in persiano o in devanagari sono contenuto, non rumore.
La punteggiatura che un word processor ha cambiato al posto tuo
La correzione automatica sostituisce caratteri tipografici mentre scrivi, e la sostituzione segue il testo fuori dal documento.
| Hai digitato | Adesso hai | Code point |
|---|---|---|
' | virgoletta singola destra | U+2019 |
" e " | virgolette doppie sinistra e destra | U+201C, U+201D |
- fra due parole | trattino medio o trattino lungo | U+2013, U+2014 |
... | puntini di sospensione | U+2026 |
- prima di un numero | segno meno o trattino insecabile | U+2212, U+2011 |
Va benissimo nella prosa ed è fatale in qualsiasi cosa venga interpretata da una macchina.
{ “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
In un CSV il guasto è più silenzioso. Un parser riconosce come delimitatore di campo solo la virgoletta doppia dritta, quindi un valore che un word processor ha racchiuso fra virgolette curve viene trattato come non quotato; qualsiasi virgola al suo interno spezza allora la riga e ogni colonna successiva scivola. Una colonna i cui numeri negativi usano U+2212 viene importata come testo, e le somme la escludono in silenzio.
Trova e sostituisci con un breve elenco di sostituzioni è la soluzione, ma applicalo solo al testo destinato a codice, CSV o a una chiave. Passarlo sulla prosa che stai pubblicando appiattisce una punteggiatura che era voluta.
Fine riga e spazi in coda
Gli strumenti Windows chiudono una riga con CR LF (U+000D U+000A), quelli Unix con il solo LF, e qualche esportazione Mac molto vecchia usa ancora il solo CR. La maggior parte dei parser se la cava. Una divisione ingenua no.
"UK\r\n" split on "\n" -> "UK\r"
"UK\r" === "UK" false
"UK\r".length 3
Due stringhe che appaiono identiche in una tabella, in un log o in una vista di confronto possono comunque differire per un ritorno a capo, uno spazio finale o una tabulazione. Quando Confronto testi segna una riga come modificata e nessuna modifica è visibile, la risposta è questa. È anche ciò che infila uno spazio vagante dentro le virgolette quando una colonna incollata passa da Virgoletta le righe o da Unisci righe.
Normalizza in un solo passaggio, intercettando insieme CR LF e il CR isolato (\r\n? sostituito con \n), altrimenti una sostituzione in due passaggi raddoppia le righe vuote.
Una lettera, due modi di scriverla
Unicode permette che una lettera accentata sia un singolo code point oppure una lettera di base seguita da un segno combinante. Entrambe le forme sono corrette, e non sono uguali.
| Forma | é è memorizzata come | Unità di codice in JavaScript |
|---|---|---|
| NFC (composta) | U+00E9 | 1 |
| NFD (decomposta) | U+0065 U+0301 | 2 |
Storicamente macOS ha restituito nomi di file in forma decomposta, e diversi generatori di PDF e metodi di input emettono testo decomposto, quindi un valore che arriva da una di queste fonti non corrisponderà allo stesso valore digitato su una tastiera.
"é" === "é" false
"é".normalize("NFC") === "é" true
Le conseguenze vanno oltre l'uguaglianza. Un ordinamento che confronta code point mette le forme decomposte accanto alla e semplice e quelle composte lontanissimo, quindi un elenco di nomi torna diviso in due grappoli. I conteggi dei caratteri differiscono fra le due forme, il che conta per un limite di 280 caratteri o per una colonna VARCHAR(50), e Statistiche del testo riporta numeri diversi per quello che sembra lo stesso testo. Il troncamento è peggio: tagliare una stringa decomposta a una lunghezza fissa può recidere fra una lettera di base e il suo accento, lasciando quell'accento ad attaccarsi a ciò che segue, quindi Tronca testo su un input non normalizzato può produrre un ultimo carattere visibilmente sbagliato.
Le forme di compatibilità, NFKC e NFKD, vanno oltre e appiattiscono i caratteri che si limitano ad assomigliare ad altri: la legatura U+FB01 diventa fi, la A a larghezza piena U+FF21 diventa A, il segno micro U+00B5 diventa il mu greco U+03BC, e l'apice ² diventa un semplice 2. È eccellente per una chiave di ricerca o di deduplicazione ed è distruttivo per qualsiasi cosa tu debba mostrare, perché x² diventa silenziosamente x2.
La regola pratica: NFC per l'archiviazione e la visualizzazione, NFKC solo per chiavi che nessuno vede mai.
La conversione fra maiuscole e minuscole non è un'operazione sola
Passare alle maiuscole non è una mappatura carattere per carattere, e non è indipendente dalla lingua.
- La
ßtedesca U+00DF in maiuscolo diventaSS, quindi la stringa si allunga e un ritorno al minuscolo non restituisce l'originale. - Il sigma greco in minuscolo diventa
ςalla fine di una parola eσaltrove, il che rompe di nuovo l'andata e ritorno. - Il turco e l'azero hanno una
ısenza punto U+0131 e una maiuscolaİcon il punto U+0130. In quelle lingueIin minuscolo diventaıeiin maiuscolo diventaİ.
Quest'ultimo è il classico bug che arriva in produzione. Del codice che porta in minuscolo il nome di un header, un'estensione di file o una stringa di protocollo funziona ovunque finché non gira su una macchina la cui lingua predefinita è il turco, dove "FILE" in minuscolo diventa fıle e smette di corrispondere a file. In Java e in .NET i metodi senza argomenti usano la lingua predefinita della macchina, quindi toUpperCase() e ToUpper() sono la trappola; indica una locale invariante per qualsiasi cosa destinata a essere letta da una macchina. Per i confronti, il case folding (casefold() in Python) batte il passaggio al minuscolo, perché tratta ß come ss.
Lo stile titolo non ha una definizione unica, ed è per questo che «maiuscola a ogni parola» produce «The Lord Of The Rings» e «IPhone». La maggior parte delle guide di stile mette la maiuscola alla prima e all'ultima parola più a tutto tranne che ad articoli, congiunzioni coordinanti e preposizioni brevi, tiene maiuscole entrambe le metà di un composto con trattino, e non tocca mai gli acronimi né i nomi con maiuscole interne come McDonald, O'Brien o iPhone.
Le convenzioni di scrittura usate in programmazione hanno un loro problema di confini. Convertire HTTPResponseCode in snake case dipende interamente da come chi divide tratta una sequenza di maiuscole; il Convertitore di maiuscole dà http_response_code, uno ingenuo dà h_t_t_p_response_code.
Un ordine che funziona
Ogni passo presuppone che il precedente sia già stato eseguito. Fuori ordine, si ostacolano a vicenda.
- Sistema la codifica. Un mojibake come
cafésignifica che i byte sono stati decodificati con la codifica sbagliata, quindi ridecodifica i byte originali invece di rattoppare i sintomi. Rimuovi un BOM solo se si trova proprio all'inizio del testo. - Normalizza i fine riga a LF in un solo passaggio.
- Rimuovi i caratteri di formattazione che hai giudicato rumore: trattini morbidi, spazio a larghezza zero, word joiner, marcatori bidirezionali. Fallo prima di normalizzare, perché NFC non può comporre una lettera di base con il suo accento attraverso un carattere invisibile che sta in mezzo.
- Sostituisci la punteggiatura sosia, se la destinazione è codice, CSV, JSON o una chiave.
- Applica la normalizzazione Unicode, NFC come scelta predefinita.
- Adesso occupati degli spazi. Converti in U+0020 gli spazi esotici rimasti, comprimi le sequenze, poi taglia i bordi. Farlo prima del punto 3 lascia spazi doppi ovunque uno spazio insecabile abbia incontrato uno normale, e tagliare i bordi prima del punto 2 lascia un ritorno a capo incollato all'ultimo valore di ogni riga.
- Converti le maiuscole per ultimo, indicando esplicitamente la locale.
Anche le operazioni a livello di parola vanno dopo il punto 3. Dividere il testo con Dividi testo mentre restano i trattini morbidi dà conteggi gonfiati e mezze parole, e lo stesso vale per qualsiasi cosa vada a caccia di termini, compreso Censura testo.
Quando un valore continua a non corrispondere dopo tutto questo, smetti di guardarlo. Stampane la lunghezza, convertilo in escape di code point e confronta direttamente le due forme con escape; lì la differenza è ovvia in un modo che a schermo non lo è mai.