2010-10-14 24 views
6

HTML::TableExtract gibi bir şey arıyorum, sadece HTML girişi için değil, girinti ve aralıklarla biçimlendirilmiş "tablolar" içeren düz metin girişi için. Bu gibi görünebilirPerl'deki bir metin dosyasından tablo verilerini nasıl ayırım/ayrıştırabilirim?

veri:

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

sağlamak ve örnek verin. – DVK

+0

Bir çözüm sağladım, ancak SIX sütunları üretecek. Bir sütun sütun ayırıcısı> 1 boşluk olması gereken bir varsayım mı yapıyorsunuz? – DVK

+0

Hayır, ancak sütun başlık dizelerini bildiğimi ve sütun verilerinin üstbilgiler altında düzgün şekilde hizalandığını varsayabiliriz. – Thilo

cevap

1

herhangi ambalajlı çözümün farkında değil, ama çok esnek değil bir şey dosyanın üzerine iki geçiş yapabilir varsayarak yapmak oldukça basittir: (Aşağıdaki kısmen Perlish olduğunu pseudocode örnek)

  • Varsayım: veri boşluk içerebilir ve bir boşluk varsa CSV ala alıntı dEĞİLDİR - bu sadece Text::CSV(_XS) kullanmak, durum yoksa.
  • Varsayım: Biçimlendirme için sekme kullanılmıyor.
  • Mantık, boşluklarla% 100 doldurulmuş ardışık herhangi bir dikey satır kümesi olacak şekilde bir "sütun ayırıcı" tanımlar.
  • Her satırda, ofset M karakterlerinde verilerin bir parçası olan bir boşluk varsa, mantık M'yi bir sütun ayırıcı olarak görecektir, çünkü daha iyi bilmeyecektir. YALNIZCA daha iyi bilmenin yolu, X> 1'un en az X alanı olması için sütun ayırma gerektiriyorsa bunun için ikinci kod parçasına bakın.

örnek kod: Şimdi

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; 

, X> 1, aynı zamanda yapılabilir ama sütun konumlarının çözümleyici biraz olması gereken yerde en az X alanlarda olmak üzere sütun ayrılmasını gerektiren eğer daha karmaşık:

# 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

İşte çok hızlı bir çözüm, genel bir bakışla yorumladı. (Uzunluğun için özür dilerim.) Temel olarak, bir sütun başlığı n'un başlangıcından sonra bir "sözcük" belirirse, n numaralı sütunun gövdesinin çoğunun gövdesi n + 1 sütununa girmediği sürece, Bu durumda bunun yerine orada biter. Bunu toplama, birden fazla farklı tabloyu desteklemek için genişletme, vb. Ayrıca, sütun başlığının sol ofseti dışında, merkez gibi bir sınır işareti veya sütun numarasıyla belirlenen bir değer olarak da kullanabilirsiniz.

#!/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. 

Örnek çıktı:

 
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. 
İlgili konular