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   190856 use strict;
  11         23  
  11         303  
11 11     11   48 use Carp;
  11         18  
  11         512  
12              
13 11     11   50 use vars qw($VERSION @ISA);
  11         25  
  11         456  
14              
15             $VERSION = '2.14';
16              
17 11     11   4713 use HTML::Parser;
  11         55173  
  11         414  
18             @ISA = qw(HTML::Parser);
19              
20 11     11   68 use HTML::Entities;
  11         16  
  11         662  
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   340 BEGIN { *TREE = sub { 0 } }
  891     891   1768  
27              
28             sub import {
29 10     10   77 my $class = shift;
30 11     11   52 no warnings;
  11         19  
  11         19436  
31 10 50   24233   65 *TREE = @_ ? sub { 1 } : sub { 0 };
  0         0  
  31353         63492  
32 10 50       10010 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 577496 my $that = shift;
73 23   33     136 my $class = ref($that) || $that;
74              
75 23         51 my(%pass, %parms, $k, $v);
76 23         139 while (($k,$v) = splice(@_, 0, 2)) {
77 31 100       614 if ($k eq 'headers') {
    50          
78 12 50       46 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         104 $parms{$k} = $v;
84             }
85             else {
86 0         0 $pass{$k} = $v;
87             }
88             }
89              
90 23         185 my $self = $class->SUPER::new(%pass);
91 23         1679 bless $self, $class;
92 23         163 foreach (keys %parms, keys %Defaults) {
93             $self->{$_} = exists $parms{$_} && defined $parms{$_} ?
94 376 100 66     1176 $parms{$_} : $Defaults{$_};
95             }
96 23 100       116 if ($self->{headers}) {
97 0         0 $self->_emsg("TE here, headers: ", join(',', @{$self->{headers}}), "\n")
98 12 50       38 if $self->{debug};
99 12         39 $self->{gridmap} = 1;
100             }
101              
102             # Initialize counts and containers
103 23         98 $self->_reset_state;
104              
105 23         71 $self;
106             }
107              
108             ### HTML::Parser overrides
109              
110             sub start {
111 10650     10650 1 14764 my $self = shift;
112 10650         11442 my @res;
113              
114 10650 50       15250 @res = $self->SUPER::start(@_) if TREE();
115              
116             # Create a new table state if entering a table.
117 10650 100       24996 if ($_[0] eq 'table') {
    100          
118 111         309 my $ts = $self->_enter_table(@_);
119 111 50       261 $ts->tree($res[0]) if @res;
120             }
121             elsif ($self->{_in_a_table}) {
122             # Rows and cells are next.
123 10389         16337 my $ts = $self->current_table;
124 10389 100 66     26078 if ($_[0] eq 'tr') {
    100 33        
    50          
125 984         1619 $ts->_enter_row;
126             }
127             elsif ($_[0] eq 'td' || $_[0] eq 'th') {
128 9312         17702 $ts->_enter_cell(@_);
129 9312 50       15093 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         13760  
130 9312   100     24985 my $rspan = $attrs{rowspan} || 1;
131 9312   100     24442 my $cspan = $attrs{colspan} || 1;
132 9312         15173 $ts->_rasterizer->($ts->row_count, $rspan, $cspan);
133 9312         17057 $ts->_anchor_item(@res);
134             }
135             elsif (! TREE() && $ts->{in_cell}) {
136 93 50 66     275 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         7 $self->text("\n");
143             }
144             }
145             }
146              
147 10650         41600 @res;
148             } # end start
149              
150             sub end {
151 10647     10647 1 14940 my $self = shift;
152 10647         12068 my @res;
153            
154 10647 50       15589 @res = $self->SUPER::end(@_) if TREE();
155              
156 10647 100       20119 if ($self->{_in_a_table}) {
157 10497         16081 my $ts = $self->current_table;
158 10497 100 66     26561 if ($_[0] eq 'td' || $_[0] eq 'th') {
    100          
    100          
    50          
159 9312         14930 $ts->_exit_cell;
160             }
161             elsif ($_[0] eq 'tr') {
162 984         1774 $ts->_exit_row;
163             }
164             elsif ($_[0] eq 'table') {
165 111         243 $self->_exit_table;
166             }
167             elsif (! TREE()) {
168 90 0 33     172 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         34376 @res;
176             } # end end
177              
178             sub text {
179 9756     9756 1 13884 my $self = shift;
180 9756         11210 my @res;
181              
182 9756 50       14387 if (TREE()) {
    100          
183 0         0 @res = $self->SUPER::text(@_);
184             }
185             elsif ($self->{_in_a_table}) {
186 9449         15602 my $ts = $self->current_table;
187 9449 100       17608 if ($ts->{in_cell}) {
188 9322 50 33     30227 if ($self->{decode} && !$self->{keep_html}) {
189 9322         30452 $ts->_add_text(decode_entities($_[0]));
190             }
191             else {
192 0         0 $ts->_add_text($_[0]);
193             }
194             }
195             }
196              
197 9756         31132 @res;
198             } # end text
199              
200             sub parse {
201 333     333 1 6375 my $self = shift;
202 333 100       761 $self->_reset_state unless $self->{_parsing};
203 333   100     725 $self->{_parsing} ||= 1;
204 333         1829 $self->SUPER::parse(@_);
205             }
206              
207             sub eof {
208 23     23 1 1235 my $self = shift;
209 23         57 $self->{_parsing} = 0;
210 23         179 $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 1148 my $self = shift;
245 10 50       50 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 33 my $self = shift;
254 14         57 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         26 @{$self->{_ts_sequential}};
  14         831  
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   313 my($self, @args) = @_;
297              
298 111         172 ++$self->{_cdepth};
299 111         144 ++$self->{_in_a_table};
300              
301 111         167 my $depth = $self->{_cdepth};
302              
303             # Table tag attributes, if present
304 111   50     253 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         217 my $pts = $self->current_table;
311              
312             # Counts are tracked for each depth.
313 111         162 my $counts = $self->{_counts};
314 111 100       268 $counts->[$depth] = -1 unless defined $counts->[$depth];
315 111         151 ++$counts->[$depth];
316 111         148 my $count = $counts->[$depth];
317              
318             $self->_emsg("TABLE: cdepth $depth, ccount $count, it: $self->{_in_a_table}\n")
319 111 50       258 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         151 my $umbrella = 0;
324 111 100 66     580 if (! defined $self->{depth} && ! defined $self->{count} &&
      66        
      66        
325             ! $self->{attribs} && ! $self->{headers}) {
326 30         34 ++$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         750 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       242 if (!$umbrella) {
353 81         150 $tsparms{tdepth} = $self->{depth};
354 81         132 $tsparms{tcount} = $self->{count};
355 81         147 $tsparms{tattribs} = $self->{attribs};
356 81         138 $tsparms{headers} = $self->{headers};
357             }
358              
359             # Abracadabra
360 111         584 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         208 push(@{$self->{_tablestack}}, $ts);
  111         219  
365              
366 111         423 $ts;
367             }
368              
369             sub _exit_table {
370 111     111   148 my $self = shift;
371 111         200 my $ts = $self->current_table;
372              
373             # Last ditch fix for HTML mangle
374 111 50       274 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       235 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         283 $ts->_grid_map();
385              
386 111 100       263 $self->_capture_table($ts) if $ts->_check_triggers;
387              
388             # Restore last table state
389 111         155 pop(@{$self->{_tablestack}});
  111         187  
390 111         164 --$self->{_in_a_table};
391 111         214 my $lts = $self->current_table;
392 111 100       228 if (ref $lts) {
393 54         111 $self->{_cdepth} = $lts->{depth};
394             }
395             else {
396             # Back to the top level
397 57         99 $self->{_cdepth} = -1;
398             }
399             $self->_emsg("LEAVE: cdepth: $self->{_cdepth}, ccount: $ts->{count}, it: $self->{_in_a_table}\n")
400 111 50       956 if $self->{debug} >= 2;
401             }
402              
403             sub _capture_table {
404 89     89   172 my($self, $ts, $type) = @_;
405 89 50       199 croak "Table state ref required\n" unless ref $ts;
406 89 50       218 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       166 $ts->tree(HTML::ElementTable->new_from_tree($ts->tree)) if TREE();
413 89 100       209 if ($self->{subtables}) {
414 11         13 foreach my $child (@{$ts->{children}}) {
  11         25  
415 8 100       18 next if $child->{captured};
416 6         16 $self->_capture_table($child, 'subtable');
417 6         9 $child->{slice_columns} = 0;
418 6         8 $child->{keep_headers} = 1;
419 6         9 $child->{headers} = '';
420             }
421             }
422 89         140 $ts->{captured} = 1;
423 89         314 $self->{_tables}{$ts->{depth}}{$ts->{count}} = $ts;
424 89         122 push(@{$self->{_ts_sequential}}, $ts);
  89         176  
425             }
426              
427             sub current_table {
428 30668     30668 1 34919 my $self = shift;
429 30668         34506 $self->{_tablestack}[$#{$self->{_tablestack}}];
  30668         49848  
430             }
431              
432             sub _reset_state {
433 46     46   75 my $self = shift;
434 46         83 $self->{_cdepth} = -1;
435 46         103 $self->{_tablestack} = [];
436 46         106 $self->{_tables} = {};
437 46         90 $self->{_ts_sequential} = [];
438 46         80 $self->{_counts} = [];
439 46         87 $self->{_in_a_table} = 0;
440 46         77 $self->{_parsing} = 0;
441 46 50       104 $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   73 use strict;
  11         18  
  11         227  
458 11     11   43 use Carp;
  11         16  
  11         37746  
459              
460             *TREE = *HTML::TableExtract::TREE;
461              
462             sub new {
463 111     111   169 my $that = shift;
464 111   33     425 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         755 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         354 $self->{_rastamon} = HTML::TableExtract::Rasterize->make_rasterizer();
486 111         232 bless $self, $class;
487              
488 111         540 my %parms = @_;
489              
490             # Depth and Count -- this is the absolute address of the table.
491 111 50       262 croak "Absolute depth required\n" unless defined $parms{depth};
492 111 50       231 croak "Count required\n" unless defined $parms{count};
493 111 50       237 croak "Counts required\n" unless defined $parms{counts};
494              
495 111         404 foreach (keys %parms) {
496 1767         2489 $self->{$_} = $parms{$_};
497             }
498              
499             # Register lineage
500 111         210 my $pts = $self->{parent_table};
501 111   100     471 $self->lineage($pts || undef);
502 111 100       218 push(@{$pts->{children}}, $self) if ($pts);
  54         107  
503 111         188 delete $self->{parent_table};
504              
505 111         336 $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   13951 my($self, @res) = @_;
513 9312         12787 my $row = $self->{grid}[-1];
514 9312         10368 my $item;
515 9312 50 33     20620 if (@res && ref $res[0]) {
516 0         0 $item = $res[0];
517             }
518             else {
519 9312         10331 my $scalar_ref;
520 9312         11288 $item = \$scalar_ref;
521             }
522 9312         18082 push(@$row, $item);
523             }
524              
525             sub _gridalias {
526 3     3   9516 my $self = shift;
527 3   66     15 $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   154 my $self = shift;
534 111         199 my $template = $self->_rasterizer->();
535 111         198 my $grid = $self->{grid};
536             # drop empty rows
537 111 50       234 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         549 @$grid = grep(@$_, @$grid);
544 111         265 foreach my $r (0 .. $#$template) {
545 984         1195 my $row = $grid->[$r];
546 984         1150 my $trow = $template->[$r];
547 984 50       1701 $self->_emsg("Flesh row $r ($#$row) to $#$trow\n") if $self->{debug} > 1;
548 984         1393 foreach my $c (0 .. $#$trow) {
549 9342 0       15082 print STDERR $trow->[$c] ? '1' : '0' if $self->{debug} > 1;
    50          
550 9342 100       13294 if ($trow->[$c]) {
551 9312 50       14942 if (! defined $row->[$c]) {
552 0         0 $row->[$c] = \undef;
553             }
554 9312         10585 next;
555             }
556             else {
557 30         33 my $scalar;
558 30         49 splice(@$row, $c, 0, \$scalar);
559             }
560             }
561 984 50       1694 print STDERR "\n" if $self->{debug} > 1;
562 984 50       2008 croak "row $r splice mismatch: $#$row vs $#$trow\n"
563             unless $#$row == $#$trow;
564             }
565 111         173 $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   4 my $self = shift;
572 2         4 my $grid = $self->{grid};
573 2         5 my $template = $self->_rasterizer->();
574 2         4 my(@gridalias, @translation);
575 2         5 $gridalias[$_] = [@{$grid->[$_]}] foreach 0 .. $#$grid;
  14         29  
576 2         6 foreach my $r (0 .. $#gridalias) {
577 14         17 my $row = $gridalias[$r];
578 14         21 foreach my $c (0 .. $#$row) {
579 56   100     96 my $tcell = $template->[$r][$c] || next;
580 36         52 my($rspan, $cspan) = @$tcell;
581 36         51 foreach my $rs (0 .. $rspan-1) {
582 42         52 foreach my $cs (0 .. $cspan-1) {
583 56         84 $gridalias[$r + $rs][$c + $cs] = $grid->[$r][$c];
584 56         122 $translation[$r + $rs][$c + $cs] = "$r,$c";
585             }
586             }
587             }
588             }
589 2         4 $self->{translation} = \@translation;
590 2         12 $self->{gridalias} = \@gridalias;
591             }
592              
593             ### Constraint tests
594              
595             sub _check_dtrigger {
596             # depth
597 81     81   135 my $self = shift;
598 81 100       355 return 1 unless defined $self->{tdepth};
599 15 100       71 $self->{tdepth} == $self->{depth} ? 1 : 0;
600             }
601              
602             sub _check_ctrigger {
603             # count
604 74     74   105 my $self = shift;
605 74 100       387 return 1 unless defined $self->{tcount};
606             return 1 if (exists $self->{counts}[$self->{depth}] &&
607 11 100 66     83 $self->{tcount} == $self->{counts}[$self->{depth}]);
608 7         34 return 0;
609             }
610              
611             sub _check_atrigger {
612             # attributes
613 67     67   140 my $self = shift;
614 67 100       98 return 1 unless scalar keys %{$self->{tattribs}};
  67         414  
615 15 50       18 return 0 unless scalar keys %{$self->{attribs}};
  15         44  
616 15         17 my $a_hit = 1;
617 15         21 foreach my $attrib (keys %{$self->{tattribs}}) {
  15         33  
618 15 100       34 if (! defined $self->{attribs}{$attrib}) {
619 8         12 $a_hit = 0; last;
  8         10  
620             }
621 7 100       16 if (! defined $self->{tattribs}{$attrib}) {
622             # undefined, but existing, target attribs are wildcards
623 1         3 next;
624             }
625 6 100       16 if ($self->{tattribs}{$attrib} ne $self->{attribs}{$attrib}) {
626 2         5 $a_hit = 0; last;
  2         4  
627             }
628             }
629 15 50 33     44 $self->_emsg("Matched attributes\n") if $self->{debug} > 3 && $a_hit;
630 15         66 $a_hit;
631             }
632              
633             sub _check_htrigger {
634             # headers
635 57     57   90 my $self = shift;
636 57 50       137 return 1 if $self->{umbrella};
637 57 100       200 return 1 unless $self->{headers};
638 46         84 ROW: foreach my $r (0 .. $#{$self->{grid}}) {
  46         103  
639 70         172 $self->_reset_hits;
640 70         153 my $hpat = $self->_header_pattern;
641 70         100 my @hits;
642 70         99 foreach my $c (0 .. $#{$self->{grid}[$r]}) {
  70         173  
643 631         946 my $ref = $self->{grid}[$r][$c];
644 631         733 my $target = '';
645 631         856 my $ref_type = ref $ref;
646 631 50       1096 if ($ref_type) {
647 631 50       1058 if ($ref_type eq 'SCALAR') {
648 631         746 my $item = $$ref;
649 631 50 33     1349 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         852 $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       937 $target = defined $target ? $target : '';
668             $self->_emsg("attempt match on $target ($hpat): ")
669 631 50       1096 if $self->{debug} >= 5;
670 631 100       2933 if ($target =~ $hpat) {
    50          
671 180         390 my $hit = $1;
672 180 50       375 $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         247 my $real_hit;
676 180         216 foreach (sort _header_string_sort keys %{$self->{hits_left}}) {
  180         579  
677 228 100       1387 if ($hit =~ /$_/im) {
678 180         358 delete $self->{hits_left}{$_};
679 180         229 $real_hit = $_;
680 180         340 $hpat = $self->_header_pattern;
681 180         345 last;
682             }
683             }
684 180 50       389 if (defined $real_hit) {
685 180 50       344 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         285 push(@hits, $hit);
690             #
691 180         352 $self->{hits}{$c} = $real_hit;
692 180         213 push(@{$self->{order}}, $c);
  180         316  
693 180 100       213 if (!%{$self->{hits_left}}) {
  180         710  
694             # Successful header row match
695 42         173 ++$self->{head_found};
696 42         77 $self->{hrow_index} = $r;
697 42         74 $self->{hrow} = $self->{grid}[$r];
698 42         129 last ROW;
699             }
700             }
701             }
702             elsif ($self->{debug} >= 5) {
703 0         0 $self->_emsg("0\n");
704             }
705             }
706 28 50 33     79 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         276 $self->{head_found};
715             }
716              
717             sub _check_triggers {
718 111     111   158 my $self = shift;
719 111 100       302 return 1 if $self->{umbrella};
720 81 100 100     213 $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   1155 my $self = shift;
730 984 50       1877 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         1209 ++$self->{rc};
735 984         1143 ++$self->{in_row};
736 984         1096 push(@{$self->{grid}}, [])
  984         1945  
737             }
738              
739             sub _exit_row {
740 984     984   1174 my $self = shift;
741 984 50       1593 if ($self->{in_row}) {
742 984 50       1776 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         1204 $self->{in_row} = 0;
747 984         1352 $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   10541 my $self = shift;
757 9312 50       16554 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       16506 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         10802 ++$self->{cc};
768 9312         10410 ++$self->{in_cell};
769 9312 50       15536 my %attrs = ref $_[1] ? %{$_[1]} : {};
  9312         18870  
770 9312   100     28385 my $rspan = $attrs{rowspan} || 1;
771 9312   100     26242 my $cspan = $attrs{colspan} || 1;
772             }
773              
774             sub _exit_cell {
775 9312     9312   10643 my $self = shift;
776 9312 50       14528 if ($self->{in_cell}) {
777 9312         12698 $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   450 my($self, @headers) = @_;
789             my $str = join('|',
790             map("($_)",
791 250         320 sort _header_string_sort keys %{$self->{hits_left}}
  250         894  
792             ));
793 250         2667 my $hpat = qr/($str)/im;
794 250 50       697 $self->_emsg("HPAT: /$hpat/\n") if $self->{debug} >= 2;
795 250         496 $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 1079 50   1079   9147 if ($a =~ /^$b/) {
    50          
803 0         0 return -1;
804             }
805             elsif ($b =~ /^$a/) {
806 0         0 return 1;
807             }
808             else {
809 1079         2602 return $b cmp $a;
810             }
811             }
812              
813             # Report methods
814              
815 54     54   128 sub depth { shift->{depth} }
816 54     54   128 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   17038 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   214 my $self = shift;
832 165   100     570 $self->{lineage} ||= [];
833 165 100       359 if (@_) {
834 111         157 my $pts = shift;
835 111         163 my(@lineage, $pcoords);
836 111 100       219 if ($pts) {
837 54         141 foreach my $pcoord ($pts->lineage) {
838 20         40 push(@lineage, [@$pcoord]);
839             }
840 54         150 $pcoords = [$pts->depth, $pts->count, $pts->{rc}, $pts->{cc}];
841 54         97 push(@lineage, $pcoords);
842             }
843 111         234 $self->{lineage} = \@lineage;
844             }
845 165         203 @{$self->{lineage}};
  165         294  
846             }
847              
848 36     36   45730 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   62 my $self = shift;
857 36         57 my $alias = shift;
858 36         89 my @ri = $self->row_indices;
859 36         58 my @rows;
860 36 50       87 my $grid = $alias ? $self->_gridalias : $self->{grid};
861 36         68 foreach ($self->row_indices) {
862 218         417 push(@rows, scalar $self->_slice_and_normalize_row($grid->[$_]));
863             }
864 36 50       219 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   101 my $self = shift;
882 78         94 my $start_index = 0;
883 78 100       179 if ($self->{headers}) {
884 38         75 $start_index = $self->hrow_index;
885 38 100       91 $start_index += 1 unless $self->{keep_headers};
886             }
887 78         112 $start_index .. $#{$self->{grid}};
  78         256  
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   21 my $self = shift;
898 6         7 my $r = shift;
899 6 50       9 $r <= $#{$self->{grid}}
  6         16  
900 0         0 or croak "row $r out of range ($#{$self->{grid}})\n";
901             my @row = $self->_slice_and_normalize_row(
902 6         14 $self->{grid}[($self->row_indices)[$r]]
903             );
904 6 50       17 wantarray ? @row : \@row;
905             }
906              
907             sub _slice_and_normalize_row {
908 224     224   259 my $self = shift;
909 224         264 my $rowref = shift;
910 224         249 my @row;
911 224 100 66     597 if ($self->{automap} && $self->_map_makes_a_difference) {
912 128         233 @row = @{$rowref}[$self->column_map];
  128         348  
913             }
914             else {
915 96         216 @row = @$rowref;
916             }
917 224         483 @row = map($self->_cell_to_content($_), @row);
918 224 100       564 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   17 my $self = shift;
933 4         7 my($r, $c) = @_;
934 4         7 my $row = $self->row($r);
935 4 50       10 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
936 4         8 $self->_cell_to_content($row->[$c]);
937             }
938              
939             sub _cell_to_content {
940 877     877   973 my $self = shift;
941 877 50       1472 @_ or croak "cell item required\n";
942 877         944 my $cell = shift;
943 877 100       1440 return $cell unless ref $cell;
944 873 50       1156 return $cell if TREE();
945 873         1683 return $$cell;
946             }
947              
948             sub space {
949 2     2   8 my $self = shift;
950 2         3 my($r, $c) = @_;
951 2         6 my $gridalias = $self->_gridalias;
952 2 50       5 $r <= $#$gridalias
953             or croak "row $r out of range ($#$gridalias)\n";
954 2         4 my $row = $gridalias->[$r];
955 2 50       6 $c <= $#$row or croak "Column $c out of range ($#$row)\n";
956 2         4 $self->_cell_to_content($row->[$c]);
957             }
958              
959             sub source_coords {
960 28     28   30486 my $self = shift;
961 28         50 my($r, $c) = @_;
962 28 50       34 $r <= $#{$self->{translation}}
  28         79  
963 0         0 or croak "row $r out of range ($#{$self->{translation}})\n";
964 28         48 my $row = $self->{translation}[$r];
965 28 50       47 $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   1442 my $self = shift;
971 41         68 $self->{hrow_index};
972             }
973              
974             sub hrow {
975 3     3   14 my $self = shift;
976 3 50 33     13 if ($self->{automap} && $self->_map_makes_a_difference) {
977 3 50       9 return map(ref $_ ? $$_ : $_, @{$self->{hrow}}[$self->column_map]);
  3         35  
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   55797 my $self = shift;
988 419 100       702 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         384 my %order;
994 344         366 foreach (keys %{$self->{hits}}) {
  344         717  
995 1026         1509 $order{$self->{hits}{$_}} = $_;
996             }
997 344         445 return @order{@{$self->{headers}}};
  344         1280  
998             }
999             else {
1000 75         94 return 0 .. $#{$self->{grid}[0]};
  75         173  
1001             }
1002             }
1003              
1004             sub _map_makes_a_difference {
1005 227     227   269 my $self = shift;
1006 227 100       426 return 0 unless $self->{slice_columns};
1007 225         263 my $diff = 0;
1008 225         376 my @order = $self->column_map;
1009 225         543 my @sorder = sort { $a <=> $b } @order;
  1022         1407  
1010 225 50       450 ++$diff if $#order != $#sorder;
1011 225 100       294 ++$diff if $#sorder != $#{$self->{grid}[0]};
  225         475  
1012 225         393 foreach (0 .. $#order) {
1013 597 100       1109 if ($order[$_] != $sorder[$_]) {
1014 131         180 ++$diff;
1015 131         188 last;
1016             }
1017             }
1018 225         702 $diff;
1019             }
1020              
1021             sub _add_text {
1022 9322     9322   15082 my($self, $txt) = @_;
1023 9322         11760 my $r = $self->{rc};
1024 9322         10981 my $c = $self->{cc};
1025 9322         11109 my $row = $self->{grid}[$r];
1026 9322         10437 ${$row->[$c]} .= $txt;
  9322         17067  
1027 9322         14414 $txt;
1028             }
1029              
1030             sub _reset_hits {
1031 70     70   100 my $self = shift;
1032 70 50       162 return unless $self->{headers};
1033 70         152 $self->{hits} = {};
1034 70         153 $self->{order} = [];
1035 70         103 foreach (@{$self->{headers}}) {
  70         146  
1036 254         451 ++$self->{hits_left}{$_};
1037             }
1038 70         101 1;
1039             }
1040              
1041 9425     9425   17066 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   161 my $pkg = shift;
1089 111         171 my(@grid, @row_spinner, @col_spinner);
1090 111         151 my $empty_row_offset = 0;
1091             sub {
1092 9425 100   9425   16985 return \@grid unless @_;
1093 9312         13166 my($row_num, $rspan, $cspan) = @_;
1094 9312 100       16923 $rspan = 1 unless $rspan > 1;
1095 9312 100       16089 $cspan = 1 unless $cspan > 1;
1096 9312         11156 my($rspin_propogate, $row_added);
1097 9312         12262 my $trigger = $#grid + $empty_row_offset;
1098 9312 100       17135 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       1962 if ($row_num - $trigger > 1) {
1103 0         0 $empty_row_offset += $row_num - $trigger - 1;
1104             }
1105             # add new row
1106 984         1112 $row_added = 1;
1107 984         1130 my @new_row;
1108             # first add new row spinner
1109 984 50 33     2219 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         1587 push(@row_spinner, $cspan - 1);
1115             }
1116             # spin columns
1117 984         1573 foreach (@col_spinner) {
1118 8382 100       11535 if ($_) {
1119 15         20 push(@new_row, 0);
1120 15         23 --$_;
1121             }
1122             else {
1123 8367         11257 push(@new_row, undef);
1124             }
1125             }
1126 984 100       1999 @new_row = (undef) unless @new_row;
1127 984         1767 push(@grid, \@new_row);
1128             }
1129 9312         11203 my $current_row = $grid[-1];
1130             # locate next available cell in row
1131 9312         10068 my $col;
1132 9312         16929 foreach my $ci (0 .. $#$current_row) {
1133 49821 100       85173 if (! defined $current_row->[$ci]) {
1134 8463         9697 $col = $ci;
1135 8463         11245 last;
1136             }
1137             }
1138 9312 100       16287 if (! defined $col) {
1139 849         1564 ADDCOL: while (! defined $col) {
1140             # if no cells were available, add a column
1141 849         1298 foreach my $ri (0 .. $#grid) {
1142 849         1043 my $row = $grid[$ri];
1143 849         993 my $cspan_count = $row_spinner[$ri];
1144 849 50       1332 if (!$cspan_count) {
1145 849         1579 push(@$row, undef);
1146             }
1147             else {
1148 0         0 push(@$row, 0);
1149 0         0 --$row_spinner[$ri];
1150             }
1151             }
1152 849         1204 push(@col_spinner, $col_spinner[-1]);
1153 849         1306 foreach my $ci (0 .. $#$current_row) {
1154 4989 100       8304 if (! defined $current_row->[$ci]) {
1155 849         943 $col = $ci;
1156 849         1220 last ADDCOL;
1157             }
1158             }
1159             }
1160 849 50       1838 $col_spinner[-1] = $rspan - 1 if $col == $#$current_row;
1161 849         1198 $row_spinner[$#grid] = $cspan - 1;
1162             }
1163              
1164             # we now have correct coordinates for this element
1165 9312         17604 $current_row->[$col] = [$rspan, $cspan];
1166 9312         12975 $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       17035 if ($col < $#$current_row) {
1171 7482         8758 my $offset = 1;
1172 7482         8799 my $row_span = $col_spinner[$col];
1173 7482 100 100     28782 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         1108 $row_spinner[-1] = 0;
1178             }
1179 7482         15393 while ($offset < $cspan) {
1180 15         21 my $cursor = $col + $offset;
1181 15         20 $current_row->[$cursor] = 0;
1182 15         20 $col_spinner[$cursor] = $row_span;
1183 15         16 ++$offset;
1184 15 100       40 if ($col + $offset > $#$current_row) {
1185 3         4 $row_spinner[-1] = $cspan - $offset;
1186 3         4 last;
1187             }
1188             }
1189             }
1190              
1191 9312 50       16387 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         13404 return \@grid;
1212             }
1213 111         1023 }
1214              
1215             }
1216              
1217             ##########
1218              
1219             {
1220              
1221             package HTML::TableExtract::StripHTML;
1222              
1223 11     11   77 use vars qw(@ISA);
  11         20  
  11         473  
1224              
1225 11     11   58 use HTML::Parser;
  11         17  
  11         2070  
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__