Plik CSV może wyglądać poprawnie w edytorze tekstu, a po otwarciu w Excelu zamienić „ą”, „ę” i „ł” w przypadkowe symbole. Najczęściej problem nie leży w samym formacie CSV, lecz w niedopasowaniu kodowania, znacznika BOM albo separatora. Pokażę, jak poprawnie zapisywać i odczytywać takie pliki w C# i .NET oraz jak rozpoznać, gdzie dokładnie powstaje błąd.
UTF-8 z BOM najczęściej rozwiązuje problem w Excelu
- UTF-8 to najlepszy domyślny wybór dla nowych plików CSV.
- BOM, czyli bajty EF BB BF, pomagają Excelowi rozpoznać kodowanie UTF-8.
- Separator przecinek lub średnik to osobna kwestia i nie naprawi błędnego kodowania.
- W .NET użyj UTF8Encoding(true), gdy plik ma być otwierany bezpośrednio w Excelu.
- Dla starych systemów może być potrzebne Windows-1250, ale nie warto wybierać go bez konkretnego powodu.

Skąd biorą się problemy z polskimi znakami w CSV
CSV jest zwykłym plikiem tekstowym. Sam format opisuje układ kolumn, separatorów i cudzysłowów, ale nie narzuca jednego kodowania znaków. To oznacza, że zapisane bajty mogą reprezentować tekst jako UTF-8, Windows-1250 albo jeszcze inne kodowanie.
Problem pojawia się wtedy, gdy program zapisuje plik w jednym kodowaniu, a aplikacja odczytuje go w innym. Przykładowo znak „ł” zapisany w UTF-8 zajmuje dwa bajty. Jeśli Excel potraktuje je jak dane z kodowania jednobajtowego, zobaczysz krzaczki zamiast poprawnego tekstu.
W praktyce rozdzielam trzy niezależne kwestie:
- kodowanie, czyli sposób zapisu znaków,
- BOM, czyli opcjonalny znacznik na początku pliku,
- separator, najczęściej przecinek albo średnik.
Zmiana średnika na przecinek nie naprawi znaków „ż” i „ó”. Z kolei dodanie BOM nie rozwiąże problemu, jeśli zawartość została wcześniej zapisana jako Windows-1250, a odbiorca oczekuje UTF-8.
UTF-8 czy Windows-1250 dla polskich danych
Dla nowych aplikacji wybieram UTF-8. Jest to kodowanie Unicode, więc obsługuje nie tylko polskie litery, lecz także znaki innych języków, symbole walut i emoji. Dzięki temu plik nie będzie wymagał zmiany kodowania, gdy aplikacja zacznie obsługiwać dane na przykład z Czech, Niemiec albo Ukrainy.
| Kodowanie | Kiedy użyć | Najważniejsze ograniczenie |
|---|---|---|
| UTF-8 z BOM | Eksport do Excela i pracy użytkowników | Niektóre integracje techniczne nie oczekują BOM |
| UTF-8 bez BOM | API, systemy Linux, integracje między aplikacjami | Starsze wersje Excela mogą źle rozpoznać kodowanie |
| Windows-1250 | Współpraca ze starym systemem wymagającym konkretnej strony kodowej | Nie obsługuje pełnego zakresu Unicode |
Windows-1250 nie jest „polskim UTF-8”. To osobna strona kodowa, która może działać w starszym oprogramowaniu, ale łatwo prowadzi do problemów przy znakach spoza Europy Środkowej. Sięgam po nią tylko wtedy, gdy dokumentacja systemu odbiorcy wyraźnie tego wymaga.
Warto też pamiętać, że słowo „ANSI” jest nieprecyzyjne. W jednym środowisku może oznaczać Windows-1250, w innym lokalną stronę kodową systemu. W kodzie lepiej wskazać konkretny numer, na przykład 1250, niż polegać na ustawieniach komputera.
Jak zapisać CSV w C# z poprawnym kodowaniem
Najprostszy wariant dla pliku otwieranego bezpośrednio w Excelu wygląda tak:
using System.Text;
var csv = """
Imię;Nazwisko;Miasto
Łukasz;Żak;Łódź
Małgorzata;Kiełbasa;Gdańsk
""";
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
File.WriteAllText("osoby.csv", csv, encoding);Argument true powoduje zapisanie BOM na początku pliku. Microsoft dokumentuje, że domyślne kodowanie UTF-8 używane przez część klas .NET nie musi zawierać BOM, dlatego jawne utworzenie obiektu UTF8Encoding usuwa niejasność.
Jeżeli generujesz dane w pętli, lepiej użyć StreamWriter z tym samym kodowaniem:
using System.Text;
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
using var writer = new StreamWriter("osoby.csv", append: false, encoding);
writer.WriteLine("Imię;Nazwisko;Miasto");
writer.WriteLine("Łukasz;Żak;Łódź");
writer.WriteLine("Małgorzata;Kiełbasa;Gdańsk");Sam zapis tekstu nie wystarcza, jeśli pola mogą zawierać średnik, cudzysłów albo znak nowej linii. Wtedy wartości trzeba poprawnie cytować. Cudzysłów wewnątrz pola zapisuje się jako dwa cudzysłowy.
Imię;Opis
Łukasz;"Specjalista C#; zna także ""stary"" .NET"W prostym eksporcie można przygotować funkcję zabezpieczającą pojedyncze pole:
static string EscapeCsv(string value, char separator = ';')
{
var mustQuote = value.Contains(separator)
|| value.Contains('"')
|| value.Contains('\r')
|| value.Contains('\n');
var escaped = value.Replace("\"", "\"\"");
return mustQuote ? $"\"{escaped}\"" : escaped;
}Przy większych projektach nie budowałbym całego formatu ręcznie. Biblioteka obsługująca CSV ogranicza ryzyko błędów przy cudzysłowach, pustych wartościach i wielowierszowych polach. Kodowanie nadal trzeba jednak ustawić świadomie, bo żadna biblioteka nie zgadnie za każdym razem, czego oczekuje program odbiorcy.
Dlaczego Excel czasem potrzebuje BOM
BOM to krótki znacznik zapisany na początku pliku. Dla UTF-8 ma postać EF BB BF i nie jest częścią danych tabeli. Jego zadaniem jest podpowiedzenie aplikacji, że plik został zapisany jako UTF-8.
Współczesne narzędzia zwykle radzą sobie z UTF-8 bez BOM, ale Excel, ustawienia regionalne i sposób otwierania pliku nadal mają znaczenie. Gdy użytkownik dwukrotnie kliknie plik CSV, UTF-8 z BOM jest często najbardziej praktycznym wariantem.
Jeśli plik ma być konsumowany przez API albo importowany przez system działający na Linuxie, BOM może być niepożądany. Wtedy użyj:
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
File.WriteAllText("eksport.csv", csv, encoding);Nie traktuję więc BOM jako uniwersalnej reguły. Dla pliku przeznaczonego dla człowieka pracującego w Excelu zwykle go dodaję. Dla komunikacji między usługami wybieram UTF-8 bez BOM, o ile kontrakt integracji nie mówi inaczej.
Jak odczytywać pliki CSV w .NET
Przy odczycie najlepiej jawnie ustalić, jakiego kodowania oczekujesz. Jeśli plik jest UTF-8 i może zawierać BOM, możesz pozwolić StreamReader na jego wykrycie:
using System.Text;
using var reader = new StreamReader(
"osoby.csv",
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true),
detectEncodingFromByteOrderMarks: true);
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
Console.WriteLine(line);
}Parametr detectEncodingFromByteOrderMarks pozwala rozpoznać BOM, jeśli znajduje się na początku pliku. Gdy BOM nie ma, czytnik użyje kodowania przekazanego w konstruktorze, więc w tym przykładzie zakładamy UTF-8.
Jeżeli stary system dostarcza pliki w Windows-1250, trzeba wskazać to wprost. W aplikacjach .NET Core i .NET należy wcześniej zarejestrować dostawcę stron kodowych:
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var polishEncoding = Encoding.GetEncoding(1250);
using var reader = new StreamReader(
"legacy.csv",
polishEncoding,
detectEncodingFromByteOrderMarks: true);Nie próbowałbym odgadywać kodowania na podstawie pojedynczego wiersza. Bez BOM albo informacji od systemu źródłowego nie ma niezawodnej metody, która zawsze odróżni UTF-8 od Windows-1250. Najbezpieczniejszy kontrakt integracyjny zawiera wprost informację o kodowaniu, separatorze i sposobie cytowania pól.
Separator, Excel i ustawienia regionalne
W Polsce Excel często oczekuje średnika jako separatora pól, ponieważ przecinek jest używany jako separator dziesiętny. Dlatego plik rozdzielany przecinkami może otworzyć się w jednej kolumnie, mimo że kodowanie jest całkowicie poprawne.
Przykład pliku wygodnego dla polskich ustawień regionalnych:
Produkt;Cena;Miasto
Kawa;12,50;Łódź
Herbata;9,99;GdańskJeżeli odbiorca wymaga przecinka, wartości liczbowe i tekstowe trzeba odpowiednio cytować:
Produkt,Cena,Miasto
Kawa,"12,50",Łódź
Herbata,"9,99",GdańskGdy Excel nadal otwiera plik niepoprawnie, użyj importu Dane > Z tekstu/CSV zamiast dwukrotnego kliknięcia. W oknie importu ustaw pochodzenie pliku jako UTF-8, wybierz właściwy separator i dopiero wtedy załaduj dane.
To ważne rozróżnienie. Błędne znaki oznaczają zwykle problem z kodowaniem, a wszystkie dane w jednej kolumnie wskazują raczej na separator. Te dwa objawy mogą wystąpić jednocześnie, ale wymagają innej naprawy.
Eksport CSV z ASP.NET Core
W aplikacji webowej przygotuj bajty pliku tak, aby BOM znalazł się przed właściwą zawartością. Sam nagłówek MIME z informacją charset=utf-8 jest pomocny, ale nie zmieni bajtów już zapisanych w treści.
using System.Text;
var csv = """
Imię;Nazwisko
Łukasz;Żak
""";
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
var bytes = encoding.GetBytes(csv);
return Results.File(
bytes,
contentType: "text/csv; charset=utf-8",
fileDownloadName: "osoby.csv");W tym przykładzie użyto UTF8Encoding(true), ale trzeba uważać na szczegół. GetBytes koduje sam tekst, natomiast BOM należy dodać osobno, jeśli korzystasz z tej metody bez zapisu przez StreamWriter:
var preamble = encoding.GetPreamble();
var content = encoding.GetBytes(csv);
var bytes = preamble
.Concat(content)
.ToArray();Alternatywnie możesz zapisać treść do MemoryStream przez StreamWriter. Przy eksporcie dla Excela testuję zawsze pobrany plik, a nie tylko tablicę znaków w pamięci. To właśnie na granicy między bajtami, nagłówkami HTTP i programem użytkownika pojawia się wiele trudnych do zauważenia różnic.
Krótka lista testów przed wysłaniem pliku
Zanim przekażesz eksport klientowi albo podłączysz go do integracji, sprawdź go na danych, które łatwo ujawniają błędy:
- użyj znaków ą, ć, ę, ł, ń, ó, ś, ź, ż,
- dodaj tekst zawierający separator, na przykład „C#; .NET”,
- przetestuj cudzysłów i znak nowej linii w jednej komórce,
- sprawdź puste wartości oraz znaki spoza alfabetu łacińskiego,
- otwórz plik w Excelu, edytorze tekstu i docelowym systemie importującym,
- zweryfikuj, czy pierwsza nazwa kolumny nie zaczyna się od niewidocznego BOM.
Jeśli tekst jest poprawny w Notatniku, ale nie w Excelu, sprawdź BOM i separator. Jeśli jest błędny już w edytorze, problem powstał wcześniej, najczęściej przy konwersji kodowania albo podczas odczytu danych źródłowych.
Najprostsza reguła dla stabilnych eksportów
Dla nowego rozwiązania w C# przyjąłbym konkretny standard: UTF-8, jasno określony separator, poprawne cytowanie pól i test z polskimi znakami. Jeśli plik otwierają użytkownicy w Excelu, dodałbym BOM. Jeśli trafia bezpośrednio do innej usługi, ustaliłbym z odbiorcą, czy BOM jest akceptowany.
Najwięcej problemów bierze się nie z samego CSV, lecz z domyślnych ustawień. Jednoznacznie ustawione kodowanie w .NET, świadomy wybór separatora i test na prawdziwych danych zwykle wystarczą, aby „Łódź”, „Żółć” i „Gdańsk” przestały zamieniać się w nieczytelne znaki.
