Compare Two Texts and Find Invisible Differences
Already know one of them is contaminated and want it gone? Use the zero-width space remover or the JSON cleaner, the Word and Docs cleaner or the Excel cell cleaner. This page is for finding the difference first.
How to read the result
For each differing position you get the codepoint of the character in A and the codepoint of the character in B, with the Unicode name if it has one. Position is counted in Unicode code points, not bytes — a position that lands on an emoji family with a zero width joiner inside it will still make sense.
If you press Clean both, then compare, the page strips invisible characters from both sides and compares again. If the cleaned versions are equal, the mismatch was hidden characters and nothing more. If they still differ, there is real wording or visible-punctuation difference to investigate.
Why this is useful
The classic case is a string that renders identically but will not match a key, will not compare equal in code, or will not find a record. Most of the time, invisible characters are the cause, and you want to know exactly where and which ones before you delete anything.
This page tells you exactly that, with codepoints, so you can decide whether to strip them and whether to trust the cleaned version.
Which engine sees it, and which one does not
Two strings that look the same and compare as different are not a contradiction: two engines, two rules. The part that catches people out is that the rule can change underneath you. The same pair comes out equal on one machine and different on another, and the same file gives two different counts to two tools in the same pipeline — so the number you are looking at may be correct for the tool that produced it and wrong for the question you asked.
Take one record, INV-1001 Paid, and the same record with a pair of directional
marks wrapped around the number — U+202D before it, U+202C after it, two code points that
print nothing. It goes from 13 bytes to 19. Handed both versions, this is what nine engines
report:
| Engine | Verdict on the same pair |
|---|---|
== in Python, === in JavaScript |
different |
SQLite =, COUNT(DISTINCT), GROUP BY |
different — 2 groups, with COLLATE NOCASE as well |
cmp |
differ: byte 1 |
md5sum |
different |
Intl.Collator('en').compare (ICU 78.2) |
0 — equal |
sort -u with LC_ALL=C |
keeps both |
sort -u with LC_ALL=en_US.UTF-8 |
merges them |
The same split turns into a counting problem as soon as the strings are lines in a file. A five-line log was written with the same error message three times: once clean, once wrapped in U+202D and U+202C, once with a U+200B dropped inside the word payments, plus two identical warnings:
| Command | Result on that file |
|---|---|
wc -l |
5 lines, 130 bytes |
sort -u | wc -l, LC_ALL=C |
4 |
sort -u | wc -l, LC_ALL=en_US.UTF-8 |
3 |
sort | uniq -c, either locale |
4 groups — 1, 1, 1, 2 |
awk '!seen[$0]++' |
4 |
grep -c "payments" |
2 of the 3 |
cmp against a clean copy |
differ: byte 1, and byte 5 for the one inside the word |
Three copies of one error, and two tools in the same pipeline disagree about how many distinct
errors there are: sort -u says three, sort | uniq says four, on the
same file under the same locale. Neither is broken. sort -u compares by the
locale's collation rules, uniq compares bytes. Under en_US.UTF-8 the
collation treats the directional marks as if they were not there and the wrapped line collapses
into the clean one; the zero-width space inside the word survives that comparison, because
these tables do not ignore U+200B while ICU's do. The two collation implementations disagree
with each other about which invisible characters count.
How big that set is depends on which tables you are running. Of 26 invisible code points run through ICU's English collator, 17 compare as equal to nothing at all: the zero-width space, the joiner and non-joiner, the word joiner, the byte order mark, the soft hyphen, the combining grapheme joiner, the directional marks U+200E, U+200F, U+202A, U+202D, U+202C, U+2066, U+2069 and U+061C, the Mongolian vowel separator, and the tag characters. Nine are not ignorable: the spaces U+00A0, U+2007, U+2009, U+202F and U+3000, the line and paragraph separators, and the two Hangul fillers. A merge, a join, or a unique index on a collation-aware engine silently folds the first 17 into the record you already have; one built on byte equality rejects all of them; and the characters that behave like spaces are rejected by both.
Where they come from is less disputed than what to do about them. People who have lost hours to this describe the same handful of sources: a certificate fingerprint copied out of a Windows dialog, a name out of a word processor that carries a left-to-right mark on the end, a chat client that turns tabs into non-breaking spaces inside pasted configuration files and breaks the build, an input method, a paragraph out of a PDF. The disagreement is about blame. One camp says the application that inserted the character is at fault. The other, smaller and more convincing, says the field should never have accepted it — the character arrived, the record was stored, and every lookup afterwards failed for a reason nobody could see. A third report has the opposite shape: the merge failed while the spreadsheet's own comparison of the same two values returned true, which is the split in the table above showing up inside one product.
The checks people reach for first are the ones that miss. A widely-read answer to the question
"how do I find the difference between two strings that look identical" suggests testing with
Contains("\u8203"). The escape is read as hexadecimal, so that looks for U+8203, a
CJK ideograph, not for U+200B — 8203 is the decimal value of the zero-width
space. Measured: on a string with the character at position 4, the search with
"\u8203" returns -1, while chr(8203) finds it at 4. Length is not a
test either. The two records above are 15 characters against 13 and 19 bytes against 13, and in
the other direction one person checked the length, saw it match, concluded there were no
invisible characters, and spent the afternoon proving both halves of that wrong.
What does work, in order of cost. Compare bytes, not characters: cmp gives you
the offset of the first difference, which is the position you need, and it costs nothing to
run. Paste both sides into the box at the top if you want the code point named. Only then
decide what to delete — normalising first is worth one row of the table, because NFKC
folded the U+00A0 pair into a single record and left every other pair untouched. The blunt
fix, keep only U+0020 to U+007E, does remove the marks: on a 37-code-point line carrying two
directional marks, an em dash, two accented letters and a non-breaking space, it left 31 code
points — and took the em dash, both accents and the spacing with it.
Limits
Up to fifty differing positions are listed. If there are more than fifty, the page tells you the total and shows the first fifty. The first-difference panel always shows position 1, 2 or 3 with surrounding context, regardless of how many differences there are.
Nothing is uploaded. The comparison runs in this browser tab.