【问题标题】:Split large csv file into multiple files based on column(s)根据列将大型 csv 文件拆分为多个文件
【发布时间】:2022-01-16 02:21:53
【问题描述】:

我想知道在任何程序(awk/perl/python)中将 csv 文件(比如 10k 列)拆分为多个小文件的快速/有效方式,每个小文件包含 2 列。我会在 unix 机器上执行此操作。

#contents of large_file.csv
1,2,3,4,5,6,7,8
a,b,c,d,e,f,g,h
q,w,e,r,t,y,u,i
a,s,d,f,g,h,j,k
z,x,c,v,b,n,m,z

我现在想要多个这样的文件:

# contents of 1.csv
1,2
a,b
q,w
a,s
z,x

# contents of 2.csv
1,3
a,c
q,e
a,d
z,c

# contents of 3.csv
1,4
a,d
q,r
a,f
z,v

and so on...

我目前可以在小文件(比如 30 列)上使用 awk 执行此操作,如下所示:

awk -F, 'BEGIN{OFS=",";} {for (i=1; i < NF; i++) print $1, $(i+1) > i ".csv"}' large_file.csv

以上对于大文件需要很长时间,我想知道是否有更快、更有效的方法来做同样的事情。

提前致谢。

【问题讨论】:

  • 所以您需要编写大约 10,000 个文件?原始 CSV 文件中有多少行?
  • 你必须看看你的操作系统是否可以处理那么多打开的文件句柄。
  • @G4143 虽然这对于解决方案很重要,但不必同时打开它们

标签: perl awk data-manipulation


【解决方案1】:

这里的主要障碍是编写这么多文件。

这是一种方法

use warnings;
use strict;
use feature 'say';
    
my $file = shift // die "Usage: $0 csv-file\n";

my @lines = do { local @ARGV = $file; <> };
chomp @lines;

my @fhs = map { 
    open my $fh, '>', "f${_}.csv" or die $!; 
    $fh 
} 
1 .. scalar( split /,/, $lines[0] );

for (@lines) { 
    my ($first, @cols) = split /,/; 
    say {$fhs[$_]} join(',', $first, $cols[$_]) 
        for 0..$#cols;
}

我没有针对任何其他方法计时。首先为每个文件组装数据,然后在一次操作中将其转储到每个文件中可能会有所帮助,但首先让我们知道原始 CSV 文件有多大。

一次打开​​这么多输出文件(对于@fhs 文件句柄)可能会带来问题。如果是这种情况,那么最简单的方法是首先组装所有数据,然后一次打开并写入一个文件

use warnings;
use strict;
use feature 'say';

my $file = shift // die "Usage: $0 csv-file\n";

open my $fh, '<', $file or die "Can't open $file: $!";

my @data;
while (<$fh>) {
    chomp;
    my ($first, @cols) = split /,/;
    push @{$data[$_]}, join(',', $first, $cols[$_]) 
        for 0..$#cols;
}

for my $i (0..$#data) {
    open my $fh, '>', $i+1 . '.csv' or die $!;
    say $fh $_ for @{$data[$i]};
}

这取决于整个原始 CSV 文件是否可以保存在内存中。

