2010-10-14 16 views
6

Sto cercando qualcosa come HTML::TableExtract, non solo per l'input HTML, ma per l'input di testo semplice che contiene "tabelle" formattate con indentazione e spaziatura.Come posso estrarre/analizzare i dati tabulari da un file di testo in Perl?

dati potrebbe essere la seguente:

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

Si prega di fornire e l'esempio. – DVK

+0

Ho fornito una soluzione, ma produrrà colonne SIX. Stai facendo un'ipotesi perché il separatore di colonne DEVE essere> 1 spazio? – DVK

+0

No, ma possiamo presumere che conosca le stringhe di intestazione della colonna e che i dati della colonna siano allineati correttamente sotto le intestazioni. – Thilo

risposta

1

Non a conoscenza di qualsiasi soluzione pacchettizzata, ma qualcosa non molto flessibile è abbastanza semplice da fare supponendo che si può fare due passaggi sul file: (il seguente è parzialmente la Perl esempio pseudocodice)

  • Ipotesi: i dati possono contenere spazi e non è citata ala CSV se c'è uno spazio - se questo non è il caso, basta usare Text::CSV(_XS).
  • Presupposto: nessuna scheda utilizzata per la formattazione.
  • La logica definisce un "separatore di colonna" come qualsiasi serie consecutiva di righe verticali popolate al 100% con spazi.
  • Se per caso ogni riga ha uno spazio che fa parte dei dati nei caratteri di offset M, la logica considererà l'offset M come un separatore di colonna, poiché non può conoscere meglio. L'UNICO modo che può conoscere meglio è se si richiede che la separazione delle colonne sia almeno X spazi in cui X> 1 - vedere il secondo frammento di codice per quello.

codice di esempio:

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; 

Ora, se si richiedono la separazione di colonna per essere almeno spazi X dove X> 1, è anche fattibile, ma il parser di posizioni di colonna deve essere un po ' più complesso:

# 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

Ecco una soluzione molto rapida, commentata con una panoramica. (Le mie scuse per la lunghezza.) Fondamentalmente, se una "parola" appare dopo l'inizio dell'intestazione della colonna n, allora finisce nella colonna n, a meno che la maggior parte del suo corpo scia nella colonna n + 1, nel qual caso finisce invece lì. Riordinando ciò, estendendolo per supportare più tavoli diversi, ecc., Viene lasciato come esercizio. È inoltre possibile utilizzare qualcosa di diverso dall'offset sinistro dell'intestazione di colonna come segno di limite, ad esempio il centro o un valore determinato dal numero di colonna.

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

Esempio di output:

 
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. 
Problemi correlati