jeudi 24 mars 2011

BeautifulSoup - comment extraire ou manipuler une page html en Python

Introduction
Dans la vie d'un programmeur, il peut être un jour nécessaire d'extraire des informations depuis une page web.
Mais tout le monde n'a pas envie de se prendre la tête avec le format HTML ni envie de savoir exactement comment cela fonctionne.
Si vous avez seulement besoin d'extraire des informations depuis le Web, Beautiful Soup est le module qu'il vous faut.

Beautiful Soup est un parser HTML/XML pour Python qui permet de créer un arbre à partir d'un document HTML même si ce dernier contient des incohérences dans les balises.
Beautiful Soup offre des possibilités de navigation idéomatique simple, il est également possible de faire des recherches et des modifications de document HTML.

Beautiful Soup fait partie de cette catégorie d'outil qui permet de d'épargner de nombreuses et laborieuses heures/journées de travail.

Beautiful Soap stocke uniquement des chaines de caractère Unicode.
Une méthode de détection heuristique est utilisé par beautiful soup pour déterminé le type d'encodage du document, ce qui permet son décodage et stockage en unicode en mémoire. Il n'est donc pas nécessaire de se prendre la tête avec la problématique de conversion lorsque l'on veut extraire des informations depuis un site japonais :-)

Beautiful Soup et XML
Jusqu'en février 2011, Beautiful Soup n'était pas vraiment un outil approprié pour traité des document XML... de l'aveu même du concepteur, Beautiful Soup ne fonctionnait pas très bien avec XML. Il était alors conseillé d'utiliser la classe BeautifulStoneSoup.
Cependant, depuis Beautiful Soup 4, l'auteur du package à fait le nécessaire pour utiliser invariablement BeautifulSoup pour l'HTML et XML. Pour obtenir une instance de BeautifulSoup spécialisé dans le traitement XML, il suffit alors de coder BeautifulSoup(markup, "xml"). Sinon, par défaut, BeautifulSoup considerera qu'il s'agit d'un document HTML. Il est aussi possible d'utiliser la notation explicite BeautifulSoup(markup, "html" )

Documentation
La documentation de Beautiful Soup est concise et regorge d'exemples.
Une large variété de cas y sont abordés.

http://www.crummy.com/software/BeautifulSoup/documentation.html

Installation

sudo apt-get install python-beautifulsoup

Note 22/03/2011: la version actuelle de Beautiful Soup installée sur Ubuntu 10.10 est la 3.1.0.1.
Il est possible de consulter la version à l'aide de BeautifulSoup.__version__

Les limites de BeautifulSoup
Dans un premier temps, j'ai voulu rédiger cet exemple en partant de l'article "HttpLib: Extraction de page Web avec Python"
Pour rappel, cet exemple chargeait cette page du site Amazon.

Malheureusement, BeautifulSoup n'a pas réussi à charger le document de 193 Kb à cause d'une erreur de parsing.

HTMLParser.HTMLParseError: malformed start tag, at line 4439, column 757

Après vérification, je dois avouer que moi aussi je m'y emmelerais les pinceaux.
Le nombre de surcharge de quotes dans le script est assez important et manque cruellement d'élégance et de clareté!
Une preuve vivante des préceptes de BeautifulSoup: "sur le WEB, peu de développeurs respectent les standards... alors BeautifulSoup fait au mieu".

Je me suis d'ailleurs permis de reprende la section en erreur (en raccourcissant un peu les URLs).
<script>
  registerImage("original_image", "http://XXXXXXSL500_AA240_.jpg",
"<a href="+'"'+"http://www.amazon.fr/gp/ref=dp_image_0?ie=UTF8&n=301061&s=books"+'"'+
" target="+'"'+"AmazonHelp"+'"'+" onclick="+'"'+
"return amz_js_PopWin(this.href,'AmazonHelp','width=700,height=600');"+
'"'+"  ><img onload="+'"'+"if (typeof uet == 'function') { uet('af'); }"+'"'+
" src="+'"'+"http://XXXXX_SL500_AA240_.jpg"+'"'+" id="+'"'+"prodImage"+'"'+
"  width="+'"'+"240"+'"'+" height="+'"'+"240"+'"'+"   border="+'"'+"0"+'"'+
" alt="+'"'+"Le Secret de l'enclos du Temple"+'"'+" onmouseover="+'"'+""+'"'+
" /></a>", "<br /><a href="+'"'+
"http://www.amazon.fr/gp/XXXXXref=dp_image_text_0?ie=UTF8&n=301061&s=books"+'"'+
" target="+'"'+"AmazonHelp"+'"'+" onclick="+'"'+
"return amz_js_PopWin(this.href,'AmazonHelp','width=700,height=600');"+'"'+
"  >Agrandissez cette image</a>", "");
  var ivStrings = new Object();
