—#!/perl
#ABSTRACT: join all csv files in folder into one and remove duplicates
package
csvjoin;
$csvjoin::VERSION
=
'1.01'
;
# TO-DO: move business logic to CSV::Processor or separate module and write tests
use
strict;
use
warnings;
# use Regexp::Common;
use
Carp;
use
Cwd;
use
Pod::Usage;
use
Text::CSV;
use
File::Slurp;
use
CSV::Processor;
$SIG
{__DIE__} =
sub
{
Carp::longmess(
$_
[0] );
};
my
%opts
;
GetOptions(
"d|dir=s"
=> \
$opts
{dir},
"i|in=s"
=> \
$opts
{unique_column},
"j|join=s"
=> \
$opts
{join_column},
"f|file=s"
=> \
$opts
{out_file},
"v|verbose"
=> \
$opts
{verbose}
) or
die
(
"Error in command line arguments\n"
);
# pod2usage("No obligatory option --in specified\n") unless defined $opts{file};
# say "Verbose mode enabled" if $opts{verbose};
# defaults
$opts
{dir} = getcwd
unless
defined
$opts
{dir};
say
"Reading all csv files in dir "
.
$opts
{dir}
if
$opts
{verbose};
opendir
my
$dir
,
$opts
{dir} or
die
"Cannot open directory: $opts{dir} : $!"
;
my
@files
=
grep
{
$_
=~ m/\.csv$/ }
readdir
$dir
;
no
warnings
'utf8'
;
say
"Found files: "
.
join
(
', '
,
@files
);
closedir
$dir
;
$opts
{unique_column} =
'ID'
unless
defined
$opts
{unique_column};
say
"Looking for unique data in "
.
$opts
{unique_column}
if
$opts
{verbose};
$opts
{join_column} =
'SOURCE'
unless
defined
$opts
{join_column};
$opts
{out_file} =
'unique.csv'
unless
defined
$opts
{out_file};
say
"Unique results will be written in "
.
$opts
{out_file}
.
", joined column is "
.
$opts
{join_column}
if
$opts
{verbose};
my
@new_files
;
for
my
$f
(
@files
) {
my
$bot
= CSV::Processor->new(
in_file
=>
$f
,
prefix
=>
'csvuniq_'
);
$bot
->add_same(
$opts
{unique_column},
$opts
{join_column},
value
=>
$f
)
;
# add source = filename column to each file
push
@new_files
,
'csvuniq_'
.
$f
;
}
# `touch $opts{out_file}`;
my
$buffer_file
=
'joined.csv'
;
# Combine to single file considering first string as header
for
my
$i
( 0 ..
$#new_files
) {
my
$lines_ref
= read_file(
$new_files
[
$i
],
array_ref
=> 1 );
my
$header
=
shift
@$lines_ref
;
append_file(
$buffer_file
, [
$header
] )
if
(
$i
eq 0 );
append_file(
$buffer_file
,
@$lines_ref
);
unlink
$new_files
[
$i
];
}
my
$csv
= Text::AutoCSV->new(
in_file
=>
$buffer_file
);
$csv
->
read
();
my
$nb_rows
=
$csv
->get_nb_rows();
my
@cols
=
$csv
->get_fields_names();
my
@ids
=
$csv
->get_values(
$opts
{unique_column} );
@ids
= uniq
@ids
;
# say "Found unique ids:\n".join( "\n", @ids ) if $opts{verbose};
my
@data_in_new_csv
;
my
$repeated_records
= 0;
for
my
$id
(
@ids
) {
my
$found_ar
=
$csv
->search(
$opts
{unique_column},
$id
);
my
$row_hr
=
$csv
->get_row_hr(
$found_ar
->[0] );
if
(
scalar
@$found_ar
> 1 ) {
say
"Duplicate found : "
.
$opts
{unique_column} .
" : "
.
$id
if
$opts
{verbose};
$repeated_records
++;
my
@a
;
for
my
$row_number
(
@$found_ar
) {
push
@a
,
$csv
->get_cell(
$row_number
,
$opts
{join_column} );
}
$row_hr
->{
$opts
{join_column} } =
join
(
','
,
@a
)
}
push
@data_in_new_csv
,
$row_hr
;
}
unlink
$buffer_file
;
$csv
= Text::CSV->new() or
die
"Cannot use CSV: "
. Text::CSV->error_diag();
$csv
->eol(
"\012"
);
$csv
->sep_char(
";"
);
open
my
$fh
,
">:encoding(utf8)"
,
$opts
{out_file} or
die
"$opts{out_file}: $!"
;
$csv
->column_names(
@cols
);
$csv
->print_hr(
$fh
,
$_
)
for
@data_in_new_csv
;
close
$fh
or
die
"$opts{out_file}: $!"
;
say
"Total records in : "
.
$nb_rows
if
$opts
{verbose};
say
"Total records out : "
.
scalar
@data_in_new_csv
if
$opts
{verbose};
say
"Repeated records : "
.
$repeated_records
if
$opts
{verbose};
my
$duplicates
=
$nb_rows
-
scalar
@data_in_new_csv
;
say
"Total duplicates : "
.
$duplicates
if
$opts
{verbose};
my
$percentage
= (
$duplicates
/
$nb_rows
) * 100;
printf
(
"CSV reduce percentage : %.2f \n"
,
$percentage
)
if
$opts
{verbose};
# Text::AutoCSV: error: illegal call while read is in progress, would lead to infinite recursion
# $csv->set_walker_hr(sub {
# my $hr = shift;
# my $found_ar = $csv->search( $opts{unique_column}, $hr->{ $opts{unique_column} } );
# my $row_hr = $csv->get_row_hr( $found_ar->[0] );
#
# if ( scalar @$found_ar > 1 ) {
# my @a;
# for my $row_number ( @$found_ar ) {
# push @a, $csv->get_cell($row_number, $opts{unique_column});
# }
# $row_hr->{ $opts{join_column} } = join ( ',' , @a)
# }
#
# return $hr;
# });
exit
0;
__END__
=pod
=encoding UTF-8
=head1 NAME
csvjoin - join all csv files in folder into one and remove duplicates
=head1 VERSION
version 1.01
=head1 SYNOPSIS
csvjoin -d </user/pavel/csv> -i <in_column_name_or_index> -o <column name to store source> -v
<in_column_name_or_index> could be column name if csv has header or just row number.
In case of using row number please make sure that csv column structure is same
Read csv files without subdirectories
All available options:
-d | --dir name of directory to process. by default is cwd
-i | --in number or name of column, data from which will be checked for unique, by default is id or first column
-j | --join number or name of column where data is different and where it is needed to join values
-f | --file name of output file, by default is unique.csv
-v | --verbose verbose mode
Examples of usage:
csvjoin
csvjoin -i 2
csvjoin ---in ID -v
=head1 DESCRIPTION
CLI tool that makes one csv with unique data from all csv in current folder
Removes duplicates in csv file assuming that one column could be different ( content in this column will be joined )
Criteria that records are same is same field C<ID>. However you can pass your own column names
NOW WORKS FINE ONLY FOR NAMED CSV FILES AND ON LINUX SYSTEMS
=head1 SIMILAR PACKAGES
See more:
L<Text::CSV>
L<Text::CSV_XS>
L<Text::CSV::Simple>
L<Tie::CSV_File>
L<Text::CSV::Merge>
=head1 AUTHOR
Pavel Serikov <pavelsr@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Pavel Serikov.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut