16. april 2007 - 00:50Der er
28 kommentarer og 1 løsning
Læsning i flere niveauer med Regex
Hej, vil lige høre om det er muligt at lave en regex sætning som vil være istand til at læse noget data som er delt op i flere niveauer.
f.eks. hvis man har en skole, så vil den bestå af følgende. Skolens navn, Klasse navn, og elev navn med fornavn og efternavn.
og text filen. Skole: Gauerslund Klasse: 7.A Elev: Jens Jensen Elev: Ole Olesen Klasse: 7.B Elev: Mette Hansen Elev: Ida Nielsen ovs....
Er det muligt ved hjælp af Regex at lave noget kode der kan læs det data ind, således at jeg kan få placeret eleverne i de rigtige klasser i nogle tilsvarende objekter bagefter.
Umiddelbart tror jeg da at det nemmeste vil være at læse en linie ad gangen, kalde Split på kolon, og udføre forskelligt alt efter hvad første del af linien er.
Det var nu egentlig mest fordi jeg aldrig har brugt regex i mine programmer før, så ville godt lige prøve og se om jeg kunne få noget pænt kode udaf det.
F.eks. hvis jeg nu gerne vil hente For- og Efternavn ud enkeltvis, må det vel blive noget pænere kode med regex, og hvordan vil man tage højde for at et evt. mellemnavn, eller hvad nu hvis jeg vil hente klasse ud som et tal 7 og retningen A
Split linie ved ":" giver dig f.eks: [0] = "Skole", [1] = " Gauerslund" [0] = "Klasse", [1] = " 7.A" [0] = "Elev", [1] = " Jens Jensen"
Du kan derefter bruge Switch, eller IF-sætninger, til at behandle arrayet der dannes.
Husk at fjerne det første mellemrum i [1].
Hvis [0] er "skole", er det nemt nok
Hvis [0] er "klasse", kan du enten splitte ved "." og få "[0]=7 og [1]=b", eller bruge substring, for at få samme resultat.
Hvis [0] er "elev", kan du splitte [1] ved mellemrum. Antallet af dele du får af det andet split, fortæller dig om der mellemnavne med. Eksempel: Antal dele: 2 = fornavn + efternavn Antal dele: 3 = fornavn + mellemnavn + efternavn
For at få fat på fornavn og efternavn, skal du bruge: Fornavn: [0] Efternavn: [max antal dele], dvs: arrayet.length (eller arrayet.length - 1.. kan aldrig huske det)
I dit tilfælde vil koden ikke blive pænere med regex, da du skal til at rive fat i mange forskellige ting som Match collections, Group Collections og hvis du i forvejen har en ordnet fil som det der, så er det langt næmmere det andet...
Det er nok er den vigtigste parameter er at flytte funktionelt kode til et regex udtryk kan måske i nogle tilfælde køre koden mere kompakt (nok ikke her), men et regex kan være yderst complext og svært at forstå...
Men som svar til om det kan lade sig gøre, så jow... det kan det som sådan godt...
Min erfaring er, at det er hurtigere at læse hele filen og derefter parse den, fremfor at læse 1 linie, parse den, læse næste linie osv..
Dvs:
Skole skole = null; StreamReader sr = new StreamReader(@"C:\skole.txt"); string indhold = sr.ReadToEnd(); sr.close
string[] linier = indhold.split("\r\n".toCharArray(); for(int i = 0, len = linier.length; i < len; i++) { string line = linier[i]; string[] parts = line.Split(':'); ........ resten af koden .......; } osv..
Og der bør altid være TryCatch udenom koden.. specielt når man arbejder med filer, der jo kan flyttes mellem mapper..
Hvis man undrer sig over denne "erfaring", er svaret her:
Jeg skulle lave et program for en ven, der løb en tekstfil igennem, og ændre kommentar til STORE bogstaver..
Min erfaring viste, at ReadToEnd-metoden var meget hurtigere end ReadLine-metoden, specielt ved store tekstfiler..
Jeg testede med filer på et par KB og op til et par MB.. Forskellen var på ca: ReadToEnd: ~30 sekunder for MB filen ReadLine: 2-3 minutter for MB filen
Der manglede en slut-parantes i split-linier-linien (og ";" ved "sr.close")... rettelsen:
Skole skole = null; StreamReader sr = new StreamReader(@"C:\skole.txt"); string indhold = sr.ReadToEnd(); sr.close;
string[] linier = indhold.split("\r\n".toCharArray()); for(int i = 0, len = linier.length; i < len; i++) { string line = linier[i]; string[] parts = line.Split(':'); ........ resten af koden .......; }
ReadToEnd og Split kan måske nok læse hurtigere men den bruger også memory svarende til det ca. 4 gane filens størrelse, hvilket godt kan blive et problem ved store filer.
Man kan opnå tilsvarende effekt ved at angive en buffer størrelse til StreamReader og læse en linie adf gangen.
Jeg kan iøvrigt ikke helt genskabe det at ReadToEnd er hurtigere:
using System; using System.IO; using System.Text;
namespace E { public class MainClass { public static void TestStd() { StreamReader sr = new StreamReader(@"C:\avuarc.ext"); DateTime t1 = DateTime.Now; string line; int n = 0; while((line = sr.ReadLine()) != null) { n++; } DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine("one line: " + n + " lines " + (t2 - t1).TotalSeconds + " secs " + GC.GetTotalMemory(false)/1000000 + " MB"); sr.Close(); } public static void TestOne() { StreamReader sr = new StreamReader(@"C:\avuarc.ext"); DateTime t1 = DateTime.Now; string all = sr.ReadToEnd(); string[] lines = all.Split("\r\n".ToCharArray()); int n = lines.Length/2; // <---- hack because we get a lot of fake empty lines DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine("all and split: " + n + " lines " + (t2 - t1).TotalSeconds + " secs " + GC.GetTotalMemory(false)/1000000 + " MB"); sr.Close(); } public static void TestBuf() { StreamReader sr = new StreamReader(@"C:\avuarc.ext", Encoding.Default, false, 100000); DateTime t1 = DateTime.Now; string line; int n = 0; while((line = sr.ReadLine()) != null) { n++; } DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine("one line with buffer: " + n + " lines " + (t2 - t1).TotalSeconds + " secs " + GC.GetTotalMemory(false)/1000000 + " MB"); sr.Close(); } public static void Main(string[] args) { TestStd(); TestOne(); TestBuf(); TestStd(); TestOne(); TestBuf(); TestStd(); TestOne(); TestBuf(); Console.ReadLine(); } } }
skriver:
one line: 731937 lines 0,390625 secs 0 MB all and split: 731936 lines 0,78125 secs 124 MB one line with buffer: 731937 lines 0,28125 secs 0 MB one line: 731937 lines 0,390625 secs 0 MB all and split: 731936 lines 0,796875 secs 124 MB one line with buffer: 731937 lines 0,265625 secs 0 MB one line: 731937 lines 0,375 secs 0 MB all and split: 731936 lines 0,796875 secs 124 MB one line with buffer: 731937 lines 0,265625 secs 0 MB
Jeg har før oplevet, at hvis man kun splitter ved "\n", vil der stadig være "\r" i linien.. derfor splitter jeg ved "\r\n"..
Men hvis det ikke er det linien gør (som jeg tror at det er), hvad gør den så? Eller, hvordan skal det se ud, for at den gør hvad jeg gerne vil have den til..? :-)
namespace E { public class MainClass { public static void Main(string[] args) { string s = "A\r\nBB\r\nCCC"; string[] parts = s.Split("\r\n".ToCharArray()); for(int i = 0; i < parts.Length; i++) { Console.WriteLine("part[" + i + "]=#" + parts[i] + "#"); } } } }
Ahh, ja.. Jeg glemte at C# virkeligt stinker til split.. :-)
I gamle dage, da man programmerede i Microsoft Visual Basic 6, og ikke .Net udgaven, splittede "Split" ved samtlige karakterer på een gang, og ikke "multi-split" ved hvert eneste karakter..
I skolen er vi bl.a. igang med programmering med Java på mobile enheder.. J2ME.. I J2ME har man String[], men man har ikke Split-funktionen.. hvilket jeg synes er underligt..
Så jeg lavede den selv:
public int getNumberOfSplits(String input, String delimiter) { int c = 0; if (input.indexOf(delimiter) > -1) c = 1; while(input.indexOf(delimiter) > -1) { input = input.substring(input.indexOf(delimiter) + 1); c++; } return c; }
public String[] split(String input, String delimiter) { int ndx = 0; String part = ""; String[] sTmp = new String[getNumberOfSplits(input, delimiter)]; while(input.indexOf(delimiter) > -1) { part = input.substring(0, input.indexOf(delimiter)); sTmp[ndx] = part; input = input.substring(input.indexOf(delimiter) + 1); ndx++; } sTmp[ndx] = input; return sTmp; }
Denne custom Split-funktion splitter ved samtlige tegn på een gang, i modsætning til standard-Split i C#..
Hvis den kan optimeres, er du velkommen til at skrive en rettelse.. eller skrive det om.. :-)
PS: Custom Split-funktionen må benyttes og modificeres som man lyster.. Offentliggør gerne bedre versioner.. :-)
jeg ved ikke om det er en daarlig funktionalitet af Split - man skal bare vide hvad den goer
using System; using System.Text.RegularExpressions;
namespace E { public class MainClass { public static void Main(string[] args) { string s = "A\r\nBB\r\nCCC"; string[] parts = Regex.Split(s, "\r\n"); for(int i = 0; i < parts.Length; i++) { Console.WriteLine("part[" + i + "]=#" + parts[i] + "#"); } } } }
JSE fra og med version 1.4 har en indbygget split paa String (som goer hvad du vil i dette tilfaelde - men man skal vaere opmaerksom paa at det ogsaa er en regex)
JME har ikke naer saa mange features som JSE - ikke overraskende
Den kommentar er jeg da helt uenig i... overvej lige hvad det er du ber den om... Det du fortæller den er: Split på \r OG DEREFTER OGSÅ på \n... så du giver den ikke en Delimiter som er "\r\n" men 2 som er '\r' og '\n'...
Det du leder efter en en som splitter på "\r\n" og derefter på "\r" og så på "\n"... at .NET 1.1 så mangler den funktion til at splitte på strings frem for chars, er en helt anden sag. det var ikke noget super tiltag kan vi godt blive enige om. Det er der i .NET 2.0... og her er et eksempel...:
using System; using System.Collections.Generic; using System.Text;
namespace ConsoleApplication3 { class Program { static void Main( string[] args ) { string str = "Dette\r\ner\nen\rtekst\r\npå\rflere\nlinier"; Console.WriteLine( "-------------------------------" ); Console.WriteLine( "Split på \\n og så på \\r" ); foreach ( string s in str.Split( '\n', '\r' ) ) { Console.WriteLine( "#" + s + "#" ); }
Console.WriteLine( "-------------------------------" ); Console.WriteLine( "Split på \\r\\n så på \\n og så på \\r" ); foreach ( string s in str.Split(new string[]{"\r\n", "\n", "\r"}, StringSplitOptions.None) ) { Console.WriteLine( "#" + s + "#" ); }
Console.WriteLine( "-------------------------------" ); Console.WriteLine( "Split på \\r\\n så på \\n og så på \\r" ); foreach ( string s in str.Split( new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.None ) ) { Console.WriteLine( "#" + s + "#" ); } } } }
Resultat:
------------------------------- Split på \n OG OGSÅ \r #Dette# ## #er# #en# #tekst# ## #på# #flere# #linier# ------------------------------- Split på \n\r først så \n OG OGSÅ \r #Dette# #er# #en# #tekst# #på# #flere# #linier#
og den følger følgende regler:
To avoid ambiguous results when strings in separator have characters in common, the Split operation proceeds from the beginning to the end of the value of the instance, and matches the first element in separator that is equal to a delimiter in the instance. The order in which substrings are encountered in the instance takes precedence over the order of elements in separator.
For example, consider an instance whose value is "abcdef". If the first element in separator was "ef" and the second element was "bcde", the result of the split operation would be "a" and "f". This is because the substring in the instance, "bcde", is encountered and matches an element in separator before the substring "f" is encountered.
However, if the first element of separator was "bcd" and the second element was "bc", the result of the split operation would be "a" and "ef". This is because "bcd" is the first delimiter in separator that matches a delimiter in the instance. If the order of the separators was reversed so the first element was "bc" and the second element was "bcd", the result would be "a" and "def".
Mit eksempel inkluderer i modsætning til det Arne V poster i 17/04-2007 22:15:29 en split på "\r" og "\n" enkeltvis...
Så den prøver først med "\r\n" har den et match så splitter den der, hvis ikke prøver den med "\r" og så til sidst med "\n"...
Et alternativ til hele det her hurlumhej med splits er jo at bruge en StringReader istedet... hvis man nu vil læse hele filen op i hukommelsen...
Nok ser det ikke ud til at det performer bedre i det senarie vi har her hvor man skal indlæse data... men hvis man istedet skulle behandle data, ændre hist og pist og evt. løbende efterhånden som brugeren inteagere med programmet... Så ville man performance vis godt kunne vinde noget i og med at det trods alt er hurtigere at arbejde med ram end med disk IO...
Men der er den åbenlyse downside som Arne understreger at det jo sluger godt på hukommelsen, og når man op hvor der begynder at skulle swappes så taber man evt. det man vinder i det at den skal til det...
Men den interassante pointe her kunne være... hvordan performer ReadToEnd + Split vs. ReadToEnd + StringReader ???....
Det er faktisk et eller andet sted et interassant resultat:
using System; using System.IO; using System.Text;
namespace E { public class MainClass { public static void TestStd() { StreamReader sr = new StreamReader( @"C:\avuarc.ext" ); DateTime t1 = DateTime.Now; string line; int n = 0; while ( ( line = sr.ReadLine() ) != null ) { n++; } DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine( "one line: " + n + " lines " + ( t2 - t1 ).TotalSeconds + " secs " + GC.GetTotalMemory( false ) / 1000000 + " MB" ); sr.Close(); } public static void TestStringRd() { StreamReader so = new StreamReader( @"C:\avuarc.ext" ); StringReader sr = new StringReader( so.ReadToEnd() ); DateTime t1 = DateTime.Now; string line; int n = 0; while ( ( line = sr.ReadLine() ) != null ) { n++; } DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine( "all and reader: " + n + " lines " + ( t2 - t1 ).TotalSeconds + " secs " + GC.GetTotalMemory( false ) / 1000000 + " MB" ); sr.Close(); } public static void TestOne() { StreamReader sr = new StreamReader( @"C:\avuarc.ext" ); DateTime t1 = DateTime.Now; string all = sr.ReadToEnd(); string[] lines = all.Split( "\r\n".ToCharArray() ); int n = lines.Length / 2; // <---- hack because we get a lot of fake empty lines DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine( "all and split: " + n + " lines " + ( t2 - t1 ).TotalSeconds + " secs " + GC.GetTotalMemory( false ) / 1000000 + " MB" ); sr.Close(); } public static void TestBuf() { StreamReader sr = new StreamReader( @"C:\avuarc.ext", Encoding.Default, false, 100000 ); DateTime t1 = DateTime.Now; string line; int n = 0; while ( ( line = sr.ReadLine() ) != null ) { n++; } DateTime t2 = DateTime.Now; GC.Collect(); Console.WriteLine( "one line with buffer: " + n + " lines " + ( t2 - t1 ).TotalSeconds + " secs " + GC.GetTotalMemory( false ) / 1000000 + " MB" ); sr.Close(); } public static void Main( string[] args ) { StreamWriter sr = new StreamWriter( @"C:\avuarc.ext" ); for ( int i = 0; i < 5000000; i++ ) sr.WriteLine( "line {0:D5}" ); sr.Close();
TestStd(); TestOne(); TestBuf(); TestStringRd();
TestStd(); TestOne(); TestBuf(); TestStringRd();
TestStd(); TestOne(); TestBuf(); TestStringRd();
Console.ReadLine();
File.Delete( @"C:\avuarc.ext" ); } } }
one line: 5000000 lines 0,53125 secs 0 MB all and split: 5000000 lines 1,96875 secs 374 MB one line with buffer: 5000000 lines 0,59375 secs 0 MB all and reader: 5000000 lines 0,375 secs 134 MB one line: 5000000 lines 0,5 secs 0 MB all and split: 5000000 lines 1,984375 secs 374 MB one line with buffer: 5000000 lines 0,59375 secs 0 MB all and reader: 5000000 lines 0,375 secs 134 MB one line: 5000000 lines 0,484375 secs 0 MB all and split: 5000000 lines 1,984375 secs 374 MB one line with buffer: 5000000 lines 0,578125 secs 0 MB all and reader: 5000000 lines 0,375 secs 134 MB
for det første - hvis du alligevel vil have det hele i en enkelt streng, så er ReadToEnd naturligvis hurtigere end mange ReadLine og Append
for det andet er:
indhold += linie + "\n";
en rigtig performance dræber. Hvis du prøver at akkumulere i en StringBuilder vil du se en klar forbedret performannce.
Synes godt om
Ny brugerNybegynder
Din løsning...
Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.