</script>
Pas des plus lisibles, n'est ce pas!

Je vais donc opter pour un autre site, par exemple un blog de Blogspot.

Exemple 1: Décortiquer du Html
le but de cet exemple est de naviguer dans le document pour y retrouver toutes les balises h3 (qui contiennent les titres des différents articles).

>>> import httplib

>>> # Lecture du contenu HTML
>>> domainName = "domeu.blogspot.com"
>>> uri = "/"
>>> conn = httplib.HTTPConnection( domainName )
>>> conn.request( "GET", uri )
>>> r1 = conn.getresponse()
>>> print( "%s - %s" % (r1.status, r1.reason) )
200 - OK
>>> htmlData = r1.read()

>>> # parsing Html
>>> import BeautifulSoup
>>> soup = BeautifulSoup.BeautifulSoup( htmlData )

>>> # Parcours des titres
>>> for title in soup.findAll( 'h3' ):
...     print( title )
... 
<h3 class="post-title entry-title">
<a href="http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html">ELF - Executable And Linking Format</a>
</h3>

>>> # Décortiquer le contenu
>>> firstTitle = soup.findAll( 'h3' )[0]
>>> firstTitle.contents
[u'\n', <a href="http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html">ELF - Executable And Linking Format</a>, u'\n']

>>> firstTitleLink = firstTitle.find( "a" )
>>> firstTitleLink
<a href="http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html">ELF - Executable And Linking Format</a>
>>> firstTitleLink.get( "href" )
u'http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html'
>>> firstTitleLink.contents
[u'ELF - Executable And Linking Format']
>>> # autre facon de lire les attributs
... 
>>> firstTitleLink["href"]
u'http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html'

Exemple 2: des recherches avancées
Suivant la structure de la page, il est aussi possible de trouver les libellés des titres en localisant les balises <a> ayant une référence vers "http://domeu.blogspot.com".

