[JAVA] Analyse CSV avec des caractères de saut de champ

Trouvez un but pour les moyens.

Un jour, je pensais que j'aimerais faire un analyseur utilisant javaCC à cause de l'influence du livre "Faisons un compilateur normal" que j'ai rencontré à la bibliothèque.

«Le fichier CSV qui vide le DB d'événements de jp1base contient des sauts de ligne et des virgules dans les champs entre guillemets, il est donc difficile de le coller dans Excel.

Alors, j'ai décidé de faire un analyseur immédiatement.


Choses à faire

Convertissez les données CSV, y compris les virgules et les sauts de ligne, sous forme de données de champ dans un formulaire facile à lire dans Excel et imprimez-le en standard.

manière

  1. Écoulement brutal

  2. Analyser les données CSV en données de tableau.

  3. Après avoir lu une ligne de données, formatez-la et sortez-la.

  4. Réalisez ce qui précède avec java.

  5. Définition des données CSV

  6. données séparées par des virgules

  7. Les champs entre guillemets (") contiennent des virgules et des sauts de ligne.

  8. De plus, si vous souhaitez représenter la double citation elle-même dans le champ entouré par la double citation, échappez-la avec une barre oblique inverse ().

  9. Format de sortie

  10. Sortez chaque champ séparé par des virgules

  11. Sortez chaque champ en le mettant entre guillemets

  12. Remplacez les guillemets et les caractères de saut de ligne dans chaque champ de données par des espaces.

environnement

  1. OS Tout système d'exploitation pris en charge par le JDK convient. (Ce que j'ai fait sur mon Mac à la maison fonctionnait comme sur le serveur RHEL6 et Window10 au travail.)

  2. JDK
    Il a été confirmé que cet article fonctionne dans un environnement JDK 1.6. Depuis avril 2020, selon le Site officiel, Java est 100% pur, il n'y a donc aucune dépendance à Runtime.

  3. JavaCC J'ai utilisé 6.0. Veuillez consulter cet article pour la méthode de configuration. Article de référence

Définition du scanner

Tout d'abord, définissez le scanner. L'analyseur est la partie responsable de l'analyse des phrases et génère des phrases significatives (TOKEN) à partir d'une liste de chaînes. Écrivez la définition du scanner dans un fichier texte avec l'extension ".jj". Voici les définitions de scanner que j'ai créées cette fois.

CSVParser.jj


//C'est la définition de SKIP pour ignorer les caractères blancs.
//TOKEN n'est pas généré à partir des caractères ignorés.
//Je vous demanderai d'ignorer les espaces et les onglets ici.
SKIP : {
    " "
  | "\t"
}

//Définition de la lecture d'une chaîne entre guillemets
//En utilisant la directive MORE,
//Si vous trouvez un double guillemet,"IN_DOUBLE_QUOTE"En raison de la transition vers le mode!
//Je vais instruire.
MORE : {
    "\"" : IN_DOUBLE_QUOTE
}

//C'est la règle de lecture des guillemets doubles.
// IN_DOUBLE_En mode QUOTE
// 1.Lorsque vous recevez un caractère autre que la barre oblique inverse, continuez à lire le caractère suivant.
// 2.Si un caractère vient après la barre oblique inverse, lisez le caractère suivant.
//Est instruit.(Selon la règle 2, la double citation suivant la barre oblique inverse est lue comme un simple caractère.
<IN_DOUBLE_QUOTE> MORE: {
    < ~["\\"] >
  | < "\\" ~[] >
}

//Vient ensuite la définition pour sortir du double revêtement
// IN_DOUBLE_En mode QUOTE
//Lorsqu'un guillemet simple apparaît, générez un jeton appelé DQFIELD et
//Mode par défaut(DEFAULT)Revenir à! Est instruit.
// "DQFIELD"C'est le nom que j'ai choisi moi-même.
//Au fait EN_DOUBLE_QUOTE est aussi un nom que j'ai choisi moi-même.
<IN_DOUBLE_QUOTE> TOKEN: {
    <DQFIELD : "\""> : DEFAULT
}

//En mode par défaut
//virgule,Double devis,Une liste de caractères qui n'inclut pas de caractère de saut de ligne est définie comme un jeton STDFIELD.
//Aussi, la virgule<SEPARATOR>Appelons cela un jeton.
//De plus, en tant que caractère représentant la fin d'une ligne"\n"Ou"\r"Continu<EOL>Traitez-les comme des jetons.
TOKEN : {
    <STDFIELD : (~["\"", ",", "\r", "\n"])+ >
  | <SEPARATOR : "," >
  | <EOL : (["\r", "\n"])+ >
}

