File Coverage

blib/lib/HTML/TableExtract.pm
Criterion Covered Total %
statement 514 677 75.9
branch 198 340 58.2
condition 59 109 54.1
subroutine 65 88 73.8
pod 16 16 100.0
total 852 1230 69.2


ignored after row $self->{rc}\n") as row $self->{rc}\n") ignored in row $self->{rc}\n")
line stmt bran cond sub pod time code
1             package HTML::TableExtract;
2              
3             # This package extracts tables from HTML. Tables of interest may be
4             # specified using header information, depth, order in a depth, table tag
5             # attributes, or some combination of the four. See the POD for more
6             # information.
7             #
8             # Author: Matthew P. Sisk. See the POD for copyright information.
9              
10 11     11   216279 use strict;
  11         28  
  11         273  
11 11     11   56 use Carp;
  11         22  
  11         545  
12              
13 11     11   59 use vars qw($VERSION @ISA);
  11         27  
  11         524  
14              
15             $VERSION = '2.15';
16              
17 11     11   5294 use HTML::Parser;
  11         62770  
  11         440  
18             @ISA = qw(HTML::Parser);
19              
20 11     11   84 use HTML::Entities;
  11         19  
  11         703  
21              
22             # trickery for subclassing from HTML::TreeBuilder rather than the
23             # default HTML::Parser. (use HTML::TableExtract qw(tree);) Also installs
24             # a mode constant TREE().
25              
26 11     11   351 BEGIN { *TREE = sub { 0 } }
  891     891   1788  
