[JAVA] CSV-Analyse mit Feldumbruchzeichen

Finden Sie einen Zweck für die Mittel.

Eines Tages dachte ich, dass ich aufgrund des Einflusses des Buches "Let's make a normal compiler", das ich in der Bibliothek kennengelernt habe, einen Parser mit javaCC erstellen möchte.

"Die CSV-Datei, die die Ereignisdatenbank von jp1base ausgibt, enthält Zeilenumbrüche und Kommas in den Feldern in doppelten Anführungszeichen. Daher ist es schwierig, sie in Excel einzufügen."

Also habe ich beschlossen, sofort einen Parser zu erstellen.


Dinge die zu tun sind

Konvertieren Sie CSV-Daten einschließlich Kommas und Zeilenumbrüche als Felddaten in ein in Excel leicht lesbares Formular und geben Sie es standardmäßig aus.

Weise

  1. Rauer Fluss

  2. Analysieren Sie CSV-Daten in Array-Daten.

  3. Formatieren Sie nach dem Lesen einer Datenzeile diese und geben Sie sie aus.

  4. Realisieren Sie das Obige mit Java.

  5. Definition von CSV-Daten

  6. Kommagetrennte Daten

  7. In doppelte Anführungszeichen (") eingeschlossene Felder enthalten Kommas und Zeilenumbrüche.

  8. Wenn Sie das doppelte Anführungszeichen selbst in dem vom doppelten Anführungszeichen eingeschlossenen Feld darstellen möchten, maskieren Sie es mit einem Backslash ().

  9. Ausgabeformat

  10. Geben Sie jedes durch Kommas getrennte Feld aus

  11. Geben Sie jedes Feld aus, indem Sie es in doppelte Anführungszeichen setzen

  12. Ersetzen Sie doppelte Anführungszeichen und Zeilenvorschubzeichen in jedem Feld durch Leerzeichen.

Umgebung

  1. OS Jedes vom JDK unterstützte Betriebssystem ist in Ordnung. (Was ich zu Hause auf meinem Mac gemacht habe, hat genauso funktioniert wie auf dem RHEL6-Server und Windows 10 bei der Arbeit.)

  2. JDK
    Es wurde bestätigt, dass dieser Artikel in einer JDK 1.6-Umgebung funktioniert. Laut der offiziellen Website ist Java ab April 2020 zu 100% rein, sodass keine Abhängigkeit von Runtime besteht.

  3. JavaCC Ich habe 6.0 verwendet. Informationen zur Einrichtung finden Sie in diesem Artikel. Referenzartikel

Scannerdefinition

Definieren Sie zunächst den Scanner. Der Scanner ist der Teil, der für die Phrasenanalyse verantwortlich ist, und generiert aus einer Liste von Zeichenfolgen aussagekräftige Phrasen (TOKEN). Schreiben Sie die Scannerdefinition in eine Textdatei mit der Erweiterung ".jj". Im Folgenden sind die Scannerdefinitionen aufgeführt, die ich dieses Mal erstellt habe.

CSVParser.jj


//Dies ist die Definition von SKIP, um weiße Zeichen zu ignorieren.
//TOKEN wird nicht aus den übersprungenen Zeichen generiert.
//Ich bitte Sie, die Leerzeichen und Tabulatoren hier zu ignorieren.
SKIP : {
    " "
  | "\t"
}

//Definition zum Lesen einer Zeichenfolge in doppelten Anführungszeichen
//Verwenden der MORE-Direktive,
//Wenn Sie ein doppeltes Anführungszeichen finden,"IN_DOUBLE_QUOTE"Wegen des Übergangs in den Modus!
//Ich werde anweisen.
MORE : {
    "\"" : IN_DOUBLE_QUOTE
}

//Dies ist die Regel zum Lesen von doppelten Anführungszeichen.
// IN_DOUBLE_Im QUOTE-Modus
// 1.Wenn Sie ein anderes Zeichen als den Schrägstrich erhalten, lesen Sie das nächste Zeichen weiter.
// 2.Wenn nach dem Backslash ein Zeichen steht, lesen Sie das nächste Zeichen.
//Wird angewiesen.(Gemäß Regel 2 wird das doppelte Anführungszeichen nach dem Schrägstrich nur als Zeichen gelesen.
<IN_DOUBLE_QUOTE> MORE: {
    < ~["\\"] >
  | < "\\" ~[] >
}

//Als nächstes folgt die Definition, um aus der Doppelbeschichtung herauszukommen
// IN_DOUBLE_Im QUOTE-Modus
//Wenn ein einfaches Anführungszeichen angezeigt wird, generieren Sie ein Token mit dem Namen DQFIELD und
//Standardmodus(DEFAULT)Geh zurück zu! Wird angewiesen.
// "DQFIELD"Das ist der Name, den ich selbst entschieden habe.
//Übrigens IN_DOUBLE_QUOTE ist auch ein Name, den ich selbst festgelegt habe.
<IN_DOUBLE_QUOTE> TOKEN: {
    <DQFIELD : "\""> : DEFAULT
}

//Im Standardmodus
//Komma,Doppeltes Zitat,Eine Liste von Zeichen, die kein Zeilenvorschubzeichen enthält, wird als STDFIELD-Token definiert.
//Auch das Komma<SEPARATOR>Nennen wir es ein Token.
//Außerdem als Zeichen, das das Ende einer Zeile darstellt"\n"Oder"\r"Kontinuierlich<EOL>Behandle sie als Token.
TOKEN : {
    <STDFIELD : (~["\"", ",", "\r", "\n"])+ >
  | <SEPARATOR : "," >
  | <EOL : (["\r", "\n"])+ >
}

Definition des Parsers

Ein Parser analysiert eine Liste der von einem Scanner generierten TOKENs und führt die erforderlichen Arbeiten aus. Hier möchten wir eine Zeile mit CSV-Daten als Array zurückgeben. Definieren wir zunächst die CSV-Daten.

Definition von CSV-Daten

CSV-Daten sind eine Reihe von Zeilen (Datensätzen), in denen alle Daten (als Feld bezeichnet) durch Kommas getrennt angeordnet sind.

CSV-Datenbild


Feld 1-1,Feld 1-2,Feld 1-3,・ ・ ・ ・
Feld 2-1,Feld 2-2,Feld 2-3,・ ・ ・ ・
 :

Lassen Sie uns diese Struktur zunächst mit einem Parser ausdrücken. Zuerst die Felddefinition. Die Parser-Definition wird übrigens auch in derselben Datei wie die Scanner-Definition beschrieben. (Weil es nicht so groß wird, wenn es um CSV geht.)

CSVParser.jj(Felddefinition)


String field() : {
} {
  (
    <DQFIELD> | <STDFIELD>
  )
}
/////Kommentar//////
//Rückgabewert:Da ich die Daten als Zeichenfolgendaten generieren möchte, ist der Rückgabetyp String.
//Name:Zum leichteren Verständnis später"filed"Ich habe es genannt.
//Inhalt:Zeichenfolge in doppelten Anführungszeichen(DQFIELD)Oder eine gewöhnliche Saite(STDFIELD)Ist.
//Ist definiert.

Als nächstes folgt die Definition einer Datenzeile (hier nennen wir es einen Datensatz).

jp1EventParser.jj(Datensatzdefinition)


//Definiert als 0 oder mehr aufeinanderfolgende Felder, die durch SEPARATOR getrennt sind.
//Liste der Felder<String>Es ist definiert als.
List<String> record() : {
} {
  //Zuerst gibt es ein Feld
  field()
  //Gefolgt von 0 oder mehr durch SEPARATOR getrennten Feldern(=Es kann nicht da sein.)
  //Außerdem entspricht es nicht, wenn plötzlich ein Komma kommt.
  (
    <SEPARATOR>
    (field())?
  )*
}

Schließlich die Definition der gesamten CSV-Datei.

CSVParser.jj(CSV-Dateidefinition)


// csvContents()(Durch den Inhalt von CSV)Ist
//Sie sagen, dass die Datensätze in mehreren Zeilen angeordnet sind.
//Ich gebe es als Standard für jede Zeile aus und habe nicht die Absicht, etwas zurückzugeben, also habe ich es ungültig gemacht.
void csvContents() : {
} {
  (
    record()
    <EOL>
  )+
  <EOF> 
}

Und den Parser ausarbeiten.

Was tun Sie, nachdem Sie die Struktur der CSV-Datei definiert haben, wenn Sie diese Struktur lesen? Ich werde die spezifische Verarbeitung schreiben. Hier ist der Code, den ich tatsächlich geschrieben habe.

CSVParser.jj(Eine Parser-Definition mit hinzugefügter tatsächlicher Verarbeitung.)


//Es sieht schrecklich anders aus, aber im Grunde()Ich füge nur den Prozess hinzu.
//Felddefinition
String field() : {
  String data = "";  //Variable zum Speichern der gelesenen Zeichenfolge (Mit einer leeren Zeichenfolge initialisieren)
  Token fieldToken;  //Variable zum Speichern des gelesenen Tokens
} {
  (
    //Wenn Sie DQFIELD lesen, das Bild(Tatsächliche Zeichenfolge)Wird in den variablen Daten gespeichert.
    fieldToken = <DQFIELD> {
      data = fieldToken.image;
    }
    //Oder, wenn Sie STDIELD lesen, das Bild(Tatsächliche Zeichenfolge)Wird schließlich in den variablen Daten gespeichert.
    | fieldToken = <STDFIELD> {
      data = fieldToken.image;
    }
  ) {
    //Nach dem Lesen von DQFILED oder STDFIELD wird der Wert der variablen Daten zurückgegeben.
    return data;
  }
}

//Datensatzdefinition
List<String> record() : {
  List<String> fieldList = new ArrayList<String>();
  String fieldData;
} {
  //Zuerst gibt es ein Feld
  fieldData = field(){
    //Fügen Sie dem Array das erste Feld hinzu
    fieldList.add(fieldData);
  }
  //Gefolgt von 0 oder mehr durch SEPARATOR getrennten Feldern(=Es kann nicht da sein.)
  //Außerdem entspricht es nicht, wenn plötzlich ein Komma kommt.
  (
    <SEPARATOR>
    (fieldData = field(){
      //Wenn Sie die dahinter stehenden Daten durch SEPARATOR getrennt finden, fügen Sie dem Array weitere hinzu.
      fieldList.add(fieldData);
    })?
  )*
  {
    //Nach dem Lesen einer Zeile wird das resultierende Array zurückgegeben.
    return fieldList;
  }
}

//Definition der gesamten CSV-Datei
void csvContents() : {
  List<String> csvRecord; //Variable, die eine Datenzeile speichert
} {
  (
    csvRecord = record(){
      //Nach dem Lesen einer Zeile wird diese an die Standardausgabe ausgegeben.
      //Der Autor hier ist eine selbstgemachte Klasse.(Wird später herauskommen.)
      CSVWriter.writeLine(csvRecord);
    }
    <EOL>
  )+
  <EOF> 
}

Konvertieren Sie den Inhalt der CSV-Datei und der Ausgabe

Dies ist das Ende des Schreibens eines Parsers, aber da es eine große Sache ist, werde ich ihn an den Punkt bringen, an dem er tatsächlich verschoben werden kann. Definieren Sie zunächst die CSVWriter-Klasse, die das zuvor ausgegebene Zeichenfolgenarray ausgibt, indem Sie es in durch Kommas getrennte doppelte Anführungszeichen setzen.

CSVWriter.java


import java.util.List;
public class CSVWriter {
  public static void writeLine(List<String> record) {
    String line = "";
    String comma = "";

    for ( String field : record ) {
      //Verketten Sie die Zeichenfolgen in jedem Feld durch Kommas getrennt.
      line = line + comma + "\"" + sanitizeString(field) + "\"";
      //Bei der Erstellung von durch Kommas getrennten Datensätzen werden leere Zeichenketten lange Zeit nur am Anfang so verkettet, aber ich würde gerne wissen, ob es einen anderen guten Weg gibt.
      comma = ",";
    }

    System.out.println(line);

  }

  private static sanitaizeString(String input){
    //Es tut mir leid, dass es angemessen ist.
    //Das Zeilenvorschubzeichen und das doppelte Anführungszeichen werden entfernt.
    return input.replace("\n", " ").replace("\r", " ").replace("\"", "");

  }
}

Als nächstes wird CSVParser.jj abgeschlossen.

CSVParser.jj


//Das ist magisch.
options {
//  DEBUG_PARSER=true;
  UNICODE_INPUT=true;
}

//Definition der Parser-Klasse(Java-Code)Ist das PARSER_BEGIN〜PARSER_Schreiben Sie zwischen ENDE.
PARSER_BEGIN(CSVParser)

import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.io.InputStream;
import java.io.FileInputStream;

public class CSVParser {
  public void parseCSV() {
    try {
      csvContents();
    } catch(Exception ex) {
      System.out.println("ParseError occured: " + ex.toString());
    }
  }
}

PARSER_END(CSVParser)

//Definition des Scanners von hier(Kommentare werden weggelassen.)
SKIP : {
    " "
  | "\t"
}

MORE : {
    "\"" : IN_DOUBLE_QUOTE
  | "'" : IN_SINGLE_QUOTE
}

<IN_DOUBLE_QUOTE> MORE: {
    < ~["\"", "\\"] >
  | < "\\" ~[] >
  | < "\"" "\"" >
}

<IN_SINGLE_QUOTE> MORE: {
    < ~["'", "\\"] >
  | < "\\" ~[] >
  | < "'" "'" >
}

<IN_DOUBLE_QUOTE> TOKEN: {
  <DQSTR : "\""> : DEFAULT
}

TOKEN : {
    <STDFIELD : (~["\"", ",", "\r", "\n" ])+ >
  | <SEPERATOR : "," >
  | <EOL : (["\r", "\n"])+ >
}


//Definition des Parsers von hier(Kommentare werden weggelassen.)
void csvContents() : {
  List<String> csvRecord;
} {
  (
    csvRecord = record() {
      CSVWriter.writeLine(csvRecord);
    }
    <EOL>
  )+
  <EOF> 
}

List<String> record() : {
  List<String> fieldList = new ArrayList<String>();
  String fieldData;
} {
  fieldData = field() {
    fieldList.add(fieldData);
  }
  (
    <SEPERATOR>
    (fieldData = field(){
       fieldList.add(fieldData);
    })?
  )*
  {
    return fieldList;
  }
}

String field() : {
  String data = "";
  Token fieldToken;
} {
  (
    fieldToken = <DQSTR> {
      data = fieldToken.image;
    }
  | 
    fieldToken = <STDFIELD> {
      data = fieldToken.image;
    }
  ) {
    return data;
  }

}

Schließlich benötigen Sie den Haupteinstiegspunkt. Es ist wirklich geeignet. .. .. Weil es ein Beispiel für den Anruf sein wird.

CSVConv.java


import java.io.InputStream;
import java.io.FileInputStream;

public class CSVConv {
  public static void main(String[] args) {
    if ( args.length != 1 ) {
      return;
    }

    //Während Sie den Junioren beibringen, dass es eine schlechte Idee ist, das Argument so zu führen, wie es ist. Dies. ..
    try(InputStream csvReader = new FileInputStream(args[0])) {
      //Ich kann mich nicht erinnern, einen Konstruktor definiert zu haben, der einen solchen InputStream empfängt.
      //Keine Sorge, JavaCC wird es für Sie machen.
      CSVParser parser = new CSVParser(csvReader, "utf8");

      //Analysieren Sie die Datei tatsächlich.
      //So schreiben Sie, da die Lesezeile standardmäßig ohne Erlaubnis ausgegeben wird.
      parser.parseCSV();
      
    } catch(Exception ex) {
      System.out.println("Error occured: " + ex.toString());
    }
  }
}

Wie zu kompilieren

Ich werde das Kompilierungsverfahren vorerst schreiben.

shell


#Führen Sie javaCC aus=> CSVParser.Lesen Sie jj und CSVParser.Es wird Java machen.
javacc CSVParser.jj
#Dann kompilieren Sie alles zusammen damit.
javac CSVConv.java

Erstellen Sie also eine CSV-Testdatei (Es gibt einige mittlere Daten, die solche Zeilenumbrüche enthalten.)

hoge.csv


abc,"def
ghi",jkl,"mno,pqr"
stu,vwx,yz

Machen wir das! !!

shell


java CSVConv hoge.csv

#Sie sollten diese Art von Ausgabe erhalten.
# "abc","def ghi","jkl", "mno,pqr"
# "stu","vwx","yz"

Ich habe es lange geschrieben, aber kann es als eines der Mittel zur Lösung von "Ist es nicht ein Problem, wenn solche Daten als Text übergeben werden?" Verwendet werden. Ich dachte, ich hätte einen Artikel gemacht.

Recommended Posts

CSV-Analyse mit Feldumbruchzeichen
Maßnahmen gegen verstümmelte Charaktere in Multipart Request mit Quarkus
Konvertieren von TSV-Dateien in CSV-Dateien (mit Stückliste) in Ruby
CSV-Import mit Stückliste
Zip-Komprimierung, die in einer Java-Umgebung nicht beeinträchtigt wird