Définition de l'analyseur

Un analyseur analyse une liste de TOKEN générés par un scanner et effectue le travail nécessaire. Ici, nous visons à renvoyer une ligne de données CSV sous forme de tableau. Tout d'abord, définissons les données CSV.

Définition des données CSV

Les données CSV sont une série de lignes (enregistrements) dans lesquelles chaque donnée (appelée champ) est alignée et séparée par des virgules.

Image de données CSV


Champ 1-1,Champ 1-2,Champ 1-3,・ ・ ・ ・
Champ 2-1,Champ 2-2,Champ 2-3,・ ・ ・ ・
 :

Tout d'abord, exprimons cette structure avec un analyseur. Tout d'abord la définition du champ. À propos, la définition de l'analyseur est également décrite dans le même fichier que la définition de l'analyseur. (Parce que ça ne devient pas si gros s'il s'agit de CSV.)

CSVParser.jj(Définition du champ)


String field() : {
} {
  (
    <DQFIELD> | <STDFIELD>
  )
}
/////Commentaire//////
//Valeur de retour:Puisque je souhaite générer les données sous forme de chaîne de caractères, le type de retour est String.
//Nom:Pour une compréhension plus facile plus tard"filed"Je l'ai nommé.
//Contenu:Chaîne de caractères entre guillemets(DQFIELD)Ou une corde ordinaire(STDFIELD)Est.
//Est défini.

Vient ensuite la définition d'une ligne de données (ici nous l'appelons un enregistrement).

jp1EventParser.jj(Définition de l'enregistrement)


//Défini comme 0 ou plusieurs champs consécutifs séparés par SEPARATOR.
//Liste des champs<String>Il est défini comme.
List<String> record() : {
} {
  //Il y a d'abord un champ
  field()
  //Suivi par 0 ou plusieurs champs séparés par SEPARATOR(=Ce n'est peut-être pas là.)
  //De plus, cela ne correspond pas lorsqu'une virgule vient soudainement.
  (
    <SEPARATOR>
    (field())?
  )*
}

Enfin, la définition de l'ensemble du fichier CSV.

CSVParser.jj(Définition de fichier CSV)


// csvContents()(Par le contenu du CSV)Est
//Vous dites que les enregistrements sont alignés sur plusieurs lignes.
//Je l'ai sorti en standard pour chaque ligne, et je n'ai aucune intention de renvoyer quoi que ce soit, donc je l'ai rendu nul.
void csvContents() : {
} {
  (
    record()
    <EOL>
  )+
  <EOF> 
}

Et étoffez l'analyseur.

Maintenant que vous avez défini la structure du fichier CSV, que faites-vous lorsque vous lisez cette structure? J'écrirai le traitement spécifique. Voici le code que j'ai écrit.

CSVParser.jj(Une définition d'analyseur avec le traitement réel ajouté.)


//Cela a l'air horriblement différent, mais fondamentalement()J'ajoute simplement le processus à l'intérieur.
//Définition du champ
String field() : {
  String data = "";  //Variable de stockage de la chaîne de caractères lue (Initialiser avec une chaîne de caractères vide)
  Token fieldToken;  //Variable pour stocker le jeton de lecture
} {
  (
    //Si vous lisez DQFIELD, l'image(Chaîne réelle)Est stocké dans les données variables.
    fieldToken = <DQFIELD> {
      data = fieldToken.image;
    }
    //Ou, si vous lisez STDIELD, l'image(Chaîne réelle)Est stocké dans les données variables après tout.
    | fieldToken = <STDFIELD> {
      data = fieldToken.image;
    }
  ) {
    //Après avoir lu un DQFILED ou STDFIELD, la valeur des données variables est renvoyée.
    return data;
  }
}

//Définition de l'enregistrement
List<String> record() : {
  List<String> fieldList = new ArrayList<String>();
  String fieldData;
} {
  //Il y a d'abord un champ
  fieldData = field(){
    //Ajouter le premier champ au tableau
    fieldList.add(fieldData);
  }
  //Suivi par 0 ou plusieurs champs séparés par SEPARATOR(=Ce n'est peut-être pas là.)
  //De plus, cela ne correspond pas lorsqu'une virgule vient soudainement.
  (
    <SEPARATOR>
    (fieldData = field(){
      //Si vous trouvez les données derrière elles séparées par SEPARATOR, ajoutez-en plus au tableau.
      fieldList.add(fieldData);
    })?
  )*
  {
    //Après avoir lu une ligne, il renvoie le tableau résultant.
    return fieldList;
  }
}

//Définition de l'ensemble du fichier CSV
void csvContents() : {
  List<String> csvRecord; //Variable qui stocke une ligne de données
} {
  (
    csvRecord = record(){
      //Après avoir lu une ligne, elle sera sortie vers la sortie standard.
      //L'écrivain ici est une classe autodidacte.(Sortira plus tard.)
      CSVWriter.writeLine(csvRecord);
    }
    <EOL>
  )+
  <EOF> 
}

Convertir le contenu du fichier CSV et la sortie

C'est la fin de la façon d'écrire un analyseur, mais comme c'est un gros problème, je l'amènerai au point où il peut être déplacé. Tout d'abord, définissez la classe CSVWriter qui génère le tableau de chaînes de caractères sorti précédemment en le mettant entre guillemets doubles séparés par des virgules.

CSVWriter.java


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

    for ( String field : record ) {
      //Concaténez les chaînes de chaque champ séparées par des virgules.
      line = line + comma + "\"" + sanitizeString(field) + "\"";
      //Pendant longtemps, lors de la création d'enregistrements séparés par des virgules, les chaînes de caractères vides ne sont concaténées qu'au début comme ceci, mais j'aimerais savoir s'il existe un autre bon moyen.
      comma = ",";
    }

    System.out.println(line);

  }

  private static sanitaizeString(String input){
    //Je suis désolé que ce soit approprié.
    //Il supprime le caractère de saut de ligne et les guillemets doubles.
    return input.replace("\n", " ").replace("\r", " ").replace("\"", "");

  }
}