27              
28             sub import {
29 10     10   83 my $class = shift;
30 11     11   56 no warnings;
  11         19  
  11         21038  
31 10 50   27289   70 *TREE = @_ ? sub { 1 } : sub { 0 };
  0         0  
  31353         62959  
32 10 50       10695 return unless @_;
33 0         0 my $mode = shift;
34 0 0       0 croak "Unknown mode '$mode'\n" unless $mode eq 'tree';
35 0         0 eval "use HTML::TreeBuilder";
36 0 0       0 croak "Problem loading HTML::TreeBuilder : $@\n" if $@;
37 0         0 eval "use HTML::ElementTable 1.17";
38 0 0       0 croak "problem loading HTML::ElementTable : $@\n" if $@;
39 0         0 @ISA = qw(HTML::TreeBuilder);
40 0         0 $class;
41             }
42              
43             # Backwards compatibility for deprecated methods
44             *table_state = *table;
45             *table_states = *tables;
46             *first_table_state_found = *first_table_found;
47              
48             ###
49              
50             my %Defaults = (
51             headers => undef,
52             depth => undef,
53             count => undef,
54             attribs => undef,
55             subtables => undef,
56             gridmap => 1,
57             decode => 1,
58             automap => 1,
59             slice_columns => 1,
60             keep_headers => 0,
61             br_translate => 1,
62             error_handle => \*STDOUT,
63             debug => 0,
64             keep_html => 0,
65             strip_html_on_match => 1,
66             );
67             my $Dpat = join('|', sort keys %Defaults);
68              
69             ### Constructor
70              
71             sub new {
72 23     23 1 576753 my $that = shift;
73 23   33     152 my $class = ref($that) || $that;
74              
75 23         53 my(%pass, %parms, $k, $v);
76 23         168 while (($k,$v) = splice(@_, 0, 2)) {
77 31 100       653 if ($k eq 'headers') {
    50          
78 12 50       53 ref $v eq 'ARRAY'
79             or croak "Param '$k' must be passed in ref to array\n";
80 12         56 $parms{$k} = $v;
81             }
82             elsif ($k =~ /^$Dpat$/) {
83 19         114 $parms{$k} = $v;
84             }
85             else {
86 0         0 $pass{$k} = $v;
87             }
88             }
89              
90 23         207 my $self = $class->SUPER::new(%pass);
91 23         1650 bless $self, $class;
92 23         181 foreach (keys %parms, keys %Defaults) {
93             $self->{$_} = exists $parms{$_} && defined $parms{$_} ?
94 376 100 66     1210 $parms{$_} : $Defaults{$_};
95             }
96 23 100       131 if ($self->{headers}) {
97 0         0 $self->_emsg("TE here, headers: ", join(',', @{$self->{headers}}), "\n")
98 12 50       45 if $self->{debug};
99 12         31 $self->{gridmap} = 1;
100             }
101              
102             # Initialize counts and containers
103 23         92 $self->_reset_state;
104              
105 23         82 $self;
106             }
107              
108             ### HTML::Parser overrides
109              
110             sub start {
111 10650     10650 1 14521 my $self = shift;
112 10650         11661 my @res;
113              
114 10650 50       15210 @res = $self->SUPER::start(@_) if TREE();
115              
116             # Create a new table state if entering a table.
117 10650 100       24695 if ($_[0] eq 'table') {
    100          
118 111         343 my $ts = $self->_enter_table(@_);
119 111 50       278 $ts->tree($res[0]) if @res;
120             }
121             elsif ($self->{_in_a_table}) {
122             # Rows and cells are next.
123 10389         15994 my $ts = $self->current_table;
124 10389 100 66     26741 if ($_[0] eq 'tr') {
    100 33        
    50          
125 984         1613 $ts->_enter_row;
126             }
127             elsif ($_[0] eq 'td' || $_[0] eq 'th') {
128 9312         17535 $ts->_enter_cell(@_);
129 9312 50       14966 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         13623  
130 9312   100     24546 my $rspan = $attrs{rowspan} || 1;
131 9312   100     23741 my $cspan = $attrs{colspan} || 1;
132 9312         15018 $ts->_rasterizer->($ts->row_count, $rspan, $cspan);
133 9312         16316 $ts->_anchor_item(@res);
134             }
135             elsif (! TREE() && $ts->{in_cell}) {
136 93 50 66     276 if ($self->{keep_html}) {
    100          
137             # capture full text of tag
138 0         0 $self->text($_[3]);
139             }
140             elsif ($_[0] eq 'br' && $self->{br_translate}) {
141             # Replace
with newlines if requested
142 3         8 $self->text("\n");
143             }
144             }
145             }
146              
147 10650         42965 @res;
148             } # end start
149              
150             sub end {
151 10647     10647 1 14168 my $self = shift;
152 10647         11843 my @res;
153            
154 10647 50       15383 @res = $self->SUPER::end(@_) if TREE();
155              
156 10647 100       19584 if ($self->{_in_a_table}) {
157 10497         16087 my $ts = $self->current_table;
158 10497 100 66     26380 if ($_[0] eq 'td' || $_[0] eq 'th') {
    100          
    100          
    50          
159 9312         15247 $ts->_exit_cell;
160             }
161             elsif ($_[0] eq 'tr') {
162 984         1751 $ts->_exit_row;
163             }
164             elsif ($_[0] eq 'table') {
165 111         285 $self->_exit_table;
166             }
167             elsif (! TREE()) {
168 90 0 33     179 if ($self->{keep_html} && $ts->{in_cell}) {
169             # capture full text of tag
170 0         0 $self->text($_[1]);
171             }
172             }
173             }
174              
175 10647         33710 @res;
176             } # end end
177              
178             sub text {
179 9756     9756 1 13891 my $self = shift;
180 9756         10392 my @res;
181              
182 9756 50       14698 if (TREE()) {
    100          
183 0         0 @res = $self->SUPER::text(@_);
184             }
185             elsif ($self->{_in_a_table}) {
186 9449         15148 my $ts = $self->current_table;
187 9449 100       17325 if ($ts->{in_cell}) {
188 9322 50 33     29223 if ($self->{decode} && !$self->{keep_html}) {
189 9322         29938 $ts->_add_text(decode_entities($_[0]));
190             }
191             else {
192 0         0 $ts->_add_text($_[0]);
193             }
194             }
195             }
196              
197 9756         30512 @res;
198             } # end text
199              
200             sub parse {
201 333     333 1 6248 my $self = shift;
202 333 100       868 $self->_reset_state unless $self->{_parsing};
203 333   100     761 $self->{_parsing} ||= 1;
204 333         1927 $self->SUPER::parse(@_);
205             }
206              
207             sub eof {
208 23     23 1 789 my $self = shift;
209 23         56 $self->{_parsing} = 0;
210 23         183 $self->SUPER::eof(@_);
211             }
212              
213             ### End HTML::Parser overrides
214              
215             ### Report Methods
216              
217             sub depths {
218             # Return all depths where valid tables were located.
219 0     0 1 0 my $self = shift;
220 0 0       0 return () unless ref $self->{_tables};
221 0         0 sort { $a <=> $b } keys %{$self->{_tables}};
  0         0  
  0         0  
222             }
223              
224             sub counts {
225             # Given a depth, return the counts of all valid tables found therein.
226 0     0 1 0 my($self, $depth) = @_;
227 0 0       0 defined $depth or croak "Depth required\n";
228 0 0       0 return () unless exists $self->{_tables}{$depth};
229 0         0 sort { $a <=> $b } keys %{$self->{_tables}{$depth}};
  0         0  
  0         0  
230             }
231              
232             sub table {
233             # Return the table state for a particular depth and count
234 0     0 1 0 my($self, $depth, $count) = @_;
235 0 0       0 defined $depth or croak "Depth required\n";
236 0 0       0 defined $count or croak "Count required\n";
237 0 0 0     0 if (! $self->{_tables}{$depth} || ! $self->{_tables}{$depth}{$count}) {
238 0         0 return undef;
239             }
240 0         0 $self->{_tables}{$depth}{$count};
241             }
242              
243             sub first_table_found {
244 10     10 1 2037 my $self = shift;
245 10 50       57 ref $self->{_ts_sequential}[0] ? $self->{_ts_sequential}[0] : undef;
246             }
247              
248 0     0 1 0 sub rows { shift->first_table_found->rows(@_) }
249              
250             sub tables {
251             # Return all valid table records found, in the order that they
252             # were seen.
253 14     14 1 34 my $self = shift;
254 14         62 while ($self->{_in_a_table}) {
255 0         0 my $ts = $self->current_table;
256             $self->_emsg("Mangled HTML in table ($ts->{depth},$ts->{count}), inferring closing table tag.\n")
257 0 0       0 if $self->{debug};
258 0         0 $self->_exit_table;
259             }
260 14         30 @{$self->{_ts_sequential}};
  14         864  
261             }
262              
263             # in tree mode, we already are an HTML::TreeBuilder, which is an
264             # HTML::Element structure after parsing...but we provide this for
265             # consistency with the table object method for accessing the tree
266             # structures.
267              
268 0     0 1 0 sub tree { shift }
269              
270             sub tables_report {
271             # Print out a summary of extracted tables, including depth/count
272 0     0 1 0 my $self = shift;
273 0         0 my $str;
274 0         0 foreach my $ts ($self->tables) {
275 0         0 $str .= $ts->report(@_);
276             }
277 0         0 $str;
278             }
279              
280             sub tables_dump {
281 0     0 1 0 my $self = shift;
282 0         0 $self->_emsg($self->tables_report(@_));
283             }
284              
285             # for testing/debugging
286             sub _attribute_purge {
287 0     0   0 my $self = shift;
288 0         0 foreach (keys %Defaults) {
289 0         0 delete $self->{$_};
290             }
291             }
292              
293             ### Runtime
294              
295             sub _enter_table {
296 111     111   342 my($self, @args) = @_;
297              
298 111         174 ++$self->{_cdepth};
299 111         163 ++$self->{_in_a_table};
300              
301 111         173 my $depth = $self->{_cdepth};
302              
303             # Table tag attributes, if present
304 111   50     282 my $attribs = $args[1] || {};
305              
306             # Table states can come and go on the stack...here we retrieve the
307             # table state for the table surrounding the current table tag (parent
308             # table state). If the current table tag belongs to a top level table,
309             # then this will be undef.
310 111         228 my $pts = $self->current_table;
311              
312             # Counts are tracked for each depth.
313 111         183 my $counts = $self->{_counts};
314 111 100       286 $counts->[$depth] = -1 unless defined $counts->[$depth];
315 111         161 ++$counts->[$depth];
316 111         181 my $count = $counts->[$depth];
317              
318             $self->_emsg("TABLE: cdepth $depth, ccount $count, it: $self->{_in_a_table}\n")
319 111 50       283 if $self->{debug} >= 2;
320              
321             # Umbrella status means that this current table and all of its
322             # descendant tables will be harvested.
323 111         175 my $umbrella = 0;
324 111 100 66     671 if (! defined $self->{depth} && ! defined $self->{count} &&
      66        
      66        
325             ! $self->{attribs} && ! $self->{headers}) {
326 30         42 ++$umbrella;
327             }
328              
329             # Basic parameters for the soon-to-be-created table state.
330             my %tsparms = (
331             depth => $depth,
332             count => $count,
333             attribs => $attribs,
334             umbrella => $umbrella,
335             automap => $self->{automap},
336             slice_columns => $self->{slice_columns},
337             keep_headers => $self->{keep_headers},
338             counts => $counts,
339             error_handle => $self->{error_handle},
340             debug => $self->{debug},
341             keep_html => $self->{keep_html},
342             strip_html_on_match => $self->{strip_html_on_match},
343 111         862 parent_table => $pts,
344             );
345              
346             # Target constraints. There is no point in passing any of these along
347             # if we are under an umbrella. Notice that with table states, "depth"
348             # and "count" are absolute coordinates recording where this table was
349             # created, whereas "tdepth" and "tcount" are the target constraints.
350             # Headers have "absolute" meaning, therefore are passed by the
351             # same name.
352 111 100       264 if (!$umbrella) {
353 81         159 $tsparms{tdepth} = $self->{depth};
354 81         151 $tsparms{tcount} = $self->{count};
355 81         157 $tsparms{tattribs} = $self->{attribs};
356 81         154 $tsparms{headers} = $self->{headers};
357             }
358              
359             # Abracadabra
360 111         643 my $ts = HTML::TableExtract::Table->new(%tsparms);
361              
362             # Push the newly created and configured table state onto the stack.
363             # This will now be the current_table().
364 111         213 push(@{$self->{_tablestack}}, $ts);
  111         206  
365              
366 111         408 $ts;
367             }
368              
369             sub _exit_table {
370 111     111   171 my $self = shift;
371 111         217 my $ts = $self->current_table;
372              
373             # Last ditch fix for HTML mangle
374 111 50       306 if ($ts->{in_cell}) {
375 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), forcing exit of cell ($ts->{rc},$ts->{cc}) due to table exit\n") if $self->{debug};
376 0         0 $ts->_exit_cell;
377             }
378 111 50       278 if ($ts->{in_row}) {
379 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), forcing exit of row $ts->{rc} due to table exit\n") if $self->{debug};
380 0         0 $ts->_exit_row;
381             }
382              
383             # transform from tree to grid using our rasterized template
384 111         323 $ts->_grid_map();
385              
386 111 100       294 $self->_capture_table($ts) if $ts->_check_triggers;
387              
388             # Restore last table state
389 111         157 pop(@{$self->{_tablestack}});
  111         194  
