Laden...

[Strategie Gesuch] Text Datei einlesen mit Informationsblöcken

Erstellt von Pardasus vor 7 Jahren Letzter Beitrag vor 7 Jahren 3.887 Views
P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren
[Strategie Gesuch] Text Datei einlesen mit Informationsblöcken

Moin,
ich Programmiere in C# und bin auf der suche nach einer Strategie, wie ich folgendes einlesen kann.

Beispiel.txt

  1. 000215545345TestMuster
  2. 45346666gegergeg463HFDFDF9999
  3. 545454GGGGGhhhh
  4. 430xxxhhrhTester123
  5. 00021343453TestMmmma
  6. 453453450000geeea3333DDD
  7. gergeg4345435
  8. 9999444GGGMusterTester

In der Datei sind Information die ich in eine Datenbank bringen will. Ich habe hier einfach nur etwas Blödsinn eingegeben, keine echten Daten 😉
Ein neuer Datensatz beginnt immer z.b. mit 00021 und dann folgen weiter Information.

Ich lese die ganze Datei in z.B. daten[] ein. Dann Iterieren ich mit foreach durch alle Zeilen der Datei.

Wie ich die Zeilen selbst auswerte, und in die DB bringe weis ich. Aber mir fehlt irgendwie die Strategie, wie ich immer wenn 00021 auftaucht, einen neuen Datensatz in der DB anfange. Ein Datensatz kann über mehrere Zeilen gehen.

Jemand eine Idee?

T
461 Beiträge seit 2013
vor 7 Jahren

Hey,

du hast dir die Antwort ja schon selbst gegeben, du schleifst einfach, wie dus schon tust, durch daten[] und jedes Mal wenn ?string.StartWith("00021") daherkommt, weißt du, daß du einen neuen Datensatz schreiben sollst.

Wie du die Daten im Code handhabst bis zum UPDATE ist eine andere Sache, da müßtest schon mehr Infos rüberbringen 😉

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Ist gar nicht so einfach zu erklären. Es klingt anfangst ja auch ganz einfach, immer wenn 00021 auftaucht, einen neuen Datensatz in der DB anfangen. Aber so einfach ist es nicht, irgendwie will mir einfach nicht die Lösung einfallen.

Wenn in der Text Datei jede Zeile eine Datenbank eintrag wäre, wäre es ja Kinderleicht. Aber ein Datensatz geht ja über mehrere Zeilen.

Mal ganz banal gesagt


foreach (var zeile in zeilen) {
 if(zeile == "00021") {
  zeile a = dbeintrag
  zeile b += dbeintrag
  zeile c += dbeintrag
  zeile d += dbeintrag
 }
}

Wenn zeile e wieder 00021 ist, müsste der dbeintrag geschrieben werden und es geht wieder von vorne los. Eigentlich müsste wenn 00021 gefunden wird, die nächsten z.b. 4 Zeilen mit ausgewertet werden und die foreach Schleife müsste dann in der 5 Zeile erst weiter machen.

Sorry, gar nicht so einfach zu beschreiben 😉 er müsst also solange die Zeilen "Sammeln" bis wieder 00021 auftaucht und erst dann den DB Eintrag machen.

3.003 Beiträge seit 2006
vor 7 Jahren

er müsst also solange die Zeilen "Sammeln" bis wieder 00021 auftaucht und erst dann den DB Eintrag machen.

Genau. Jetzt musst du das nur noch in Code gießen.


Datensatzobjekt meinObjekt;
solange zeilen da sind
{
    beginnt zeile mit 00021 ?
    ja: wenn meinObjekt gefüllt ist, speichern und meinObjekt neu erstellen
    nein: meinObjekt weiter füllen
}
meinObjekt speichern. (letzter Datensatz)

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

W
955 Beiträge seit 2010
vor 7 Jahren

..wobei Zeile 8 zu erweitern wäre mit "wenn meinObjekt gefüllt ist, speichern" 😃

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Das mit dem Objekt ist eine gute Idee. Ich baue mir dort eine Methode ein, mit der ich neue zeilen übergeben kann. Wenn 00021 wieder vorkommt muss er beim Objekt nur einmal die Methode speicher öffnen und das gleiche Objekt wieder neu erstellen oder noch besser, die Daten im Objekt wieder löschen.
Im Objekt arbeite ich dann mit eine Liste die dann die Information auswertet.

Vielen Dank 😃

D
985 Beiträge seit 2014
vor 7 Jahren

Warum alles in eine Methode/Funktion quetschen?

Mach das doch einfach immer Schritt für Schritt, das schon auch noch den RAM-Verbrauch.
1.Die Text-Datei als IEnumerable<string> öffnen 1.Aus so einem IEnumerable<string> liest man jetzt die einzelnen Blöcke raus und gibt diese als IEnumerable<IList<string>> zurück 1.Aus jedem dieser Blöcke kann nun ein Builder/Factory die gewünschte Instanz erzeugen und die übergibt man dann der Datenbank

