PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Parser du HTML avec BeautifulSoup

jeudi 23 janvier 2014 à 23:46

Ceci est un post invité de k3c posté sous licence creative common 3.0 unported.

Un exemple de parsing HTML avec BeautifulSoup.

Cet article ne traitera pas l’écriture ou la modification de HTML, et pompera allègrement la doc BeautifulSoup (traduite).

De manière générale, pour télécharger une vidéo sur un site de replay, il faut

Prenons un exemple sur les replays de d8.tv, par exemple

une vidéo de D8

(attention cet exemple sera rapidement obsolète, mais c’est le principe qui nous intéresse)

L’installation de BeautifulSoup 4 se fait avec, au choix

$ apt-get install python-bs4
$ easy_install beautifulsoup4
$ pip install beautifulsoup4

La documentation de BeautifulSoup 4 est à

http://www.crummy.com/software/BeautifulSoup/bs4/doc/

Si on regarde le code source de la page (CTRL U sous Firefox, sinon voyez avec votre navigateur préféré), on voit que la partie qui nous intéresse et qui contient videoId est courte

Ici l’identifiant recherché est 943696

En Python, on va donc faire quelque chose comme

from urllib2 import urlopen
import bs4 as BeautifulSoup
html = urlopen('http://www.d8.tv/d8-series/pid6654-d8-longmire.html').read()
soup = BeautifulSoup.BeautifulSoup(html)

Comme le dit la doc BeautifulSoup

début du pompage de la doc BeautifulSoup

Beautiful Soup transforme un document HTML complexe en un arbre complexe d’objets Python. Mais vous aurez à manipuler seulement quatre types d’objets : Tag, NavigableString, BeautifulSoup, et Comment.

fin du pompage de la doc BeautifulSoup

On peut faire un

print soup.prettify()

pour voir à quoi ressemble le code HTML de la page

Il faut d’abord analyser la page et rechercher ce qui suit videoId

Pour commencer nous allons naviguer dans le document.

BeautifulSoup permet de multiples syntaxes, par exemple, on n’est pas obligé de donner le chemin complet

soup.head.meta

ou

soup.meta

affichent le meme résultat, vu que la première balise meta est sous la balise head

<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>

Si on regarde les méthodes disponibles

dir(soup.meta)
['FORMATTERS', '__call__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__doc__', '__eq__', '__format__', '__getattr__', '__getattribute__', '__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_all_strings', '_attr_value_as_string', '_attribute_checker', '_find_all', '_find_one', '_lastRecursiveChild', '_last_descendant', 'append', 'attribselect_re', 'attrs', 'can_be_empty_element', 'childGenerator', 'children', 'clear', 'contents', 'decode', 'decode_contents', 'decompose', 'descendants', 'encode', 'encode_contents', 'extract', 'fetchNextSiblings', 'fetchParents', 'fetchPrevious', 'fetchPreviousSiblings', 'find', 'findAll', 'findAllNext', 'findAllPrevious', 'findChild', 'findChildren', 'findNext', 'findNextSibling', 'findNextSiblings', 'findParent', 'findParents', 'findPrevious', 'findPreviousSibling', 'findPreviousSiblings', 'find_all', 'find_all_next', 'find_all_previous', 'find_next', 'find_next_sibling', 'find_next_siblings', 'find_parent', 'find_parents', 'find_previous', 'find_previous_sibling', 'find_previous_siblings', 'format_string', 'get', 'getText', 'get_text', 'has_attr', 'has_key', 'hidden', 'index', 'insert', 'insert_after', 'insert_before', 'isSelfClosing', 'is_empty_element', 'name', 'namespace', 'next', 'nextGenerator', 'nextSibling', 'nextSiblingGenerator', 'next_element', 'next_elements', 'next_sibling', 'next_siblings', 'parent', 'parentGenerator', 'parents', 'parserClass', 'parser_class', 'prefix', 'prettify', 'previous', 'previousGenerator', 'previousSibling', 'previousSiblingGenerator', 'previous_element', 'previous_elements', 'previous_sibling', 'previous_siblings', 'recursiveChildGenerator', 'renderContents', 'replaceWith', 'replaceWithChildren', 'replace_with', 'replace_with_children', 'select', 'setup', 'string', 'strings', 'stripped_strings', 'tag_name_re', 'text', 'unwrap', 'wrap']

on voit que de nombreuses méthodes sont disponibles, et suivant la doc, on peut donc le traiter comme un dictionnaire

>>> soup.meta['http-equiv']
'Content-Type'

et tester quelques méthodes

