Grafikus C# – Windows Forms tanfolyam › 10.5 lecke

Windows Forms · 10.5 lecke · CSV

CSV fájl kezelése

A CSV (comma-separated values) a legegyszerűbb táblázatos formátum: minden sor egy rekord, az értékeket elválasztó jel tagolja. Az Excel, az iskolai nyilvántartók és a legtöbb program képes beolvasni és írni. A 8.9 leckében egyszerű CSV-t mentettél és töltöttél be Split módszerrel, ami addig működik, amíg a mezőkben nincs pontosvessző, idézőjel vagy sortörés. A valódi CSV-fájlok ennél sokkal alattomosabbak. Ebben a leckében megírod a helyes feldolgozót (idézőjelek, többsoros mezők), felismered az elválasztót és a fejlécet, számokat olvasol a magyar tizedesvesszővel, és biztonságosan exportálsz (Excel-barát kódolás, képletbeszúrás elleni védelem).

  • kb. 110 perc
  • haladó szint
  • CSV
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • ismered a CSV szabályait (idézőjel, dupla idézőjel, többsoros mező), és tudod, miért nem elég a Split;
  • állapotgépes feldolgozót írsz karakterenként (StreamReader.Read, Peek);
  • felismered az elválasztót (;, ,, tabulátor), és megfelelteted az oszlopokat a fejléc szerint;
  • számot és dátumot olvasol a megfelelő kultúrával (tizedesvessző vagy tizedespont);
  • helyesen írsz CSV-t (idézés, UTF-8 BOM az Excelnek), és védekezel a képletbeszúrás ellen;
  • a hibás sorokat jelented, a jókat betöltöd (nincs adatvesztés, nincs összeomlás).

Mielőtt elkezded

Ehhez a leckéhez a 8.9 lecke (egyszerű CSV), a 10.2–10.3 lecke (StreamReader, kódolás) és a 8.8 lecke (adatellenőrzés) szükséges.

10.4 Fájlválasztók →

A CSV szabályai és a Split csapdái

termekek.csvszöveg
Név;Ár;Megjegyzés
Toll;120,5;kék
"Füzet; A4";350;"Vonalas, 60 lapos"
"Marker ""XL""";480;"első sor
második sor"
Idézőjeles mezőHa a mező tartalmazza az elválasztót (;), az idézőjelet vagy a sortörést, " idézőjelek közé kerül: "Füzet; A4". Az idézőjelen belül az elválasztó nem választ el.
Idézőjel a mezőbenAz idézőjelet megkettőzik: "Marker ""XL""" jelentése: Marker "XL".
Többsoros mezőAz idézőjeles mezőben sortörés is lehet. A fájl egy rekordja ezért több fizikai sor: a ReadLine alapú feldolgozás itt hibás.
Nincs szabvány elválasztóAz angol Excel vesszőt, a magyar pontosvesszőt használ (mert a tizedesjel a vessző). A tabulátor és a | is gyakori. A feldolgozónak az elválasztót paraméterként kell kapnia (vagy felismernie).

Jósolj!

A sor.Split(';') hívás a fenti "Füzet; A4";350;"Vonalas, 60 lapos" sorra hány mezőt ad, és mi az első?

Jóslat ellenőrzése

4 mezőt (a helyes 3 helyett): az első "Füzet (az idézőjellel), a második A4". A Split nem ismeri az idézőjeles mezőt: a feldolgozót karakterenként kell írni.

Állapotgépes feldolgozó

A feldolgozó karakterenként olvas, és egy jelzőt (idezoben) tart: az idézőjelben a karakterek a mező részei, azon kívül az elválasztó mezőt, a sortörés rekordot zár:

