2010-06-17 38 views
19

Tôi đang làm việc trên một dự án liên quan đến phân tích cú pháp tệp csv có định dạng lớn trong Perl và tôi đang tìm cách làm cho mọi thứ hiệu quả hơn.Làm cách nào để phân tích cú pháp tệp CSV trong Perl hiệu quả?

Cách tiếp cận của tôi đã từng là split() tệp theo dòng đầu tiên, sau đó lại split() mỗi dòng một lần nữa bằng dấu phẩy để nhận các trường. Nhưng điều này tối ưu vì ít nhất hai lần truyền dữ liệu được yêu cầu. (một lần để phân chia bởi các dòng, sau đó một lần nữa cho mỗi dòng). Đây là một tập tin rất lớn, do đó việc cắt giảm một nửa sẽ là một cải tiến đáng kể cho toàn bộ ứng dụng.

Câu hỏi của tôi là, phương pháp phân tích cú pháp tệp CSV lớn nhất có hiệu quả nhất bằng cách nào chỉ sử dụng các công cụ được tích hợp sẵn?

lưu ý: Mỗi dòng có một số lượng mã thông báo khác nhau, vì vậy chúng tôi không thể bỏ qua các dòng và được phân tách bằng dấu phẩy. Ngoài ra, chúng tôi có thể giả định các trường sẽ chỉ chứa dữ liệu ascii chữ và số (không có ký tự đặc biệt hoặc các thủ thuật khác). Ngoài ra, tôi không muốn xử lý song song, mặc dù nó có thể hoạt động hiệu quả.

chỉnh sửa

Nó chỉ có thể liên quan đến công cụ tích hợp mà tàu với Perl 5.8. Vì những lý do quan liêu, tôi không thể sử dụng bất kỳ các module bên thứ ba (thậm chí nếu được lưu trữ trên CPAN)

chỉnh sửa khác

Giả sử rằng giải pháp của chúng tôi chỉ được phép để đối phó với các dữ liệu tập tin khi nó hoàn toàn được nạp vào ký ức.

chưa khác chỉnh sửa

Tôi chỉ nắm cách ngu ngốc câu hỏi này là. Xin lỗi vì làm mất thời gian của bạn. Bỏ phiếu để đóng.

+4

Bất kỳ lý do bạn cần các công cụ chỉ built-in (tôi giả sử không có quyền admin). Nếu không, hãy thử sử dụng mô-đun «Văn bản :: CSV' perl. Nó làm cho việc phân tích cú pháp CSV dễ dàng hơn rất nhiều: http://search.cpan.org/~erangel/Text-CSV/CSV.pm –

+5

Tại sao đọc toàn bộ tệp và 'tách()' theo dòng? Nếu bạn chỉ cần mở tệp và sử dụng thành ngữ ' ', bạn có thể lặp lại trên các dòng để bạn chỉ cần lưu trữ một dòng tại một thời điểm trong bộ nhớ. – mob

+1

@ Giống như một số mô-đun perl từ cpan không yêu cầu bất kỳ biên dịch và có thể được sử dụng mà không có quyền admin ... nếu có một trong những loại nó vẫn sẽ được liệt kê ra khỏi mẫu cần thiết của bạn? – Prix

Trả lời

42

Cách đúng để thực hiện - theo thứ tự độ lớn - là sử dụng Text::CSV_XS. Nó sẽ nhanh hơn nhiều và mạnh mẽ hơn nhiều so với bất cứ điều gì bạn có khả năng làm một mình. Nếu bạn quyết tâm chỉ sử dụng chức năng cốt lõi, bạn có một vài tùy chọn tùy thuộc vào tốc độ và độ mạnh.

Về nhanh nhất bạn sẽ nhận được cho tinh khiết-Perl là để đọc các dòng tập tin bằng cách dòng và sau đó ngây thơ tách dữ liệu:

my $file = 'somefile.csv'; 
my @data; 
open(my $fh, '<', $file) or die "Can't read file '$file' [$!]\n"; 
while (my $line = <$fh>) { 
    chomp $line; 
    my @fields = split(/,/, $line); 
    push @data, \@fields; 
} 

này sẽ thất bại nếu bất kỳ lĩnh vực chứa dấu phẩy nhúng. Một cách tiếp cận mạnh mẽ hơn (nhưng chậm hơn) là sử dụng Text :: ParseWords. Để làm điều đó, hãy thay split với điều này:

