2010-10-14 4 views
6

Я ищу что-то вроде HTML::TableExtract, просто не для ввода HTML, а для ввода в виде обычного текста, который содержит «таблицы», отформатированные с отступом и интервалом.Как я могу извлечь/проанализировать табличные данные из текстового файла в Perl?

данных может выглядеть следующим образом:

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

Просьба представить и пример. – DVK

+0

Я предоставил решение, но оно будет производить столбцы SIX. Вы делаете предположение, что разделитель столбцов taht ДОЛЖЕН быть> 1 пробелом? – DVK

+0

Нет, но мы можем предположить, что я знаю строки заголовка столбца и что данные столбца выровнены правильно под заголовками. – Thilo

ответ

1

Не известно ни стандартизированное решение, но что-то не очень гибок достаточно просто сделать, если вы можете сделать два прохода над файлом: (следующее частично Perlish псевдокод пример)

  • Предположение: данные могут содержать пробелы и не котируется ала CSV, если есть пространство - если это не так, просто используйте Text::CSV(_XS).
  • Предположения: для форматирования нет вкладок.
  • Логика определяет «разделитель столбцов» как любой последовательный набор вертикальных строк, заполненных на 100% пробелами.
  • Если случайно каждая строка имеет пространство, которое является частью данных со смещением M символов, логика будет считать смещение M разделителем столбцов, поскольку он не может знать ничего лучше. ЕДИНСТВЕННЫЙ способ, которым он может знать, лучше, если вам требуется разделение столбцов как минимум X пробелов, где X> 1 - см. Второй фрагмент кода для этого.

Пример кода:

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 пространства, где X> 1, это также выполнимо, но анализатор местоположений столбцов должен быть немного более сложный:

# 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

Вот очень быстрое решение, прокомментированное с обзором. (Мои извинения за длину.) В принципе, если после начала заголовка столбца n n появляется «слово», оно заканчивается в столбце n, если только большинство его тела не направляется в столбец n + 1, в этом случае он попадает туда вместо этого. Убирая это, расширяя его, чтобы поддерживать несколько разных таблиц и т. Д., Остаются в качестве упражнения. Вы также можете использовать что-то другое, кроме смещения слева от заголовка столбца, как граничную метку, такую ​​как центр, или некоторое значение, определенное номером столбца.

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

Пример вывода:

 
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. 

 Смежные вопросы

  • Нет связанных вопросов^_^