← All guides
Guide by Arif Aslam 7 min read

How to fix CSV encoding problems

A CSV is bytes. An encoding is the agreement about what those bytes mean. When the writer and the reader disagree, you get one of three very distinct symptoms, and knowing which one you have tells you whether the file is repairable.

This matters more than it sounds. One of these three is recoverable with a single re-read. Another is recoverable with a small transformation. The third means the data is already gone and you need a fresh export. People routinely spend hours on the third one.

Symptom: é where é should be

Cause. The file is UTF-8 and something read it as a single-byte encoding, almost always Windows-1252. In UTF-8, é is the two-byte sequence C3 A9. Read one byte at a time as Windows-1252, C3 is à and A9 is ©, giving é. The Japanese word for this is mojibake, and there is no better one.

A small phrasebook, because recognizing the pattern saves time:

You see It was UTF-8 bytes
ééC3 A9
ööC3 B6
ññC3 B1
€E2 82 AC
’'E2 80 99

Fix. The good news is that the bytes are intact. Nothing was lost, because Windows-1252 maps every possible byte to some character, so the decode did not fail, it just produced nonsense. Re-read the file declaring UTF-8 and the accents come back.

If the mojibake has already been written into a new file, you can usually undo it by encoding the text back to Windows-1252 bytes and decoding those as UTF-8. That is a round trip, and it is exactly what a repair script does. It only works when the intermediate step preserved every character, which Windows-1252 does and ASCII does not.

Fix it: open the file and let the loader normalize the encoding →

Symptom: a black diamond with a question mark, or a bare ?

Cause. This is U+FFFD, the Unicode replacement character. A decoder hit a byte sequence it could not map and substituted this marker. Unlike mojibake, this is destructive. The original bytes are not in the file any more; the marker is.

The most common route is a pipeline step that read the file as UTF-8 in non-strict mode, hit Windows-1252 bytes, replaced them, and wrote the result back out as a "UTF-8" file. The file is now genuinely valid UTF-8. It just contains diamonds where the names used to be.

Fix. There is no fix inside that file. Go back one step in the chain and get the version that had the original bytes. If the chain is a script you control, make the decode strict so it fails loudly instead of replacing characters silently, and then handle the failure by choosing the right encoding.

That principle is worth generalizing. In our own loader, the UTF-8 probe runs with fatal: true precisely so that a bad decode throws instead of quietly producing replacement characters. The failure is then caught and the bytes are re-decoded as Windows-1252, which cannot fail. A decoder that never fails is not a feature; it is a way of losing data without a stack trace.

Fix it: check whether the damage is already in the file →

Symptom:  before the first column name

Cause. A UTF-8 byte order mark. Three bytes, EF BB BF, at the very start of the file. UTF-8 has no byte order to mark, so the sequence is purely a signal that says "this is UTF-8". Excel on Windows uses it, which is genuinely helpful. Many other parsers do not strip it, and then it becomes part of the first field.

The tell is subtle and infuriating: a column named order_id that does not match a join, a lookup that fails on the first column only, a WHERE clause that never matches. The column is really named order_id and it prints identically.

Fix. Decide who the file is for. Going to Excel on Windows and double-clicked? Keep the BOM, it is doing useful work. Going into a data pipeline, a database import or an API? Strip it. Do not try to have it both ways with a single artifact.

To detect one from the terminal:

head -c 3 yourfile.csv | od -An -tx1
# efbbbf  -> UTF-8 BOM present

Fix it: detect a BOM and other structural surprises →

Symptom: the file looks like it has a null between every letter

Cause. It is UTF-16, and something is reading it as a single-byte encoding. UTF-16 stores each ASCII character as two bytes, the character and a zero. Displayed by a byte-at-a-time reader you get o.r.d.e.r._.i.d with nulls in the gaps. Exports from older SQL Server tooling and some Windows PowerShell defaults land here.

If the file starts with FF FE it is UTF-16 little endian; FE FF is big endian. Some files have no mark at all, which is why our loader also checks null density in the first kilobyte: if more than a quarter of those bytes are zero, it treats the file as BOM-less UTF-16 little endian rather than guessing at Latin.

Fix. Convert to UTF-8 before anything else touches the file. DuckDB's CSV reader, which is what runs under our workbench, rejects anything that is not UTF-8 outright, so the conversion happens in the loader before the parse and you get a notice telling you it happened.

Fix it: load the file and export it as clean UTF-8 CSV →

Check what is really in the file

Encoding problems are the class where guessing costs the most, so establish the facts before you change anything.

  1. Look at the first three bytes. head -c 3 file.csv | od -An -tx1. A BOM answers the encoding question immediately.
  2. Ask the operating system. file -I file.csv gives a charset guess based on a sample.
  3. Search for the marker. If the file contains the U+FFFD character anywhere, that data is already lost and the rest of the investigation is about the pipeline, not the file.
  4. Open it and look at accented names. Nothing beats reading twenty rows of a customer table with your own eyes.

Then standardize. Pick UTF-8, with a BOM only if Excel is the destination, and enforce it at the point where files enter your process. Encoding problems are cheap to prevent at the boundary and expensive to chase three systems later.

Fix it: hand Excel a workbook instead, and skip the encoding question entirely →

Questions people actually ask

Can I recover a file that shows the black diamond replacement character?

Not from that file. U+FFFD means the bytes were already thrown away by a decoder that could not map them, and the original values are not recoverable from the output. You need the file as it was before that decode step.

Is a BOM good or bad?

It depends on the consumer. Excel on Windows uses it to auto-detect UTF-8 on a double click, which is genuinely useful. Many Unix tools and some parsers do not strip it, so it ends up glued to the first column name. If your file is for Excel, include it. If it is for a pipeline, leave it out.

What is Windows-1252 and why does it keep appearing?

It is the single-byte encoding that Windows used as its Western European default for decades, and it is a superset of Latin-1. Almost every legacy export that is not UTF-8 is this. It cannot fail to decode, because every one of the 256 byte values maps to something, which is exactly why bad guesses go undetected.

Why does my file open fine on a Mac but not on Windows?

Different default encodings. macOS assumes UTF-8 almost everywhere; Windows applications frequently assume the system code page. The file has not changed, only the assumption about how to read it.

How do I check the encoding without special tools?

On macOS or Linux, run file -I yourfile.csv. It reports a charset guess. It is only a guess, but it distinguishes utf-8 from us-ascii and iso-8859-1 reliably enough to decide what to do next.

AA

Arif Aslam

Staff engineer in Bangalore. By day at Mammoth Analytics; building ExploreMyData on the side. More on my author page or LinkedIn.

Let the loader work out the encoding

Our CSV loader normalizes UTF-16 and Windows-1252 to UTF-8 before parsing, and tells you when it had to.

Open a CSV