my @fields = Text::ParseWords::parse_line(',', 0, $line); 
+0

Khi bạn nói cách tiếp cận chậm hơn, bạn có nghĩa là mô-đun này đã biết các vấn đề về hiệu năng hay nó chỉ chậm hơn một chút? – MikeKulls

+2

@MikeKulls: Tôi sẽ không gọi đó là vấn đề hiệu suất cho mỗi lần. Đó là hệ quả của việc phân tích cú pháp thực tế thay vì mù quáng giả định rằng mỗi dấu phẩy là một dấu tách trường. Điều đó nói rằng, nó không phải là "hơi chậm hơn." Trong một tiêu chuẩn đơn giản, một 'phân tách 'trống là nhanh hơn 10-20x so với' parse_line'. –

+0

Tôi đoán nó cũng chậm hơn vì nó được viết bằng perl như trái ngược với C cho hàm tách. Về lý thuyết nó có thể có được hiệu suất gần hơn với chức năng tách, ví dụ như có thể 2-3x chậm hơn. – MikeKulls

2

Bạn có thể làm điều đó trong một lần nếu bạn đọc từng dòng tệp. Không cần phải đọc toàn bộ nội dung vào bộ nhớ cùng một lúc.

#(no error handling here!)  
open FILE, $filename 
while (<FILE>) { 
    @csv = split /,/ 

    # now parse the csv however you want. 

} 

Không thực sự chắc chắn nếu điều này hiệu quả hơn đáng kể, Perl xử lý chuỗi khá nhanh.

BẠN CẦN PHẢI ĐƯỢC CHẤM DỨT NHẬP KHẨU CỦA BẠN để xem điều gì gây ra sự chậm lại. Ví dụ: nếu bạn đang thực hiện chèn db chiếm 85% thời gian, tối ưu hóa này sẽ không hoạt động.

Sửa

Mặc dù đây cảm thấy như golf mã, thuật toán chung là để đọc toàn bộ tập tin hoặc một phần của fie vào một bộ đệm.

Lặp lại byte bằng byte thông qua bộ đệm cho đến khi bạn tìm thấy dấu phân tách csv hoặc một dòng mới.

  • Khi bạn tìm thấy dấu tách, tăng số cột của bạn.
  • Khi bạn tìm thấy một dòng mới tăng số lượng hàng của bạn.
  • Nếu bạn nhấn vào cuối bộ đệm, hãy đọc thêm dữ liệu từ tệp và lặp lại.

Vậy đó. Nhưng đọc một tập tin lớn vào bộ nhớ thực sự không phải là cách tốt nhất, xem câu trả lời ban đầu của tôi cho cách bình thường này được thực hiện.

+0

cảm ơn phản hồi. xin vui lòng xem chỉnh sửa – Mike

+0

Vì perl 5.8, khi tệp nằm trong bộ nhớ (ví dụ, trong một biến có tên là '$ scalar'), bạn vẫn có thể sử dụng trình soạn thảo tập tin trên nó với' mở (FILE, "<", \ $ vô hướng) ' – mob

8

Như những người khác đã đề cập, cách chính xác để làm điều này là với Text::CSV, và một trong hai Text::CSV_XS back-end (để đọc nhanh nhất) hoặc Text::CSV_PP back-end (nếu bạn không thể biên dịch mô-đun XS).

Nếu bạn đang cho phép để có được thêm mã địa phương (ví dụ, mô-đun cá nhân của riêng bạn), bạn có thể mất Text::CSV_PP và đặt nó ở đâu đó tại địa phương, sau đó truy cập nó thông qua use lib workaround:

use lib '/path/to/my/perllib'; 
use Text::CSV_PP; 

Ngoài , nếu không có cách nào khác để có toàn bộ tệp được đọc vào bộ nhớ và (tôi giả định) được lưu trữ trong một vô hướng, bạn vẫn có thể đọc nó như một tay cầm tệp, bằng cách mở một tay cầm vào vô hướng:

my $data = stupid_required_interface_that_reads_the_entire_giant_file(); 

open my $text_handle, '<', \$data 
    or die "Failed to open the handle: $!"; 

Và sau đó đọc qua các văn bản :: giao diện CSV:

