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
위 스크립트는 한 줄로 작성할 수도 있습니다.
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은 세 번째 파일에 두 번 나타나므로 이 키에 대한 연결된 출력에는 두 줄이 있을 것으로 예상됩니다.
참고: 데이터를 정렬할 필요는 없으며 조인에 해싱이 사용됩니다.
$ 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 #/[^,]+|"[^"]*"|,/))))