mitmario.dev

CSV

PHP Sandbox 4 Min Lesezeit 5 BeispieleLektion 4 von 7

CSV ist das Format, in dem Tabellen die Tabellenkalkulation verlassen: eine Zeile je Datensatz, ein Trennzeichen zwischen den Feldern. Es sieht so einfach aus, dass fast jeder es zuerst mit explode versucht. Das geht eine Weile gut und dann nicht mehr.

Warum explode nicht reicht

Warum explode hier scheitert
<?php

echo "Mit explode:\n";
foreach (file("komma.csv", FILE_IGNORE_NEW_LINES) as $zeile) {
    $felder = explode(",", $zeile);
    echo count($felder), " Felder: ", implode(" | ", $felder), "\n";
}

echo "\nMit fgetcsv:\n";
$datei = fopen("komma.csv", "r");
while (($felder = fgetcsv($datei, escape: "")) !== false) {
    echo count($felder), " Felder: ", implode(" | ", $felder), "\n";
}
fclose($datei);

Ein CSV-Feld darf das Trennzeichen enthalten. Es muss dann in Anführungszeichen stehen, und die gehören zur Notation und nicht zum Inhalt.

Im Beispiel steht "Kaffee, gemahlen" als ein Feld. explode(",") weiß davon nichts, macht daraus zwei und liefert für diese Zeile vier Felder statt drei. Die Anführungszeichen bleiben dabei auch noch stehen.

fgetcsv() kennt die Regel und liefert drei Felder, mit dem Komma im ersten und ohne Anführungszeichen. Das ist der ganze Unterschied, und er ist der Grund, warum man CSV nicht selbst zerlegt.

fgetcsv arbeitet auf einem offenen Zeiger

fgetcsv() liest eine Zeile und rückt dabei weiter. Deshalb steht davor ein fopen() und dahinter ein fclose(), und deshalb steht der Aufruf in einer while-Schleife: Solange noch etwas kommt, mach weiter.

Am Dateiende gibt fgetcsv false zurück, und genau darauf wird mit !== false geprüft. Der strenge Vergleich sagt hier, was gemeint ist: Schluss ist beim Dateiende und nicht dann, wenn eine Zeile zufällig nach nichts aussieht. Eine leere Zeile mitten in der Datei liefert nämlich ein Array mit einem einzigen null darin, und das ist ein Ergebnis wie jedes andere. Der strenge Vergleich aus Lektion 3.6, an einer Stelle, an der er die Absicht deutlich macht.

Das deutsche Semikolon

Semikolon, weil die Preise Kommas haben
<?php

$datei = fopen("bestellungen.csv", "r");

while (($felder = fgetcsv($datei, separator: ";", escape: "")) !== false) {
    echo implode(" | ", $felder), "\n";
}

fclose($datei);

In Deutschland ist das Trennzeichen fast immer ein Semikolon, und der Grund steht in jeder Preisspalte: Bei uns ist das Komma das Dezimaltrennzeichen. Eine Datei, in der 12,45 steht und das Komma zugleich Felder trennt, wäre nicht zu lesen.

Welches Zeichen gilt, sagst du fgetcsv mit separator. Und weil das Semikolon die Felder trennt, darf das Komma jetzt einfach im Text stehen, ganz ohne Anführungszeichen.

Warum überall escape steht

Neben separator steht in diesem Kurs immer escape: "", und das braucht eine Erklärung, weil du es in älteren Anleitungen nicht findest.

PHP kennt neben den Anführungszeichen noch einen zweiten Maskiermechanismus mit dem Backslash. Den gibt es in keiner anderen CSV-Umsetzung, kein Tabellenprogramm erzeugt so etwas, und er führt dazu, dass ein Backslash am Feldende die nächsten Zeichen verschluckt. Ein leerer Text als escape schaltet ihn ab. Das ist die Einstellung, die du willst.

Seit PHP 8.4 verlangt PHP, dass du das Argument ausdrücklich angibst, und schreibt sonst bei jedem Aufruf eine Deprecated-Meldung auf den Fehlerkanal. Dieser Kurs läuft auf PHP 8.5, du siehst die Meldung also sofort, und die Prüfung deiner Challenge sieht sie auch. Wenn du sie einmal lesen willst, ohne eine Datei umzubauen: php -r '$d = fopen("bestellungen.csv", "r"); var_dump(fgetcsv($d));' im Terminal schreibt sie vor das Ergebnis, und das Ergebnis ist obendrein eine einzige Spalte, weil auch separator fehlt.

Geschrieben wird das hier mit benannten Argumenten aus Lektion 6.3, sonst müsstest du zwei Parameter, die dich nicht interessieren, in der richtigen Reihenfolge mitschleppen.

Die Kopfzeile

Die Kopfzeile wird zum Schlüssel
<?php

$datei = fopen("bestellungen.csv", "r");
$kopf = fgetcsv($datei, separator: ";", escape: "");

echo "Die Kopfzeile: ", implode(", ", $kopf), "\n\n";

while (($felder = fgetcsv($datei, separator: ";", escape: "")) !== false) {
    $zeile = array_combine($kopf, $felder);
    echo $zeile["artikel"], " kostet ", $zeile["einzelpreis"], "\n";
}

fclose($datei);

echo "\nUnd so sieht so eine Zeile aus:\n";
var_dump(array_combine($kopf, ["Tee", "1", "4,20"]));

Die erste Zeile einer CSV enthält meistens die Spaltennamen. Wenn du sie einmal getrennt einliest, bevor die Schleife anfängt, hast du sie als Liste in der Hand.

Damit wird aus jeder folgenden Zeile eine Zuordnung: array_combine() nimmt die Kopfzeile als Schlüssel und die Datenzeile als Werte. Statt $felder[2] schreibst du dann $zeile["einzelpreis"], und in einem halben Jahr weißt du immer noch, was gemeint war.

Das setzt voraus, dass beide Listen gleich lang sind. Sind sie es nicht, wirft array_combine einen ValueError, und das ist die richtige Reaktion: Eine Zeile mit der falschen Spaltenzahl ist kaputt.

Und im Browser steht sie als Tabelle

Eine CSV, die zur Tabelle wird
<?php

$datei = fopen("bestellungen.csv", "r");
$kopf = fgetcsv($datei, separator: ";", escape: "");

echo "<style>";
echo "body{font-family:system-ui;padding:1rem}";
echo "table{border-collapse:collapse}";
echo "th,td{border:1px solid #999;padding:.35rem .6rem;text-align:left}";
echo "td.zahl{text-align:right}";
echo "</style>\n";

echo "<table>\n";
echo "  <tr><th>", implode("</th><th>", $kopf), "</th></tr>\n";

while (($felder = fgetcsv($datei, separator: ";", escape: "")) !== false) {
    echo "  <tr>";
    echo "<td>", $felder[0], "</td>";
    echo "<td class=\"zahl\">", $felder[1], "</td>";
    echo "<td class=\"zahl\">", $felder[2], " EUR</td>";
    echo "</tr>\n";
}

echo "</table>\n";
fclose($datei);

Dieses eine Beispiel gibt kein reines Textergebnis aus, sondern Markup, und deshalb läuft dafür ein Server: Im Reiter Browser steht die Tabelle, die daraus wird. Tipp php index.php im Terminal, dann siehst du dieselbe Ausgabe als Zeichen, mit <tr> und <td>.

Das ist mehr als hübsch: Tabellendaten in eine Tabelle zu verwandeln ist der Alltagsfall dieser Lektion, und ab Abschnitt 10 machst du genau das für Seiten, die ein Browser anfragt. In den anderen fünf Lektionen dieses Abschnitts steht die Ausgabe im Terminal, weil dort der Text die Aussage ist und ein Browser Leerraum zusammenfaltet.

Das <style> am Anfang muss übrigens mit ausgegeben werden. Der Server liefert hier genau eine Datei aus, eine externe CSS-Datei daneben gibt es nicht.

Schreiben mit fputcsv

Und wieder hinausschreiben
<?php

$zeilen = [
    ["artikel", "menge"],
    ["Notizbuch", 5],
    ["Kaffee, gemahlen", 2],
    ["Tee; lose", 1],
];

$datei = fopen("neu.csv", "w");
foreach ($zeilen as $zeile) {
    fputcsv($datei, $zeile, separator: ";", escape: "");
}
fclose($datei);

echo "So sieht die Datei aus:\n";
echo file_get_contents("neu.csv");

fputcsv() ist das Gegenstück und nimmt dir die Regeln ab, die du beim Lesen kennengelernt hast: Es setzt die Anführungszeichen selbst, überall dort, wo sie beim Wiedereinlesen gebraucht werden.

Im Beispiel siehst du alle drei Fälle in einer Datei. Notizbuch bleibt blank, da gibt es nichts zu schützen. Tee; lose bekommt Anführungszeichen, weil dort das Trennzeichen im Text steht. Und Kaffee, gemahlen bekommt ebenfalls welche, obwohl das Komma hier gar nichts trennt: PHP setzt sie schon bei einem Leerzeichen im Feld. Das ist großzügiger als nötig und stört nicht, denn beim Lesen fallen sie ohnehin wieder weg. Die Datei selbst zeigt dir cat neu.csv im Terminal, genau so, wie PHP sie geschrieben hat.

Zum Mitnehmen

CSV sieht aus, als könnte man es mit explode zerlegen. Kann man nicht, und der Beweis steht gleich im ersten Beispiel. Dafür gibt es zwei Funktionen, die die Sache wirklich beherrschen.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 5 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.