BeautifulSoup permet de faire ce genre de recherche grâce à une recherche de lien stricte (l'url doit être complète et donc inefficace dans notre cas) ou grâce a une recherche basée sur une expression régulière (ce qui conviendra).

Lecture par lien stricte
>>> liens = soup.findAll( href = "http://domeu.blogspot.com" )
>>> for lien in liens:
...     print( "%s (url: %s )" % ( lien.contents, lien["href"] ) )

Lecture par expression régulière
Retrouver tous les liens commençant par "http://domeu.blogspot.com/", suivit de 4 chiffres, suivit de "/" et finalement suivit de 2 chiffres.
NB: les valeurs numériques sont une fois identifiés à l'aide de \d et l'autre fois de [0-9]. {4} et {2} étant le nombre d'itérations respectivement attendu.

>>> liens = soup.findAll( href = re.compile( "^http://domeu.blogspot.com/\d{4}/[0-9]{2}" ))
>>> len( liens )
65

Cela retourne 65 liens, il faut donc affiner la recherche sur les balise <a> n'ayant pas de class.

Lecture via les attributs
S'il est nécessaire de contrôler plusieurs attributs durant la recherche, il est possible comparer les attributs avec un dictionnaire de valeurs.
Dans le cas présent, l'on recherche tous les tags ayant un attribut href correspondant au critère précédement défini ET un attribut class NON DEFINI (NB: pour un attribut class de valeur quelconque on aurait utilisé True au lieu de None)

>>> liens = soup.findAll( attrs = { 'href' : re.compile("^http://domeu.blogspot.com/\d{4}/[0-9]{2}"), 'class' : None } )
>>> len(liens)
58
>>> ''.join( [ str(lien.contents) for lien in liens ]  ) # using comprehension list
"[u'ELF - Executable And Linking Format'][u'Installer Excel sur Ubuntu'][u'Ubuntu: Compatibilit\\xe9 des imprimantes'][u'HttpLib: Extraction de page Web avec Python'][u'Monitorer les processus sous Ubuntu'][u'Aide m\\xe9moire des raccourcis clavie...

Autres exemples
La documentation de Beautiful Soup (http://www.crummy.com/software/BeautifulSoup/documentation.html) regorge d'exemple.

Modification de code html
Beautiful Soup ne permet pas seulement de naviguer et extraire des informations depuis un document html, il permet aussi de modifier des attributs, d'ajouter et de retirer des tag d'un document existant.

Modification d'attribut
En repartant des exemples précédents, il est par exemple possible d'ajouter un style à tous les liens en ajoutant une CSS Class aux liens.
>>> for lien in liens:
...     lien["class"]="yellowLink"

Effacement de noeud
Le terme employé dans Beautiful Soup est une extraction.
Je vais donc enlever tous le liens que nous avons trouvés (tag <a>)

>>> for lien in liens:
...     lien.extract().encode('utf-8') # voir note sur erreur d'encodage
>>> # vérification de l'effacement des liens
>>> # NB: un précédent exemple avait ajouté la classe CSS yellowLink
>>> liens2 = soup.findAll( attrs = { 'href' : re.compile("^http://domeu.blogspot.com/\d{4}/[0-9]{2}") , 'class' : 'yellowLink' } )
>>> len( liens2 )
0

Création et manipulation de noeuds
Toujours en repartant des précédents exemples, je vais récupérer les liens et modifier la structure du troisième lien qui me tombe sous la main.

>>> soup = BeautifulSoup.BeautifulSoup( htmlData )
>>> liens = soup.findAll( "a" )
>>> len( liens )
242
>>> liens[3]
<a href="http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html">ELF - Executable And Linking Format</a>
>>> lien = liens[3]
>>> lien
<a href="http://domeu.blogspot.com/2011/03/elf-executable-and-linking-format.html">ELF - Executable And Linking Format</a>
>>> # Create a new Tag
>>> tag = BeautifulSoup.Tag( soup, "span", [("class","linkySpan"),("id","testSpan001")] )
>>> tag
<span class="linkySpan" id="testSpan001"></span>
>>> tag.contents = lien.contents
>>> tag
<span class="linkySpan" id="testSpan001">ELF - Executable And Linking Format</span>
>>> lien.replaceWith(tag) 
>>> len( soup.findAll( "a" ) )
3 
>>> #Oups!!! mais ou sont donc passés tous mes liens !

Après l'opération lien.replaceWith, je remarque qu'il ne reste presque plus de lien dans le document!
J'imagine que j'ai fais une mauvaise opération ayant eu des répercusion sur le document...

UnicodeEncodeError - l'erreur d'encodage
Dans l'exemple ci-dessois, l'on remarque clairement l'usage de encode('utf-8') lors de l'extraction (effacement) des noeuds.

for lien in liens:
lien.extract().encode('utf-8')

Cela peu sembler assez anormal comme utilisation pourtant comme les instructions sont exécutées depuis la console Python, l'exécution de
lien.extract()
extrait le noeud et retourne une référence vers l'objet à l'interpréteur de la console.
La console ayant reçue une référence, elle va donc essaye d'afficher le contenu de l'objet.
Comme la console est un périphérique ascii, que le noeud contient de l'unicode et probablement des caractères n'ayant pas d'équivalent ASCII, cela se termine généralement par une erreur d'encode et une interruption d'exécution !
Exemple de message d'erreur:

"UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 125: ordinal not in range(128)"

En demandant explicitement l'encodage en 'utf-8', l'object devient affichable à la console... et par conséquent l'exécution peut se poursuivre sans heurt.

UnicodeDammit: charger et convertir facilement n'importe quel document vers unicode
Si BeautifulSoup est clairement axé sur la manipulation des documents HTML, il contient aussi quelques classes qui peuvent se montrer fort utile dans de nombreux domaines.

C'est le clas de la classe UnicodeDammit qui essaye de détecter l'encodage d'un document et qui le converti en Unicode (classe qui est utilisée par Beautiful Soup lui même).
Il est donc possible d'utiliser directement UnicodeDammit sans BeautifulSoup pour pouvoir charger, soit même, des documents dont on veut inspecter le contenu.
UnicodeDammit permet de s'affranchir de la phase de détection du type d'encodage et de la conversion du contenu.
N'oubliez cependant pas d'utiliser encode('utf-8') lorsque vous voulez inspecter le contenu dans une console.

Pour plus d'information, voir la section "Beautiful Soup Gives You Unicode, Dammit" de la documentation de Beautiful Soup

Aucun commentaire: