2010-10-14 21 views
6

Estoy buscando algo como HTML::TableExtract, simplemente no para entrada de HTML, sino para la entrada de texto sin formato que contiene "tablas" formateadas con indentación y espaciado.¿Cómo puedo extraer/analizar datos tabulares de un archivo de texto en Perl?

de datos podría tener este aspecto:

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

Sírvanse proporcionar y el ejemplo. – DVK

+0

Proporcioné una solución, pero producirá SEIS columnas. ¿Está haciendo una suposición de que el separador de columna DEBE ser> 1 espacio? – DVK

+0

No, pero podemos suponer que sé las cadenas del encabezado de columna y que los datos de la columna están alineados correctamente debajo de los encabezados. – Thilo

Respuesta

1

no tiene conocimiento de ninguna solución empaquetada, pero algo no muy flexible es bastante simple de hacer suponiendo que usted puede hacer dos pasadas sobre el archivo: (Lo que sigue es parte Perlish ejemplo pseudocódigo)

  • Supuesto: los datos pueden contener espacios y no está citado ala CSV si hay un espacio - si este no es el caso, sólo tiene que utilizar Text::CSV(_XS).
  • Supuesto: no se utilizan pestañas para formatear.
  • La lógica define un "separador de columna" como cualquier conjunto consecutivo de filas verticales pobladas al 100% con espacios.
  • Si por accidente cada fila tiene un espacio que forma parte de los datos con caracteres M de compensación, la lógica considerará el desplazamiento M como un separador de columna, ya que no puede saber nada mejor. La ÚNICA forma en que puede saber mejor es si necesita separación de columnas para que tenga al menos X espacios donde X> 1 - vea el segundo fragmento de código para eso.

Código de ejemplo:

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; 

Ahora, si requiere la separación columna para que sea por lo menos espacios X, donde X> 1, también es factible, pero el analizador de lugares de columna tiene que ser un poco más complejo:

# 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

Aquí hay una solución muy rápida, comentó con una visión general. (Mis disculpas por la longitud). Básicamente, si aparece una "palabra" después del encabezado de la columna n, termina en la columna n , a menos que la mayoría de sus cuerpos se arrastren a la columna n + 1, en cuyo caso termina allí en su lugar. Ordenarlo, ampliarlo para admitir varias tablas diferentes, etc. se deja como un ejercicio. También puede usar algo que no sea el desplazamiento a la izquierda del encabezado de columna como la marca de límite, como el centro, o algún valor determinado por el número de columna.

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

Resultado de muestra:

 
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. 
Cuestiones relacionadas