Csv.cs (using System.IO; using System.Text; using System.Linq; using System.Collections.Generic;)C#
public static class Csv
{
public static List<string[]> Elemez(TextReader olvaso, char elvalaszto)
{
List<string[]> sorok = new List<string[]>();
List<string> mezok = new List<string>();
StringBuilder mezo = new StringBuilder();
bool idezoben = false;
bool vanTartalom = false; // a nem üres sor jelzése (az üres sorokat kihagyjuk)
int c;

while ((c = olvaso.Read()) != -1)
{
char ch = (char)c;

if (idezoben)
{
if (ch == '"')
{
if (olvaso.Peek() == '"') { mezo.Append('"'); olvaso.Read(); } // "" → egy idézőjel
else { idezoben = false; } // az idézett rész vége
}
else { mezo.Append(ch); } // az idézőjelben minden karakter a mező része (sortörés is)
}
else if (ch == '"') { idezoben = true; vanTartalom = true; }
else if (ch == elvalaszto) { mezok.Add(mezo.ToString()); mezo.Clear(); vanTartalom = true; }
else if (ch == '\r') { } // a CRLF „\r” részét elhagyjuk
else if (ch == '\n')
{
if (vanTartalom || mezo.Length > 0)
{
mezok.Add(mezo.ToString());
sorok.Add(mezok.ToArray());
}

mezok.Clear();
mezo.Clear();
vanTartalom = false;
}
else { mezo.Append(ch); vanTartalom = true; }
}

if (idezoben) { throw new FormatException("Lezáratlan idézőjel a fájl végén."); }

if (vanTartalom || mezo.Length > 0) // az utolsó sor sortörés nélkül is rekord
{
mezok.Add(mezo.ToString());
sorok.Add(mezok.ToArray());
}

return sorok;
}

// az elválasztó felismerése az első sorból (az idézőjelen belüli karaktereket nem számolja)
public static char ElvalasztoTipp(string elsoSor)
{
char[] jeloltek = { ';', ',', '\t', '|' };
int legtobb = -1;
char nyertes = ';';

foreach (char j in jeloltek)
{
int db = 0;
bool idez = false;

foreach (char ch in elsoSor)
{
if (ch == '"') { idez = !idez; }
else if (!idez && ch == j) { db++; }
}

if (db > legtobb) { legtobb = db; nyertes = j; }
}

return nyertes;
}
}
Az állapotAz idezoben jelző mondja meg, hogy az aktuális karakter az idézett rész belsejében van-e. Ugyanaz az állapotgép gondolkodás, mint az egéres húzásnál (9.6 lecke): egy jelző és szabályok.
PeekA Peek() a következő karaktert mutatja olvasás nélkül: így döntjük el, hogy az idézőjel a megkettőzött ("") vagy a záró. -1 a fájl vége.
Lezáratlan idézőjelHa a fájl idézőjelben ér véget, a fájl hibás: a feldolgozó hibát jelez (FormatException), nem találgat. A hívó try–catch blokkal kezeli, és érthető üzenetet mutat.
Üres sorokA fájl végi üres sor nem rekord. A vanTartalom jelző a valódi üres rekordot (például ;;) megkülönbözteti a teljesen üres sortól.

Jósolj!

A feldolgozó a "Marker ""XL""";480 sort kapja (; elválasztóval). Milyen mezőket ad?

Jóslat ellenőrzése

Két mezőt: Marker "XL" (a megkettőzött idézőjelek egy idézőjellé váltak, a külső idézőjelek elmaradtak) és 480.

Oszlopok megfeleltetése, számok és kultúra

Form1.cs (using System.Globalization;)C#
// a fejléc neveinek egységesítése: kisbetű, ékezet és szóköz nélkül (a „Név”, „nev”, „ NÉV ” ugyanaz)
private string Egyseges(string s)
{
string bontott = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();

foreach (char c in bontott)
{
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark && c != ' ') { sb.Append(c); }
}

return sb.ToString();
}

// a fejléc alapján: mező neve → oszlop sorszáma
Dictionary<string, int> oszlopok = new Dictionary<string, int>();

for (int i = 0; i < fejlec.Length; i++) { oszlopok[Egyseges(fejlec[i])] = i; }

int arOszlop;
if (!oszlopok.TryGetValue("ar", out arOszlop)) { Hiba("Hiányzik az Ár oszlop."); }

// számok: a kultúra számít! A magyar „120,5” invariáns kultúrával NEM 120,5
CultureInfo magyar = CultureInfo.GetCultureInfo("hu-HU");
CultureInfo invariant = CultureInfo.InvariantCulture;

double ar;

if (!double.TryParse("120,5", NumberStyles.Float, magyar, out ar)) { /* hibás szám */ } // 120,5
double.TryParse("120,5", NumberStyles.Float, invariant, out ar); // 1205 (a vessző ezres elválasztó!)

