Question

Imagine an HTML page that is a report with repetitive structure:

<html>
  <body>
    <h1>Big Hairy Report Page</h1>

    <div class="customer">
      <div class="customer_id">001</div>
      <div class="customer_name">Joe Blough</div>
      <div class="customer_addr">123 That Road</div>
      <div class="customer_city">Smallville</div>
      <div class="customer_state">Nebraska</div>
      <div class="order_info">
        <div class="shipping_details">
          <ul>
             <li>Large crate</li>
             <li>Fragile</li>
             <li>Express</li>
          </ul>
        </div>
        <div class="order_item">Deluxe Hoodie</div>
        <div class="payment">35.95</div>
        <div class="order_id">000123456789</div>
      </div>
      <div class="comment">StackOverflow rocks!</div>
    </div>

   <div class="customer">
     <div class="customer_id">002</div>
 ....  and so forth for a list of 150 customers

This kind of report page appears often. My goal is to extract each customer's related information into some reasonable data structure using HTML::TreeBuilder::XPath.

I know to do the basics and get the file read into $tree. But how can one concisely loop through that tree and get associated clusters of information per each customer? How, for example, would I create a list of address labels sorted by customer number based on this information? What if I want to sort all my customer information by state?

I'm not asking for the whole perl (I can read my file, output to file, etc). I just need help understanding how to ask HTML::TreeBuilder::XPath for those bundles of related data, and then how to dereference them. If it's easier to express this in terms of an output statement (i.e., Joe Blough ordered 1 Deluxe Hoodie and left 1 comment) then that's cool, too.

Thank you very much for those of you who tackle this one, it seems a bit overwhelming to me.

Was it helpful?

Solution

This will do what you need.

It starts by pulling all the <div class="customer"> elements into array @customers and extracting the information from there.

I have taken your example of the address label, sorted by the customer number (by which I assume you mean the field with class="customer_id"). All of the address values are pulled from the array into the hash %customers, keyed by the customer ID and the name of the element class. The information is then printed in the order of the ID.

use strict;
use warnings;

use HTML::TreeBuilder::XPath;

my $tree = HTML::TreeBuilder::XPath->new_from_file('html.html');

my @customers = $tree->findnodes('/html/body/div[@class="customer"');

my %customers;
for my $cust (@customers) {
  my $id = $cust->findvalue('div[@class="customer_id"]');
  for my $field (qw/ customer_name customer_addr customer_city customer_state /) {
    my $xpath = "div[\@class='$field']";
    my $val = $cust->findvalue($xpath);
    $customers{$id}{$field} = $val;
  }
}

for my $id (sort keys %customers) {

  my $info = $customers{$id};

  print "Customer ID $id\n";
  print $info->{customer_name}, "\n";
  print $info->{customer_addr}, "\n";
  print $info->{customer_city}, "\n";
  print $info->{customer_state}, "\n";
  print "\n";
}

output

Customer ID 001
Joe Blough
123 That Road
Smallville
Nebraska

OTHER TIPS

use HTML::TreeBuilder::XPath;
...

my @customers;
my $tree = HTML::TreeBuilder::XPath->new_from_content( $mech->content() );

foreach my $customer_section_node ( $tree->findnodes('//div[ @class = "customer" ]') ) {

   my $customer = {};
   $customer->{id} = find_customer_id($customer_section_node);
   $customer->{name} = find_customer_name($customer_section_node);
   ...
   push @customers, $customer;
}

$tree->delete();

sub find_customer_id {
    my $node = shift;

    my ($id) = $node->findvalues('.//div[ @class = "customer_id" ]');
    return $id
}

I'll use XML::LibXML since it's faster and I'm familiar with it, but it should be pretty straightforward to convert what I post to from XML::LibXML to HTML::TreeBuilder::XPath if you so desire.

use XML::LibXML qw( );

sub get_text { defined($_[0]) ? $_[0]->textContent() : undef }

my $doc = XML::LibXML->load_html(...);

my @customers;
for my $cust_node ($doc->findnodes('/html/body/div[@class="customer"]')) {
   my $id   = get_text( $cust_node->findnodes('div[@class="customer_id"]') );
   my $name = get_text( $cust_node->findnodes('div[@class="customer_name"]') );
   ...
   push @customers, {
      id   => $id,
      name => $name,
      ...
   };
}

Actually, given the regularity of the data, you don't have to hardcode the field names.

use XML::LibXML qw( );

sub parse_list {
   my ($node) = @_;
   return [
      map parse_field($_),
       $node->findnodes('li')
   ];
}

sub parse_field {
   my ($node) = @_;
   my @children = $node->findnodes('*');
   return $node->textContent() if !@children;
   return parse_list($children[0]) if $children[0]->nodeName() eq 'ul';
   return {
      map { $_->getAttribute('class') => parse_field($_) }
       @children
   };
}

{
   my $doc = XML::LibXML->load_html( ... );
   my @customers =
      map parse_field($_),
       $doc->findnodes('/html/body/div[@class="customer"]');

   ...
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top