Perl Brasil

Pesquisar

Documentação

Artigos

Planeta

Eventos


FISL 10 - Software Livre, a tecnologia que liberta

Comunidade

r5 - 05 Dec 2008 - AndreCarneiro

Como fazer parsing de HTML

Problema

Extrair informacões de um arquivo HTML

Solução

use HTML::TreeBuilder

Discussão

Você poderia usar uma simples expressão regular para extrair informacões de um arquivo HTML. Mas, porque fazer isso se existe um módulo que permite ir diretamente à informacão desejada?

O HTML::TreeBuilder faz o parsing utilizando HTML::Parser e outros módulos, e devolve um objeto, que na verdade é um conjunto de objetos HTML::Element organizados em árvore. Se você estiver familiarizado com uma estrutura de árvore, saberá que cada nó de uma árvore pode ter nós, ramos e nós-filhos, e é exatamente disso que a estrutura de um objeto HTML::TreeBuider trata.

Dado uma tabela escrita em HTML:

<table id='table_001' class='t1'>
    <tr>
                   <td id='td_tituloprod_001'>Titulo da Imagem1</td>
                   <td id='td_imagem_001'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
    </tr>
</table>

No código HTML acima, o nó raiz é o HTML todo. Mas suponhamos que eu quisesse a informação que está em um dos 'tds', por exemplo, como fazer isso com HTML::TreeBuider?

Imagine uma árvore de cabeça para baixo, onde a raíz é um objeto que representa o documento HTML inteiro, e os nós abaixo desse objeto, também são objetos que representam as tags HTML do documento. Primeiro, devemos obter o objeto que representa o 'nó-raiz'. Para isso, é necessário instanciar a classe HTML::TreeBuider, usando o construtor 'new_from_content', passando como parâmetro a string com o código HTML. Depois, pode-se acessar qualquer nó abaixo do nó raiz. Observem o código abaixo:

#! /usr/bin/perl
use strict;
use warnings;
use HTML::TreeBuilder;

my $htmlcode = q{
<html>
    <head><title>test</title></head>
    <body>
    <table id='table_001' class='t1'>
        <tr>
            <td id='td_tituloprod_001'>Titulo da Imagem1</td>
            <td id='td_imagem_001'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
        <tr>
            <td id='td_tituloprod_002'>Titulo da Imagem2</td>
            <td id='td_imagem_002'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
    </table>
}; 

#Abaixo, eu obtenho o objeto que representa o código HTML inteiro, instanciando o objeto HTML::Treebuilder, passando a string com o código HTML para o construtor.
my $tree = HTML::TreeBuilder->new_from_content($htmlcode);

#Capturando a informação do td
my $td = $tree->look_down( _tag => 'td', id => 'td_tituloprod_001' );

#Obtendo a informação desejada, pode ser, por exemplo, o texto que está dentro do primeiro td da tabela.
my $texto = $td->as_text if $td;
print "$texto\n"; # imprime 'Titulo da Imagem 1'

Repare que nesse caso, eu fui direto no nó que representa o código HTML

 '<td id='td_tituloprod_001'> 
Titulo da Imagem1', através do método 'look_down'. Esse método, permite que eu olhe a partir de um nó, para os seus filhos, ou seja, se você lembrar da imagem da árvore de cabeça para baixo, eu na verdade estou dizendo ao objeto para olhar para os objetos 'abaixo'... Existe o método look_up também, mas pra facilitar, vamos nos atentar em look_down.

Para o problema que estou tentando te explicar, look_down foi feito a partir do objeto '$tree', ou seja, a partir do nó raiz da árvore. Isso significa que eu poderia ter alcançado qualquer outra tag(ou nó) da árvore, já que é o nó raiz. Pois bem, uma vez que eu guardei esse objeto em $td - lembre-se:

'my $td  = $tree->look_down(_tag => 'td',id=>'td_tituloprod_001');'
, agora eu tenho em '$td' um objeto que representa a tag
 '<td id='td_tituloprod_001'>Titulo da Imagem1</td>' 
. Repare que, a partir de '$td', seguindo o conceito de árvore, eu não consigo chegar a nó nenhum abaixo dele(porque não existe nenhuma tag dentro da 'td'), com o método look_down, pois já cheguei na 'folha' da árvore, ou seja, não há mais ninguém abaixo.

É a mesma coisa se vc for pensar em diretórios, suponha que eu tenha a seguinte estrutura:

perl
   |
   ->rules
               |
               ->the
                   |
                   ->world

foo
|
->subfoo

Uma vez que eu tenha percorrido os diretórios até 'world', não há mais nada abaixo dele, portanto não tem como acessar alguma coisa abaixo dele. Se eu quisse acesar o diretório 'subfoo', a partir de world, teria que subir os diretórios para depois descer novamente e chegar em subfoo. HTML::TreeBuider funciona da mesma forma. Transferindo a analogia do sistema de diretórios para as tags HTML, teria-se o seguinte:

table id='table_001' class='t1'
|
->tr
    |
    ->td id='td_tituloprod_001'>Titulo da Imagem1

No entanto, existem uma diferença importante. Diferentemente da analogia com os diretórios, não necessariamente eu tenho que percorrer todos os nós um a um para chegar no meu objetivo, desde que o nó que se quer alcançar, esteja abaixo do nó onde se está no momento, ou seja, para alcançar o meu td, eu não tive que passar por 'tr', para depois ir a td. Posso ir direto ao 'td' que quero alcançar, porque no caso estava no nó raiz do objeto.