// dátum: a formátumot is megadjuk
DateTime datum;
bool ok = DateTime.TryParseExact("2026. 10. 28.", "yyyy. MM. dd.", magyar, DateTimeStyles.None, out datum);
Fejléc szerinti kötésAz oszlopok sorrendje exportról exportra változhat; a fejléc neve megbízható. A nevek egységesítése (kisbetű, ékezet nélkül) elnézőbb, mint a pontos egyezés. Hiányzó kötelező oszlopnál a program jelezze, melyik hiányzik.
Kultúra és tizedesjelA double.Parse("120,5") a gép kultúrájától függ. Fájlból olvasott számnál mindig add meg a kultúrát: a magyar CSV (;) általában tizedesvesszős, a vesszős (,) angol CSV tizedespontos. Az Invariant kultúra a tizedespontot várja.
TryParseA TryParse nem dob kivételt hibás szövegnél, false értéket ad: sorról sorra ellenőrizhető, és a hibás sor jelenthető. A Parse kivétele a teljes betöltést megszakítaná.
Üres és hiányzó mezőkEgy sorban kevesebb mező lehet, mint a fejlécben. Az indexelés előtt ellenőrizd a hosszt (sor.Length > oszlop), különben IndexOutOfRangeException.

Jósolj!

A program double.Parse("120,5", CultureInfo.InvariantCulture) hívással olvas számot, a vessző tizedesjelnek szánva. Mi lesz az eredmény?

Jóslat ellenőrzése

1205: az invariáns kultúra a vesszőt ezres elválasztónak tekinti, és kivétel nélkül tízszeresére rontja az értéket. A Parse alapértelmezett NumberStyles.Float beállításával ez csendes adathiba; magyar fájlnál hu-HU kultúrát adj meg.

Biztonságos írás: idézés, Excel, képletvédelem

Csv.csC#
// egy mező megfelelő idézése
public static string Mezo(string ertek, char elvalaszto)
{
if (ertek == null) { return ""; }

// képletbeszúrás elleni védelem: az Excel a = + - @ jellel kezdődő cellát képletként értelmezi
if (ertek.Length > 0 && ("=+-@".IndexOf(ertek[0]) >= 0 || ertek[0] == '\t' || ertek[0] == '\r'))
{
ertek = "'" + ertek; // az aposztróf szöveggé teszi (csak SZÖVEGES mezőkre alkalmazd, a számokra nem!)
}

bool idezni = ertek.IndexOf(elvalaszto) >= 0 || ertek.IndexOf('"') >= 0 || ertek.IndexOf('\n') >= 0 || ertek.IndexOf('\r') >= 0
|| ertek != ertek.Trim();

return idezni ? '"' + ertek.Replace("\"", "\"\"") + '"' : ertek;
}

public static string Sor(char elvalaszto, params string[] mezok)
{
return string.Join(elvalaszto.ToString(), mezok.Select(m => Mezo(m, elvalaszto)));
}