【讨论】:

    【解决方案2】:

    用你的展示样本,尝试;请尝试关注awk 代码。由于您同时打开文件,因此可能会因臭名昭著的“打开的文件过多错误”而失败,因此为避免将所有值放入数组中,并在此 awk 代码的 END 块中一一打印它们,我将关闭他们尽快将所有内容打印到输出文件。

    awk '
    BEGIN{ FS=OFS="," }
    {
      for(i=1;i<NF;i++){
        value[i]=(value[i]?value[i] ORS:"") ($1 OFS $(i+1))
      }
    }
    END{
      for(i=1;i<=NF;i++){
        outFile=i".csv"
        print value[i] > (outFile)
        close(outFile)
      }
    }
    ' large_file.csv
    

    【讨论】:

    • 当我尝试这个时,文件 1.csv 出来是空的 - 我错过了什么
    • @user10101904,我已经对答案进行了编辑,请你现在检查一下,让我知道它是怎么回事,干杯。
    【解决方案3】:

    我需要相同的功能并用 bash 编写。 不确定它是否会比ravindersingh13 的答案更快,但我希望它会对某人有所帮助。

    实际版本:https://github.com/pgrabarczyk/csv-file-splitter

    #!/usr/bin/env bash
    set -eu
    
    SOURCE_CSV_PATH="${1}"
    LINES_PER_FILE="${2}"
    DEST_PREFIX_NAME="${3}"
    DEBUG="${4:-0}"
    
    split_files() {
      local source_csv_path="${1}"
      local lines_per_file="${2}"
      local dest_prefix_name="${3}"
      local debug="${4}"
    
      _print_log "source_csv_path: ${source_csv_path}"
      local dest_prefix_path="$(pwd)/output/${dest_prefix_name}"
      _print_log "dest_prefix_path: ${dest_prefix_path}"
    
      local headline=$(awk "NR==1" "${source_csv_path}")
      local file_no=0
      
      mkdir -p "$(dirname ${dest_prefix_path})"
    
      local lines_in_files=$(wc -l "${source_csv_path}" | awk '{print $1}')
      local files_to_create=$(((lines_in_files-1)/lines_per_file))
      _print_log "There is ${lines_in_files} lines in file. I will create ${files_to_create} files per ${lines_per_file} (Last file may have less)"
    
      _print_log "Start processing."
    
      for (( start_line=1; start_line<=lines_in_files; )); do
        last_line=$((start_line+lines_per_file))
        file_no=$((file_no+1))
        local file_path="${dest_prefix_path}$(printf "%06d" ${file_no}).csv"
    
        if [ $debug -eq 1 ]; then
          _print_log "Creating file ${file_path} with lines [${start_line};${last_line}]"
        fi
    
        echo "${headline}" > "${file_path}"
        awk "NR>${start_line} && NR<=${last_line}" "${source_csv_path}" >> "${file_path}"
    
        start_line=$last_line
      done
    
      _print_log "Done."
    }
    
    _print_log() {
      local log_message="${1}"
      local date_time=$(date "+%Y-%m-%d %H:%M:%S.%3N")
      printf "%s - %s\n" "${date_time}" "${log_message}" >&2
    }
    
    split_files "${SOURCE_CSV_PATH}" "${LINES_PER_FILE}" "${DEST_PREFIX_NAME}" "${DEBUG}"
    

    执行:

    bash csv-file-splitter.sh "sample.csv" 3 "result_" 1
    

    【讨论】:

      【解决方案4】:

      尝试使用模块 Text::CSV 的解决方案。

      #! /usr/bin/env perl
      
      use warnings;
      use strict;
      use utf8;
      use open qw<:std :encoding(utf-8)>;
      use autodie;
      use feature qw<say>;
      use Text::CSV;
      
      my %hsh = ();
      
      my $csv = Text::CSV->new({ sep_char => ',' });
      
      print "Enter filename: ";
      chomp(my $filename = <STDIN>);
      
      open (my $ifile, '<', $filename);
      
      while (<$ifile>) {
          chomp;
          if ($csv->parse($_)) {
          
          my @fields = $csv->fields();
          my $first = shift @fields;
          while (my ($i, $v) = each @fields) {
              push @{$hsh{($i + 1).".csv"}}, "$first,$v";   
          }   
          } else {
          die "Line could not be parsed: $_\n";
          }
      }
      
      close($ifile);
      
      while (my ($k, $v) = each %hsh) {
          open(my $ifile, '>', $k);
          say {$ifile} $_ for @$v;
          close($ifile);
      }
      
      exit(0);
      

      【讨论】:

        猜你喜欢
        • 2019-06-14
        • 2022-01-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-01-27
        • 2012-04-14
        • 2015-09-03
        • 1970-01-01
        相关资源
        最近更新 更多