File Coverage

blib/lib/HTML/TableExtract.pm
Criterion Covered Total %
statement 513 676 75.8
branch 198 338 58.5
condition 59 109 54.1
subroutine 65 88 73.8
pod 16 16 100.0
total 851 1227 69.3


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   262671 use strict;
  11         23  
  11         502  
11 11     11   53 use Carp;
  11         19  
  11         827  
12              
13 11     11   54 use vars qw($VERSION @ISA);
  11         20  
  11         712  
14              
15             $VERSION = '2.13';
16              
17 11     11   9858 use HTML::Parser;
  11         58219  
  11         678  
18             @ISA = qw(HTML::Parser);
19              
20 11     11   279 use HTML::Entities;
  11         17  
  11         1012  
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   378 BEGIN { *TREE = sub { 0 } }
  889     889   2021  
27              
28             sub import {
29 10     10   90 my $class = shift;
30 11     11   59 no warnings;
  11         14  
  11         25240  
31 10 50   24625   116 *TREE = @_ ? sub { 1 } : sub { 0 };
  0         0  
  31309         67717  
32 10 50       14358 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 533528 my $that = shift;
73 23   33     202 my $class = ref($that) || $that;
74              
75 23         46 my(%pass, %parms, $k, $v);
76 23         182 while (($k,$v) = splice(@_, 0, 2)) {
77 31 100       1031 if ($k eq 'headers') {
    50          
78 12 50       61 ref $v eq 'ARRAY'
79             or croak "Param '$k' must be passed in ref to array\n";
80 12         67 $parms{$k} = $v;
81             }
82             elsif ($k =~ /^$Dpat$/) {
83 19         143 $parms{$k} = $v;
84             }
85             else {
86 0         0 $pass{$k} = $v;
87             }
88             }
89              
90 23         331 my $self = $class->SUPER::new(%pass);
91 23         1865 bless $self, $class;
92 23         200 foreach (keys %parms, keys %Defaults) {
93 376 100 66     1384 $self->{$_} = exists $parms{$_} && defined $parms{$_} ?
94             $parms{$_} : $Defaults{$_};
95             }
96 23 100       205 if ($self->{headers}) {
97 12 50       45 $self->_emsg("TE here, headers: ", join(',', @{$self->{headers}}), "\n")
  0         0  
98             if $self->{debug};
99 12         27 $self->{gridmap} = 1;
100             }
101              
102             # Initialize counts and containers
103 23         102 $self->_reset_state;
104              
105 23         85 $self;
106             }
107              
108             ### HTML::Parser overrides
109              
110             sub start {
111 10650     10650 1 10583 my $self = shift;
112 10650         8375 my @res;
113              
114 10650 50       13563 @res = $self->SUPER::start(@_) if TREE();
115              
116             # Create a new table state if entering a table.
117 10650 100       27082 if ($_[0] eq 'table') {
    100          
118 111         526 my $ts = $self->_enter_table(@_);
119 111 50       322 $ts->tree($res[0]) if @res;
120             }
121             elsif ($self->{_in_a_table}) {
122             # Rows and cells are next.
123 10389         15135 my $ts = $self->current_table;
124 10389 100 66     29302 if ($_[0] eq 'tr') {
    100 33        
    50          
125 984         1840 $ts->_enter_row;
126             }
127             elsif ($_[0] eq 'td' || $_[0] eq 'th') {
128 9312         15642 $ts->_enter_cell(@_);
129 9312 50       13466 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         12328  
130 9312   100     24913 my $rspan = $attrs{rowspan} || 1;
131 9312   100     27816 my $cspan = $attrs{colspan} || 1;
132 9312         15386 $ts->_rasterizer->($ts->row_count, $rspan, $cspan);
133 9312         18367 $ts->_anchor_item(@res);
134             }
135             elsif (! TREE() && $ts->{in_cell}) {
136 93 50 66     443 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         10 $self->text("\n");
143             }
144             }
145             }
146              
147 10650         63320 @res;
148             } # end start
149              
150             sub end {
151 10647     10647 1 10691 my $self = shift;
152 10647         8294 my @res;
153            
154 10647 50       13478 @res = $self->SUPER::end(@_) if TREE();
155              
156 10647 100       19710 if ($self->{_in_a_table}) {
157 10497         14462 my $ts = $self->current_table;
158 10497 100 66     29414 if ($_[0] eq 'td' || $_[0] eq 'th') {
    100          
    100          
    50          
159 9312         14795 $ts->_exit_cell;
160             }
161             elsif ($_[0] eq 'tr') {
162 984         2059 $ts->_exit_row;
163             }
164             elsif ($_[0] eq 'table') {
165 111         346 $self->_exit_table;
166             }
167             elsif (! TREE()) {
168 90 50 33     217 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         47816 @res;
176             } # end end
177              
178             sub text {
179 9756     9756 1 10787 my $self = shift;
180 9756         7711 my @res;
181              
182 9756 50       13483 if (TREE()) {
    100          
183 0         0 @res = $self->SUPER::text(@_);
184             }
185             elsif ($self->{_in_a_table}) {
186 9449         14714 my $ts = $self->current_table;
187 9449 100       17594 if ($ts->{in_cell}) {
188 9322 50 33     32960 if ($self->{decode} && !$self->{keep_html}) {
189 9322         41299 $ts->_add_text(decode_entities($_[0]));
190             }
191             else {
192 0         0 $ts->_add_text($_[0]);
193             }
194             }
195             }
196              
197 9756         42304 @res;
198             } # end text
199              
200             sub parse {
201 333     333 1 11069 my $self = shift;
202 333 100       1140 $self->_reset_state unless $self->{_parsing};
203 333   100     911 $self->{_parsing} ||= 1;
204 333         3010 $self->SUPER::parse(@_);
205             }
206              
207             sub eof {
208 23     23 1 1396 my $self = shift;
209 23         69 $self->{_parsing} = 0;
210 23         268 $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 1607 my $self = shift;
245 10 50       78 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 35 my $self = shift;
254 14         73 while ($self->{_in_a_table}) {
255 0         0 my $ts = $self->current_table;
256 0 0       0 $self->_emsg("Mangled HTML in table ($ts->{depth},$ts->{count}), inferring closing table tag.\n")
257             if $self->{debug};
258 0         0 $self->_exit_table;
259             }
260 14         21 @{$self->{_ts_sequential}};
  14         1101  
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   366 my($self, @args) = @_;
297              
298 111         222 ++$self->{_cdepth};
299 111         208 ++$self->{_in_a_table};
300              
301 111         190 my $depth = $self->{_cdepth};
302              
303             # Table tag attributes, if present
304 111   50     354 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         334 my $pts = $self->current_table;
311              
312             # Counts are tracked for each depth.
313 111         220 my $counts = $self->{_counts};
314 111 100       357 $counts->[$depth] = -1 unless defined $counts->[$depth];
315 111         176 ++$counts->[$depth];
316 111         196 my $count = $counts->[$depth];
317              
318 111 50       364 $self->_emsg("TABLE: cdepth $depth, ccount $count, it: $self->{_in_a_table}\n")
319             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         168 my $umbrella = 0;
324 111 100 66     1045 if (! defined $self->{depth} && ! defined $self->{count} &&
      66        
      66        
325             ! $self->{attribs} && ! $self->{headers}) {
326 30         44 ++$umbrella;
327             }
328              
329             # Basic parameters for the soon-to-be-created table state.
330 111         1495 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             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       355 if (!$umbrella) {
353 81         188 $tsparms{tdepth} = $self->{depth};
354 81         166 $tsparms{tcount} = $self->{count};
355 81         212 $tsparms{tattribs} = $self->{attribs};
356 81         184 $tsparms{headers} = $self->{headers};
357             }
358              
359             # Abracadabra
360 111         917 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         215 push(@{$self->{_tablestack}}, $ts);
  111         269  
365              
366 111         427 $ts;
367             }
368              
369             sub _exit_table {
370 111     111   175 my $self = shift;
371 111         248 my $ts = $self->current_table;
372              
373             # Last ditch fix for HTML mangle
374 111 50       388 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       378 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         395 $ts->_grid_map();
385              
386 111 100       439 $self->_capture_table($ts) if $ts->_check_triggers;
387              
388             # Restore last table state
389 111         165 pop(@{$self->{_tablestack}});
  111         275  
390 111         190 --$self->{_in_a_table};
391 111         302 my $lts = $self->current_table;
392 111 100       344 if (ref $lts) {
393 54         173 $self->{_cdepth} = $lts->{depth};
394             }
395             else {
396             # Back to the top level
397 57         138 $self->{_cdepth} = -1;
398             }
399 111 50       1599 $self->_emsg("LEAVE: cdepth: $self->{_cdepth}, ccount: $ts->{count}, it: $self->{_in_a_table}\n")
400             if $self->{debug} >= 2;
401             }
402              
403             sub _capture_table {
404 89     89   161 my($self, $ts, $type) = @_;
405 89 50       263 croak "Table state ref required\n" unless ref $ts;
406 89 50       283 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       270 $ts->tree(HTML::ElementTable->new_from_tree($ts->tree)) if TREE();
413 89 100       253 if ($self->{subtables}) {
414 11         16 foreach my $child (@{$ts->{children}}) {
  11         28  
415 8 100       22 next if $child->{captured};
416 6         17 $self->_capture_table($child, 'subtable');
417 6         11 $child->{slice_columns} = 0;
418 6         7 $child->{keep_headers} = 1;
419 6         12 $child->{headers} = '';
420             }
421             }
422 89         169 $ts->{captured} = 1;
423 89         500 $self->{_tables}{$ts->{depth}}{$ts->{count}} = $ts;
424 89         116 push(@{$self->{_ts_sequential}}, $ts);
  89         259  
425             }
426              
427             sub current_table {
428 30668     30668 1 27534 my $self = shift;
429 30668         29762 $self->{_tablestack}[$#{$self->{_tablestack}}];
  30668         49245  
430             }
431              
432             sub _reset_state {
433 46     46   86 my $self = shift;
434 46         97 $self->{_cdepth} = -1;
435 46         97 $self->{_tablestack} = [];
436 46         110 $self->{_tables} = {};
437 46         91 $self->{_ts_sequential} = [];
438 46         90 $self->{_counts} = [];
439 46         87 $self->{_in_a_table} = 0;
440 46         105 $self->{_parsing} = 0;
441             }
442              
443             sub _emsg {
444 0     0   0 my $self = shift;
445 0         0 my $fh = $self->{error_handle};
446 0 0       0 return unless defined $_[0];
447 0         0 print $fh @_;
448             }
449              
450             ##########
451              
452             {
453              
454             package HTML::TableExtract::Table;
455              
456 11     11   97 use strict;
  11         15  
  11         464  
457 11     11   54 use Carp;
  11         14  
  11         48897  
458              
459             *TREE = *HTML::TableExtract::TREE;
460              
461             sub new {
462 111     111   205 my $that = shift;
463 111   33     524 my $class = ref($that) || $that;
464             # Note:
465             # - 'depth' and 'count' are where this table were found.
466             # - 'tdepth' and 'tcount' are target constraints on which to trigger.
467             # - 'headers' represent a target constraint, location independent.
468             # - 'attribs' represent target table tag constraints
469 111         1340 my $self = {
470             umbrella => 0,
471             in_row => 0,
472             in_cell => 0,
473             rc => -1,
474             cc => -1,
475             grid => [],
476             translation => [],
477             hrow => [],
478             order => [],
479             children => [],
480             captured => 0,
481             debug => 0,
482             };
483              
484 111         584 $self->{_rastamon} = HTML::TableExtract::Rasterize->make_rasterizer();
485 111         416 bless $self, $class;
486              
487 111         661 my %parms = @_;
488              
489             # Depth and Count -- this is the absolute address of the table.
490 111 50       373 croak "Absolute depth required\n" unless defined $parms{depth};
491 111 50       366 croak "Count required\n" unless defined $parms{count};
492 111 50       295 croak "Counts required\n" unless defined $parms{counts};
493              
494 111         599 foreach (keys %parms) {
495 1767         2571 $self->{$_} = $parms{$_};
496             }
497              
498             # Register lineage
499 111         368 my $pts = $self->{parent_table};
500 111   100     688 $self->lineage($pts || undef);
501 111 100       251 push(@{$pts->{children}}, $self) if ($pts);
  54         124  
502 111         306 delete $self->{parent_table};
503              
504 111         399 $self;
505             }
506              
507             sub _anchor_item {
508             # anchor the reference to a cell in our grid -- in TREE mode this is
509             # a reference to a data element, otherwise it's a reference to an
510             # empty scalar in which we will collect our text.
511 9312     9312   11729 my($self, @res) = @_;
512 9312         12563 my $row = $self->{grid}[-1];
513 9312         7254 my $item;
514 9312 50 33     20594 if (@res && ref $res[0]) {
515 0         0 $item = $res[0];
516             }
517             else {
518 9312         7239 my $scalar_ref;
519 9312         10107 $item = \$scalar_ref;
520             }
521 9312         21720 push(@$row, $item);
522             }
523              
524             sub _gridalias {
525 3     3   15390 my $self = shift;
526 3   66     29 $self->{gridalias} ||= $self->_make_gridalias;
527             }
528              
529             sub _grid_map {
530             # using our rasterized template, flesh out our captured items which
531             # are still in 'tree' format
532 111     111   162 my $self = shift;
533 111         270 my $template = $self->_rasterizer->();
534 111         220 my $grid = $self->{grid};
535             # drop empty rows
536 111 50       418 if ($self->{debug}) {
537 0         0 foreach (0 .. $#$grid) {
538 0 0       0 next if @{$grid->[$_]};
  0         0  
539 0         0 $self->_emsg("Dropping empty row $_\n");
540             }
541             }
542 111         1098 @$grid = grep(@$_, @$grid);
543 111         404 foreach my $r (0 .. $#$template) {
544 984         1075 my $row = $grid->[$r];
545 984         996 my $trow = $template->[$r];
546 984 50       1641 $self->_emsg("Flesh row $r ($#$row) to $#$trow\n") if $self->{debug} > 1;
547 984         1541 foreach my $c (0 .. $#$trow) {
548 9342 0       13130 print STDERR $trow->[$c] ? '1' : '0' if $self->{debug} > 1;
    50          
549 9342 100       11597 if ($trow->[$c]) {
550 9312 50       13573 if (! defined $row->[$c]) {
551 0         0 $row->[$c] = \undef;
552             }
553 9312         8812 next;
554             }
555             else {
556 30         26 my $scalar;
557 30         76 splice(@$row, $c, 0, \$scalar);
558             }
559             }
560 984 50       2016 print STDERR "\n" if $self->{debug} > 1;
561 984 50       2271 croak "row $r splice mismatch: $#$row vs $#$trow\n"
562             unless $#$row == $#$trow;
563             }
564 111         253 $grid;
565             }
566              
567             sub _make_gridalias {
568             # our aliased grid will have references in masked cells to the same
569             # cell that is covering it via spanning.
570 2     2   4 my $self = shift;
571 2         5 my $grid = $self->{grid};
572 2         9 my $template = $self->_rasterizer->();
573 2         5 my(@gridalias, @translation);
574 2         9 $gridalias[$_] = [@{$grid->[$_]}] foreach 0 .. $#$grid;
  14         49  
575 2         9 foreach my $r (0 .. $#gridalias) {
576 14         20 my $row = $gridalias[$r];
577 14         27 foreach my $c (0 .. $#$row) {
578 56   100     128 my $tcell = $template->[$r][$c] || next;
579 36         48 my($rspan, $cspan) = @$tcell;
580 36         62 foreach my $rs (0 .. $rspan-1) {
581 42         49 foreach my $cs (0 .. $cspan-1) {
582 56         123 $gridalias[$r + $rs][$c + $cs] = $grid->[$r][$c];
583 56         259 $translation[$r + $rs][$c + $cs] = "$r,$c";
584             }
585             }
586             }
587             }
588 2         8 $self->{translation} = \@translation;
589 2         16 $self->{gridalias} = \@gridalias;
590             }
591              
592             ### Constraint tests
593              
594             sub _check_dtrigger {
595             # depth
596 81     81   127 my $self = shift;
597 81 100       628 return 1 unless defined $self->{tdepth};
598 15 100       110 $self->{tdepth} == $self->{depth} ? 1 : 0;
599             }
600              
601             sub _check_ctrigger {
602             # count
603 74     74   120 my $self = shift;
604 74 100       566 return 1 unless defined $self->{tcount};
605 11 100 66     128 return 1 if (exists $self->{counts}[$self->{depth}] &&
606             $self->{tcount} == $self->{counts}[$self->{depth}]);
607 7         47 return 0;
608             }
609              
610             sub _check_atrigger {
611             # attributes
612 67     67   105 my $self = shift;
613 67 100       89 return 1 unless scalar keys %{$self->{tattribs}};
  67         597  
614 15 50       13 return 0 unless scalar keys %{$self->{attribs}};
  15         45  
615 15         16 my $a_hit = 1;
616 15         15 foreach my $attrib (keys %{$self->{tattribs}}) {
  15         40  
617 15 100       39 if (! defined $self->{attribs}{$attrib}) {
618 8         11 $a_hit = 0; last;
  8         9  
619             }
620 7 100       19 if (! defined $self->{tattribs}{$attrib}) {
621             # undefined, but existing, target attribs are wildcards
622 1         3 next;
623             }
624 6 100       24 if ($self->{tattribs}{$attrib} ne $self->{attribs}{$attrib}) {
625 2         4 $a_hit = 0; last;
  2         3  
626             }
627             }
628 15 50 33     60 $self->_emsg("Matched attributes\n") if $self->{debug} > 3 && $a_hit;
629 15         73 $a_hit;
630             }
631              
632             sub _check_htrigger {
633             # headers
634 57     57   98 my $self = shift;
635 57 50       170 return 1 if $self->{umbrella};
636 57 100       265 return 1 unless $self->{headers};
637 46         82 ROW: foreach my $r (0 .. $#{$self->{grid}}) {
  46         144  
638 70         210 $self->_reset_hits;
639 70         186 my $hpat = $self->_header_pattern;
640 70         109 my @hits;
641 70         99 foreach my $c (0 .. $#{$self->{grid}[$r]}) {
  70         263  
642 631         883 my $ref = $self->{grid}[$r][$c];
643 631         565 my $target = '';
644 631         760 my $ref_type = ref $ref;
645 631 50       954 if ($ref_type) {
646 631 50       867 if ($ref_type eq 'SCALAR') {
647 631         664 my $item = $$ref;
648 631 50 33     1374 if ($self->{keep_html} && $self->{strip_html_on_match}) {
649 0         0 my $stripper = HTML::TableExtract::StripHTML->new;
650 0         0 $target = $stripper->strip($item);
651             }
652             else {
653 631         768 $target = $item;
654             }
655             }
656             else {
657 0 0 0     0 if (($self->{keep_html} || TREE()) &&
      0        
658             $self->{strip_html_on_match}) {
659 0         0 $target = $ref->as_text;
660             }
661             else {
662 0         0 $target = $ref->as_HTML;
663             }
664             }
665             }
666 631 50       856 $target = defined $target ? $target : '';
667 631 50       1001 $self->_emsg("attempt match on $target ($hpat): ")
668             if $self->{debug} >= 5;
669 631 100       3766 if ($target =~ $hpat) {
    50          
670 180         424 my $hit = $1;
671 180 50       384 $self->_emsg("($hit)\n") if $self->{debug} >= 5;
672             # Get rid of the header segment that matched so we can tell
673             # when we're through with all header patterns.
674 180         195 my $real_hit;
675 180         182 foreach (sort _header_string_sort keys %{$self->{hits_left}}) {
  180         672  
676 228 100       1906 if ($hit =~ /$_/im) {
677 180         441 delete $self->{hits_left}{$_};
678 180         188 $real_hit = $_;
679 180         370 $hpat = $self->_header_pattern;
680 180         364 last;
681             }
682             }
683 180 50       422 if (defined $real_hit) {
684 180 50       347 if ($self->{debug} >= 4) {
685 0 0       0 my $str = $ref_type eq 'SCALAR' ? $$ref : $ref->as_HTML;
686 0         0 $self->_emsg("HIT on '$hit' ($real_hit) in $str ($r,$c)\n");
687             }
688 180         263 push(@hits, $hit);
689             #
690 180         456 $self->{hits}{$c} = $real_hit;
691 180         172 push(@{$self->{order}}, $c);
  180         308  
692 180 100       166 if (!%{$self->{hits_left}}) {
  180         1028  
693             # Successful header row match
694 42         233 ++$self->{head_found};
695 42         85 $self->{hrow_index} = $r;
696 42         111 $self->{hrow} = $self->{grid}[$r];
697 42         161 last ROW;
698             }
699             }
700             }
701             elsif ($self->{debug} >= 5) {
702 0         0 $self->_emsg("0\n");
703             }
704             }
705 28 50 33     114 if ($self->{debug} && @hits) {
706 0         0 my $str = "Incomplete header match ";
707 0         0 $str .= "(left: " . join(', ', sort keys %{$self->{hits_left}}) . ") ";
  0         0  
708 0         0 $str .= "in row $r, resetting scan";
709 0         0 $str .= "\n";
710 0         0 $self->_emsg($str);
711             }
712             }
713 46         403 $self->{head_found};
714             }
715              
716             sub _check_triggers {
717 111     111   180 my $self = shift;
718 111 100       482 return 1 if $self->{umbrella};
719 81 100 100     280 $self->_check_dtrigger &&
      100        
720             $self->_check_ctrigger &&
721             $self->_check_atrigger &&
722             $self->_check_htrigger;
723             }
724              
725             ### Maintain table context
726              
727             sub _enter_row {
728 984     984   1024 my $self = shift;
729 984 50       1983 if ($self->{in_row}) {
730 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};
731 0         0 $self->_exit_row;
732             }
733 984         1120 ++$self->{rc};
734 984         1032 ++$self->{in_row};
735 984         975 push(@{$self->{grid}}, [])
  984         2630  
736             }
737              
738             sub _exit_row {
739 984     984   1032 my $self = shift;
740 984 50       1702 if ($self->{in_row}) {
741 984 50       1981 if ($self->{in_cell}) {
742 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};
743 0         0 $self->_exit_cell;
744             }
745 984         1209 $self->{in_row} = 0;
746 984         1632 $self->{cc} = -1;
747             }
748             else {
749 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), extraneous
750             if $self->{debug};
751             }
752             }
753              
754             sub _enter_cell {
755 9312     9312   8216 my $self = shift;
756 9312 50       15882 if ($self->{in_cell}) {
757 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};
758 0         0 $self->_exit_cell;
759             }
760 9312 50       15697 if (!$self->{in_row}) {
761             # Go ahead and try to recover from mangled HTML, because we care.
762 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), inferring
763             if $self->{debug};
764 0         0 $self->_enter_row;
765             }
766 9312         9619 ++$self->{cc};
767 9312         8658 ++$self->{in_cell};
768 9312 50       14776 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         22144  
769 9312   100     29372 my $rspan = $attrs{rowspan} || 1;
770 9312   100     27841 my $cspan = $attrs{colspan} || 1;
771             }
772              
773             sub _exit_cell {
774 9312     9312   7871 my $self = shift;
775 9312 50       14047 if ($self->{in_cell}) {
776 9312         13895 $self->{in_cell} = 0;
777             }
778             else {
779 0 0       0 $self->_emsg("Mangled HTML in table ($self->{depth},$self->{count}), extraneous
780             if $self->{debug};
781             }
782             }
783              
784             # Header stuff
785              
786             sub _header_pattern {
787 250     250   361 my($self, @headers) = @_;
788 250         1176 my $str = join('|',
789             map("($_)",
790 250         274 sort _header_string_sort keys %{$self->{hits_left}}
791             ));
792 250         3686 my $hpat = qr/($str)/im;
793 250 50       739 $self->_emsg("HPAT: /$hpat/\n") if $self->{debug} >= 2;
794 250         611 $self->{hpat} = $hpat;
795             }
796              
797             sub _header_string_sort {
798             # this ensures that supersets appear before subsets in our header
799             # search pattern, eg, '10' appears before '1' and 'hubbabubba'
800             # appears before 'hubba'.
801 1138 50   1138   10927 if ($a =~ /^$b/) {
    50          
802 0         0 return -1;
803             }
804             elsif ($b =~ /^$a/) {
805 0         0 return 1;
806             }
807             else {
808 1138         2447 return $b cmp $a;
809             }
810             }
811              
812             # Report methods
813              
814 54     54   225 sub depth { shift->{depth} }
815 54     54   198 sub count { shift->{count} }
816             sub coords {
817 0     0   0 my $self = shift;
818 0         0 ($self->depth, $self->count);
819             }
820              
821 9312     9312   19604 sub row_count { shift->{rc} }
822 0     0   0 sub col_count { shift->{cc} }
823              
824             sub tree {
825 0     0   0 my $self = shift;
826 0 0       0 @_ ? $self->{_tree_ref} = shift : $self->{_tree_ref};
827             }
828              
829             sub lineage {
830 165     165   232 my $self = shift;
831 165   100     697 $self->{lineage} ||= [];
832 165 100       401 if (@_) {
833 111         164 my $pts = shift;
834 111         136 my(@lineage, $pcoords);
835 111 100       290 if ($pts) {
836 54         185 foreach my $pcoord ($pts->lineage) {
837 20         60 push(@lineage, [@$pcoord]);
838             }
839 54         216 $pcoords = [$pts->depth, $pts->count, $pts->{rc}, $pts->{cc}];
840 54         114 push(@lineage, $pcoords);
841             }
842 111         305 $self->{lineage} = \@lineage;
843             }
844 165         186 @{$self->{lineage}};
  165         392  
845             }
846              
847 36     36   49187 sub rows { shift->_rows(0) }
848              
849             sub space_rows {
850 0     0   0 my $self = shift;
851 0         0 $self->_rows(1);
852             }
853              
854             sub _rows {
855 36     36   71 my $self = shift;
856 36         52 my $alias = shift;
857 36         116 my @ri = $self->row_indices;
858 36         63 my @rows;
859 36 50       106 my $grid = $alias ? $self->_gridalias : $self->{grid};
860 36         86 foreach ($self->row_indices) {
861 218         463 push(@rows, scalar $self->_slice_and_normalize_row($grid->[$_]));
862             }
863 36 50       345 wantarray ? @rows : \@rows;
864             }
865              
866             sub columns {
867 0     0   0 my $self = shift;
868 0         0 my @cols;
869 0         0 my @rows = $self->rows;
870 0         0 foreach my $row (@rows) {
871 0         0 foreach my $c (0 .. $#$row) {
872 0   0     0 $cols[$c] ||= [];
873 0         0 push(@{$cols[$c]}, $row->[$c]);
  0         0  
874             }
875             }
876 0         0 @cols;
877             }
878              
879             sub row_indices {
880 78     78   108 my $self = shift;
881 78         85 my $start_index = 0;
882 78 100       285 if ($self->{headers}) {
883 38         87 $start_index = $self->hrow_index;
884 38 100       105 $start_index += 1 unless $self->{keep_headers};
885             }
886 78         113 $start_index .. $#{$self->{grid}};
  78         299  
887             }
888              
889             sub col_indices {
890 0     0   0 my $self = shift;
891 0         0 my $row = $self->{grid}[0];
892 0         0 0 .. $#$row;
893             }
894              
895             sub row {
896 6     6   19 my $self = shift;
897 6         9 my $r = shift;
898 6 50       8 $r <= $#{$self->{grid}}
  6         24  
899 0         0 or croak "row $r out of range ($#{$self->{grid}})\n";
900 6         23 my @row = $self->_slice_and_normalize_row(
901             $self->{grid}[($self->row_indices)[$r]]
902             );
903 6 50       22 wantarray ? @row : \@row;
904             }
905              
906             sub _slice_and_normalize_row {
907 224     224   214 my $self = shift;
908 224         222 my $rowref = shift;
909 224         235 my @row;
910 224 100 66     697 if ($self->{automap} && $self->_map_makes_a_difference) {
911 128         183 @row = @{$rowref}[$self->column_map];
  128         369  
912             }
913             else {
914 96         397 @row = @$rowref;
915             }
916 224         554 @row = map($self->_cell_to_content($_), @row);
917 224 100       747 wantarray ? @row : \@row;
918             }
919              
920             sub column {
921 0     0   0 my $self = shift;
922 0         0 my $c = shift;
923 0         0 my @column;
924 0         0 foreach my $row ($self->rows) {
925 0         0 push(@column, $self->cell($row, $c));
926             }
927 0 0       0 wantarray ? @column : \@column;
928             }
929              
930             sub cell {
931 4     4   24 my $self = shift;
932 4         6 my($r, $c) = @_;
933 4         10 my $row = $self->row($r);
934 4 50       14 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
935 4         11 $self->_cell_to_content($row->[$c]);
936             }
937              
938             sub _cell_to_content {
939 877     877   868 my $self = shift;
940 877 50       1330 @_ or croak "cell item required\n";
941 877         711 my $cell = shift;
942 877 100       1411 return $cell unless ref $cell;
943 873 50       1027 return $cell if TREE();
944 873         2150 return $$cell;
945             }
946              
947             sub space {
948 2     2   13 my $self = shift;
949 2         5 my($r, $c) = @_;
950 2         9 my $gridalias = $self->_gridalias;
951 2 50       8 $r <= $#$gridalias
952             or croak "row $r out of range ($#$gridalias)\n";
953 2         5 my $row = $gridalias->[$r];
954 2 50       7 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
955 2         8 $self->_cell_to_content($row->[$c]);
956             }
957              
958             sub source_coords {
959 28     28   65552 my $self = shift;
960 28         67 my($r, $c) = @_;
961 28 50       49 $r <= $#{$self->{translation}}
  28         148  
962 0         0 or croak "row $r out of range ($#{$self->{translation}})\n";
963 28         77 my $row = $self->{translation}[$r];
964 28 50       82 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
965 28         218 split(/,/, $self->{translation}[$r][$c]);
966             }
967              
968             sub hrow_index {
969 41     41   1337 my $self = shift;
970 41         80 $self->{hrow_index};
971             }
972              
973             sub hrow {
974 3     3   13 my $self = shift;
975 3 50 33     15 if ($self->{automap} && $self->_map_makes_a_difference) {
976 3 50       7 return map(ref $_ ? $$_ : $_, @{$self->{hrow}}[$self->column_map]);
  3         34  
977             }
978             else {
979 0 0       0 return map(ref $_ ? $$_ : $_, @{$self->{hrow}});
  0         0  
980             }
981             }
982              
983             sub column_map {
984             # Return the column numbers of this table in the same order as the
985             # provided headers.
986 419     419   54882 my $self = shift;
987 419 100       677 if ($self->{headers}) {
988             # First we order the original column counts by taking a hash slice
989             # based on the original header order. The resulting original
990             # column numbers are mapped to the actual content indices since
991             # we could have a sparse slice.
992 344         330 my %order;
993 344         265 foreach (keys %{$self->{hits}}) {
  344         741  
994 1026         1526 $order{$self->{hits}{$_}} = $_;
995             }
996 344         399 return @order{@{$self->{headers}}};
  344         1150  
997             }
998             else {
999 75         93 return 0 .. $#{$self->{grid}[0]};
  75         285  
1000             }
1001             }
1002              
1003             sub _map_makes_a_difference {
1004 227     227   211 my $self = shift;
1005 227 100       437 return 0 unless $self->{slice_columns};
1006 225         206 my $diff = 0;
1007 225         376 my @order = $self->column_map;
1008 225         614 my @sorder = sort { $a <=> $b } @order;
  1022         1457  
1009 225 50       480 ++$diff if $#order != $#sorder;
1010 225 100       255 ++$diff if $#sorder != $#{$self->{grid}[0]};
  225         546  
1011 225         408 foreach (0 .. $#order) {
1012 597 100       1139 if ($order[$_] != $sorder[$_]) {
1013 131         141 ++$diff;
1014 131         125 last;
1015             }
1016             }
1017 225         955 $diff;
1018             }
1019              
1020             sub _add_text {
1021 9322     9322   10973 my($self, $txt) = @_;
1022 9322         10236 my $r = $self->{rc};
1023 9322         9653 my $c = $self->{cc};
1024 9322         10469 my $row = $self->{grid}[$r];
1025 9322         6866 ${$row->[$c]} .= $txt;
  9322         16587  
1026 9322         14625 $txt;
1027             }
1028              
1029             sub _reset_hits {
1030 70     70   98 my $self = shift;
1031 70 50       196 return unless $self->{headers};
1032 70         202 $self->{hits} = {};
1033 70         168 $self->{order} = [];
1034 70         112 foreach (@{$self->{headers}}) {
  70         203  
1035 254         601 ++$self->{hits_left}{$_};
1036             }
1037 70         125 1;
1038             }
1039              
1040 9425     9425   16850 sub _rasterizer { shift->{_rastamon} }
1041              
1042             sub report {
1043             # Print out a summary of this table, including depth/count
1044 0     0   0 my($self, $include_content, $col_sep) = @_;
1045 0   0     0 $col_sep ||= ':';
1046 0         0 my $str;
1047 0         0 $str .= "TABLE(" . $self->depth . ", " . $self->count . ')';
1048 0 0       0 if ($include_content) {
1049 0         0 $str .= ":\n";
1050 0         0 foreach my $row ($self->rows) {
1051 0         0 $str .= join($col_sep, @$row) . "\n";
1052             }
1053             }
1054             else {
1055 0         0 $str .= "\n";
1056             }
1057 0         0 $str;
1058             }
1059              
1060             sub dump {
1061 0     0   0 my $self = shift;
1062 0         0 $self->_emsg($self->report(@_));
1063             }
1064              
1065             sub _emsg {
1066 0     0   0 my $self = shift;
1067 0         0 my $fh = $self->{error_handle};
1068 0         0 print $fh @_;
1069             }
1070              
1071             }
1072              
1073             ##########
1074              
1075             {
1076              
1077             package HTML::TableExtract::Rasterize;
1078              
1079             # Provide a closure that will rasterize (turn into a grid) a table
1080             # from a tree structure based on repeated data element calls with
1081             # rowspan and colspan information. Not as straight forward as it
1082             # seems...see test cases for an example bugaboo.
1083              
1084             my $DEBUG = 0;
1085              
1086             sub make_rasterizer {
1087 111     111   198 my $pkg = shift;
1088 111         140 my(@grid, @row_spinner, @col_spinner);
1089 111         159 my $empty_row_offset = 0;
1090             sub {
1091 9425 100   9425   15624 return \@grid unless @_;
1092 9312         9828 my($row_num, $rspan, $cspan) = @_;
1093 9312 100       16550 $rspan = 1 unless $rspan > 1;
1094 9312 100       14203 $cspan = 1 unless $cspan > 1;
1095 9312         7816 my($rspin_propogate, $row_added);
1096 9312         12829 my $trigger = $#grid + $empty_row_offset;
1097 9312 100       16203 if ($row_num > $trigger) {
1098             # adjust for having been handed a row that skips a prior row,
1099             # otherwise the next cell will land in a wrong row. Hopefully
1100             # this doesn't happen too often but I've seen it in the wild!
1101 984 50       2030 if ($row_num - $trigger > 1) {
1102 0         0 $empty_row_offset += $row_num - $trigger - 1;
1103             }
1104             # add new row
1105 984         933 $row_added = 1;
1106 984         849 my @new_row;
1107             # first add new row spinner
1108 984 50 33     2317 if ($row_spinner[-1] && $col_spinner[-1]) {
1109 0         0 push(@row_spinner, $row_spinner[-1]);
1110 0         0 $rspin_propogate = 1;
1111             }
1112             else {
1113 984         1764 push(@row_spinner, $cspan - 1);
1114             }
1115             # spin columns
1116 984         1895 foreach (@col_spinner) {
1117 8382 100       9091 if ($_) {
1118 15         19 push(@new_row, 0);
1119 15         18 --$_;
1120             }
1121             else {
1122 8367         10972 push(@new_row, undef);
1123             }
1124             }
1125 984 100       2319 @new_row = (undef) unless @new_row;
1126 984         2023 push(@grid, \@new_row);
1127             }
1128 9312         9412 my $current_row = $grid[-1];
1129             # locate next available cell in row
1130 9312         7033 my $col;
1131 9312         18275 foreach my $ci (0 .. $#$current_row) {
1132 49821 100       76896 if (! defined $current_row->[$ci]) {
1133 8463         6813 $col = $ci;
1134 8463         10530 last;
1135             }
1136             }
1137 9312 100       16004 if (! defined $col) {
1138 849         1453 ADDCOL: while (! defined $col) {
1139             # if no cells were available, add a column
1140 849         1393 foreach my $ri (0 .. $#grid) {
1141 849         946 my $row = $grid[$ri];
1142 849         756 my $cspan_count = $row_spinner[$ri];
1143 849 50       1225 if (!$cspan_count) {
1144 849         2108 push(@$row, undef);
1145             }
1146             else {
1147 0         0 push(@$row, 0);
1148 0         0 --$row_spinner[$ri];
1149             }
1150             }
1151 849         1306 push(@col_spinner, $col_spinner[-1]);
1152 849         1197 foreach my $ci (0 .. $#$current_row) {
1153 4989 100       7399 if (! defined $current_row->[$ci]) {
1154 849         723 $col = $ci;
1155 849         1261 last ADDCOL;
1156             }
1157             }
1158             }
1159 849 50       1896 $col_spinner[-1] = $rspan - 1 if $col == $#$current_row;
1160 849         1148 $row_spinner[$#grid] = $cspan - 1;
1161             }
1162              
1163             # we now have correct coordinates for this element
1164 9312         18712 $current_row->[$col] = [$rspan, $cspan];
1165 9312         11637 $col_spinner[$col] = $rspan - 1;
1166              
1167             # if this is an embedded placement (not a trailing element), use up
1168             # the cspan
1169 9312 100       16738 if ($col < $#$current_row) {
1170 7482         6609 my $offset = 1;
1171 7482         6643 my $row_span = $col_spinner[$col];
1172 7482 100 100     31727 if ($col + $row_spinner[-1] < $#$current_row &&
      66        
1173             $row_added && !$rspin_propogate) {
1174             # cell is spun out -- clear spinner unless it inherited cspan
1175             # from a cell above
1176 870         943 $row_spinner[-1] = 0;
1177             }
1178 7482         15891 while ($offset < $cspan) {
1179 15         17 my $cursor = $col + $offset;
1180 15         19 $current_row->[$cursor] = 0;
1181 15         16 $col_spinner[$cursor] = $row_span;
1182 15         15 ++$offset;
1183 15 100       41 if ($col + $offset > $#$current_row) {
1184 3         9 $row_spinner[-1] = $cspan - $offset;
1185 3         5 last;
1186             }
1187             }
1188             }
1189              
1190 9312 50       14243 if ($DEBUG) {
1191 0         0 foreach my $r (0 .. $#grid) {
1192 0         0 my $row = $grid[$r];
1193 0         0 foreach my $c (0 .. $#$row) {
1194 0 0       0 if (defined $row->[$c]) {
1195 0 0       0 print STDERR $row->[$c] ? 1 : 0;
1196             }
1197             else {
1198 0         0 print STDERR '?';
1199             }
1200             }
1201 0         0 print STDERR " $row_spinner[$r]\n";
1202             }
1203 0         0 print STDERR "\n";
1204 0         0 foreach (@col_spinner) {
1205 0 0       0 print STDERR defined $_ ? $_ : '?';
1206             }
1207 0         0 print STDERR "\n\n-----\n\n";
1208             }
1209              
1210 9312         14656 return \@grid;
1211             }
1212 111         2052 }
1213              
1214             }
1215              
1216             ##########
1217              
1218             {
1219              
1220             package HTML::TableExtract::StripHTML;
1221              
1222 11     11   120 use vars qw(@ISA);
  11         19  
  11         971  
1223              
1224 11     11   74 use HTML::Parser;
  11         27  
  11         4271  
1225             @ISA = qw(HTML::Parser);
1226              
1227             sub tag {
1228 0     0     my($self, $tag, $num) = @_;
1229 0           $self->{_htes_inside}{$tag} += $num;
1230             }
1231              
1232             sub text {
1233 0     0     my $self = shift;
1234 0 0 0       return if $self->{_htes_inside}{script} || $self->{_htes_inside}{style};
1235 0           $self->{_htes_tidbit} .= $_[0];
1236             }
1237              
1238             sub new {
1239 0     0     my $class = shift;
1240 0           my $self = HTML::Parser->new(
1241             api_version => 3,
1242             handlers => [start => [\&tag, "self, tagname, '+1'"],
1243             end => [\&tag, "self, tagname, '-1'"],
1244             text => [\&text, "self, dtext"],
1245             ],
1246             marked_sections => 1,
1247             );
1248 0           bless $self, $class;
1249             }
1250              
1251             sub strip {
1252 0     0     my $self = shift;
1253 0           $self->parse(shift);
1254 0           $self->eof;
1255 0           $self->{_htes_tidbit};
1256             }
1257              
1258             }
1259              
1260             1;
1261              
1262             __END__