
3つのcsvファイルがあり、最初の列(id列)に結合したいと思います。
各ファイルには同じ3つの列があります。
ライン例:
id | timestamp | Name
3792318, 2014-07-15 00:00:00, "A, B"
3つのcsvファイルを合わせるとき
join -t, <(join -t, csv1 csv2) csv3 > out.csv
ファイルout.csv
の行ごとの列数が異なります。おそらく、区切り文字がコンマであり、一部の行(上記の例のように)には、セルの内容にコンマが含まれている可能性があります。
答え1
明らかにcsvパーサーを使用する方が良いでしょうが、安全であると仮定できる場合
- 最初のフィールドにはカンマは含まれません。
- 最初のファイルに存在するIDのみが必要です(IDがfile2またはfile3にあるがfile1にはない場合は無視してください)。
- これらのファイルはRAMに収まるほど小さいです。
これにより、このPerlメソッドが機能します。
#!/usr/bin/env perl
use strict;
my %f;
## Read the files
while (<>) {
## remove trailing newlines
chomp;
## Replace any commas within quotes with '|'.
## I am using a while loop to deal with multiple commas.
while (s/\"([^"]*?),([^"]*?)\"/"$1|$2"/){}
## match the id and the rest.
/^(.+?)(,.+)/;
## The keys of the %f hash are the ids
## each line with the same id is appended to
## the current value of the key in the hash.
$f{$1}.=$2;
}
## Print the lines
foreach my $id (keys(%f)) {
print "$id$f{$id}\n";
}
上記のスクリプトを別の名前で保存し、次のfoo.pl
ように実行します。
perl foo.pl file1.csv file2.csv file3.csv
上記のスクリプトは1行で書くこともできます。
perl -lne 'while(s/\"([^"]*?),([^"]*)\"/"$1|$2"/){} /^(.+?)(,.+)/; $k{$1}.=$2;
END{print "$_$k{$_}" for keys(%k)}' file1 file2 file3
答え2
TxR言語:
@(do
(defun csv-parse (str)
(let ((toks (tok-str str #/[^\s,][^,]+[^\s,]|"[^"]*"|[^\s,]/)))
[mapcar (do let ((l (match-regex @1 #/".*"/)))
(if (eql l (length @1))
[@1 1..-1] @1)) toks]))
(defun csv-format (list)
(cat-str (mapcar (do if (find #\, @1) `"@1"` @1) list) ", "))
(defun join-recs (recs-left recs-right)
(append-each ((l recs-left))
(collect-each ((r recs-right))
(append l r))))
(let ((hashes (collect-each ((arg *args*))
(let ((stream (open-file arg)))
[group-by first [mapcar csv-parse (gun (get-line stream))]
:equal-based]))))
(when hashes
(let ((joined (reduce-left (op hash-isec @1 @2 join-recs) hashes)))
(dohash (key recs joined)
(each ((rec recs))
(put-line (csv-format rec))))))))
サンプル。
注:キー3792318は3番目のファイルに2回表示されるため、このキーへのリンクされた出力には2行があると予想されます。
注:データをソートする必要はなく、結合にハッシュが使用されます。
$ for x in csv* ; do echo "File $x:" ; cat $x ; done
File csv1:
3792318, 2014-07-15 00:00:00, "A, B"
3792319, 2014-07-16 00:00:01, "B, C"
3792320, 2014-07-17 00:00:02, "D, E"
File csv2:
3792319, 2014-07-15 00:02:00, "X, Y"
3792320, 2014-07-11 00:03:00, "S, T"
3792318, 2014-07-16 00:02:01, "W, Z"
File csv3:
3792319, 2014-07-10 00:04:00, "M"
3792320, 2014-07-09 00:06:00, "N"
3792318, 2014-07-05 00:07:01, "P"
3792318, 2014-07-16 00:08:01, "Q"
ランニング:
$ txr join.txr csv1 csv2 csv3
3792319, 2014-07-16 00:00:01, "B, C", 3792319, 2014-07-15 00:02:00, "X, Y", 3792319, 2014-07-10 00:04:00, M
3792318, 2014-07-15 00:00:00, "A, B", 3792318, 2014-07-16 00:02:01, "W, Z", 3792318, 2014-07-05 00:07:01, P
3792318, 2014-07-15 00:00:00, "A, B", 3792318, 2014-07-16 00:02:01, "W, Z", 3792318, 2014-07-16 00:08:01, Q
3792320, 2014-07-17 00:00:02, "D, E", 3792320, 2014-07-11 00:03:00, "S, T", 3792320, 2014-07-09 00:06:00, N
より「正しい」csv-parse
機能は次のとおりです。
;; Include the comma separators as tokens; then parse the token
;; list, recognizing consecutive comma tokens as an empty field,
;; and stripping leading/trailing whitespace and quotes.
(defun csv-parse (str)
(labels ((clean (str)
(set str (trim-str str))
(if (and (= [str 0] #\")
(= [str -1] #\"))
[str 1..-1]
str))
(post-process (tokens)
(tree-case tokens
((tok sep . rest)
(if (equal tok ",")
^("" ,*(post-process (cons sep rest)))
^(,(clean tok) ,*(post-process rest))))
((tok . rest)
(if (equal tok ",")
'("")
^(,(clean tok)))))))
(post-process (tok-str str #/[^,]+|"[^"]*"|,/))))