>>> soup.meta.name
'meta'
>>> soup.meta.find_next_sibling()
'<meta content="D8" name="author"/>'
soup.meta.find_previous_sibling()

Nous voyons que soup.meta a un sibling (frère ou soeur) suivant, mais pas de précédent, c’est le premier de l’arborescence.
Bon, la balise meta a pour nom meta, pas un scoop, on continue avec les clés de dictionnaire, sans surprise

>>> soup.meta.find_next_sibling()
'<meta content="D8" name="author"/>'
>>> soup.meta.find_next_sibling()['content']
'D8'
>>> soup.meta.find_next_sibling()['name']
'author'

Pour le fun, regardez ce que renvoie

soup.meta.find_next_sibling().parent

et

soup.meta.find_next_sibling().parent.parent

et je vous laisse deviner la prochaine commande que vous allez passer…

Revenons à une recherche qui va trouver de nombreuses occurences

soup.find('div')

va trouver la première balise div, et

soup.findall('div')

va renvoyer une liste contenant tous les div de la page, mais cela ne permet pas de trouver facilement la portion contenant videoId, par contre, la documentation de BeautifulSoup montre comment trouver spécifiquement une CSS class, voir
searching by css class dans la doc BeautifulSoup
dans la documentation BeautifulSoup
Voici la syntaxe à utiliser

soup.find('div',attrs={"class":u"block-common block-player-programme"})

va renvoyer la partie qui nous intéresse, par exemple

        <div class="block-common block-player-programme">
 
            <div class="bpp-player">
                <div class="playerVideo player_16_9">
 
		<div class="itemprop" itemprop="video" itemscope itemtype="http://schema.org/VideoObject">
			<h1>Vidéo : <span itemprop="name">Longmire - Samedi 30 novembre à 20h50</span></h1>
			<meta itemprop="duration" content="" />
			<meta itemprop="thumbnailUrl" content="http://media.canal-plus.com/wwwplus/image/53/1/1/LONGMIRE___BANDE_ANNONCE__131120_UGC_3279_image_L.jpg" />			
 
			<meta itemprop="embedURL" content="http://player.canalplus.fr/embed/flash/CanalPlayerEmbarque.swf?vid=975153" />
			<meta itemprop="uploadDate" content="2013-11-29T00:00:00+01:00" />
			<meta itemprop="expires" content="2014-02-18T00:00:00+01:00" />
 
    <canal:player videoId="975153" width="640" height="360" id="CanalPlayerEmbarque"></canal:player>

Le type de cette donnée est bs4 élément tag

Comme l’a dit un homme célèbre
si vous ne savez pas ce que contient une variable, vous ne comprenez pas le programme
on peut donc faire un type, dir, help, doc, repr, par exemple

>>> type(soup.find('div',attrs={"class":u"block-common block-player-programme"}))
class 'bs4.element.Tag'

donc nous pouvons rechercher un tag, comme
canal:player

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player')
'<canal:player height="360" id="CanalPlayerEmbarque" videoid="786679" width="640"></canal:player>'
>>> soup.findAll('div', attrs={"class":u"tlog-inner"})

renvoie une liste

[<div class="tlog-inner">
<div class="tlog-account">
<span class="tlog-avatar"><img height="30" src="http://media.canal-plus.com/design/front_office_d8/images/xtrans.gif" width="30"/></span>
<a class="tlog-logout le_btn" href="#">x</a>
</div>
<form action="#" method="post">
<label class="switch-fb">
<span class="cursor traa"> </span>
<input checked="" id="check-switch-fb" name="switch-fb" type="checkbox" value="1"/>
</label>
</form>
<div id="headerFbLastActivity">
<input id="name_facebook_user" type="hidden"/>
<div class="top-arrow"></div>
<div class="top">
<div class="top-bg"></div>
<div class="top-title">Activité récente</div>
</div>
<div class="middle">
<div class="wrap-last-activity">
<div class="entry">Aucune</div>
</div>
<div class="wrap-notification"></div>
</div>
<div class="bottom">
<a class="logout" href="#logout">Déconnexion</a>
</div>
</div>
</div>]

On peut prendre le premier élément de cette liste

soup.findAll('div', attrs={"class":u"tlog-inner"})[0]

et ne vouloir que la ligne commençant par “span class”

soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span

ce qui affiche

<span class="tlog-avatar"><img height="30" src="http://media.canal-plus.com/design/front_office_d8/images/xtrans.gif" width="30"/></span>

Voyons le type de donnée

>>> type(soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span)
<class 'bs4.element.Tag'>

et voyons les méthodes disponibles

