{"id":282,"date":"2018-10-15T07:14:02","date_gmt":"2018-10-15T07:14:02","guid":{"rendered":"http:\/\/lapoc.de\/zblog\/?p=282"},"modified":"2018-10-15T07:24:27","modified_gmt":"2018-10-15T07:24:27","slug":"eine-praktische-aufgabe-losen-mit-sed-dem-stream-editor-in-linux","status":"publish","type":"post","link":"https:\/\/lapoc.de\/zblog\/?p=282","title":{"rendered":"Eine praktische Aufgabe l\u00f6sen mit sed, dem Stream Editor in Linux"},"content":{"rendered":"<p>Der StreamEDitor sed ist ein programmierbarer Texteditor. Sein Vorteil gegen\u00fcber normalen, interaktiven Editoren wie VIM oder einem beliebigen grafischen Texteditor ist, dass man f\u00fcr sed Vorg\u00e4nge programmieren kann, die man nicht per Hand ausf\u00fchren m\u00f6chte oder kann. Meistens geht es dabei nat\u00fcrlich um das Suchen und Ersetzen von Textbestandteilen.<\/p>\n<p>Ich m\u00f6chte an dieser Stelle eine praktische Arbeit erkl\u00e4ren, die ich vor kurzem mit sed gel\u00f6st habe:<\/p>\n<h3> Das Problem: ein fehlerhafter CSV Export einer Excel Datei<\/h3>\n<p>Die folgenden 4 Zeilen CSV sind ein Ausschnitt aus einem Dokument, das insgesamt 500 derartige Zeilen enth\u00e4lt.<\/p>\n<pre>\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,\"0,05\",\"0,1\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n\"CS\",GMA_BF_F2,\"F 2\",,,,,,,,\"0,07\",\"0,11\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2016,\r\n\"CS\",GMA_BF_CDD,\"Bo F\",,,,,\"28,4\",\"39,6\",\"6,5\",\"0,59\",\"0,63\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n\"CS\",GMA_BF_CDD_F1,\"F 1\",,,,,,,,\"0,57\",\"0,59\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2017,\r\n<\/pre>\n<p>Wie man sieht, handelt es sich um gleichartige Zeilen, die eine Art ID im zweiten Feld und eine Jahreszahl im vorletzten Feld enthalten. Die Daten dazwischen sind Messwerte, die in Flie\u00dfkommazahlen angegeben werden.<\/p>\n<p>Hier das Problem: die Spezialisten, die die Werte eingeben, sind frankophon. Das bedeutet: sie verwenden das Dezimal<strong>komma<\/strong> und nicht, wie Angelsachsen, einen Dezimalpunkt.<\/p>\n<p>F\u00fcr einen Menschen ist das kein Problem: 28,4 und 28.4 ist f\u00fcr einen Menschen ohne weiteres <em>Achtungzwanzig Komma Vier<\/em>. Roboter sind allerdings doof, f\u00fcr sie sind Punkt und Komma genauso unterschiedlich wie X und ?. Das m\u00fcssen sie auch sein, sonst w\u00fcrden wir die Maschinen kaum beherrschen k\u00f6nnen.<\/p>\n<p>Ganz praktisch wirkt sich der Unterschied so aus:<\/p>\n<ul>\n<li>Wenn die Zeilen eingelesen werden, um sie als einzelne Felder in eine Datenbank zu importieren, verwendet der Mechanismus, der die Zeile in Felder zerhackt, das Komma als Trennzeichen(sog. Delimiter). In menschlicher Sprache ausgedr\u00fcckt:\n<pre>\r\n  Lese diese Datei Zeile f\u00fcr Zeile, am Ende der Zeile steht das Zeichen f\u00fcr den Zeilenumbruch.\r\n  Lege f\u00fcr jedes Zeichen in der Zeile einen durchnummerierten Speicher an, bis Du ein Komma siehst\r\n  Dann lege einen weiteren Speicher an, bis zum Ende der Zeile.\r\n <\/pre>\n<p>Und f\u00fcr den Computer in die Programmiersprache PHP \u00fcbersetzt:<\/p>\n<pre>\r\n  $speicher=array();\r\n  foreach ($zeilen as $zeile){\r\n   $speicher[] = explode(\",\",$zeile);\r\n  }\r\n <\/pre>\n<\/li>\n<li>Das Ergebnis ist ein geordneter Datensatz, der Array genannt wird, der enth\u00e4lt 500 Eintr\u00e4ge. In jedem dieser Eintr\u00e4ge steht wiederum ein Array (also ein geordneter Datensatz als Eintrag in einem geordneten Datensatz). Und dieses enth\u00e4lt f\u00fcr jeden Abschnitt einer Zeile, der links von einem Komma steht, einen nummerierten Eintrag.<\/p>\n<p>Schau Dir die Zeilen oben an&#8230;.<br \/>\n Wo ist das Problem?<br \/>\n Nat\u00fcrlich! Die Kommata in den franz\u00f6sischen Flie\u00dfkommazahlen!\n <\/p>\n<p>Wenn der Algorithmus von <em>explode()<\/em> einfach immer einen neuen Eintrag anlegt, wenn er ein Komma sieht, haben wir schon in der ersten Zeile einen Eintrag mit: <em>&#8220;0<\/em> und gleich danach einen mit <em>05&#8243;<\/em>. Korrekt w\u00e4re nat\u00fcrlich nur <strong>ein<\/strong> Eintrag mit <em>0,05<\/em>. Nun k\u00f6nnte man sagen: &#8220;Aber pass auf, wenn ein Anf\u00fchrungszeichen gleich rechts von einem Komma steht, nimm das n\u00e4chste Komma erst wahr, wenn vorher ein weiteres Anf\u00fchrungszeichen kommt&#8221;. Das geht aber nicht, weil man PHP <em>explode()<\/em> das nicht anweisen kann. Den Automatismus, Zeichen wie das Komma in Ausschnitten, die in Anf\u00fchrungszeichen stehen, zu ignorieren, benutzt PHP <em>explode()<\/em> nicht. Man k\u00f6nnte jetzt sagen, dass explode au\u00dfer dem Komma auch das Anf\u00fchrungszeichen als Delimiter verwenden soll aber auch das ist nicht nur auf den zweiten Blick logischer Unsinn, es geht auch gar nicht: man kann zwar mehr als ein Zeichen als Delimter verwenden aber nicht verschiedene Delimiter im gleichen Arbeitsgang.<\/p>\n<p>Rein theoretisch k\u00f6nnte eine explode Funktion durchaus eine Option haben, die die Anf\u00fchrungszeichen ber\u00fccksichtigt, PHP <em>explode()<\/em> hat das aber nicht.<\/p>\n<\/li>\n<\/ul>\n<p>Wir haben also erst einmal zerhackte Einzeldaten und noch ein weiteres Problem: die Datens\u00e4tze sind unterschiedlich lang. Nicht alle Felder sind in jeder Zeile ausgef\u00fcllt und so kann man nicht sicher vorraussagen, wie die Nummer des Felds mit der Jahreszahl lautet. Auch das ist ein Fehler, der den Import sicher verhindert. Was nun? <\/p>\n<h3>L\u00f6sungsans\u00e4tze<\/h3>\n<p>Programmieren geht immer von W\u00fcnschen aus. Pr\u00e4zise formulierte, vollst\u00e4ndige W\u00fcnsche f\u00fcr eine verbesserte Situation.<\/p>\n<p>Wir w\u00fcnschen uns gleichf\u00f6rmige Datens\u00e4tze mit vollst\u00e4ndigen Eintr\u00e4gen.<\/p>\n<p>Damit entf\u00e4llt schon mal das L\u00f6schen der fiesen Flie\u00dfkommaeintr\u00e4ge. Das w\u00fcrde zwar gleich lange Datens\u00e4tze erm\u00f6glichen, aber die Messwerte sollen ja durchaus importiert werden.<\/p>\n<p>Ach! W\u00e4ren die Ersteller der Datens\u00e4tze doch Kenianer oder Iren gewesen! Dann w\u00e4ren in den Zeilen <em>Dezimalpunkte<\/em> und alles w\u00fcrde super esasy funktionieren&#8230;.. Wenn explode ein Komma sieht, k\u00f6nnte man sich dann darauf verlassen, dass es ein Feld abtrennt und nicht eine Dezimalstelle.<\/p>\n<p> Wie w\u00e4re es, wenn wir dem Schicksal nachhelfen? Ersetzen wir doch einfach die Kommata durch Punkte! Cool, nur: dann m\u00fcsste man das aber so machen, dass nur die Kommata in den Flie\u00dfkommazahlen, <strong>nicht aber <\/strong> die zwischen den Datenfeldern ersetzt werden. Also so:<\/p>\n<pre>\r\nAus:\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,\"0,05\",\"0,1\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\nWird:\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,\"0.05\",\"0.1\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n<\/pre>\n<p>Toll&#8230; genau so und wie?<\/p>\n<p>Formulieren wir den Algorithmus erst mal in menschlicher Sprache:<\/p>\n<pre>\r\nWenn Du ein Anf\u00fchrungszeichen siehst und gleich danach eine oder mehr Zahlen \r\nund dann ein Komma, wieder Zahlen oder eine Zahl \r\nund noch ein Anf\u00fchrungszeichen...\r\nDann ersetze das Komma durch einen Punkt.\r\n<\/pre>\n<h3>Wie man das mit sed machen kann<\/h3>\n<p>Man kann sed ein Muster wie <em>&#8220;ZahlenKommaZahlen&#8221;<\/em> jederzeit erkl\u00e4ren, er kann aber nicht <strong>in<\/strong> so einem Muster begrenzt suchen und ersetzen. Stattdessen kann sed aber durchaus <em>das ganze Muster<\/em> ersetzen. Nur durch was? Im Grunde muss sed das Muster durch sich selbst ersetzen, nur mit Punkt statt Komma. Also so:<\/p>\n<pre>\r\nFinde:\r\n\"0,05\"\r\nErsetze es durch:\r\n\"0.05\"\r\n<\/pre>\n<p>Schau Dir das Muster an&#8230;. <br \/>\nDas sind 3 Teile: <em>&#8220;0<\/em>, das Komma, das ein Punkt werden soll und <em>05&#8243;<\/em>.\n<\/p>\n<p>Kann ich mit sed diese Musterauftrennung vornehmen und dann die Teile nach meinen W\u00fcnschen neu einsetzen?<\/p>\n<p>Na klar, das geht. Aber versuchen wir erst einmal, ob wir das Muster sicher finden k\u00f6nnen. Dazu geben wir nur das Ende der Datei mit <em>tail<\/em> aus und leiten das Ergebnis mit der Pipeline | weiter an sed, sed bekommt in einfachen Anf\u00fchrungszeichen den Befehl <em>s<\/em> f\u00fcr <em>substitute<\/em>, dazu nach einem Querstrich das Suchmuster <em> \\&#8221;0,<\/em> dann nach einem weiteren Querstrich das, was stattdessen in der Zeile stehen soll und die Option <em>g<\/em> f\u00fcr <em>global<\/em> oder <em>greedy<\/em>, damit sed das Muster so oft ersetzt, wie es gefunden wird. Ersetzen wir es erst mal einfach komplett durch &#8220;mehh&#8221;:<\/p>\n<pre>\r\ntail daten.csv | sed 's\/\\\"0,\/mehh\/g'\r\nErgebnis:\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,mehh05\",mehh1\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n<\/pre>\n<p>Das fiese Komma ist schon mal weg und unsere Suche hat nur die richtigen getroffen. Toll. Es ist klar, dass das auch f\u00fcr den rechten Teil des Musters geht. Nur bedenken wir, dass es nicht immer eine Null ist, die da steht, sondern irgendwelche Zahlen, die Suche nach irgendwelchen Zahlen irgendwelcher Anzahl geht so:<\/p>\n<pre>\r\ntail daten.csv |sed 's\/\\(\\\"[0-9]*\\),\\([0-9]*\\\"\\)\/mehh\/g'\r\nDas ergibt:\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,mehh,mehh,-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n<\/pre>\n<p>Jetzt ist alles mehh und das geht so: <\/p>\n<ul>\n<li>\n<em>\\&#8221;<\/em> Finde ein Anf\u00fchrungszeichen(der Backslash verhindert, dass es als Programmiersymbol interpretiert wird).\n<\/li>\n<li><em>[0-9]*<\/em> die eckigen Klammern <strong>sollen<\/strong> interpretiert werden und zwar als Bereich von Zeichen, in diesem Fall alle Dezimalzahlen von 0 bis 9, nach dieser Bereichsbeschreibung sagt das Sternchen Asterisk: das, was vor mir steht, kann einmal oder mehrmals vorkommen, also 22367 oder 666 oder 0 etc.\n<\/li>\n<li>Dann wird die am Anfang ge\u00f6ffnete einfache runde Klammer geschlossen, denn das w\u00e4re der erste Teil des Musters, nach dem wir suchen, der zweite ist einfach das diabolische Komma, dann kommt noch mal der gleiche Teil(also wieder beliebig viele Zahlen und eine Anf\u00fchrung). Die einfachen runden Klammern m\u00fcssen genau wie das Anf\u00fchrungszeichen mit Backslashes maskiert (<em>escaped<\/em>) werden&#8230;. isso&#8230;\n<\/li>\n<\/ul>\n<p>Wir k\u00f6nnen jetzt schon in menschlicher Sprache sagen:<\/p>\n<pre>\r\n Ersetze das Suchmuster durch folgende drei Teile:\r\n  1.) den Inhalt den Du mit dem Muster der ersten runden Klammer gefunden hast.\r\n  2.) einen Punkt\r\n  3.) den Inhalt dessen, was Du mit dem Muster der zweiten runden Klammer gefunden hast.\r\n  \r\n  also wird aus \"0 nur 0\r\n  aus , wird .\r\n  und aus 05\" wird 05\r\n  \r\n<\/pre>\n<p>Die Anf\u00fchrungszeichen brauchen wir nicht mehr, sobald die Punkte ersetzt sind. Man <strong>k\u00f6nnte<\/strong> sie auch in die Klammern \u00fcbernehmen, dann w\u00fcrden sie erhalten bleiben, genau wie die Zahlen, die wir ja als Messerwerte behalten wollen.<\/p>\n<p>Bleibt nur noch ein Schritt: wir m\u00fcssen herausfinden, wie man die Funde der Muster in den runden Klammern in den Teil des sed Befehls einsetzt, der oben einfach alles mit &#8220;mehh&#8221; \u00fcberschreibt.<\/p>\n<p>F\u00fcr diesen Zweck bietet sed einen auch in vielen anderen Systemen automatisch eingebauten aber nur im &#8220;unsichtbaren&#8221; Hintergrund agierenden Mechanismus: die Suchergebnisse in Klammern sind als durchnummerierte Variablen gespeichert, solange der Befehl ausgef\u00fchrt wird.  Der Befehl beginnt mit <em>&#8216;s\/<\/em> und endet mit <em>\/g&#8217;<\/em>, dazwischen k\u00f6nnen wir die Fundst\u00fccke durch Zahlen beginnend mit <em>1<\/em> aufrufen.<\/p>\n<p>so:<\/p>\n<pre>\r\n\r\nsed 's\/\\(\\\"[0-9]*\\),\\([0-9]*\\\"\\)\/\\1.\\2\/g'\r\n\r\n<\/pre>\n<p>Die Aufrufe der Fundst\u00fccke sind <em>\\1<\/em> und <em>\\2<\/em>. Das wird zu <em>0<\/em> und <em>05<\/em>, dazwischen ist im Befehl einfach ein Punkt notiert, der nicht mehr als ein Punkt ist. Ohne die Backslashes w\u00e4ren die Zahlen einfach Ziffern, mit den Escapezeichen vor ihnen erkennt sie sed als Referenzen auf seine internen Speicherpl\u00e4tze, die dank der runden Klammern gef\u00fcllt sind.<\/p>\n<p>Alles zusammen:<\/p>\n<pre>\r\n\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,\"0,05\",\"0,1\",-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n\r\n sed 's\/\\(\\\"[0-9]*\\),\\([0-9]*\\\"\\)\/\\1.\\2\/g'\r\n\r\nResultat:\r\n\"CS\",GMA_BF_F1,\"F1\",,,,,,,,0.05,0.1,-,-,-,-,,,,,,,,,,,,,,,,,,,,,2018,\r\n<\/pre>\n<p>Jetzt sind alle Zeilen Arrays 38 Felder lang, die Jahreszahl ist <strong>sicher<\/strong> in Feld 36 zu finden(weil sie im vorletzten Feld steht und weil im Gegensatz zu den Variablen in sed die Nummerierung mit 0 beginnt&#8230;)<\/p>\n<p>Es g\u00e4be noch andere Methoden, diese Aufgabe zu l\u00f6sen. Zum einen k\u00f6nnte man sed in einer Schleife laufen lassen und so jedes einzelne Feld bearbeiten und in das Array zur\u00fcckschreiben lassen. Zum anderen k\u00f6nnte man \u00e4hnliche Funktionen wie <em>preg_replace()<\/em> von PHP verwenden und auch Python und Perl w\u00e4ren Alternativen.<\/p>\n<p>Die erste Methode w\u00e4re vergleichsweise umst\u00e4ndlich zu programmieren, h\u00e4tte aber unter Umst\u00e4nden den Vorteil, flexibler zu sein, wenn man schwierigere Muster finden muss. Die zweite w\u00e4re besser als Automatikfunktion in Webseiten geeignet, w\u00e4re aber auch einigerma\u00dfen umst\u00e4ndlich.<\/p>\n<p>Wenn man in einem Terminal mit sed arbeiten kann, bietet er f\u00fcr Jobs der hier beschriebenen Art eine der elegantesten und zuverl\u00e4ssigsten Methoden.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Der StreamEDitor sed ist ein programmierbarer Texteditor. Sein Vorteil gegen\u00fcber normalen, interaktiven Editoren wie VIM oder einem beliebigen grafischen Texteditor ist, dass man f\u00fcr sed Vorg\u00e4nge programmieren kann, die man nicht per Hand ausf\u00fchren m\u00f6chte oder kann. Meistens geht es dabei nat\u00fcrlich um das Suchen und Ersetzen von Textbestandteilen. Ich m\u00f6chte an dieser Stelle eine [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[22,21,23],"tags":[],"_links":{"self":[{"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/posts\/282"}],"collection":[{"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=282"}],"version-history":[{"count":3,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/posts\/282\/revisions"}],"predecessor-version":[{"id":284,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=\/wp\/v2\/posts\/282\/revisions\/284"}],"wp:attachment":[{"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=282"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=282"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lapoc.de\/zblog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=282"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}