390 111         173 --$self->{_in_a_table};
391 111         276 my $lts = $self->current_table;
392 111 100       259 if (ref $lts) {
393 54         115 $self->{_cdepth} = $lts->{depth};
394             }
395             else {
396             # Back to the top level
397 57         107 $self->{_cdepth} = -1;
398             }
399             $self->_emsg("LEAVE: cdepth: $self->{_cdepth}, ccount: $ts->{count}, it: $self->{_in_a_table}\n")
400 111 50       1177 if $self->{debug} >= 2;
401             }
402              
403             sub _capture_table {
404 89     89   188 my($self, $ts, $type) = @_;
405 89 50       218 croak "Table state ref required\n" unless ref $ts;
406 89 50       224 if ($self->{debug} >= 2) {
407 0         0 my $msg = "Captured table (" . $ts->depth . ',' . $ts->count . ")";
408 0 0       0 $msg .= " ($type)" if $type;
409 0         0 $msg .= "\n";
410 0         0 $self->_emsg($msg);
411             }
412 89 50       179 $ts->tree(HTML::ElementTable->new_from_tree($ts->tree)) if TREE();
413 89 100       230 if ($self->{subtables}) {
414 11         14 foreach my $child (@{$ts->{children}}) {
  11         24  
415 8 100       20 next if $child->{captured};
416 6         17 $self->_capture_table($child, 'subtable');
417 6         8 $child->{slice_columns} = 0;
418 6         10 $child->{keep_headers} = 1;
419 6         10 $child->{headers} = '';
420             }
421             }
422 89         155 $ts->{captured} = 1;
423 89         338 $self->{_tables}{$ts->{depth}}{$ts->{count}} = $ts;
424 89         127 push(@{$self->{_ts_sequential}}, $ts);
  89         203  
425             }
426              
427             sub current_table {
428 30668     30668 1 34528 my $self = shift;
429 30668         34960 $self->{_tablestack}[$#{$self->{_tablestack}}];
  30668         48383  
430             }
431              
432             sub _reset_state {
433 46     46   83 my $self = shift;
434 46         91 $self->{_cdepth} = -1;
435 46         104 $self->{_tablestack} = [];
436 46         110 $self->{_tables} = {};
437 46         96 $self->{_ts_sequential} = [];
438 46         93 $self->{_counts} = [];
439 46         84 $self->{_in_a_table} = 0;
440 46         86 $self->{_parsing} = 0;
441 46 50       110 $self->tree->delete_content() if TREE();
442             }
443              
444             sub _emsg {
445 0     0   0 my $self = shift;
446 0         0 my $fh = $self->{error_handle};
447 0 0       0 return unless defined $_[0];
448 0         0 print $fh @_;
449             }
450              
451             ##########
452              
453             {
454              
455             package HTML::TableExtract::Table;
456              
457 11     11   85 use strict;
  11         20  
  11         207  
458 11     11   45 use Carp;
  11         23  
  11         39909  
459              
460             *TREE = *HTML::TableExtract::TREE;
461              
462             sub new {
463 111     111   194 my $that = shift;
464 111   33     439 my $class = ref($that) || $that;
465             # Note:
466             # - 'depth' and 'count' are where this table were found.
467             # - 'tdepth' and 'tcount' are target constraints on which to trigger.
468             # - 'headers' represent a target constraint, location independent.
469             # - 'attribs' represent target table tag constraints
470 111         916 my $self = {
471             umbrella => 0,
472             in_row => 0,
473             in_cell => 0,
474             rc => -1,
475             cc => -1,
476             grid => [],
477             translation => [],
478             hrow => [],
479             order => [],
480             children => [],
481             captured => 0,
482             debug => 0,
483             };
484              
485 111         432 $self->{_rastamon} = HTML::TableExtract::Rasterize->make_rasterizer();
486 111         259 bless $self, $class;
487              
488 111         547 my %parms = @_;
489              
490             # Depth and Count -- this is the absolute address of the table.
491 111 50       290 croak "Absolute depth required\n" unless defined $parms{depth};
492 111 50       262 croak "Count required\n" unless defined $parms{count};
493 111 50       242 croak "Counts required\n" unless defined $parms{counts};
494              
495 111         418 foreach (keys %parms) {
496 1767         2567 $self->{$_} = $parms{$_};
497             }
498              
499             # Register lineage
500 111         228 my $pts = $self->{parent_table};
501 111   100     529 $self->lineage($pts || undef);
502 111 100       238 push(@{$pts->{children}}, $self) if ($pts);
  54         104  
503 111         197 delete $self->{parent_table};
504              
505 111         360 $self;
506             }
507              
508             sub _anchor_item {
509             # anchor the reference to a cell in our grid -- in TREE mode this is
510             # a reference to a data element, otherwise it's a reference to an
511             # empty scalar in which we will collect our text.
512 9312     9312   13750 my($self, @res) = @_;
513 9312         12446 my $row = $self->{grid}[-1];
514 9312         10513 my $item;
515 9312 50 33     20315 if (@res && ref $res[0]) {
516 0         0 $item = $res[0];
517             }
518             else {
519 9312         10199 my $scalar_ref;
520 9312         11403 $item = \$scalar_ref;
521             }
522 9312         17816 push(@$row, $item);
523             }
524              
525             sub _gridalias {
526 3     3   12453 my $self = shift;
527 3   66     17 $self->{gridalias} ||= $self->_make_gridalias;
528             }
529              
530             sub _grid_map {
531             # using our rasterized template, flesh out our captured items which
532             # are still in 'tree' format
533 111     111   184 my $self = shift;
534 111         226 my $template = $self->_rasterizer->();
535 111         208 my $grid = $self->{grid};
536             # drop empty rows
537 111 50       274 if ($self->{debug}) {
538 0         0 foreach (0 .. $#$grid) {
539 0 0       0 next if @{$grid->[$_]};
  0         0  
540 0         0 $self->_emsg("Dropping empty row $_\n");
541             }
542             }
543 111         625 @$grid = grep(@$_, @$grid);
544 111         300 foreach my $r (0 .. $#$template) {
545 984         1201 my $row = $grid->[$r];
546 984         1172 my $trow = $template->[$r];
547 984 50       1713 $self->_emsg("Flesh row $r ($#$row) to $#$trow\n") if $self->{debug} > 1;
548 984         1452 foreach my $c (0 .. $#$trow) {
549 9342 0       14979 print STDERR $trow->[$c] ? '1' : '0' if $self->{debug} > 1;
    50          
550 9342 100       13265 if ($trow->[$c]) {
551 9312 50       14852 if (! defined $row->[$c]) {
552 0         0 $row->[$c] = \undef;
553             }
554 9312         10583 next;
555             }
556             else {
557 30         34 my $scalar;
558 30         46 splice(@$row, $c, 0, \$scalar);
559             }
560             }
561 984 50       1750 print STDERR "\n" if $self->{debug} > 1;
562 984 50       2019 croak "row $r splice mismatch: $#$row vs $#$trow\n"
563             unless $#$row == $#$trow;
564             }
565 111         180 $grid;
566             }
567              
568             sub _make_gridalias {
569             # our aliased grid will have references in masked cells to the same
570             # cell that is covering it via spanning.
571 2     2   6 my $self = shift;
572 2         4 my $grid = $self->{grid};
573 2         6 my $template = $self->_rasterizer->();
574 2         5 my(@gridalias, @translation);
575 2         8 $gridalias[$_] = [@{$grid->[$_]}] foreach 0 .. $#$grid;
  14         39  
576 2         7 foreach my $r (0 .. $#gridalias) {
577 14         20 my $row = $gridalias[$r];
578 14         25 foreach my $c (0 .. $#$row) {
579 56   100     104 my $tcell = $template->[$r][$c] || next;
580 36         60 my($rspan, $cspan) = @$tcell;
581 36         52 foreach my $rs (0 .. $rspan-1) {
582 42         54 foreach my $cs (0 .. $cspan-1) {
583 56         95 $gridalias[$r + $rs][$c + $cs] = $grid->[$r][$c];
584 56         146 $translation[$r + $rs][$c + $cs] = "$r,$c";
585             }
586             }
587             }
588             }
589 2         6 $self->{translation} = \@translation;
590 2         16 $self->{gridalias} = \@gridalias;
591             }
592              
593             ### Constraint tests
594              
595             sub _check_dtrigger {
596             # depth
597 81     81   127 my $self = shift;
598 81 100       392 return 1 unless defined $self->{tdepth};
599 15 100       82 $self->{tdepth} == $self->{depth} ? 1 : 0;
600             }
601              
602             sub _check_ctrigger {
603             # count
604 74     74   158 my $self = shift;
605 74 100       414 return 1 unless defined $self->{tcount};
606             return 1 if (exists $self->{counts}[$self->{depth}] &&
607 11 100 66     88 $self->{tcount} == $self->{counts}[$self->{depth}]);
608 7         38 return 0;
609             }
610              
611             sub _check_atrigger {
612             # attributes
613 67     67   126 my $self = shift;
614 67 100       111 return 1 unless scalar keys %{$self->{tattribs}};
  67         466  
615 15 50       23 return 0 unless scalar keys %{$self->{attribs}};
  15         57  
616 15         21 my $a_hit = 1;
617 15         22 foreach my $attrib (keys %{$self->{tattribs}}) {
  15         44  
618 15 100       49 if (! defined $self->{attribs}{$attrib}) {
619 8         14 $a_hit = 0; last;
  8         17  
620             }
621 7 100       21 if (! defined $self->{tattribs}{$attrib}) {
622             # undefined, but existing, target attribs are wildcards
623 1         3 next;
624             }
625 6 100       17 if ($self->{tattribs}{$attrib} ne $self->{attribs}{$attrib}) {
626 2         4 $a_hit = 0; last;
  2         5  
627             }
628             }
629 15 50 33     56 $self->_emsg("Matched attributes\n") if $self->{debug} > 3 && $a_hit;
630 15         85 $a_hit;
631             }
632              
633             sub _check_htrigger {
634             # headers
635 57     57   113 my $self = shift;
636 57 50       142 return 1 if $self->{umbrella};
637 57 100       233 return 1 unless $self->{headers};
638 46         81 ROW: foreach my $r (0 .. $#{$self->{grid}}) {
  46         107  
639 70         179 $self->_reset_hits;
640 70         149 my $hpat = $self->_header_pattern;
641 70         104 my @hits;
642 70         103 foreach my $c (0 .. $#{$self->{grid}[$r]}) {
  70         182  
643 631         899 my $ref = $self->{grid}[$r][$c];
644 631         741 my $target = '';
645 631         830 my $ref_type = ref $ref;
646 631 50       1076 if ($ref_type) {
647 631 50       970 if ($ref_type eq 'SCALAR') {
648 631         755 my $item = $$ref;
649 631 50 33     1440 if ($self->{keep_html} && $self->{strip_html_on_match}) {
650 0         0 my $stripper = HTML::TableExtract::StripHTML->new;
651 0         0 $target = $stripper->strip($item);
652             }
653             else {
654 631         800 $target = $item;
655             }
656             }
657             else {
658 0 0 0     0 if (($self->{keep_html} || TREE()) &&
      0        
659             $self->{strip_html_on_match}) {
660 0         0 $target = $ref->as_text;
661             }
662             else {
663 0         0 $target = $ref->as_HTML;
664             }
665             }
666             }
667 631 50       921 $target = defined $target ? $target : '';
668             $self->_emsg("attempt match on $target ($hpat): ")
669 631 50       1117 if $self->{debug} >= 5;
670 631 100       2962 if ($target =~ $hpat) {
    50          
671 180         397 my $hit = $1;
672 180 50       346 $self->_emsg("($hit)\n") if $self->{debug} >= 5;
673             # Get rid of the header segment that matched so we can tell
674             # when we're through with all header patterns.
675 180         221 my $real_hit;
676 180         226 foreach (sort _header_string_sort keys %{$self->{hits_left}}) {
  180         540  
677 228 100       1365 if ($hit =~ /$_/im) {
678 180         359 delete $self->{hits_left}{$_};
679 180         243 $real_hit = $_;
680 180         310 $hpat = $self->_header_pattern;
681 180         341 last;
682             }
683             }
684 180 50       393 if (defined $real_hit) {
685 180 50       359 if ($self->{debug} >= 4) {
686 0 0       0 my $str = $ref_type eq 'SCALAR' ? $$ref : $ref->as_HTML;
687 0         0 $self->_emsg("HIT on '$hit' ($real_hit) in $str ($r,$c)\n");
688             }
689 180         295 push(@hits, $hit);
690             #
691 180         359 $self->{hits}{$c} = $real_hit;
692 180         206 push(@{$self->{order}}, $c);
  180         326  
693 180 100       207 if (!%{$self->{hits_left}}) {
  180         685  
694             # Successful header row match
695 42         177 ++$self->{head_found};
696 42         69 $self->{hrow_index} = $r;
697 42         78 $self->{hrow} = $self->{grid}[$r];
698 42         159 last ROW;
699             }
700             }
701             }
702             elsif ($self->{debug} >= 5) {
703 0         0 $self->_emsg("0\n");
704             }
705             }
706 28 50 33     84 if ($self->{debug} && @hits) {
707 0         0 my $str = "Incomplete header match ";
708 0         0 $str .= "(left: " . join(', ', sort keys %{$self->{hits_left}}) . ") ";
  0         0  
709 0         0 $str .= "in row $r, resetting scan";
710 0         0 $str .= "\n";
711 0         0 $self->_emsg($str);
712             }
713             }
714 46         269 $self->{head_found};
715             }
716              
717             sub _check_triggers {
718 111     111   165 my $self = shift;
719 111 100       321 return 1 if $self->{umbrella};
720 81 100 100     239 $self->_check_dtrigger &&
      100        
721             $self->_check_ctrigger &&
722             $self->_check_atrigger &&
723             $self->_check_htrigger;
724             }
725              
726             ### Maintain table context
727              
728             sub _enter_row {
729 984     984   1171 my $self = shift;
730 984 50       1827 if ($self->{in_row}) {
731 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), forcing exit of row $self->{rc} due to new row\n") if $self->{debug};
732 0         0 $self->_exit_row;
733             }
734 984         1217 ++$self->{rc};
735 984         1146 ++$self->{in_row};
736 984         1117 push(@{$self->{grid}}, [])
  984         1991  
737             }
738              
739             sub _exit_row {
740 984     984   1157 my $self = shift;
741 984 50       1620 if ($self->{in_row}) {
742 984 50       1855 if ($self->{in_cell}) {
743 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), forcing exit of cell ($self->{rc}, $self->{cc}) due to new row\n") if $self->{debug};
744 0         0 $self->_exit_cell;
745             }
746 984         1279 $self->{in_row} = 0;
747 984         1379 $self->{cc} = -1;
748             }
749             else {
750             $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), extraneous
751 0 0       0 if $self->{debug};
752             }
753             }
754              
755             sub _enter_cell {
756 9312     9312   10667 my $self = shift;
757 9312 50       16171 if ($self->{in_cell}) {
758 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), forcing exit of cell ($self->{rc},$self->{cc}) due to new cell\n") if $self->{debug};
759 0         0 $self->_exit_cell;
760             }
761 9312 50       15812 if (!$self->{in_row}) {
762             # Go ahead and try to recover from mangled HTML, because we care.
763             $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), inferring
764 0 0       0 if $self->{debug};
765 0         0 $self->_enter_row;
766             }
767 9312         10672 ++$self->{cc};
768 9312         10643 ++$self->{in_cell};
769 9312 50       15756 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         19716  
770 9312   100     28170 my $rspan = $attrs{rowspan} || 1;
771 9312   100     26168 my $cspan = $attrs{colspan} || 1;
772             }
773              
774             sub _exit_cell {
775 9312     9312   10541 my $self = shift;
776 9312 50       14581 if ($self->{in_cell}) {
777 9312         13147 $self->{in_cell} = 0;
778             }
779             else {
780             $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), extraneous
781 0 0       0 if $self->{debug};
782             }
783             }
784              
785             # Header stuff
786              
787             sub _header_pattern {
788 250     250   437 my($self, @headers) = @_;
789             my $str = join('|',
790             map("($_)",
791 250         353 sort _header_string_sort keys %{$self->{hits_left}}
  250         890  
792             ));
793 250         2642 my $hpat = qr/($str)/im;
794 250 50       705 $self->_emsg("HPAT: /$hpat/\n") if $self->{debug} >= 2;
795 250         510 $self->{hpat} = $hpat;
796             }
797              
798             sub _header_string_sort {
799             # this ensures that supersets appear before subsets in our header
800             # search pattern, eg, '10' appears before '1' and 'hubbabubba'
801             # appears before 'hubba'.
802 1093 50   1093   9073 if ($a =~ /^$b/) {
    50          
803 0         0 return -1;
804             }
805             elsif ($b =~ /^$a/) {
806 0         0 return 1;
807             }
808             else {
809 1093         2620 return $b cmp $a;
810             }
811             }
812              
813             # Report methods
814              
815 54     54   177 sub depth { shift->{depth} }
816 54     54   137 sub count { shift->{count} }
817             sub coords {
818 0     0   0 my $self = shift;
819 0         0 ($self->depth, $self->count);
820             }
821              
822 9312     9312   16699 sub row_count { shift->{rc} }
823 0     0   0 sub col_count { shift->{cc} }
824              
825             sub tree {
826 0     0   0 my $self = shift;
827 0 0       0 @_ ? $self->{_tree_ref} = shift : $self->{_tree_ref};
828             }
829              
830             sub lineage {
831 165     165   229 my $self = shift;
832 165   100     621 $self->{lineage} ||= [];
833 165 100       383 if (@_) {
834 111         162 my $pts = shift;
835 111         173 my(@lineage, $pcoords);
836 111 100       235 if ($pts) {
837 54         143 foreach my $pcoord ($pts->lineage) {
838 20         57 push(@lineage, [@$pcoord]);
839             }
840 54         172 $pcoords = [$pts->depth, $pts->count, $pts->{rc}, $pts->{cc}];
841 54         104 push(@lineage, $pcoords);
842             }
843 111         228 $self->{lineage} = \@lineage;
844             }
845 165         216 @{$self->{lineage}};
  165         299  
846             }
847              
848 36     36   47855 sub rows { shift->_rows(0) }
849              
850             sub space_rows {
851 0     0   0 my $self = shift;
852 0         0 $self->_rows(1);
853             }
854              
855             sub _rows {
856 36     36   63 my $self = shift;
857 36         51 my $alias = shift;
858 36         99 my @ri = $self->row_indices;
859 36         58 my @rows;
860 36 50       88 my $grid = $alias ? $self->_gridalias : $self->{grid};
861 36         71 foreach ($self->row_indices) {
862 218         426 push(@rows, scalar $self->_slice_and_normalize_row($grid->[$_]));
863             }
864 36 50       225 wantarray ? @rows : \@rows;
865             }
866              
867             sub columns {
868 0     0   0 my $self = shift;
869 0         0 my @cols;
870 0         0 my @rows = $self->rows;
871 0         0 foreach my $row (@rows) {
872 0         0 foreach my $c (0 .. $#$row) {
873 0   0     0 $cols[$c] ||= [];
874 0         0 push(@{$cols[$c]}, $row->[$c]);
  0         0  
875             }
876             }
877 0         0 @cols;
878             }
879              
880             sub row_indices {
881 78     78   96 my $self = shift;
882 78         96 my $start_index = 0;
883 78 100       191 if ($self->{headers}) {
884 38         80 $start_index = $self->hrow_index;
885 38 100       95 $start_index += 1 unless $self->{keep_headers};
886             }
887 78         126 $start_index .. $#{$self->{grid}};
  78         282  
888             }
889              
890             sub col_indices {
891 0     0   0 my $self = shift;
892 0         0 my $row = $self->{grid}[0];
893 0         0 0 .. $#$row;
894             }
895              
896             sub row {
897 6     6   23 my $self = shift;
898 6         8 my $r = shift;
899 6 50       9 $r <= $#{$self->{grid}}
  6         19  
900 0         0 or croak "row $r out of range ($#{$self->{grid}})\n";
901             my @row = $self->_slice_and_normalize_row(
902 6         16 $self->{grid}[($self->row_indices)[$r]]
903             );
904 6 50       19 wantarray ? @row : \@row;
905             }
906              
907             sub _slice_and_normalize_row {
908 224     224   309 my $self = shift;
909 224         271 my $rowref = shift;
910 224         254 my @row;
911 224 100 66     619 if ($self->{automap} && $self->_map_makes_a_difference) {
912 128         229 @row = @{$rowref}[$self->column_map];
  128         359  
913             }
914             else {
915 96         235 @row = @$rowref;
916             }
917 224         506 @row = map($self->_cell_to_content($_), @row);
918 224 100       563 wantarray ? @row : \@row;
919             }
920              
921             sub column {
922 0     0   0 my $self = shift;
923 0         0 my $c = shift;
924 0         0 my @column;
925 0         0 foreach my $row ($self->rows) {
926 0         0 push(@column, $self->cell($row, $c));
927             }
928 0 0       0 wantarray ? @column : \@column;
929             }
930              
931             sub cell {
932 4     4   24 my $self = shift;
933 4         8 my($r, $c) = @_;
934 4         9 my $row = $self->row($r);
935 4 50       11 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
936 4         9 $self->_cell_to_content($row->[$c]);
937             }
938              
939             sub _cell_to_content {
940 877     877   1006 my $self = shift;
941 877 50       1484 @_ or croak "cell item required\n";
942 877         948 my $cell = shift;
943 877 100       1512 return $cell unless ref $cell;
944 873 50       1166 return $cell if TREE();
945 873         1765 return $$cell;
946             }
947              
948             sub space {
949 2     2   13 my $self = shift;
950 2         7 my($r, $c) = @_;
951 2         6 my $gridalias = $self->_gridalias;
952 2 50       8 $r <= $#$gridalias
953             or croak "row $r out of range ($#$gridalias)\n";
954 2         6 my $row = $gridalias->[$r];
955 2 50       9 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
956 2         8 $self->_cell_to_content($row->[$c]);
957             }
958              
959             sub source_coords {
960 28     28   41280 my $self = shift;
961 28         56 my($r, $c) = @_;
962 28 50       38 $r <= $#{$self->{translation}}
  28         85  
963 0         0 or croak "row $r out of range ($#{$self->{translation}})\n";
964 28         47 my $row = $self->{translation}[$r];
965 28 50       62 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
966 28         117 split(/,/, $self->{translation}[$r][$c]);
967             }
968              
969             sub hrow_index {
970 41     41   1404 my $self = shift;
971 41         70 $self->{hrow_index};
972             }
973              
974             sub hrow {
975 3     3   14 my $self = shift;
976 3 50 33     12 if ($self->{automap} && $self->_map_makes_a_difference) {
977 3 50       9 return map(ref $_ ? $$_ : $_, @{$self->{hrow}}[$self->column_map]);
  3         34  
978             }
979             else {
980 0 0       0 return map(ref $_ ? $$_ : $_, @{$self->{hrow}});
  0         0  
981             }
982             }
983              
984             sub column_map {
985             # Return the column numbers of this table in the same order as the
986             # provided headers.
987 419     419   64598 my $self = shift;
988 419 100       727 if ($self->{headers}) {
989             # First we order the original column counts by taking a hash slice
990             # based on the original header order. The resulting original
991             # column numbers are mapped to the actual content indices since
992             # we could have a sparse slice.
993 344         436 my %order;
994 344         379 foreach (keys %{$self->{hits}}) {
  344         736  
995 1026         1493 $order{$self->{hits}{$_}} = $_;
996             }
997 344         496 return @order{@{$self->{headers}}};
  344         1007  
998             }
999             else {
1000 75         94 return 0 .. $#{$self->{grid}[0]};
  75         179  
1001             }
1002             }
1003              
1004             sub _map_makes_a_difference {
1005 227     227   279 my $self = shift;
1006 227 100       443 return 0 unless $self->{slice_columns};
1007 225         268 my $diff = 0;
1008 225         364 my @order = $self->column_map;
1009 225         590 my @sorder = sort { $a <=> $b } @order;
  1022         1368  
1010 225 50       458 ++$diff if $#order != $#sorder;
1011 225 100       278 ++$diff if $#sorder != $#{$self->{grid}[0]};
  225         480  
1012 225         398 foreach (0 .. $#order) {
1013 597 100       1117 if ($order[$_] != $sorder[$_]) {
1014 131         153 ++$diff;
1015 131         189 last;
1016             }
1017             }
1018 225         708 $diff;
1019             }
1020              
1021             sub _add_text {
1022 9322     9322   14843 my($self, $txt) = @_;
1023 9322         11595 my $r = $self->{rc};
1024 9322         11017 my $c = $self->{cc};
1025 9322         11257 my $row = $self->{grid}[$r];
1026 9322         9943 ${$row->[$c]} .= $txt;
  9322         16634  
1027 9322         13951 $txt;
1028             }
1029              
1030             sub _reset_hits {
1031 70     70   103 my $self = shift;
1032 70 50       165 return unless $self->{headers};
1033 70         155 $self->{hits} = {};
1034 70         143 $self->{order} = [];
1035 70         116 foreach (@{$self->{headers}}) {
  70         163  
1036 254         461 ++$self->{hits_left}{$_};
1037             }
1038 70         122 1;
1039             }
1040              
1041 9425     9425   17386 sub _rasterizer { shift->{_rastamon} }
1042              
1043             sub report {
1044             # Print out a summary of this table, including depth/count
1045 0     0   0 my($self, $include_content, $col_sep) = @_;
1046 0   0     0 $col_sep ||= ':';
1047 0         0 my $str;
1048 0         0 $str .= "TABLE(" . $self->depth . ", " . $self->count . ')';
1049 0 0       0 if ($include_content) {
1050 0         0 $str .= ":\n";
1051 0         0 foreach my $row ($self->rows) {
1052 0         0 $str .= join($col_sep, @$row) . "\n";
1053             }
1054             }
1055             else {
1056 0         0 $str .= "\n";
1057             }
1058 0         0 $str;
1059             }
1060              
1061             sub dump {
1062 0     0   0 my $self = shift;
1063 0         0 $self->_emsg($self->report(@_));
1064             }
1065              
1066             sub _emsg {
1067 0     0   0 my $self = shift;
1068 0         0 my $fh = $self->{error_handle};
1069 0         0 print $fh @_;
1070             }
1071              
1072             }
1073              
1074             ##########
1075              
1076             {
1077              
1078             package HTML::TableExtract::Rasterize;
1079              
1080             # Provide a closure that will rasterize (turn into a grid) a table
1081             # from a tree structure based on repeated data element calls with
1082             # rowspan and colspan information. Not as straight forward as it
1083             # seems...see test cases for an example bugaboo.
1084              
1085             my $DEBUG = 0;
1086              
1087             sub make_rasterizer {
1088 111     111   171 my $pkg = shift;
1089 111         206 my(@grid, @row_spinner, @col_spinner);
1090 111         145 my $empty_row_offset = 0;
1091             sub {
1092 9425 100   9425   16684 return \@grid unless @_;
1093 9312         13305 my($row_num, $rspan, $cspan) = @_;
1094 9312 100       17058 $rspan = 1 unless $rspan > 1;
1095 9312 100       16008 $cspan = 1 unless $cspan > 1;
1096 9312         11317 my($rspin_propogate, $row_added);
1097 9312         11903 my $trigger = $#grid + $empty_row_offset;
1098 9312 100       16904 if ($row_num > $trigger) {
1099             # adjust for having been handed a row that skips a prior row,
1100             # otherwise the next cell will land in a wrong row. Hopefully
1101             # this doesn't happen too often but I've seen it in the wild!
1102 984 50       1956 if ($row_num - $trigger > 1) {
1103 0         0 $empty_row_offset += $row_num - $trigger - 1;
1104             }
1105             # add new row
1106 984         1119 $row_added = 1;
1107 984         1144 my @new_row;
1108             # first add new row spinner
1109 984 50 33     2182 if ($row_spinner[-1] && $col_spinner[-1]) {
1110 0         0 push(@row_spinner, $row_spinner[-1]);
1111 0         0 $rspin_propogate = 1;
1112             }
1113             else {
1114 984         1588 push(@row_spinner, $cspan - 1);
1115             }
1116             # spin columns
1117 984         1557 foreach (@col_spinner) {
1118 8382 100       11247 if ($_) {
1119 15         20 push(@new_row, 0);
1120 15         26 --$_;
1121             }
1122             else {
1123 8367         10961 push(@new_row, undef);
1124             }
1125             }
1126 984 100       2009 @new_row = (undef) unless @new_row;
1127 984         1847 push(@grid, \@new_row);
1128             }
1129 9312         11097 my $current_row = $grid[-1];
1130             # locate next available cell in row
1131 9312         9999 my $col;
1132 9312         16781 foreach my $ci (0 .. $#$current_row) {
1133 49821 100       84920 if (! defined $current_row->[$ci]) {
1134 8463         9416 $col = $ci;
1135 8463         10565 last;
1136             }
1137             }
1138 9312 100       16288 if (! defined $col) {
1139 849         1545 ADDCOL: while (! defined $col) {
1140             # if no cells were available, add a column
1141 849         1293 foreach my $ri (0 .. $#grid) {
1142 849         1095 my $row = $grid[$ri];
1143 849         993 my $cspan_count = $row_spinner[$ri];
1144 849 50       1312 if (!$cspan_count) {
1145 849         1580 push(@$row, undef);
1146             }
1147             else {
1148 0         0 push(@$row, 0);
1149 0         0 --$row_spinner[$ri];
1150             }
1151             }
1152 849         1227 push(@col_spinner, $col_spinner[-1]);
1153 849         1314 foreach my $ci (0 .. $#$current_row) {
1154 4989 100       8711 if (! defined $current_row->[$ci]) {
1155 849         960 $col = $ci;
1156 849         1248 last ADDCOL;
1157             }
1158             }
1159             }
1160 849 50       1849 $col_spinner[-1] = $rspan - 1 if $col == $#$current_row;
1161 849         1221 $row_spinner[$#grid] = $cspan - 1;
1162             }
1163              
1164             # we now have correct coordinates for this element
1165 9312         17658 $current_row->[$col] = [$rspan, $cspan];
1166 9312         12643 $col_spinner[$col] = $rspan - 1;
1167              
1168             # if this is an embedded placement (not a trailing element), use up
1169             # the cspan
1170 9312 100       17042 if ($col < $#$current_row) {
1171 7482         8713 my $offset = 1;
1172 7482         8653 my $row_span = $col_spinner[$col];
1173 7482 100 100     28550 if ($col + $row_spinner[-1] < $#$current_row &&
      66        
1174             $row_added && !$rspin_propogate) {
1175             # cell is spun out -- clear spinner unless it inherited cspan
1176             # from a cell above
1177 870         1103 $row_spinner[-1] = 0;
1178             }
1179 7482         15149 while ($offset < $cspan) {
1180 15         24 my $cursor = $col + $offset;
1181 15         20 $current_row->[$cursor] = 0;
1182 15         18 $col_spinner[$cursor] = $row_span;
1183 15         17 ++$offset;
1184 15 100       49 if ($col + $offset > $#$current_row) {
1185 3         6 $row_spinner[-1] = $cspan - $offset;
1186 3         6 last;
1187             }
1188             }
1189             }
1190              
1191 9312 50       16238 if ($DEBUG) {
1192 0         0 foreach my $r (0 .. $#grid) {
1193 0         0 my $row = $grid[$r];
1194 0         0 foreach my $c (0 .. $#$row) {
1195 0 0       0 if (defined $row->[$c]) {
1196 0 0       0 print STDERR $row->[$c] ? 1 : 0;
1197             }
1198             else {
1199 0         0 print STDERR '?';
1200             }
1201             }
1202 0         0 print STDERR " $row_spinner[$r]\n";
1203             }
1204 0         0 print STDERR "\n";
1205 0         0 foreach (@col_spinner) {
1206 0 0       0 print STDERR defined $_ ? $_ : '?';
1207             }
1208 0         0 print STDERR "\n\n-----\n\n";
1209             }
1210              
1211 9312         13017 return \@grid;
1212             }
1213 111         1073 }
1214              
1215             }
1216              
1217             ##########
1218              
1219             {
1220              
1221             package HTML::TableExtract::StripHTML;
1222              
1223 11     11   91 use vars qw(@ISA);
  11         17  
  11         564  
1224              
1225 11     11   59 use HTML::Parser;
  11         15  
  11         1855  
1226             @ISA = qw(HTML::Parser);
1227              
1228             sub tag {
1229 0     0     my($self, $tag, $num) = @_;
1230 0           $self->{_htes_inside}{$tag} += $num;
1231             }
1232              
1233             sub text {
1234 0     0     my $self = shift;
1235 0 0 0       return if $self->{_htes_inside}{script} || $self->{_htes_inside}{style};
1236 0           $self->{_htes_tidbit} .= $_[0];
1237             }
1238              
1239             sub new {
1240 0     0     my $class = shift;
1241 0           my $self = HTML::Parser->new(
1242             api_version => 3,
1243             handlers => [start => [\&tag, "self, tagname, '+1'"],
1244             end => [\&tag, "self, tagname, '-1'"],
1245             text => [\&text, "self, dtext"],
1246             ],
1247             marked_sections => 1,
1248             );
1249 0           bless $self, $class;
1250             }
1251              
1252             sub strip {
1253 0     0     my $self = shift;
1254 0           $self->parse(shift);
1255 0           $self->eof;
1256 0           $self->{_htes_tidbit};
1257             }
1258              
1259             }
1260              
1261             1;
1262              
1263             __END__