Hier ein Beispiel, wie man diese Blöcke trennen kann:


IEnumerable<IList<string>> EnumerateBlocks( IEnumerable<string> source )
{
    IList<string> currentBlock = null;
    foreach ( string line in source )
    {
        if ( line.StartsWith( "00021" )
        {
            if ( currentBlock != null ) yield return currentBlock;
            currentBlock = new List<string>();
        }

        currentBlock.Add( line );
    }
    if ( currentBlock != null ) yield return currentBlock;
}

Lustigerweise werden diese Methoden auch noch testbar und man kann in jede einzelne noch Prüfungen einbauen um die Konsitenz zu gewährleisten.

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

@Sir Rufo Danke 😃 sowas habe ich gesucht!

Ich muss zugeben mich noch nie mit IEnumerable beschäftigt zu haben.
Habe mir nun ein paar Beispiele angesehen und auf dein Code ca. 15 Min. gestarrt und es jetzt auch verstanden und bin begeistert!

Kurz kann man sagen, dass IEnumerable ermöglich das man etwas mit z.b. Foreach durchlaufen kann, richtig?

Das mit dem currentBlock ist genau das was ich gesucht habe was die ganze sache noch Perfektioniert 😃 nachdem ich wie gesagt 15 Min. drauf gestarrt habe, konnte ich förmlich die Blöcke sehen wie sie entstehen 😛

Eine frage habe ich aber noch:


foreach(var block in EnumerateBlocks( Datei-Inhalt als Array ))
{
 foreach(var zeile in block)
 {
  ...zeile...
  }
}

Damit frage ich die Blöcke dann doch ab, oder?

p.s.: was heist eigentlich yield return? Ein Return ohne abbruch?

D
985 Beiträge seit 2014
vor 7 Jahren

Das yield return ist so ein Hilfsmittel damit du keinen kompleteen Enumerator bauen musst. Wenn der Code umgesetzt wird, wird daraus hinter den Kulissen eine Enumerator-Klasse gebaut (sehr praktisch)

Die Umwandlung der Blöcke in Instanzen würde ich auch durch ein Enumerable darstellen:


IEnumerable<DatensatzObject> GetMirDieObjekte( IEnumerable<IList<string>> source )
{
    return source.Select( b => GetMirDasObjekt( b ) );
}

DatensatzBlock GetMirDasObjekt( IList<string> source )
{
    return new DatensatzObjekt { wie auch immer das gebaut werden muss }
}

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

@Sir Rufo

Brauche jetzt doch noch mal deine Hilfe. Bekomme das irgendwie nicht eingebunden oder ich verstehe da irgendwas noch nicht richtig. Kannst du mir ein kleines Beispiel geben?



var bloecke = EnumerateBlocks(File.ReadAllLines(datei));

private IEnumerable<IList<string>> EnumerateBlocks( IEnumerable<string> source )
{
  IList<string> currentBlock = null;
  foreach (string line in source)
  {
   if (line.StartsWith("00021"))
   {
    if (currentBlock != null) yield return currentBlock;
				
    currentBlock = new List<string>();
   }
				
   currentBlock.Add(line);
  }
  if (currentBlock != null) yield return currentBlock;
}

Fehlermeldung:
Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.

Er mag das " line " nicht. Komme aber einfach nicht drauf.
Übergebe ich eigentlich eine List<string> oder ein Array[] ?

D
985 Beiträge seit 2014
vor 7 Jahren

Greife auf die Datei als IEnumerable<string> zu, dann kann die Datei auch zig Trillionen PetaByte groß sein, und du kannst das immer noch verarbeiten.


IEnumerable<string> EnumerateTextFileByLine( string path )
{
    using ( StreamReader reader = File.OpenText( path ) )
    {
        string currentLine = reader.ReadLine();
        while ( currentLine != null )
        {
            yield return currentLine;
            currentLine = reader.ReadLine();
        }
    }
}

Der Aufruf sieht dann z.B. so aus


var meineObjekte = GetMirDieObjekte( EnumerateBloecke( EnumerateTextFileByLine( datei ) ) );

foreach ( var meinObjekt in meineObjekte )
{
    SpeicherInDatenbank( meinObjekt );
}

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Ich habe das mal so umgesetzt, es kommt aber weiterhin die Fehlermeldung

Fehlermeldung:
Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.

für "line" in der EnumerateBlocks

H
523 Beiträge seit 2008
vor 7 Jahren

Poste mal Deine Funktion

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Das mit "foreach (var zeile in inhalt)" ist nur zum Testen ob es funktioniert.


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace Verarbeitung
{
	public partial class MainForm : Form
	{
		string datei = Directory.GetCurrentDirectory() + @"\test.txt";
		
		public MainForm()
		{
			InitializeComponent();
			
			button1.Click += (s,e) => this.Close();
			button2.Click += (s,e) => Einlesen();
		}
		
		private void Einlesen()
		{
			var inhalt = (EnumerateBlocks(FileLesen(datei)));

			foreach (var zeile in inhalt) {
				listBox1.Items.Add(zeile);
			}
		}
		private IEnumerable<IList<string>> EnumerateBlocks( IEnumerable<string> source )
		{
			IList<string> currentBlock = null;
			foreach ( string line in source )
			{
				if ( line.StartsWith( "00021" )) {
				    	if ( currentBlock != null ) yield return currentBlock;
				    	currentBlock = new List<string>();
				    }
				    currentBlock.Add( line );
				}
			if ( currentBlock != null ) yield return currentBlock;
		}
		
		private IEnumerable<string> FileLesen(string path)
		{
			using (StreamReader reader = File.OpenText(path)) {
				string currentLine = reader.ReadLine();
				while (currentLine != null) {
					yield return currentLine;
					currentLine = reader.ReadLine();
				}
			}
		}
	}
}

4.942 Beiträge seit 2008
vor 7 Jahren

Hallo,

benutze den [Artikel] Debugger: Wie verwende ich den von Visual Studio?

Ich sehe (bzw. vermute) zwar den Fehler, aber du sollst es ja lernen, solche einfachen Programmierfehler zu lösen...

PS: Ich würde die Methode noch allgemeiner halten (und ein Predicate übergeben):


private IEnumerable<IList<string>> EnumerateBlocks(IEnumerable<string> source, Predicate<string> predicate)
{
    // ...
    if (predicate(line))
    // ...
}

Und die andere Methode läßt sich auch noch vereinfachen:


IEnumerable<string> EnumerateTextFileByLine(string path)
{
    using (StreamReader reader = File.OpenText(path))
    {
        string currentLine;
        while ((currentLine = reader.ReadLine()) != null)
        {
            yield return currentLine;
        }
    }
}

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Konnte das Problem nun lösen dank

-> NullReferenceException

Wenn man sich den Text durchliest, klingt es auch sehr logisch. Das Exception wurde ausgelöst, weil die ersten zwei zeilen der txt Datei nicht mit 00021 begonnen haben.

Vielen Dank an euch! Jetzt macht alles, was es soll 😃

D
985 Beiträge seit 2014
vor 7 Jahren

Dann hast du aber die Struktur der Quelldatei falsch beschrieben oder diese Quelldatei ist einfach falsch/nicht konform 😉

Schön daran ist aber, dass so etwas sofort bemerkt wird und somit keine falschen Daten aus nicht konformen Quelldateien eingespielt werden, was mindestens genauso wichtig ist, wie das korrekte Auslesen aus den konformen Quelldateien.

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Da gebe ich dir recht, aber das stellt mich gerade vor einen neuen Problem. Jede Datei beginnt immer mit zwei zeilen die mich nicht Interessieren bevor es mit 00021 los geht.

Nun könnte ich im using vom StreamReader einfach zwei mal reader.ReadLine() ausführen und danach mit den while anfangen, aber geht das nicht auch schöner?

D
985 Beiträge seit 2014
vor 7 Jahren

Was ist denn schöner als exakt das zu machen, was die Konvention vorgibt?

"Die ersten beiden Zeilen sind nicht die Informationen nach denen ihr sucht!"

Und solche Scherze wie erstmal schauen, wann wir auf eine Zeile mit "00021" treffen würde ich mir tunlichst verkneifen, wenn die Konvention von einer festen Anzahl von Zeilen (hier 2) spricht.

P
Pardasus Themenstarter:in
63 Beiträge seit 2016
vor 7 Jahren

Ich habe es 😃


		private IEnumerable<IList<string>> EnumerateBlocks( IEnumerable<string> source )
		{
			IList<string> currentBlock = null;
			foreach ( string line in source )
			{
				if ( line.StartsWith( "00021" )) {
				    	if ( currentBlock != null ) yield return currentBlock;
				    	currentBlock = new List<string>();
				}
				
				if(currentBlock != null) currentBlock.Add( line );
			}
			if ( currentBlock != null ) yield return currentBlock;
		}

Nur wenn etwas mit 00021 beginnt, wird es in ein block geladen. Alles andere wird ignoriert.

D
985 Beiträge seit 2014
vor 7 Jahren

Das ist die schlechteste Variante.

Das ist wesentlich sicherer um falsche Daten gleich abzulehnen:


        private IEnumerable<IList<string>> EnumerateBlocks( IEnumerable<string> source )
        {
            IList<string> currentBlock = null;
            foreach ( string line in source.Skip( 2 ) )
            {
                if ( line.StartsWith( "00021" )) {
                        if ( currentBlock != null ) yield return currentBlock;
                        currentBlock = new List<string>();
                }

                if(currentBlock == null) throw new ParseException( "Diese Daten kann ich nicht verarbeiten!" );

                currentBlock.Add( line );
            }
            if ( currentBlock != null ) yield return currentBlock;
        }

Update
Meistens befinden sich in diesen Zeilen vor den eigentlichen Daten noch sinnvolle Informationen (Header) die man dann auch tunlichst auswerten sollte (soweit bekannt) und die man dann auch mit jedem Block weiterreicht.