2010-10-14 66 views
6

Tôi đang tìm một cái gì đó như HTML::TableExtract, chỉ cần không cho đầu vào HTML, nhưng đối với đầu vào văn bản thuần túy có chứa "bảng" được định dạng với thụt đầu dòng và khoảng cách.Làm cách nào để trích xuất/phân tích dữ liệu bảng từ một tệp văn bản trong Perl?

Dữ liệu có thể trông như thế này:

Here is some header text. 

Column One  Column Two  Column Three 
a           b 
a     b      c 


Some more text 

Another Table  Another Column 
abdbdbdb   aaaa 
+0

Vui lòng cung cấp và ví dụ. – DVK

+0

Tôi đã cung cấp một giải pháp, nhưng nó sẽ tạo ra các cột SIX. Bạn đang tạo giả định taht column separator PHẢI là> 1 không gian? – DVK

+0

Không, nhưng chúng tôi có thể giả sử tôi biết các chuỗi tiêu đề cột và dữ liệu cột được căn chỉnh đúng theo tiêu đề. – Thilo

Trả lời

1

Không biết về bất kỳ giải pháp đóng gói, nhưng một cái gì đó không phải là rất linh hoạt là khá đơn giản để làm giả sử bạn có thể làm hai đi ngang qua các tập tin: (sau đây là một phần Perlish ví dụ giả)

  • Giả sử: dữ liệu có thể chứa dấu cách và không được trích dẫn ala CSV nếu có dấu cách - nếu trường hợp này không đúng, chỉ cần sử dụng Text::CSV(_XS).
  • Giả định: không có tab nào được sử dụng để định dạng.
  • Logic xác định "dấu tách cột" là bất kỳ tập hợp hàng dọc nào liên tiếp được điền 100% với dấu cách.
  • Nếu vô tình mỗi hàng có một khoảng trống là một phần của dữ liệu tại ký tự M bù trừ, logic sẽ xem xét bù M là một dấu tách cột, vì nó không thể biết tốt hơn. Cách duy nhất nó có thể biết tốt hơn là nếu bạn yêu cầu tách cột để có ít nhất X không gian nơi X> 1 - xem đoạn mã thứ hai cho điều đó.

Mẫu mã:

my $INFER_FROM_N_LINES = 10; # Infer columns from this # of lines 
          # 0 means from entire file 