A chave para se entender isso é que toda a vez que eu uso um método look_down, eu obtenho sempre um objeto HTML::Element, onde eu posso usar look_down(ou qualquer método de HTML::Element, exemplifiquei usando look_down ), novamente para obter outro objeto HTML::Element e assim por diante. Mas isso só funciona quando eu busco informações que estão sempre em tags(nós) cada vez mais internas. Eu não consigo por exemplo, com o método look_down, acessar de dentro de uma td, por exemplo, algo que está numa div fora da td. A idéia é ter a hierarquia que o HTML provê representado(encapsulado) dentro do objeto e, obviamente com todas as informações acessíveis através de métodos. O que HTML::TreeBuider faz é agregar todas as ferramentas de HTML::Element, com o parser de HTML::Parser, de uma forma mais 'clean', ou seja, sem precisar ficar instanciando todas essas classes.

Outra coisa importante, é que você pode colocar tudo em contexto de array, por exemplo, se eu quisesse, nesse código capturar todos os tds, eu chamaria o look_down dessa maneira:

my @tds = $tree->look_down(_tag => 'td');

Agora que tenho uma lista com todos os tds, eu posso recuperar a informação de cada td simplesmente iterando com 'foreach', por exemplo:


foreach my $td(@tds){
       if($td){
              my $text = $td->as_text;
       }
}

Para recuperar os atributos, considere novamente o segmento de código HTML do exemplo:

    <table id='table_001' class='t1'>
        <tr>
            <td id='td_tituloprod_001'>Titulo da Imagem1</td>
            <td id='td_imagem_001'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
        <tr>
            <td id='td_tituloprod_002'>Titulo da Imagem2</td>
            <td id='td_imagem_002'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
    </table>

Em alguns casos, eu posso precisar de algum atributo como 'id', ou 'class', por exemplo. HTML::TreeBuider incorpora(ou agrega, se preferir), um método muito útil para isso, que é o método 'attr', que retorna o valor de um atributo de um nó(que representa uma tag, nesse caso, uma td). Como exemplo, vou capturar todos os 'ids' das tds em uma tabela, segundo o código abaixo:


#! /usr/bin/perl
use strict;
use warnings;
use HTML::TreeBuilder;

my $htmlcode = q{
<html>
    <head><title>test</title></head>
    <body>
    <table id='table_001' class='t1'>
        <tr>
            <td id='td_tituloprod_001'>Titulo da Imagem1</td>
            <td id='td_imagem_001'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
        <tr>
            <td id='td_tituloprod_002'>Titulo da Imagem2</td>
            <td id='td_imagem_002'><img src='http://www.algumhost.com.br/imagens/imagem_001.jpg'></td>
        </tr>
    </table>
}; 

#Abaixo, eu obtenho o objeto que representa o código HTML inteiro, instanciando o objeto HTML::Treebuilder, passando a string com o código HTML para o construtor.
my $tree = HTML::TreeBuilder->new_from_content($htmlcode);

#Recuperando todos os tds. 
my @tds = $tree->look_down(_tag => 'td');
my @ids = ();
foreach my $td(@tds){
    if($td){
         push @ids, $td->attr('id');
   }
}

Pode-se precisar ainda, apenas dos tds que tem as imagens, por exemplo:


#Recuperando todos os tds. 
my @tds = $tree->look_down(_tag => 'td',sub{$_[0]->attr('id') =~ m{td_imagem}});
my @ids = ();
foreach my $td(@tds){
    if($td){
         push @ids, $td->attr('id');
   }
}

Na linha

 my @tds = $tree->look_down(_tag => 'td',sub{$_[0]->attr('id') =~ m{td_imagem}}); 
o artifício que eu usei está, inclusive, documentado na classe HTML::Element. É um artifício extremamente útil, quando os atributos que você busca não tem um padrão(o que é muito comum de se ver em códigos HTML), ou quando se tem um atributo cujo nome varia, como no caso do código HTML que estamos tratando. Na verdade essa é uma daquelas 'magias negras' que tanto se falam em perl. em $_[0] eu tenho o objeto HTML::Element que representa o meu 'td'. Portanto se ele é um objeto HTML::Element, eu posso usar o método attr. O 'pulo do gato' está em usar o modificador 'sub' para executar a expressão regular que me retornará true caso exista um match com o que eu estou procurando, no caso 'td_imagem'. Para maiores informações, façam RTFM em HTML::Element.

Com isso concluo a explicação sobre parsing de HTML usando HTML::TreeBuilder. Existem muitos outros recursos disponíveis, mas com esses que eu expliquei já é possível resolver a maioria dos problemas que se possa ter com parsing de HTML.

Agradeço mais uma vez ao Nelson Ferraz, que inclusive me sugeriu para postar esse artigo no perlBR, a minha querida lista de São Paulo, que é um orgulho para a comunidade Perl mundial, devo ressaltar, onde sempre busco inspiração, e informação para meus trabalhos, e a todos que participam e colaboram com a comunidade do software livre, seja ela(a contribuição) do tamanho que for.

Dúvidas/Sugestões - <andregarciacarneiro@gmail.com>

Comentários