Att städa upp text som kommer någon annanstans ifrån
Text som kommit från en PDF, en kalkylbladscell, ett CMS-fält, en e-postklient eller en chattapp bär med sig formateringsbesluten från det som producerade den, och nästan inget av dessa beslut syns på skärmen. Resultatet är en sträng som ser korrekt ut, skrivs ut korrekt och sedan faller på en jämförelse, en sökning, en JSON-tolkning eller en databasuppslagning utan synbar anledning.
Tecken du inte kan se
För Unicode är detta riktiga tecken. De har helt enkelt ingen synlig form, eller samma form som något annat.
| Tecken | Kodpunkt | Kommer oftast från | Vad det förstör |
|---|---|---|---|
| Hårt mellanslag | U+00A0 | i HTML, Word, PDF-layout | uppdelning på mellanslag, exakta matchningar |
| Smalt hårt mellanslag | U+202F | fransk typografi, datum från Word | detsamma, fast mindre uppenbart |
| Ideografiskt mellanslag | U+3000 | inmatningsmetoder för CJK | ser ut som en bred lucka |
| Nollbrett mellanslag | U+200B | radbrytningstips i CMS, kopierad webbtext | osynligt, och inte blanktecken |
| Nollbred icke-sammanbindare / sammanbindare | U+200C, U+200D | persisk och indisk text, emojisekvenser | ordgränser, teckenräkningar |
| Ordsammanbindare | U+2060 | typsättningsverktyg | inget synligt, allt textmässigt |
| Mjukt bindestreck | U+00AD | avstavning i Word, PDF-export | ett ord slutar matcha sig självt |
| Byteordningsmarkering | U+FEFF | de första byten i en UTF-8-fil | första fältet på första raden |
| Vänster-till-höger- / höger-till-vänster-markering | U+200E, U+200F | dubbelriktat innehåll | vilsna markeringar runt tal |
| Rad- och styckeseparator | U+2028, U+2029 | vissa Mac- och layoutprogram | raduppdelning, äldre JavaScript-parsrar |
Anledningen till att en sökning misslyckas är att en sökning jämför kodpunkter, inte former. Om en PDF gav dig New U+00A0 York och du skriver New York med ett vanligt mellanslag U+0020, är de två strängarna olika och ingenting kommer att tala om varför.
"New York".includes("New York") false, the gap is U+00A0
" text".trim().length 5, the zero-width space survives
Ett hårt mellanslag räknas som blanktecken för de flesta trim-funktioner och för \s i de flesta regex-motorer, så det försvinner i strängens kanter och överlever i mitten, precis där en uppdelning förväntar sig ett vanligt mellanslag. Ett nollbrett mellanslag är inte blanktecken någonstans, så trimning och sammandragning lämnar det orört. Det är också därför Email Extractor kan returnera ingenting från text som kopierats ur en e-postklient: ett enda nollbrett tecken inuti adressen stoppar mönstermatchningen.
Hidden Character Detector visar vad som faktiskt finns där, och Unicode Escape ger de exakta kodpunkterna för ett enskilt värde. Rensa dock inte bort allt på vinst och förlust: en nollbred sammanbindare inuti en emojisekvens och en nollbred icke-sammanbindare i persiska eller devanagari är innehåll, inte brus.
Interpunktion som ett ordbehandlingsprogram ändrade åt dig
Autokorrigeringen byter ut typografiska tecken medan du skriver, och utbytet följer med texten ut ur dokumentet.
| Du skrev | Du har nu | Kodpunkt |
|---|---|---|
' | höger enkelt citattecken | U+2019 |
" och " | vänster och höger dubbelt citattecken | U+201C, U+201D |
- mellan ord | kort eller långt tankstreck | U+2013, U+2014 |
... | horisontell ellips | U+2026 |
- framför ett tal | minustecken eller hårt bindestreck | U+2212, U+2011 |
Det är utmärkt i prosa och ödesdigert i allt som en maskin tolkar.
{ “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
I CSV är felet tystare. En parser känner bara igen det raka dubbla citattecknet som fältavgränsare, så ett värde som ett ordbehandlingsprogram omgett med typografiska citattecken behandlas som ociterat; varje komma inuti det delar då raden, och alla kolumner efter det förskjuts. En kolumn vars negativa tal använder U+2212 importeras som text, och summor utesluter den tyst.
Find and Replace med en kort utbyteslista är lösningen, men tillämpa den bara på text som är på väg mot kod, CSV eller en nyckel. Att köra den över prosa du ska publicera plattar ut interpunktion som var avsiktlig.
Radslut och avslutande blanktecken
Windows-verktyg avslutar en rad med CR LF (U+000D U+000A), Unix-verktyg med enbart LF, och några mycket gamla Mac-exporter använder fortfarande enbart CR. De flesta parsrar klarar det. Naiv uppdelning gör det inte.
"UK\r\n" split on "\n" -> "UK\r"
"UK\r" === "UK" false
"UK\r".length 3
Två strängar som renderas identiskt i en tabell, en logg eller en diff-vy kan ändå skilja sig åt med en vagnretur, ett avslutande mellanslag eller en tabb. När Text Diff markerar en rad som ändrad och ingen ändring syns är det svaret. Det är också vad som stoppar in ett vilset mellanslag innanför citattecknen när en inklistrad kolumn går genom Quote Lines eller Join Lines.
Normalisera i ett enda svep, och matcha CR LF och ensamt CR tillsammans (\r\n? ersatt med \n), annars fördubblar en ersättning i två steg de tomma raderna.
En bokstav, två sätt att skriva den
Unicode tillåter att en bokstav med accent är antingen en enda kodpunkt eller en basbokstav följd av ett kombinerande tecken. Båda är korrekta, och de är inte lika.
| Form | é lagras som | Kodenheter i JavaScript |
|---|---|---|
| NFC (sammansatt) | U+00E9 | 1 |
| NFD (uppdelad) | U+0065 U+0301 | 2 |
macOS har historiskt lämnat ifrån sig uppdelade filnamn, och flera PDF-generatorer och inmatningsmetoder producerar uppdelad text, så ett värde från en källa matchar inte samma värde skrivet på ett tangentbord.
"é" === "é" false
"é".normalize("NFC") === "é" true
Följdeffekterna sträcker sig längre än till likhet. En sortering som jämför kodpunkter placerar uppdelade former bredvid rent e och sammansatta former långt bort, så en namnlista kommer tillbaka i två klungor. Teckenräkningen skiljer sig mellan formerna, vilket spelar roll för en gräns på 280 tecken eller en VARCHAR(50)-kolumn, och Text Statistics rapporterar olika siffror för vad som ser ut att vara samma text. Avkortning är värre: att klippa en uppdelad sträng vid en fast längd kan skära mellan en basbokstav och dess accent, så att accenten fäster vid vad som än följer, vilket gör att Truncate Text på onormaliserad indata kan producera ett synbart felaktigt sista tecken.
Kompatibilitetsformerna, NFKC och NFKD, går längre och slår ihop tecken som bara liknar andra: ligaturen U+FB01 blir fi, helbrett A U+FF21 blir A, mikrotecknet U+00B5 blir grekiskt my U+03BC, och upphöjt ² blir en vanlig 2. Det är utmärkt för en sök- eller dubblettnyckel och förödande för allt du visar, eftersom x² tyst blir x2.
Arbetsregeln: NFC för lagring och visning, NFKC bara för nycklar som ingen någonsin ser.
Skiftlägeskonvertering är inte en enda operation
Att versalisera är ingen mappning tecken för tecken, och det är inte oberoende av språkinställning.
- Tyskt
ßU+00DF versaliseras tillSS, så strängen blir längre och en resa tillbaka till gemener ger inte originalet. - Grekiskt sigma gemeniseras till
ςi slutet av ett ord ochσi övrigt, vilket återigen bryter rundresan. - Turkiska och azerbajdzjanska har ett punktlöst
ıU+0131 och ett punktförsett versaltİU+0130. I de språkinställningarna gemeniserasItillıochiversaliseras tillİ.
Den sista är den klassiska produktionsbuggen. Kod som gemeniserar ett huvudnamn, en filändelse eller en protokollsträng fungerar överallt tills den körs på en maskin vars standardspråkinställning är turkisk, där "FILE" gemeniseras till fıle och slutar matcha file. I Java och .NET använder metoderna utan argument maskinens standardspråkinställning, så toUpperCase() och ToUpper() är fällan; ange en invariant språkinställning för allt som är maskinläsbart. Vid jämförelser slår case folding (casefold() i Python) gemenisering, eftersom det hanterar ß som ss.
Title case har ingen entydig definition, vilket är varför "versalisera varje ord" ger "The Lord Of The Rings" och "IPhone". De flesta stilguider versaliserar första och sista ordet plus allt utom artiklar, samordnande konjunktioner och korta prepositioner, håller båda halvorna av en sammansättning med bindestreck versaliserade och rör aldrig akronymer eller namn med versaler inuti, som McDonald, O'Brien eller iPhone.
Skiftlägesformerna i programmering har sitt eget gränsproblem. Att konvertera HTTPResponseCode till snake case beror helt på hur uppdelaren behandlar en följd av versaler; Case Converter ger http_response_code, en naiv variant ger h_t_t_p_response_code.
En ordning som fungerar
Varje steg förutsätter att det föregående har körts. I fel ordning motarbetar de varandra.
- Fastställ teckenkodningen. Mojibake som
cafébetyder att byten avkodades som fel kodning, så avkoda originalbyten på nytt i stället för att lappa symptomen. Ta bort en BOM bara allra först i texten. - Normalisera radsluten till LF i ett enda svep.
- Ta bort de formattecken du bedömt vara brus: mjuka bindestreck, nollbrett mellanslag, ordsammanbindare, bidi-markeringar. Gör detta före normaliseringen, eftersom NFC inte kan sätta samman en basbokstav och dess accent tvärs över ett osynligt tecken som sitter mellan dem.
- Ersätt förväxlingsbar interpunktion, om destinationen är kod, CSV, JSON eller en nyckel.
- Tillämpa Unicode-normalisering, NFC som standard.
- Hantera nu blanktecknen. Konvertera de återstående exotiska mellanslagen till U+0020, dra ihop följder och trimma sedan. Att göra detta före steg 3 lämnar dubbla mellanslag överallt där ett hårt mellanslag mötte ett vanligt, och att trimma före steg 2 lämnar en vagnretur fastklistrad vid det sista värdet på varje rad.
- Konvertera skiftläge sist, med språkinställningen uttryckligen angiven.
Operationer på ordnivå hör också hemma efter steg 3. Att dela text med Split Text medan mjuka bindestreck finns kvar ger uppblåsta räkningar och halva ord, och detsamma gäller allt som söker efter termer, inklusive Censor Text.
När ett värde efter allt detta ändå vägrar matcha, sluta titta på det. Skriv ut dess längd, escapa det till kodpunkter och jämför de två escapade formerna direkt; där är skillnaden uppenbar på ett sätt den aldrig är på skärmen.