Ripulire il testo che arriva da qualche altra parte

I caratteri invisibili, la punteggiatura sostituita, i fine riga, le forme di normalizzazione e le regole sulle maiuscole che fanno comportare male il testo incollato, e l'ordine in cui sistemarli.

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.

CarattereCode pointDi solito arriva daChe cosa rompe
Spazio insecabileU+00A0  in HTML, Word, impaginazione PDFla divisione sugli spazi, le corrispondenze esatte
Spazio insecabile strettoU+202FTipografia francese, date provenienti da Wordlo stesso, in modo meno evidente
Spazio ideograficoU+3000Metodi di input CJKsembra uno spazio largo
Spazio a larghezza zeroU+200BSuggerimenti di a capo dei CMS, testo web copiatoinvisibile, e non è spazio bianco
Non-joiner / joiner a larghezza zeroU+200C, U+200DPersiano, testi indiani, sequenze di emojii confini di parola, i conteggi dei caratteri
Word joinerU+2060Strumenti di composizione tipograficaniente di visibile, tutto ciò che è testuale
Trattino morbidoU+00ADSillabazione di Word, esportazioni PDFuna parola smette di corrispondere a sé stessa
Byte order markU+FEFFI primi byte di un file UTF-8il primo campo della prima riga
Marcatore da sinistra a destra / da destra a sinistraU+200E, U+200FContenuti bidirezionalisegni vaganti attorno ai numeri
Separatore di riga e di paragrafoU+2028, U+2029Alcune app Mac e di impaginazionela 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 digitatoAdesso haiCode point
'virgoletta singola destraU+2019
" e "virgolette doppie sinistra e destraU+201C, U+201D
- fra due paroletrattino medio o trattino lungoU+2013, U+2014
...puntini di sospensioneU+2026
- prima di un numerosegno meno o trattino insecabileU+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 comeUnità di codice in JavaScript
NFC (composta)U+00E91
NFD (decomposta)U+0065 U+03012

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 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é 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 diventa SS, 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 lingue I in minuscolo diventa ı e i in 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.

  1. 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.
  2. Normalizza i fine riga a LF in un solo passaggio.
  3. 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.
  4. Sostituisci la punteggiatura sosia, se la destinazione è codice, CSV, JSON o una chiave.
  5. Applica la normalizzazione Unicode, NFC come scelta predefinita.
  6. 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.
  7. 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.