// mentés Excel-barát módon: ; elválasztó, tizedesvessző, UTF-8 BOM-mal
using (StreamWriter iro = new StreamWriter(ut, false, new UTF8Encoding(true)))
{
iro.WriteLine(Csv.Sor(';', "Név", "Ár", "Megjegyzés"));

foreach (Termek t in termekek)
{
iro.WriteLine(Csv.Sor(';', t.Nev, t.Ar.ToString("0.##", magyar), t.Megjegyzes)); // az ár számként, magyar tizedesjellel
}
}
Idézés szabályaIdézőjelbe tesszük a mezőt, ha elválasztót, idézőjelet, sortörést vagy kezdő/záró szóközt tartalmaz; a belső idézőjelet megkettőzzük. Idézőjel nélkül a „Füzet; A4” két mezővé esne szét.
KépletbeszúrásHa a szöveges mező =, +, -, @ jellel kezdődik (például =HYPERLINK(...)), az Excel képletként végrehajtja, amikor a fájlt megnyitják. Felhasználótól származó szöveg (megjegyzés, név) CSV-be írásakor ezért előtagot (') kap. A számmezőt (a -5 mínusz előjelét) nem szabad így módosítani: azt számként, külön formázva írd.
Excel és a BOMAz Excel a BOM nélküli UTF-8 CSV-t ANSI kódolásúnak veszi (10.3 lecke): a BOM-os UTF-8 helyesen jeleníti meg az ékezeteket. A ; elválasztó magyar Excelben automatikusan oszlopokra bontja a sort.
Számok kiírásaA számot kultúra-megadással írd (ToString("0.##", magyar)): a ToString() a gép beállítását használja, és más gépen más tizedesjelet adhat.

Jósolj!

A felhasználó a „Megjegyzés” mezőbe azt írja: =HYPERLINK("http://rossz.example";"Kattints!"). A program idézés és védelem nélkül írja a CSV-be, majd az iskolai titkárság megnyitja Excelben. Mi történik?

Jóslat ellenőrzése

Az Excel a cellát képletként értelmezi: kattintható hivatkozást (veszélyesebb esetben külső adat hívását) hoz létre. Ez a CSV-injekció. A szöveges mező elé írt ' előtag szöveggé teszi. Felhasználói bemenetet CSV-be sosem írunk védelem nélkül.

📷 Képernyőkép helye

Mit tegyél ide: a CSV-importáló: az előnézet, az oszlopok megfeleltetése, a betöltött sorok és a hibalista.Javasolt fájlnév: winforms-10-5-csv-importalo.webp · Alt szöveg: A CSV-importáló program

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. Eltolódnak az oszlopok bizonyos soroknál

Néhány sorban az ár a megjegyzés oszlopba kerül.

Ok és javítás

Az elválasztót tartalmazó mezőt a Split szétvágja. Használj idézőjel-kezelő feldolgozót (Csv.Elemez), és exportnál idézz.

2. IndexOutOfRangeException egy sor feldolgozásánál

string ar = sor[2];   // a sor csak 2 mezőből áll
Hiba és javítás

Ellenőrizd a mezőszámot (sor.Length > 2), és a hibás sort jelentsd (sorszám + ok), ne állítsd le az egész betöltést.

3. Az ár tízszeresére nőtt

double ar = double.Parse("120,5", CultureInfo.InvariantCulture);   // 1205
Hiba és javítás

Az invariáns kultúra a vesszőt ezres elválasztónak veszi. Magyar formátumhoz hu-HU; a TryParse a hibás szöveget is jelzi.

4. Az ékezetek hibásak az Excelben

A mentett CSV-t az Excel összekuszálva mutatja.

Ok és javítás

BOM nélküli UTF-8 (10.3 lecke). Mentsd new UTF8Encoding(true) kódolással.

5. Az első oszlop neve furcsa: "\uFEFFNév"

A fejléc első mezője nem egyezik a „Név” szöveggel.

Ok és javítás

A fájl elején a BOM karakter (\uFEFF) maradt a szövegben (kézi dekódolásnál). A StreamReader a BOM-ot elveszi; kézzel TrimStart('\uFEFF').

6. Az utolsó sor hiányzik

A fájl végén sortörés nélküli sor nem töltődik be.

Ok és javítás

A feldolgozó csak a \n-nél zárta a rekordot. A fájl végén a nyitott rekordot is le kell zárni (lásd a Csv.Elemez végét).

Mini projekt: CSV-importáló

Készíts importáló varázslót, amely tetszőleges CSV-ből betölti a termékeket, és jelenti a hibákat:

  1. Felület: Fájl megnyitása... (a 10.4 lecke segédosztályával), cmbKodolas és cmbElvalaszto (automatikus, ;, ,, Tab, |), chkFejlec (Az első sor fejléc), dgvElonezet (az első 20 sor), négy ComboBox a megfeleltetéshez (Név, Ár, Darab, Kategória) és Importálás; alul dgvJok (a betöltött termékek) és lstHibak (a hibás sorok); Hibák mentése..., Termékek exportálása....
  2. Beolvasás: a bájtok dekódolása a 10.3 lecke szabályai szerint (BOM → érvényes UTF-8 → Windows-1250); az elválasztó tippje az első sorból (ElvalasztoTipp); a Csv.Elemez; lezáratlan idézőjelnél érthető hibaüzenet.
  3. Előnézet: az első 20 rekord a rácsban; ha van fejléc, az oszlopok a fejléc neveit kapják, különben Oszlop 1…; a megfeleltető legördülők automatikusan kiválasztják az egyező (egységesített) nevű oszlopot.
  4. Importálás és ellenőrzés: soronként: a Név kötelező (nem üres, legfeljebb 60 karakter); az Ár szám ≥ 0 (TryParse, a kultúrát a felhasználó választja: magyar vagy angol); a Darab egész ≥ 0; hiányzó mező, hibás szám és túl hosszú név külön hibaüzenetet kap; a hibás sor a sorszámmal és okkal kerül a hibalistára, a jó sor a rácsba.
  5. Összegzés: 128 sorból 120 betöltve, 8 hibás; az ismétlődő nevek jelzése (nem hiba, figyelmeztetés).
  6. Hibák mentése: a hibás sorok eredeti tartalmával és az okkal egy új CSV-be (a felhasználó kijavíthatja, és újra importálhatja); az eredeti fájl nem változik.
  7. Exportálás: a betöltött termékek CSV-be: ; elválasztó, magyar tizedesvessző, UTF-8 BOM, idézés, képletvédelem a szöveges mezőkre; az árak számként, nem szövegként.
  8. Hibatűrés: üres fájl, csak fejléc, bináris fájl, 100 000 soros fájl (figyelmeztetés a lassúságra: 11. fejezet), a nyitott fájl zárolása.
  9. Teszteld: idézőjeles és többsoros mezők, "" az idézőben, hiányos sorok, szóközös számok ( 120,5 ), tizedespontos fájl magyar beállítással, fejléc nélküli fájl, kódolások, export Excelben megnyitva (ékezetek, a = kezdetű megjegyzés szövegként marad).
Megoldás megnyitása (próbáld ki előbb önállóan!)

A Csv osztályt a lecke mutatja; itt a varázsló logikája áll: megfeleltetés, soronkénti ellenőrzés és export.

Form1.cs (using System.Globalization; using System.IO; using System.Text; using System.Linq; using System.ComponentModel; using System.Collections.Generic;)C#
public class Termek
{
public string Nev { get; set; } = "";
public double Ar { get; set; }
public int Darab { get; set; }
public string Kategoria { get; set; } = "";
}

private List<string[]> rekordok = new List<string[]>();
private string[] fejlec = new string[0];
private BindingList<Termek> termekek = new BindingList<Termek>();

private void Form1_Load(object sender, EventArgs e)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

cmbElvalaszto.Items.AddRange(new object[] { "Automatikus", ";", ",", "Tab", "|" });
cmbElvalaszto.SelectedIndex = 0;
cmbKultura.Items.AddRange(new object[] { "Magyar (120,5)", "Angol (120.5)" });
cmbKultura.SelectedIndex = 0;
dgvJok.DataSource = termekek;
}

