web-dev-qa-db-fra.com

Quelle est la meilleure façon d'analyser un fichier délimité par des tabulations dans Ruby?

Quelle est la meilleure façon (la plus efficace) d'analyser un fichier délimité par des tabulations dans Ruby?

63
mbm

La bibliothèque Ruby CSV vous permet de spécifier le délimiteur de champ. Ruby 1.9 utilise FasterCSV . Quelque chose comme ça pourrait fonctionner:

require "csv"
parsed_file = CSV.read("path-to-file.csv", { :col_sep => "\t" })
106
jergason

Les règles pour TSV sont en fait un peu différentes de CSV. La principale différence est que CSV a des dispositions pour coller une virgule à l'intérieur d'un champ, puis utiliser des guillemets et échapper des guillemets à l'intérieur d'un champ. J'ai écrit un exemple rapide pour montrer comment la réponse simple échoue:

require 'csv'
line = 'boogie\ttime\tis "now"'
begin
  line = CSV.parse_line(line, col_sep: "\t")
  puts "parsed correctly"
rescue CSV::MalformedCSVError
  puts "failed to parse line"
end

begin
  line = CSV.parse_line(line, col_sep: "\t", quote_char: "Ƃ")
  puts "parsed correctly with random quote char"
rescue CSV::MalformedCSVError
  puts "failed to parse line with random quote char"
end

#Output:
# failed to parse line
# parsed correctly with random quote char

Si vous souhaitez utiliser la bibliothèque CSV, vous pouvez utiliser un caractère de citation aléatoire que vous ne vous attendez pas à voir si votre fichier (l'exemple le montre), mais vous pouvez également utiliser une méthodologie plus simple comme la classe StrictTsv illustrée ci-dessous pour obtenir le même effet sans avoir à se soucier des devis sur le terrain.

# The main parse method is mostly borrowed from a Tweet by @JEG2
class StrictTsv
  attr_reader :filepath
  def initialize(filepath)
    @filepath = filepath
  end

  def parse
    open(filepath) do |f|
      headers = f.gets.strip.split("\t")
      f.each do |line|
        fields = Hash[headers.Zip(line.split("\t"))]
        yield fields
      end
    end
  end
end

# Example Usage
tsv = Vendor::StrictTsv.new("your_file.tsv")
tsv.parse do |row|
  puts row['named field']
end

Le choix d'utiliser la bibliothèque CSV ou quelque chose de plus strict dépend simplement de la personne qui vous envoie le fichier et si elle s'attend à adhérer à la norme TSV stricte.

Des détails sur la norme TSV peuvent être trouvés sur http://en.wikipedia.org/wiki/Tab-separated_values

30
mmmries

Il existe en fait deux types de fichiers TSV différents.

  1. Fichiers TSV qui sont en fait des fichiers CSV avec un délimiteur défini sur Tab. C'est quelque chose que vous obtiendrez lorsque vous p. Ex. enregistrer une feuille de calcul Excel en tant que "Texte Unicode UTF-16". Ces fichiers utilisent des règles de citation CSV, ce qui signifie que les champs peuvent contenir des tabulations et des retours à la ligne, tant qu'ils sont cités, et les guillemets littéraux sont écrits deux fois. La façon la plus simple de tout analyser correctement est d'utiliser la gemme csv:

    use 'csv'
    parsed = CSV.read("file.tsv", col_sep: "\t")
    
  2. Fichiers TSV conformes à la norme IANA . Les tabulations et les sauts de ligne ne sont pas autorisés en tant que valeurs de champ, et il n'y a aucune citation. C'est quelque chose que vous obtiendrez lorsque vous p. Ex. sélectionnez une feuille de calcul Excel entière et collez-la dans un fichier texte (attention: elle sera gâchée si certaines cellules contiennent des tabulations ou des retours à la ligne). Ces fichiers TSV peuvent être facilement analysés ligne par ligne avec une simple line.rstrip.split("\t", -1) (remarque -1, Ce qui empêche split de supprimer les champs de fin vides). Si vous souhaitez utiliser la gemme csv, définissez simplement quote_char Sur nil:

    use 'csv'
    parsed = CSV.read("file.tsv", col_sep: "\t", quote_char: nil)
    
0
michau

J'aime la réponse de mmmries. CEPENDANT, je déteste la façon dont Ruby supprime toutes les valeurs vides de la fin d'une division. Il ne supprime pas non plus la nouvelle ligne à la fin des lignes.

De plus, j'avais un fichier avec des nouvelles lignes potentielles dans un champ. J'ai donc réécrit son "analyse" comme suit:

def parse
  open(filepath) do |f|
    headers = f.gets.strip.split("\t")
    f.each do |line|
      myline=line
      while myline.scan(/\t/).count != headers.count-1
        myline+=f.gets
      end
      fields = Hash[headers.Zip(myline.chomp.split("\t",headers.count))]
      yield fields
    end
  end
end

Cela concatène toutes les lignes si nécessaire pour obtenir une ligne complète de données et renvoie toujours l'ensemble complet de données (sans aucune entrée potentielle à la fin).

0
Jim MacKenzie