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
- récupérer un identifiant de la vidéo
- le passer à un autre site
- analyser le résultat pour trouver l’adresse de la vidéo.
Prenons un exemple sur les replays de d8.tv, par exemple
(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.
- Tag
Un objet Tag correspond à un tag HTML ou XML dans le document
Les Tags ont de nombreux attributs et méthodes, que nous verrons dans
naviguer dans l’arborescence
et
chercher dans l’arborescencePour l’instant, les caractéristiques les plus importantes d’un tag sont
son nom>>> tag.name u'b'
ses attributs
Un tag peut avoir n’importe quel nombre d’attributs. Le tag<b class="boldest">
possède un attribut “class” dont la valeur est “boldest”. On peut accéder les attributs d’un tag en le traitant comme un dictionaire :
>>> tag['class'] u'boldest'
On peut accéder directement ce dictionaire avec .attrs:
>>>tag.attrs {u'class': u'boldest'}
- NavigableString
Une chaîne de caractères est un peu de texte l’intérieur d’un tag. Beautiful Soup utilise la classe NavigableString class pour contenir ces morceaux de texte:>>> tag.string u'Extremely bold' >>> type(tag.string) class 'bs4.element.NavigableString'
Un NavigableString est comme une chaîne de caractères Python Unicode, sauf que elle supporte aussi quelques unes des caractéristiques décrites dans Navigating the tree et Searching the tree. Vous pouvez convertir une chaîne de caractères NavigableString en Unicode avec unicode():
>>> unicode_string = unicode(tag.string) >>> unicode_string u'Extremely bold' >>> type(unicode_string) type 'unicode'
-
BeautifulSoup
L’objet BeautifulSoup lui-même représente le document dans son ensemble. Dans la plupart des cas, vous pouvez le traiter en tant qu’objet Cela signifie qu’il supporte la plupart des méthodes décrites dans Navigating the tree et Searching the tree.Comme l’objet BeautifulSoup ne correspond pas à un tag final HTML ou XML, il n’a pas de nom et pas d’attributs. Mais il est parfois utile de rechercher son .name, donc on lui a donné le .name “[document]”:
>>> soup.name u'[document]'
- Commentaires et autres chaînes de caractères spéciales
Tag, NavigableString, et BeautifulSoup couvrent presque tout ce que vous verrez dans un fichier HTML ou XML, mais il y a quelques cas à part. Le seul qui doit vous inquiéter (un peu) est le commentaire :
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
>>> soup = BeautifulSoup(markup) >>> comment = soup.b.string >>> type(comment) class 'bs4.element.Comment'
L’objet Comment est simplement un type spécial de NavigableString:
>>> comment u'Hey, buddy. Want to buy a used parser'
Mais quand il apparaît en tant que morceau de document HTML, un Comment est affiché avec un formattage spécial :
# <b> # <!--Hey, buddy. Want to buy a used parser?--> # </b>
Beautiful Soup définit des classes pour n’importe quoi d’autre qui apparaîtrait dans un document XML : CData, ProcessingInstruction, Declaration, et Doctype. De la meme manière que Comment, ces classes sont des sous-classes de NavigableString qui ajoutent quelque chose à la chaîne de caractères. Voici un exemple qui remplace le commentaire par un block CDATA :
from bs4 import CData cdata = CData("A CDATA block") comment.replace_with(cdata) print(soup.b.prettify())
# <b> # <![CDATA[A CDATA block]]> # </b>
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.find_all('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 ?
- .contents, les enfants d’un tag sont disponibles dans une liste appelée .contents
- .has_key(‘value’) vous rendra de grands services, associé parfois à
['value'] != u''
- pour extraire un tag sans attribut, par exemple pour un tag p, la syntaxe sera simplement
soup.findAll('p', {'class': None})
- .attrs vous affichera un dictionnaire qui peut être intéressant, par exemple :
>>>soup.head.link '<link href="http://media.canal-plus.com/design_pack/front_office_d8/css/d8.d25540b7a93dba7baf89e5ca53ef00e5.min.css" rel="stylesheet" type="text/css"/>' >>> soup.head.link.attrs {'href': 'http://media.canal-plus.com/design_pack/front_office_d8/css/d8.d25540b7a93dba7baf89e5ca53ef00e5.min.css', 'type': 'text/css', 'rel': ['stylesheet']}
- .previousSibling (et .next_sibling) peut être utile, par exemple, avec un simple HTML comme ce qui suit
<div class="category_link"> Category: <a href="/category/personal">Personal</a> </div>
on peut récupérer la chaîne Category : de plusieurs manières, par exemple l’évident
>>> soup.findAll('div')[0].contents[0] u'\n Category:\n '
mais aussi en remontant depuis la balise a
>>> soup.find('a').previousSibling u'\n Category:\n '
Sinon cela est aussi utile avec un HTML mal foutu comme
<p>z1</p>tagada <p>z2</p>tsointsoin
Dans ce cas, pour récupérer tagada tsointsoin
on fera par exemplesoup.findAll('p')[0].next_sibling soup.findAll('p')[1].next_sibling
ou, pour faire plaisir à Sam/Max
>>> [p.next_sibling for p in soup.findAll('p')] [u'tagada', u'tsoitsoin']
- A quoi sert text=True ?
Simplement à récupérer juste du texte dans du Html, sans toute la syntaxe HTML.
Un exemple, toujours sur la vidéo de D8 va illustrer cela>>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a '<a class="tmlogin-btn" href="#">' '<span>Se connecter</span>' '</a>' >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.contents [u'\n', <span>Se connecter</span>, u'\n'] >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.text u'\nSe connecter\n' >>> soup.findAll('div', {"class":"tmlog-wdrw wdrw"})[0].a.findAll(text=True) [u'\n', u'Se connecter', u'\n']
- Une dernière chose, si vous avez besoin de parser des documents de plusieurs centaines de Mo et donc de performances, oubliez le parser HTML par défaut dans BeautifulSoup, et installez le rapide lxml ou un autre parser. Si comme moi vous traitez des documents “petits”, ça n’a pas d’importance.
Petit bug dans l’URL de la vidéo D8 au tout début (hhttp :-)
“possède un attribut “class” dont la valeur est “boldest”. On peut accéder les attributs d’un tag en le traitant comme un dictionaire :”
Manque
>>> tag[‘class’]
Merci à vous deux, c’est corrigé !
Elles ne sont pas fragmentées les videos dans la VOD de D8 ? Parce que la commande rtpmdump me renvoie qu’une portion…
La vidéo de D8 est une bande-annonce, donc courte. En matière de replay, on ne sait pas récupérer uniquement les vidéos de M6 qui utilisent du Flash Access (Protected Http Dynamic Streaming). La plupart des sites de replay se téléchargent avec rtmpdump ou AdobeHDS.php, certains encore plus simples, avec juste un wget/curl/msdl. Les sites étrangers demandent un proxy en général uniquement pour trouver l’adresse de la vidéo, ensuite la commande à passer n’a pas besoin de proxy.
Merci pour la réponse k3c ;)
En fait c’est carrément la bande annonce que je n’arrive pas à récupérer, ça bloque à 2.60% chez moi avec un retour erreur
Download may be incomplete (downloaded about 2.60%), try resuming
L’argument -e n’y fait rien…j’ai toujours galèrer avec rtmpdump donc je voulais savoir si ça venait de chez moi ou pas (visiblement oui…).
Vérifie si rtmpdump et librtmpdump sont bien installés, et si la version est la même pour les 2 (la 2.4 pour moi). Tu peux aussi faire un test rapide, wget d’une Ubuntu 13.10 32 bits, puis
qemu-img create -f qcow2 Ubuntu_1304_image.img 6G
puis
kvm -m 756 -cdrom Téléchargements/ubuntu-13.04-desktop-i386.iso -boot d Ubuntu_1304_image.img
installation, reboot
kvm -m 756 Ubuntu_1304_image.img
installation de rtmpdum et librtmpdump, et test de la commande rtmpdump donnée dans l’article.
rm du .img quand tu as fini.
J’ai été récemment surpris, alors que je cherchais un moyen sans prise de tête de “downgrader” du HTML en text/plain, que BeautifulSoup n’avait aucun moyen préconçu de faire ça. J’ai fini par utiliser html2text d’Aaron Swartz.
J’ai raté un truc ? (me renvoyez pas vers get_text, ça pue)
Une petite erreur dans l’article, un coup c’est
soup.findall('div')
la fois suivante c’estsoup.findAll('div')
alors que cela devrait etresoup.find_all()
Sinon, un grand merci pour cet article qui vient de me sauver un temps fou.
Corrigé pour findall. Par contre findAll est bien un alias de find_all.
Pourquoi ne pas utiliser lxml?
Ça permettrais de faire des recherches avec xpath, qui est quand même plus simple/souple surtout pour des sites mal foutu (par exemple grandement basé sur des tableau avec peut de class et id).
Parce que lxml ne gère pas les HTML mal formé aussi bien que beautiful soup qui peut avaler des trucs vraiment dégueulasses.