private string Dekodol(byte[] b)
{
if (b.Length >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) { return new UTF8Encoding(true).GetString(b, 3, b.Length - 3); }

try { return new UTF8Encoding(false, true).GetString(b); }
catch (DecoderFallbackException) { return Encoding.GetEncoding(1250).GetString(b); }
}

private char Elvalaszto(string szoveg)
{
switch (cmbElvalaszto.SelectedIndex)
{
case 1: return ';';
case 2: return ',';
case 3: return '\t';
case 4: return '|';
default:
int vege = szoveg.IndexOfAny(new char[] { '\r', '\n' });

return Csv.ElvalasztoTipp(vege < 0 ? szoveg : szoveg.Substring(0, vege));
}
}

private string Egyseges(string s)
{
string bontott = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();

foreach (char c in bontott) { if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark && c != ' ') { sb.Append(c); } }

return sb.ToString();
}

private void btnMegnyit_Click(object sender, EventArgs e)
{
string ut = FajlValaszto.Megnyit(this, "CSV (*.csv;*.txt)|*.csv;*.txt|Minden fájl (*.*)|*.*", null);

if (ut == null) { return; }

try
{
string szoveg = Dekodol(File.ReadAllBytes(ut));
char elv = Elvalaszto(szoveg);

using (StringReader olvaso = new StringReader(szoveg))
{
rekordok = Csv.Elemez(olvaso, elv);
}
}
catch (FormatException ex) { MessageBox.Show(this, ex.Message, "Hibás CSV", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
catch (IOException ex) { MessageBox.Show(this, "A fájl nem olvasható: " + ex.Message, "Megnyitás", MessageBoxButtons.OK, MessageBoxIcon.Error); return; }

if (rekordok.Count == 0) { tsslUzenet.Text = "A fájl üres."; return; }

Elonezet();
}

private void Elonezet()
{
int oszlopDb = rekordok.Max(r => r.Length);

fejlec = chkFejlec.Checked ? rekordok[0] : Enumerable.Range(1, oszlopDb).Select(i => "Oszlop " + i).ToArray();

System.Data.DataTable dt = new System.Data.DataTable();

for (int i = 0; i < oszlopDb; i++) { dt.Columns.Add(i < fejlec.Length && fejlec[i].Length > 0 ? fejlec[i] + " (" + (i + 1) + ")" : "Oszlop " + (i + 1)); }

foreach (string[] r in rekordok.Skip(chkFejlec.Checked ? 1 : 0).Take(20))
{
dt.Rows.Add(Enumerable.Range(0, oszlopDb).Select(i => i < r.Length ? r[i] : "").ToArray());
}

dgvElonezet.DataSource = dt;

foreach (ComboBox cb in new[] { cmbNev, cmbAr, cmbDarab, cmbKategoria })
{
cb.Items.Clear();
cb.Items.Add("(nincs)");
cb.Items.AddRange(Enumerable.Range(0, oszlopDb).Select(i => (object)(i + 1) + ": " + (i < fejlec.Length ? fejlec[i] : "")).ToArray());
cb.SelectedIndex = 0;
}

Automatikus(cmbNev, "nev", "termek", "megnevezes");
Automatikus(cmbAr, "ar", "egysegar", "price");
Automatikus(cmbDarab, "darab", "db", "mennyiseg");
Automatikus(cmbKategoria, "kategoria", "tipus");
}

private void Automatikus(ComboBox cb, params string[] nevek)
{
if (!chkFejlec.Checked) { return; }

for (int i = 0; i < fejlec.Length; i++)
{
if (nevek.Contains(Egyseges(fejlec[i]))) { cb.SelectedIndex = i + 1; return; }
}
}

private void chkFejlec_CheckedChanged(object sender, EventArgs e) { if (rekordok.Count > 0) { Elonezet(); } }

private string Mezo(string[] sor, ComboBox cb)
{
int i = cb.SelectedIndex - 1; // 0: (nincs)

return i >= 0 && i < sor.Length ? sor[i].Trim() : null;
}

private void btnImport_Click(object sender, EventArgs e)
{
if (cmbNev.SelectedIndex <= 0 || cmbAr.SelectedIndex <= 0) { MessageBox.Show(this, "A Név és az Ár oszlop megadása kötelező.", "Importálás"); return; }

CultureInfo kultura = CultureInfo.GetCultureInfo(cmbKultura.SelectedIndex == 0 ? "hu-HU" : "en-US");

termekek.Clear();
lstHibak.Items.Clear();
hibasSorok.Clear();

int sorszam = chkFejlec.Checked ? 1 : 0;
HashSet<string> nevek = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
int ismetlodo = 0;

foreach (string[] sor in rekordok.Skip(chkFejlec.Checked ? 1 : 0))
{
sorszam++;

string nev = Mezo(sor, cmbNev), arSz = Mezo(sor, cmbAr), db = Mezo(sor, cmbDarab);
string ok = null;
double ar = 0;
int darab = 0;

if (string.IsNullOrWhiteSpace(nev)) { ok = "hiányzó név"; }
else if (nev.Length > 60) { ok = "a név túl hosszú (" + nev.Length + " karakter)"; }
else if (arSz == null || !double.TryParse(arSz, NumberStyles.Float, kultura, out ar) || ar < 0 || double.IsInfinity(ar)) { ok = "hibás ár: '" + arSz + "'"; }
else if (cmbDarab.SelectedIndex > 0 && (db == null || !int.TryParse(db, NumberStyles.Integer, kultura, out darab) || darab < 0)) { ok = "hibás darabszám: '" + db + "'"; }

if (ok != null)
{
lstHibak.Items.Add(sorszam + ". sor: " + ok);
hibasSorok.Add(new KeyValuePair<string[], string>(sor, sorszam + ". sor: " + ok));
continue;
}

if (!nevek.Add(nev)) { ismetlodo++; }

termekek.Add(new Termek { Nev = nev, Ar = ar, Darab = darab, Kategoria = Mezo(sor, cmbKategoria) ?? "" });
}

tsslUzenet.Text = termekek.Count + " sor betöltve, " + hibasSorok.Count + " hibás" + (ismetlodo > 0 ? ", " + ismetlodo + " ismétlődő név (figyelmeztetés)" : "");
}

private List<KeyValuePair<string[], string>> hibasSorok = new List<KeyValuePair<string[], string>>();

private void btnHibakMent_Click(object sender, EventArgs e)
{
if (hibasSorok.Count == 0) { return; }

int idx;
string ut = FajlValaszto.Ment(this, "CSV (*.csv)|*.csv", null, "hibas-sorok", out idx);

if (ut == null) { return; }

try
{
using (StreamWriter iro = new StreamWriter(ut, false, new UTF8Encoding(true)))
{
iro.WriteLine(Csv.Sor(';', "Hiba", "Eredeti sor"));

foreach (var h in hibasSorok) { iro.WriteLine(Csv.Sor(';', h.Value, string.Join(" | ", h.Key))); }
}
}
catch (IOException ex) { MessageBox.Show(this, "A mentés nem sikerült: " + ex.Message, "Mentés", MessageBoxButtons.OK, MessageBoxIcon.Error); }
}

private void btnExport_Click(object sender, EventArgs e)
{
if (termekek.Count == 0) { return; }

int idx;
string ut = FajlValaszto.Ment(this, "CSV (*.csv)|*.csv", null, "termekek", out idx);

if (ut == null) { return; }

CultureInfo hu = CultureInfo.GetCultureInfo("hu-HU");

try
{
using (StreamWriter iro = new StreamWriter(ut, false, new UTF8Encoding(true))) // BOM az Excelnek
{
iro.WriteLine(Csv.Sor(';', "Név", "Ár", "Darab", "Kategória"));

foreach (Termek t in termekek) { iro.WriteLine(Csv.Sor(';', t.Nev, t.Ar.ToString("0.##", hu), t.Darab.ToString(CultureInfo.InvariantCulture), t.Kategoria)); }
}

tsslUzenet.Text = "Exportálva: " + Path.GetFileName(ut);
}
catch (IOException ex) { MessageBox.Show(this, "Az exportálás nem sikerült: " + ex.Message, "Exportálás", MessageBoxButtons.OK, MessageBoxIcon.Error); }
}
📷 Képernyőkép helye

Mit tegyél ide: a kész CSV-importáló egy hibás sorokat is tartalmazó fájllal.Javasolt fájlnév: winforms-10-5-kesz.webp · Alt szöveg: A kész CSV-importáló

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Osztálylista statisztikája

Egy nev;osztaly;jegy CSV-ből számold ki osztályonként az átlagot. Mit használsz?

Egy lehetséges megoldás

Az elemzett rekordok LINQ GroupBy csoportosítása az osztály szerint, az átlag a Average; a jegyet TryParse olvassa, a hibás sort átugorja és jelenti.

Form1.csC#
var atlagok = rekordok.Skip(1).Where(r => r.Length >= 3).Select(r => new { Osztaly = r[1], Jegy = double.TryParse(r[2], NumberStyles.Float, hu, out double j) ? (double?)j : null }).Where(x => x.Jegy != null).GroupBy(x => x.Osztaly).Select(g => new { Osztaly = g.Key, Atlag = g.Average(x => x.Jegy.Value) });

2. Táblázat exportja DataGridView-ból

Exportáld a DataGridView tartalmát CSV-be, a rejtett oszlopok nélkül. Mire figyelsz?

Egy lehetséges megoldás

Csak a Visible oszlopokat, a DisplayIndex sorrendjében; a cellák Value értékét számként formázva (kultúra), a szöveges mezőket idézve és képletvédelemmel; az új sor sort (IsNewRow) kihagyva.

3. Nagy CSV: soronkénti feldolgozás

Egy 500 MB-os CSV-ből csak az első 1000 rekordot kell megnézni. Mi a hatékony megoldás?

Egy lehetséges megoldás

A Csv.Elemez TextReader paramétert kap: a StreamReader (nem a teljes szöveg) átadásával, és egy változat, amely IEnumerable<string[]> lustán ad vissza (yield return), megáll az 1000. rekordnál. A teljes fájl beolvasása elkerülhető.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Idézés

Hogyan kerül CSV-be (; elválasztóval) a Kovács "Béla"; 5.a szöveg?

Megoldás megnyitása

"Kovács ""Béla""; 5.a": a mező idézőjelbe kerül (elválasztót tartalmaz), a belső idézőjelek megkettőződnek.

2. Melyik elválasztó?

Melyik elválasztót használja a magyar Excel, és miért?

Megoldás megnyitása

A pontosvesszőt: a magyar tizedesjel a vessző, ezért az nem lehet mezőelválasztó.

Közepes szint

3. Fejléc nélküli fájl

Hogyan döntöd el automatikusan, hogy van-e fejléc?

Megoldás megnyitása

Heurisztika: az első sor mezői mind szövegek (nem számok), míg az alatta lévő soroknál van szám. Nem tévedhetetlen: a felhasználó jelölőnégyzettel felülbírálhatja.

4. Dátum a CSV-ben

A fájlban 2026. 10. 28. és 2026-10-28 is előfordul. Hogyan olvasod?

Megoldás megnyitása

DateTime.TryParseExact több formátum tömbjével (new[] { "yyyy. MM. dd.", "yyyy-MM-dd" }), a kultúra megadásával; a nem illeszkedő érték hibás sor.

Kihívás szint

5. Lusta feldolgozó

Alakítsd a Csv.Elemez metódust IEnumerable<string[]> típusúra. Mi az előnye?

Megoldás megnyitása

Az yield return rekordonként ad vissza: a hívó korán abbahagyhatja, és a teljes fájl nem kerül a memóriába. A FormatException a felsorolás közben keletkezik.

6. Hibás fájl javítása

A fájl egy sorában lezáratlan idézőjel van. Hogyan tudnád a többi sort megmenteni?

Megoldás megnyitása

A lezáratlan idézőjel az összes további sort elnyelné. Védekezés: legfeljebb N sorig (például 50) engedélyezett többsoros mező; azon túl a feldolgozó a rekordot hibásnak jelöli, és az idézőjel-állapotot a következő sor elején visszaállítja. Ez heurisztika, a legjobb a hibás fájl jelentése.

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért nem elég a Split a CSV-hez?

Az idézőjeles mezőben lehet elválasztó, idézőjel és sortörés; a Split ezeket rosszul vágja.

2. Hogyan jelöli a CSV az idézőjelet a mezőben?

Megkettőzéssel: "".

3. Mi az állapotgépes feldolgozó lényege?

Karakterenként olvas, és az idezoben jelző dönti el, hogy az elválasztó/sortörés elválaszt vagy a mező része.

4. Miért kell kultúrát adni a számolvasásnál?

A tizedesjel kultúrafüggő: a magyar „120,5” invariáns kultúrával 1205 lesz.

5. Mi a CSV-injekció?

A =, +, -, @ kezdetű szöveget az Excel képletként hajtja végre; a szöveges mezők elé ' előtag kell.

6. Mit tesz az Excel BOM nélküli UTF-8 CSV-vel?

ANSI kódolásúnak veszi: az ékezetek hibásak lesznek; BOM-os UTF-8 kell.

7. Mit kezdesz a hibás sorokkal?

Jelented (sorszám + ok), a jó sorokat betöltöd, a hibásakat kijavításra elmented.

Mit tudsz már?

  • Ismered a CSV szabályait, és idézőjel-kezelő feldolgozót írsz.
  • Felismered az elválasztót, és fejléc szerint megfelelteted az oszlopokat.
  • Számot és dátumot olvasol a megfelelő kultúrával.
  • Excel-barát, védett CSV-t írsz (BOM, idézés, képletvédelem).
  • A hibás sorokat jelented, nem veszíted el az adatot.

Következő lecke: JSON szerializálás és visszaolvasás

Objektumok mentése és betöltése JSON formátumban: System.Text.Json, beállítások, ékezetek, enumok, dátumok és a hibás fájlok kezelése.