Ensuite, CSVParser.jj est terminé.

CSVParser.jj


//C'est magique.
options {
//  DEBUG_PARSER=true;
  UNICODE_INPUT=true;
}

//Définition de la classe d'analyseur(code java)Est-ce PARSER_BEGIN〜PARSER_Écrivez entre END.
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)

//Définition du scanner à partir d'ici(Les commentaires sont omis.)
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"])+ >
}


//Définition de l'analyseur à partir d'ici(Les commentaires sont omis.)
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;
  }

}

Enfin, vous avez besoin du point d'entrée principal. C'est vraiment approprié. .. .. Parce que ce sera un échantillon de l'appel.

CSVConv.java


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

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

    //Tout en enseignant aux juniors que c'est une mauvaise idée de faire passer l'argument tel quel. cette. ..
    try(InputStream csvReader = new FileInputStream(args[0])) {
      //Je ne me souviens pas avoir défini un constructeur qui reçoit un tel InputStream.
      //Ne vous inquiétez pas, javaCC le fera pour vous.
      CSVParser parser = new CSVParser(csvReader, "utf8");

      //En fait, analysez le fichier.
      //Voici comment écrire car la ligne de lecture est sortie en standard sans autorisation.
      parser.parseCSV();
      
    } catch(Exception ex) {
      System.out.println("Error occured: " + ex.toString());
    }
  }
}

Comment compiler

J'écrirai la procédure de compilation pour le moment.

shell


#Exécutez javaCC=> CSVParser.Lire jj et CSVParser.Cela rendra java.
javacc CSVParser.jj
#Ensuite, compilez le tout avec ceci.
javac CSVConv.java

Alors, créez un fichier CSV d'essai (certaines données moyennes incluent de tels sauts de ligne.)

hoge.csv


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

Faisons le! !!

shell


java CSVConv hoge.csv

#Vous devriez obtenir ce type de sortie.
# "abc","def ghi","jkl", "mno,pqr"
# "stu","vwx","yz"

Je l'ai écrit pendant longtemps, mais peut-il être utilisé comme l'un des moyens de résoudre "N'est-ce pas un problème si de telles données sont passées sous forme de texte?" J'ai pensé, j'ai fait un article.

Recommended Posts

Analyse CSV avec des caractères de saut de champ
Mesures contre les caractères déformés dans la requête en plusieurs parties avec Quarkus
Conversion du fichier TSV en fichier CSV (avec BOM) en Ruby
Importation CSV avec BOM
Compression Zip qui ne brouille pas dans l'environnement Java