Das einfachere UNQUOTED-Muster passt auf den Text bis zum nächsten Komma
oder dem (möglicherweise DOS-)Zeilenende und ist für einen "`normalen"'
Eintrag zuständig. Komplexe Einträge, die einen Zeilenwechsel oder ein
Komma im Text enthalten, werden mit dem QUOTED-Muster erfasst: Genau einem
öffnenden Anführungszeichen "{1,1} folgen beliebig viele
andere Zeichen oder doppelte Anführungszeichen ([^"]|"")*?, bis
das nächste einzelne Anführungszeichen "{1,1}(,|\r?\n) vor einem Komma
oder dem (DOS-)Zeilenende den Eintrag beendet. Die doppelten Anführungszeichen werden
im CSV-Format verwendet, wenn in der Applikation ein einzelnes " steht.
Die Option m am Ende des regulären Ausdrucks
weist Ruby an, die Mustererkennung mehrzeilig durchzuführen, das heißt,
Zeilenwechsel werden wie gewöhnliche Zeichen behandelt.
Ohne diese Option würden Zeilenwechsel in einem Feld den Parser
aus dem Tritt bringen.
Beide Muster liefern in md[1] den zu übernehmenden Inhalt des Datenfelds
und in md[0] den durch sie bearbeiteten Teilstring.
class CSVParser
include Enumerable
QUOTED = /"{1,1}(([^"]|"")*?)"{1,1}(,|\r?\n)/m
UNQUOTED = /(.*?)(,|\r?\n)/m
def initialize(string)
@string = string
end
## ich liefere die Datenfelder einer Zeile
## in einem Array
def each
while @string != ""
tokens = []
while @string != ""
case @string[0..0]
## das aktuelle Feld ist leer
when ','
tokens << nil
@string.slice!(0..0)
next
## das letzte Feld der Zeile ist leer
when /\r?\n/
tokens << nil
@string.slice!(0...$&.size)
break
## das Feld ist komplex
when '"'
pattern = QUOTED
dequote = true
## das Feld ist einfach
else
pattern = UNQUOTED
dequote = false
end
## Datenfeld mit Inhalt
md = pattern.match(@string)
token = md[1]
token.gsub!('""', '"') if dequote
tokens << token
@string.slice!(0...md[0].size)
## letztes Feld der Zeile
break if md[0][-1..-1] == "\n"
end
yield tokens
end
end
end
Durch die include-Anweisung und die Definition einer each-Methode
erhält ein CSVParser alle nicht sortierabhängigen Methoden des Enumerables-Moduls,
siehe Abschnitt 6.3.
Die Anwendung des Parsers sieht so aus:
csv = CSVParser.new($stdin.read)
csv.each_with_index { |row, i|
puts "#{i}: #{row.join('_')}"
}
Konsole:
1,2,3
ab,"c,d","e""f","g"",""","h
ij",kl
Ausgabe:
0: 1_2_3
1: ab_c,d_e"f_g","_h
ij_kl