my $lines_scanned = 0; 
my @non_spaces=[]; 
# First pass - find which character columns in the file have all spaces and which don't 
my $fh = open(...) or die; 
while (<$fh>) { 
    last if $INFER_FROM_N_LINES && $lines_scanned++ == $INFER_FROM_N_LINES; 
    chomp; 
    my $line = $_; 
    my @chars = split(//, $line); 
    for (my $i = 0; $i < @chars; $i++) { # Probably can be done prettier via map? 
     $non_spaces[$i] = 1 if $chars[$i] ne " "; 
    } 
} 
close $fh or die; 

# Find columns, defined as consecutive "non-spaces" slices. 
my @starts, @ends; # Index at which columns start and end 
my $state = " "; # Not inside a column 
for (my $i = 0; $i < @non_spaces; $i++) { 
    next if $state eq " " && !$non_spaces[$i]; 
    next if $state eq "c" && $non_spaces[$i]; 
    if ($state eq " ") { # && $non_spaces[$i] of course => start column 
     $state = "c"; 
     push @starts, $i; 
    } else { # meaning $state eq "c" && !$non_spaces[$i] => end column 
     $state = " "; 
     push @ends, $i-1; 
    } 
} 
if ($state eq "c") { # Last char is NOT a space - produce the last column end 
    push @ends, $#non_spaces; 
} 

# Now split lines 
my $fh = open(...) or die; 
my @rows =(); 
while (<$fh>) { 
    my @columns =(); 
    push @rows, \@columns; 
    chomp; 
    my $line = $_; 
    for (my $col_num = 0; $col_num < @starts; $col_num++) { 
     $columns[$col_num] = substr($_, $starts[$col_num], $ends[$col_num]-$starts[$col_num]+1); 
    } 
} 
close $fh or die; 

Bây giờ, nếu bạn yêu cầu tách cột có ít nhất không gian X trong đó X> 1, nó cũng có thể làm được nhưng phân tích cú pháp các địa điểm cột cần phải được một chút phức tạp hơn:

# Find columns, defined as consecutive "non-spaces" slices separated by at least 3 spaces. 
my $min_col_separator_is_X_spaces = 3; 
my @starts, @ends; # Index at which columns start and end 
my $state = "S"; # inside a separator 
NEXT_CHAR: for (my $i = 0; $i < @non_spaces; $i++) { 
    if ($state eq "S") { # done with last column, inside a separator 
     if ($non_spaces[$i]) { # start a new column 
      $state = "c"; 
      push @starts, $i; 
     } 
     next; 
    } 
    if ($state eq "c") { # Processing a column 
     if (!$non_spaces[$i]) { # First space after non-space 
           # Could be beginning of separator? check next X chars! 
      for (my $j = $i+1; $j < @non_spaces 
          || $j < $i+$min_col_separator_is_X_spaces; $j++) { 
       if ($non_spaces[$j]) { 
        $i = $j++; # No need to re-scan again 
        next NEXT_CHAR; # OUTER loop 
       } 
       # If we reach here, next X chars are spaces! Column ended! 
       push @ends, $i-1; 
       $state = "S"; 
       $i = $i + $min_col_separator_is_X_spaces; 
      } 
     } 
     next; 
    } 
} 
1

Đây là giải pháp rất nhanh, nhận xét với tổng quan. Về cơ bản, nếu một "từ" xuất hiện sau khi bắt đầu tiêu đề cột n, thì nó kết thúc trong cột n, trừ khi phần lớn thân của nó nằm trong cột n + 1, trong trường hợp đó nó kết thúc ở đó thay vào đó. Làm sạch điều này, mở rộng nó để hỗ trợ nhiều bảng khác nhau, vv được để lại như một bài tập. Bạn cũng có thể sử dụng một cái gì đó khác với phần bù trừ bên trái của tiêu đề cột làm dấu biên, chẳng hạn như trung tâm hoặc một số giá trị được xác định bởi số cột.

#!/usr/bin/perl 


use warnings; 
use strict; 


# Just plug your headers in here... 
my @headers = ('Column One', 'Column Two', 'Column Three'); 

# ...and get your results as an array of arrays of strings. 
my @result =(); 


my $all_headers = '(' . (join ').*(', @headers) . ')'; 
my $found = 0; 
my @header_positions; 
my $line = ''; 
my $row = 0; 
push @result, [] for (1 .. @headers); 


# Get lines from file until a line matching the headers is found. 

while (defined($line = <DATA>)) { 

    # Get the positions of each header within that line. 

    if ($line =~ /$all_headers/) { 
     @header_positions = @-[1 .. @headers]; 
     $found = 1; 
     last; 
    } 

} 


$found or die "Table not found! :<\n"; 


# For each subsequent nonblank line: 

while (defined($line = <DATA>)) { 
    last if $line =~ /^$/; 

    push @{$_}, "" for (@result); 
    ++$row; 

    # For each word in line: 

    while ($line =~ /(\S+)/g) { 

     my $word = $1; 
     my $position = $-[1]; 
     my $length = $+[1] - $position; 
     my $column = -1; 

     # Get column in which word starts. 

     while ($column < $#headers && 
      $position >= $header_positions[$column + 1]) { 
      ++$column; 
     } 

     # If word is not fully within that column, 
     # and more of it is in the next one, put it in the next one. 

     if (!($column == $#headers || 
      $position + $length < $header_positions[$column + 1]) && 
      $header_positions[$column + 1] - $position < 
      $position + $length - $header_positions[$column + 1]) { 

      my $element = \$result[$column + 1]->[$row]; 
      $$element .= " $word"; 

     # Otherwise, put it in the one it started in. 

     } else { 

      my $element = \$result[$column]->[$row]; 
      $$element .= " $word"; 

     } 

    } 

} 


# Output! Eight-column tabs work best for this demonstration. :P 

foreach my $i (0 .. $#headers) { 
    print $headers[$i] . ": "; 
    foreach my $c (@{$result[$i]}) { 
     print "$c\t"; 
    } 
    print "\n"; 
} 


__DATA__ 

This line ought to be ignored. 

Column One  Column Two  Column Three 
These lines are part of the tabular data to be processed. 
The data are split based on how much words overlap columns. 

This line ought to be ignored also. 

Mẫu đầu ra:

 
Column One:  These lines are   The data are split 
Column Two:  part of the tabular  based on how 
Column Three: data to be processed. much words overlap columns. 
Các vấn đề liên quan