The Bug That Isn't in Your Code
Your import script has run cleanly for eight months. Today it fails on one row out of 40,000, throwing an encoding error on a customer name that looks entirely normal on screen.
Or the search function stops matching a product that's definitely in the database. Or a CSV that opens fine in one program produces gibberish in another. Or a comparison between two seemingly identical strings returns false.
These problems share a cause: characters that exist in your data but aren't visible when you look at it. And the fastest way to find them is to stop looking at the text and start counting the characters.
What a Frequency Count Actually Reveals
Run a character frequency analysis on a body of clean English text and you get a predictable profile. Space is most common, then e, t, a, o, i, n. Punctuation appears at low frequencies. Everything is in the ASCII range.
Run it on dirty data and anomalies stick out immediately, because they appear at frequencies and in positions that don't fit the pattern.
The Character Frequency tool gives you this directly:
- Paste your text into the input.
- Read the per-character counts.
- Scan for characters you didn't expect.
- Check the counts of characters you did expect against what they should be.
That last step catches things the first misses. If you're expecting 40,000 rows in a CSV, you should see roughly 40,000 newline characters and a comma count that's close to a multiple of your column count. A mismatch tells you something structural is wrong before you've parsed a single row.
The Usual Suspects
Smart quotes and typographic punctuation
Text pasted from a word processor or a CMS brings its punctuation with it:
| Character | Name | ASCII equivalent |
|---|---|---|
' ' |
Left/right single quotation mark | ' |
" " |
Left/right double quotation mark | " |
– |
En dash | - |
— |
Em dash | - |
… |
Horizontal ellipsis | ... |
′ ″ |
Prime, double prime | ' " |
These break things constantly. A CSV quoted with typographic quotes won't parse. Code copied from a blog post with smart quotes won't compile. A string comparison between O'Brien with a typographic apostrophe and O'Brien with a straight one returns false, which is exactly the kind of failure that produces "the record is definitely there but search can't find it."
In a frequency count, they show up as unexpected non-ASCII entries. Their presence alongside a lower-than-expected count of straight quotes is the giveaway.
Non-breaking spaces
U+00A0, the non-breaking space, looks exactly like a regular space and is not one.
It arrives from HTML ( ), from word processors, from PDF extraction, and from copy-pasting out of web pages. It survives a trim() in many implementations because the default whitespace definition doesn't always include it. It breaks split(' '). It makes "Total: 100" fail to match "Total: 100".
A frequency count showing a non-ASCII character at high frequency in a mostly-English document is almost always this.
Zero-width characters
These are the worst, because they're genuinely invisible:
- U+200B zero-width space
- U+200C zero-width non-joiner
- U+200D zero-width joiner
- U+FEFF zero-width no-break space, better known as the byte order mark
They come from HTML processors, from certain PDF exports, from text run through translation tools, and from deliberate insertion for word-wrap control.
A zero-width character in the middle of an email address, a product code, or an API key produces a value that looks correct and behaves incorrectly. You cannot see it. You cannot select it separately. A frequency count is one of the few ways to find it without writing a regex.
The byte order mark
U+FEFF at the start of a UTF-8 file deserves its own mention because it causes such a specific and confusing failure.
Import a CSV with a BOM and your first column header becomes \ufeffCustomerID instead of CustomerID. Every subsequent lookup by column name fails. The header prints normally in most terminals. It looks exactly right.
If a frequency count shows exactly one instance of a strange character and your first field lookup is failing, this is it. Save the file as "UTF-8 without BOM" and the problem disappears.
Mojibake
When UTF-8 bytes get interpreted as a single-byte encoding like Latin-1, you get characteristic garbage:
café → café
naïve → naïve
– → â€"
' → ’
" → “
The pattern is recognisable once you've seen it: Ã, Â, and sequences beginning †appear at frequencies far above anything natural.
A high count of  is a particularly reliable signal, because it's the mojibake form of a non-breaking space — meaning you have two encoding problems layered on each other.
Tabs, carriage returns and mixed line endings
Tabs hiding inside CSV fields. Carriage returns from Windows line endings (\r\n) that a Unix-oriented parser leaves attached to the end of the last field on each line. Files with a mixture of both because they were edited on two platforms.
A frequency count showing a \r count that differs from the \n count tells you immediately that your line endings are inconsistent.
Homoglyphs
Characters from other scripts that look identical to Latin letters. Cyrillic а (U+0430) versus Latin a (U+0061). Greek ο versus Latin o.
These are used deliberately in phishing domains and occasionally arrive accidentally through copy-paste from multilingual sources. They're invisible to the eye and obvious in a character count.
A Practical Workflow
Before importing any external data:
- Take a sample — a few thousand characters is plenty.
- Run a frequency count.
- List every character outside your expected set.
- Decide for each: normalise it, strip it, or accept it deliberately.
When a specific record misbehaves:
- Extract just that field's value.
- Run a frequency count on it alone.
- Compare against a known-good value of the same shape.
Comparing counts between a working and a failing record isolates the difference in seconds, where staring at the two strings can take an hour and still miss it.
When a file parses to the wrong number of rows or columns:
Count delimiters and newlines. If a 40,000-row, 6-column CSV shows 241,000 commas rather than 240,000, you have a stray delimiter inside a field somewhere.
Cleaning Rules Worth Standardising
Once you know what's in your data, normalisation is straightforward. A reasonable default pipeline for user-supplied or externally sourced text:
- Strip the BOM if present at position zero
- Replace non-breaking spaces with regular spaces
- Remove zero-width characters entirely
- Normalise typographic quotes and dashes to ASCII equivalents, unless you're preserving them deliberately for display
- Normalise line endings to a single convention
- Apply Unicode normalisation (NFC is the usual choice) so that composed and decomposed forms of accented characters compare equal
- Trim whitespace using a definition that includes Unicode whitespace, not just ASCII
Apply this at the boundary — the moment data enters your system — rather than scattering fixes through your application. One normalisation function at the ingestion point prevents an entire category of bugs downstream.
Common Mistakes
Trusting your eyes. Almost none of these characters are visible. Visual inspection cannot find them.
Fixing symptoms individually. Patching the one record that failed leaves the other 300 waiting. Find the pattern, fix at the source.
Stripping all non-ASCII. Tempting and destructive. It mangles legitimate names, addresses and international content. Target specific problem characters instead.
Normalising display text and stored text differently. If you store one form and search against another, matching fails.
Assuming your editor shows you everything. Most text editors render zero-width characters as nothing at all. Some have a "show invisibles" mode; many don't cover the full set.
FAQ
How do I find a zero-width character in a large file? Run a frequency count on a sample to confirm which one is present, then search for its specific code point with a regex or a hex-aware editor.
What's the difference between a BOM and a zero-width no-break space? Same code point, U+FEFF. At the start of a file it functions as a byte order mark; elsewhere it's a zero-width no-break space. Context determines the interpretation.
Should I convert smart quotes to straight quotes? For data fields, identifiers and anything used in comparisons, yes. For prose intended for display, typographic quotes are usually the better typography — the key is being consistent about which contexts get which.
Why does my string comparison fail on visually identical text? Most often an invisible character, a non-breaking space, or two different Unicode normalisation forms of the same accented character. A frequency count distinguishes all three.
Is mojibake reversible? Often yes, if you can identify the exact encoding pair involved and the data hasn't been double-mangled. Prevention is much easier than recovery.
The Takeaway
Encoding bugs are hard because the evidence is invisible in the medium you're inspecting. Switching from reading text to counting characters turns an invisible problem into an obvious one, usually in under a minute.
Analyse any text with the free Character Frequency tool at sadiqbd.com — no sign-up, instant results.