my $csv = Text::CSV->new ({ binary => 1 }) 
      or die "Cannot use CSV: ".Text::CSV->error_diag(); 
while (my $row = $csv->getline($text_handle)) { 
    ... 
} 

hoặc chia tiểu tối ưu trên dấu phẩy:

while (my $line = <$text_handle>) { 
    my @csv = split /,/, $line; 
    ... # regular work as before. 
} 

Với phương pháp này, dữ liệu được sao chép chỉ một chút tại một thời điểm ra của vô hướng.

+8

Và cách chính xác thứ hai để làm điều này là tạo mô-đun 'Mike :: Văn bản :: CSV', sao chép mã nguồn từ' Văn bản :: CSV' vào nó và thêm tuyên bố từ chối trách nhiệm về cách nó được "lấy cảm hứng" bởi mô-đun Văn bản :: CSV nguồn mở. – mob

+0

Tôi thích nó! Tôi rất thích nó. –

+0

@RobertP, tên của $! vào cuối mệnh đề mở ... chết? – olala

1

Giả sử rằng bạn đã tập tin CSV của bạn nạp vào $csv biến và rằng bạn không cần văn bản trong biến này sau khi bạn phân tách thành công nó:

my $result=[[]]; 
while($csv=~s/(.*?)([,\n]|$)//s) { 
    push @{$result->[-1]}, $1; 
    push @$result, [] if $2 eq "\n"; 
    last unless $2; 
} 

Nếu bạn cần phải có $csv hoang sơ:

local $_; 
my $result=[[]]; 
foreach($csv=~/(?:(?<=[,\n])|^)(.*?)(?:,|(\n)|$)/gs) { 
    next unless defined $_; 
    if($_ eq "\n") { 
     push @$result, []; } 
    else { 
     push @{$result->[-1]}, $_; } 
} 
+0

Khác với việc đệm các dòng mã của bạn, theo cách nào tốt hơn là 'tách'? – mob

+0

@modrule Nếu bạn sử dụng 'split', bạn cần sử dụng nó hai lần, vì vậy dữ liệu sẽ được đọc hai lần, giải pháp của tôi chỉ đọc dữ liệu một lần. // Nhưng điều này chỉ đúng nếu dữ liệu đã được tải. – ZyX

0

Trả lời trong các ràng buộc được đặt ra bởi câu hỏi, bạn vẫn có thể cắt phần tách đầu tiên bằng cách nhập tập tin đầu vào của bạn vào một mảng chứ không phải là vô hướng:

open(my $fh, '<', $input_file_path) or die; 
my @all_lines = <$fh>; 
for my $line (@all_lines) { 
    chomp $line; 
    my @fields = split ',', $line; 
    process_fields(@fields); 
} 

Và ngay cả khi bạn không thể cài đặt (phiên bản thuần túy Perl) Text::CSV, bạn có thể thoát khỏi bằng cách kéo mã nguồn lên CPAN và sao chép/dán mã vào dự án của bạn ...

14

Đây là phiên bản cũng tôn trọng dấu ngoặc kép (ví dụ:foo,bar,"baz,quux",123 -> "foo", "bar", "baz,quux", "123").

sub csvsplit { 
     my $line = shift; 
     my $sep = (shift or ','); 

     return() unless $line; 

     my @cells; 
     $line =~ s/\r?\n$//; 

     my $re = qr/(?:^|$sep)(?:"([^"]*)"|([^$sep]*))/; 

     while($line =~ /$re/g) { 
       my $value = defined $1 ? $1 : $2; 
       push @cells, (defined $value ? $value : ''); 
     } 

     return @cells; 
} 

Sử dụng nó như thế này:

while(my $line = <FILE>) { 
    my @cells = csvsplit($line); # or csvsplit($line, $my_custom_seperator) 
} 
+1

Đừng cuộn thói quen phân tích cú pháp CSV của riêng bạn. Thật dễ dàng để làm sai và khó khăn để có được quyền, và nó có thể cắn bạn HARD. Vui lòng sử dụng Văn bản :: CSV như được đề cập bởi các áp phích khác. – MichielB

+2

Tôi sẽ không bao giờ nói không bao giờ cuộn của riêng bạn. Điều gì sẽ xảy ra nếu bạn viết một giải pháp tốt hơn các giải pháp hiện có theo một cách nào đó? – MikeKulls

Các vấn đề liên quan