Linuchs: Maskierung für die Ausgabe als CSV-Datei

Moin,

eine DB-Tabelle wird als CSV ausgegeben:

$lfd  = 0;
$csv_string  =  "lfd;titel";
while ( $row_csv = @mysql_fetch_assoc( $res_csv )) {
  $csv_string
  .=  "\n"
  .   ++$lfd            . ";"
  .   $row_csv['titel'] . ""
  ;
}
header('content-type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $csv_dateiname .'"');
echo $csv_string;
exit;

Problem: Quotes " oder Semikolone ; in den Daten bringen den Import in ein Kalkulationsprogramm (z.B. LibreOffice Calc) durcheinander, müssen irgendwie maskiert werden.

Über Google komme ich zu selfhtml vom 20.11.2005, wo fputcsv() empfohlen wird. Das setzt aber eine geöffnete Datei voraus, so kompliziert will ich das nicht.

CSV (Dateiformat) – Wikipedia meint zum Thema: „Die Formatierung der Daten selbst ist nicht festgelegt. Das bedeutet, dass die verwendeten Formate zwischen den beteiligten Benutzern abgesprochen werden müssen.“

Ist hier jemand wissend, wie störende Zeichen zu behandeln sind?

Gruß, Linuchs

  1. Tach!

    Über Google komme ich zu selfhtml vom 20.11.2005, wo fputcsv() empfohlen wird. Das setzt aber eine geöffnete Datei voraus, so kompliziert will ich das nicht.

    So kompliziert ist das auch nicht. Man kann tmpfile() nehmen.

    CSV (Dateiformat) – Wikipedia meint zum Thema: „Die Formatierung der Daten selbst ist nicht festgelegt. Das bedeutet, dass die verwendeten Formate zwischen den beteiligten Benutzern abgesprochen werden müssen.“

    Ist hier jemand wissend, wie störende Zeichen zu behandeln sind?

    Es gibt Feldabgrenzer, zum Beispiel das Komma. Wenn ein solches Zeichen oder ein Zeilenumbruch in den Daten vorkommt, müssen die Daten eingeschlossen werden, zum Beispiel in doppelte Anführungszeichen. Wenn dieses Anführungszeichen in den Daten vorkommt, ist es doppelt hinzuschreiben.

    dedlfix.

  2. Hallo,

    Problem: Quotes " oder Semikolone ; in den Daten bringen den Import in ein Kalkulationsprogramm (z.B. LibreOffice Calc) durcheinander, müssen irgendwie maskiert werden.

    da wäre IMO die Doku oder ein Nutzer-Forum von LO die bessere Anlaufstelle. Dort kann man dir vermutlich eher sagen, wie LO Calc es gern hätte.

    Über Google komme ich zu selfhtml vom 20.11.2005, wo fputcsv() empfohlen wird. Das setzt aber eine geöffnete Datei voraus, so kompliziert will ich das nicht.

    Naja, anstatt eines File-Handles tut's ja auch eins für stdout. Aber das löst nicht dein Problem des Maskierens.

    CSV (Dateiformat) – Wikipedia meint zum Thema: „Die Formatierung der Daten selbst ist nicht festgelegt. Das bedeutet, dass die verwendeten Formate zwischen den beteiligten Benutzern abgesprochen werden müssen.“

    Genau. Deshalb ist CSV auch nur bedingt als Austauschformat geeignet - nämlich dann, wenn man die Befindlichkeiten der weiterverarbeitenden Software genau kennt.

    Ist hier jemand wissend, wie störende Zeichen zu behandeln sind?

    Eine gängige Methode, Anführungszeichen in CSV zu maskieren, ist, sie zu verdoppeln. Ob LO das so versteht, weiß ich aber nicht.

    Live long and pros healthy,
     Martin

    --
    Paradox: Wieso heißen die Dinger Kühlkörper, obwohl sie höllisch heiß werden?
  3. Siehe die höchst bewertete Use Note von MagicalTux at ooKoo dot org in der Doku von fputcsv. Dort heißt es:

    If you need to send a CSV file directly to the browser, without writing in an external file, you can open the output and use fputcsv on it..

    <?php
    $out = fopen('php://output', 'w');
    fputcsv($out, array('this','is some', 'csv "stuff", you know.'));
    fclose($out);
    ?>
    
    1. Hallo,

      Siehe die höchst bewertete Use Note von MagicalTux at ooKoo dot org in der Doku von fputcsv. Dort heißt es:

      If you need to send a CSV file directly to the browser, without writing in an external file, you can open the output and use fputcsv on it.

      das schrieb ich ja bereits.
      Zwar habe ich eine andere Stelle im Handbuch verlinkt, aber die Aussage ist dieselbe.

      Live long and pros healthy,
       Martin

      --
      Paradox: Wieso heißen die Dinger Kühlkörper, obwohl sie höllisch heiß werden?
      1. Der Martin 09.10.2020 13:50

        djr 09.10.2020 13:51

        Da hast Du kurz vor mir auf "Nachricht speichern" geklickt. 😉

        1. Hallo,

          Der Martin 09.10.2020 13:50

          djr 09.10.2020 13:51

          Da hast Du kurz vor mir auf "Nachricht speichern" geklickt. 😉

          stimmt, jetzt sehe ich's auch. 😀

          Live long and pros healthy,
           Martin

          --
          Paradox: Wieso heißen die Dinger Kühlkörper, obwohl sie höllisch heiß werden?
    2. danke dir für den Tipp.

      Zunächst kam ich nicht klar, weil ich im Sinn hatte, die Werte mit ; und die Zeilen mit \n getrennt als Gesamt-String ausgeben zu müssen. So hatte ich es in anderen Projekten schon gemacht.

      Missverständlich: „fputcsv — Format line as CSV and write to file“ pointer`

      Ein zweidimensionales Array wird benötigt:

        $csv_arr    = []; // array
        $csv_arr[]  = [ "lfd", "titel", "sprache", "genre", "texter", "komponist", "verlag", "bearbeiter" ];
        $csv_arr[]  = [ "", $csv_dateiname ];
        $csv_arr[]  = [ "", "Test Zeichencode UTF-8 ÄäÖöÜöß Санкт-Петербург" ];
        $csv_arr[]  = [ "", 'Test " und ; im Feld' ];
        while ( $row_csv = @mysql_fetch_assoc( $res_csv )) {
          $csv_arr[]
          = [
            ++$lfd
          , $row_csv['titel']
          , $row_csv['sprache']
          , $row_csv['genre']
          , $row_csv['texter']
          , $row_csv['komponist']
          , $row_csv['verlag']
          , $row_csv['bearbeiter']
          ];
        }
        header('content-type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $csv_dateiname .'"');
        $out = fopen('php://output', 'w');
        foreach ( $csv_arr as $zeile ) {
          fputcsv( $out, $zeile );
        }
        fclose($out);
        exit;
      

      Jetzt klappt es und sieht im LibreOffice Calc (ich hoffe auch im Excel) halbwegs ordentlich aus:

      Obwohl kein Platzmangel ist, lappen die russischen Worte ins Nebenfeld. Und warum ist die Datei schreibgeschützt? Wird das durch den HTML header begründet?

      Linuchs

      1. Hallo Linuchs,

        Und warum ist die Datei schreibgeschützt?

        Weil du in dem temp-Verzeichnis vermutlich keine Schreibrechte hast.

        Bis demnächst
        Matthias

        --
        Du kannst das Projekt SELFHTML unterstützen,
        indem du bei Amazon-Einkäufen Amazon smile (Was ist das?) nutzt.
  4. Ich noch mal.

    Das Erzeugen eines CSV-String mit mehreren Textzeile per PHP funktioniert:

    $out = fopen('php://temp', 'r+b');
    foreach ( $csv_arr as $zeile ) {
      // fputcsv($fp, $input, $delimiter, $enclosure);
      fputcsv( $out, $zeile, ';', '"' );
    }
    rewind($out);
    $csv_string = rtrim(stream_get_contents($out), "\n");
    fclose($out);
    

    PHP erkennt auch,wenn der Feldtrenner im Text vorkommt und schließt den Text ein mit "

    ...;"Georg;";Hohmann;...

    String wird zum Browser geschickt, Javascript muss diese Zeile (row) wieder auseinandernehmen:

    var row = rows[i].split(";");
    for (let i=0; i<row.length; i++ ) {
      // einschliessende " pro Wert entfernen
      if ( row[i][0] == '"' ) row[i] = row[i].substring(1, row[i].length-1 );
      // doppelte "" werden zu einfachen "
      row[i]  = row[i].replace( /\"\"/g,          '"' );
    }
    

    Das funktioniert so nicht. Wie kann ich JS beibringen, das Zeichen ; innerhalb " nicht als Feld-Trenner zu betrachten?

    Linuchs

    1. Moin,

      warum sendest du nicht JSON zum Client?

      Viele Grüße
      Robert

      1. warum sendest du nicht JSON zum Client?

        Habe mich mit JSON noch nicht beschäftigt. Ist das die fünfte Sprache zu

        • HTML
        • PHP
        • Javascript
        • MySQL ?
        1. Tach!

          warum sendest du nicht JSON zum Client?

          Habe mich mit JSON noch nicht beschäftigt. Ist das die fünfte Sprache [...] ?

          Das ist wie CSV ein Datenformat, keine Sprache. JSON steht für Javascript Object Notation. Wenn du ein Objektliteral in Javascript schreiben kannst, kannst du im Prinzip bereits JSON.

          dedlfix.

        2. Moin,

          JSON ist ein Datenformat: JavaScript Object Notation.

          Viele Grüße
          Robert

        3. Habe mich mit JSON noch nicht beschäftigt.

          Das machen, im Kern, die entsprechenden Funktionen in PHP, JavaScript, ... für Dich.

          Das ist so einfach, das willst Du haben nun nutzen.

          1. Und dafür, respektive für "sowas" brauchst Du es auch.

            var row = rows[i].split(";");

            Tja. So einen Sch... hast Du mit JSON nicht am Hals. Ist halt besser definiert, kam ja auch als man mehr über Daten, die Benutzer eingeben, wusste...

    2. Hallo Linuchs,

      Javascript muss diese Zeile (row) wieder auseinandernehmen

      Nicht nur die Row. Du sammelst ja etliche Zeilen in der Datei und saugst sie dann als Block wieder ein. D.h. du musst vor dem Parsen der Zeilen auch noch den Gesamtstring in Zeilen zerlegen.

      Ein json_encode($csv_arr) macht's, wie von Robert vorgeschlagen, auf einen Rutsch, ohne Temp-Datei, und JavaScript ist mit JSON.parse damit auch schnell fertig. Wenn Du das überhaupt tun musst, denn:

      Einen JSON-String kannst Du mit content-type application/json über die Leitung schicken, und XMLHttpRequest konvertiert Dir das implizit in ein Objekt, wenn Du auf das response Property zugreifst.

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Hallo Rolf,

        Nicht nur die Row. Du sammelst ja etliche Zeilen in der Datei und saugst sie dann als Block wieder ein. D.h. du musst vor dem Parsen der Zeilen auch noch den Gesamtstring in Zeilen zerlegen.

        Damit habe ich kein Problem, deshalb habe ich es nicht erwähnt.

          var csv_string =                         // String der Adressen
        `[csv_string]`;
          var rows  = csv_string.split( "\n" );    // Array der Adressen
          var k     = rows[0].split( ";" );         // Feldnamen
          for ( let i=0; i<k.length; i++ ) {
            k[ k[i] ] = i;
          }
        

        Der von PHP aufbereitete CSV-String wird in den Platzhalter [csv_string] eingefügt. Zerlege dann diesen String in das Array rows und hole mir von rows[0] die Spaltennamen. k steht für key:

        if ( row[k['adress_id']] > " " ) { ... }

        So kann ich mit den Spalten-Namen weiterarbeiten, egal an welcher Stelle die adress_id in der Zeile steht.

        Nicht die leiseste Ahnung, wie andere das machen, aber ich bin stolz auf meine Idee als Eremit im Hausbüro.

        Gruß, Linuchs

        1. Tach!

          Nicht nur die Row. Du sammelst ja etliche Zeilen in der Datei und saugst sie dann als Block wieder ein. D.h. du musst vor dem Parsen der Zeilen auch noch den Gesamtstring in Zeilen zerlegen.

          Damit habe ich kein Problem, deshalb habe ich es nicht erwähnt.

          Doch, mit dem Zeilenende hast du das gleiche Problem wie mit dem Semikolon, wenn es innerhalb von Werten auftaucht.

          Nicht die leiseste Ahnung, wie andere das machen, aber ich bin stolz auf meine Idee als Eremit im Hausbüro.

          Dann schau nach! Du wirst nicht der erste mit dem Problem sein, und es gibt sicher einige, die dafür bereits einen Parser geschrieben haben. Wozu gibt es Github?

          dedlfix.

          1. @dedlfix

            Doch, mit dem Zeilenende hast du das gleiche Problem wie mit dem Semikolon, wenn es innerhalb von Werten auftaucht.

            OKay, richtiger Hinweis. Bisher habe ich noch keine textarea als CSV verschickt. Steht auch nicht an.

            Du wirst nicht der erste mit dem Problem sein

            Du weißt schon,dass zu manchen Problemen ein Wust von Diskussionen besteht. Bis der eigene Fall gefunden und getestet ist, ist oft die eigene Idee schneller.

            Gruß von weißnix (Linuchs)

            1. Tach!

              Du weißt schon,dass zu manchen Problemen ein Wust von Diskussionen besteht. Bis der eigene Fall gefunden und getestet ist, ist oft die eigene Idee schneller.

              CSV zu parsen ist aber kein "eigner Fall". Für populäre Formate, die schon ewig existieren, gibt es fertig verwendbare Pakete. Zudem, wenn die eigene Idee ist, ein Format zu nehmen, für das es auf dem Zielsystem keinen Parser gibt, dann ist die Idee nicht viel wert. Besonders dann nicht, wenn stattdessen ein anderes Format mit perfekter Unterstützung auf beiden Seiten existiert.

              dedlfix.

        2. Hallo Linuchs,

          ok, erklär mal kurz, warum Du zwingend bei CSV bleiben willst. Weil der Code für's Libre-Office eh schon existiert?

          Und erkläre nebenbei, warum Du mysql statt mysqli-Funktionen benutzt. Hast Du das noch nicht umgestellt? Dafür solltest Du erstmal Energie investieren, denn mit mysqli kannst Du nach PHP7 wechseln und das ist von Hause aus mal deutlich fixer als PHP 5.

          Wie auch immer, solchen Code kann man entkernen. Die Transformation des Query-Result in ein Array Of Array hast Du, die kann in eine Funktion.

          Und das Funktionsergebnis kannst Du nun wie gehabt als CSV in den PHP Output schreiben, oder JSON-encoden und DAS in den PHP Output schreiben.

          Die Spaltennamen in der ersten Zeile kannst Du in JS ebenfalls herausholen und daraus dein Key-Array erzeugen.

          Im PHP:

            function results_as_array($result, $filename) 
            {
              // Array, erste Zeile enthält Spaltennamen
              $csv_arr    = [ "lfd", "titel", "sprache", "genre", 
                              "texter", "komponist", "verlag", "bearbeiter" ];
              $csv_arr[]  = [ "", $filename ];
              $csv_arr[]  = [ "", "Test Zeichencode UTF-8 ÄäÖöÜöß Санкт-Петербург" ];
              $csv_arr[]  = [ "", 'Test " und ; im Feld' ];
           
              while ( $row_csv = @mysql_fetch_assoc( $res_csv )) {
                $csv_arr[] = [
                  ++$lfd
                , $row_csv['titel'] , $row_csv['sprache'], $row_csv['genre']
                , $row_csv['texter'], $row_csv['komponist']
                , $row_csv['verlag'], $row_csv['bearbeiter']
                ];
              }
            }
          
            header('content-type: application/json; charset=utf-8');
            header('Content-Disposition: attachment; filename="' . $csv_dateiname .'"');
            // fopen/fputs/fclose sind nicht nötig
            echo json_encode(results_as_array($res_csv));
            exit;
          

          Und im JavaScript

          var rows  = JSON.parse(json_string);    // Array der Adressen
          var k     = rows[0].split( ";" );       // Erste Zeile enthält Feldnamen
          for ( let i=0; i<k.length; i++ ) {
             k[ k[i] ] = i;
          }
          for (let row = 1; row < rows.length; row++) 
          {
             let titel = rows[row][k['titel']];
          }
          

          Das Keys-Array und diese umständliche Ansprache ersparst Du Dir, wenn Du auf die Darstellung als array of arrays verzichtest. Du kannst die mit fetch_assoc geholten Zeilen auch einfach als Assoc-Array stehen lassen, json_encode codiert das automatisch als JavaScript-Objekte. Damit sparst Du am Server Zeit, und der Client hat weniger Mühe. Allerdings geht das Transfervolumen hoch, weil jede Zeile alle Spaltennamen enthält.

          Kommt immer drauf an, wo der Engpass ist. Wenn es überhaupt einen gibt...

          Rolf

          --
          sumpsi - posui - obstruxi