>>> dir(soup.findAll('div', attrs={"class":u"tlog-inner"})[0].span)
['FORMATTERS', '__call__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__doc__', '__eq__', '__format__', '__getattr__', '__getattribute__',
'__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__'
, '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_all_strings', '_attr_value_as_string', '_attribute_checker', '_find_all',
'_find_one', '_lastRecursiveChild', '_last_descendant', 'append', 'attribselect_re', 'attrs', 'can_be_empty_element', 'childGenerator', 'children', 'clear', 'contents',
 'decode', 'decode_contents', 'decompose', 'descendants', 'encode', 'encode_contents', 'extract', 'fetchNextSiblings', 'fetchParents', 'fetchPrevious', 'fetchPreviousSi
blings', 'find', 'findAll', 'findAllNext', 'findAllPrevious', 'findChild', 'findChildren', 'findNext', 'findNextSibling', 'findNextSiblings', 'findParent', 'findParents
', 'findPrevious', 'findPreviousSibling', 'findPreviousSiblings', 'find_all', 'find_all_next', 'find_all_previous', 'find_next', 'find_next_sibling', 'find_next_sibling
s', 'find_parent', 'find_parents', 'find_previous', 'find_previous_sibling', 'find_previous_siblings', 'format_string', 'get', 'getText', 'get_text', 'has_attr', 'has_k
ey', 'hidden', 'index', 'insert', 'insert_after', 'insert_before', 'isSelfClosing', 'is_empty_element', 'name', 'namespace', 'next', 'nextGenerator', 'nextSibling', 'ne
xtSiblingGenerator', 'next_element', 'next_elements', 'next_sibling', 'next_siblings', 'parent', 'parentGenerator', 'parents', 'parserClass', 'parser_class', 'prefix',
'prettify', 'previous', 'previousGenerator', 'previousSibling', 'previousSiblingGenerator', 'previous_element', 'previous_elements', 'previous_sibling', 'previous_sibli
ngs', 'recursiveChildGenerator', 'renderContents', 'replaceWith', 'replaceWithChildren', 'replace_with', 'replace_with_children', 'select', 'setup', 'string', 'strings'
, 'stripped_strings', 'tag_name_re', 'text', 'unwrap', 'wrap']

Nous voulons maintenant juste ce qui suit videoId.

dir(soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player'))

montre, entre autres choses, que la méthode get est disponible.
Pour récupérer l’identifiant qui nous intéresse, on peut donc faire

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player').get('videoid')
'975153'

ou utiliser une autre syntaxe

>>> soup.find('div',attrs={"class":u"block-common block-player-programme"}).find('canal:player')['videoid']
'975153'

De la même manière, on peut récupérer le titre de la vidéo

>>> soup.find('h3',attrs={"class":u"bpp-title"})
'<h3 class="bpp-title">Longmire - Samedi 30 novembre à 20h50</h3>'

mais on veut juste le titre, donc

>>> soup.find('h3',attrs={"class":u"bpp-title"}).text
uu'Longmire - Samedi 30 novembre \xe0 20h50'

Maintenant que l’on a le numéro de la vidéo, on peut le passer au site qui contient l’adresse, et avec un peu de scripting XML, récupérer l’adresse de la vidéo (un autre article sera consacré au scripting XML)
Selon que la vidéo vient de D8 ou de canal, elle sera sur
vidéo de d8
ou
vidéo de Canal Plus
et avec un peu de code

from lxml import objectify
def get_HD(d8_cplus,vid):
    root = objectify.fromstring(urlopen('http://service.canal-plus.com/video/rest/getVideosLiees/'+d8_cplus+'/'+vid).read())
    for x in root.iter():
        if x.tag == 'VIDEO' and x.ID.text == vid:
            for vres in vidattr:
                if hasattr(x.MEDIA.VIDEOS, vres):
                    print 'Resolution :', vres
                    videoUrl = getattr(x.MEDIA.VIDEOS, vres).text
                break
        break
    print videoUrl
for x in ['d8','cplus']:
    get_HD(x,vid)

on peut trouver l’adresse de la vidéo.
Il reste juste à envoyer la commande rtmpdump, dans ce cas

rtmpdump -r rtmp://ugc-vod-fms.canalplus.fr/ondemand/videos/1311/LONGMIRE___BANDE_ANNONCE__131120_UGC_3279_video_HD.mp4 -c 1935 -m 10 -B 1 -o mavideo.mp4

Voilà, il reste à noter que BeautifulSoup peut restreindre sa recherche à une partie du document, utiliser une regex (même si c’est le mal), on peut limiter la taille de la liste renvoyée par findAll

Quelles sont les méthodes les plus utiles, si vous avez la flemme de lire toute la doc ?

flattr this!