{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Parseurs\n", "\n", "Dans ce notebook nous utiliserons le parseur [lxml](http://lxml.de/) qui est un binding de libxml2 et [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parser de l'html\n", "\n", "Beautiful Soup nous permet de parser simplement du contenu html. Même si le contenu est mal formé, le module bs reconstitue un arbre et offre des fonctions faciles à utiliser pour parcourir l'arbre ou y rechercher des éléments. \n", "Beautiful Soup n'est pas un parseur, il utilise les parseurs et offre une API simplifiée à ses utilisateurs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nous travaillerons directement avec du contenu en ligne. Fini les exercices bidons, cette fois nous allons nous confronter à une question essentielle : combien d'accordages *open tuning* Neil Young utilise et comment sont-ils répartis dans son oeuvre ? \n", "On trouve les infos sur les chansons de Neil Young et les accordages sur le fabuleux site [songx.se](http://songx.se/index.php) (le site ayant changé d'interface, nous utiliserons une archive de [wayback archive](http://web.archive.org/))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Avec le module `requests` [que nous avons déjà utilisé](./python-4.ipynb), nous allons pouvoir instancier un objet Beautiful Soup sans trop d'efforts" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import requests\n", "from bs4 import BeautifulSoup\n", "\n", "url = \"http://web.archive.org/web/20180430090903/http://songx.se/index.php\" # le lien vers le site\n", "html = requests.get(url) # on récupère le contenu\n", "soup = BeautifulSoup(html.text, 'lxml') # on crée un objet pour traiter la page" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Voilà nous avons maintenant un objet `soup` de classe [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#beautifulsoup). \n", "La [doc](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) est très claire." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# je cherche l'élement avec le tag 'title'\n", "print(soup.title)\n", "# le tag de l'élément\n", "print(soup.title.name)\n", "# le contenu textuel de l'élément\n", "print(soup.title.string)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Les informations qui nous intéressent sont contenues dans des éléments comme celui-ci (formatté pour la lisibilité) : \n", "\n", "```html\n", "
\n", " Clementine\n", " (cover)\n", "
EADGBE
\n", "
\n", "```\n", "\n", "Où on trouve 1. le nom de la chanson ('Clementine') et l'accord utilisé ('EADGBE')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Affichez les titres et les tunings des 10 premières chansons en utilisant la méthode [find_all](https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find-all). La méthode renvoie un iterable." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for item in soup.find_all([...]):\n", " print(item.[...], item.[...])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Créez un `dict` appelé `tunings` qui classe les chansons par tuning" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# notre structure de données résultat\n", "# un dictionnaire avec en clé l'accordage et en valeur la liste des chansons qui utilisent cet accordage\n", "tunings = {}\n", "for item in soup.find_all([...]):\n", " song_title = item.[...]\n", " tuning = item.[...]\n", " [...]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "'Harvest Moon' utilise l'accordage DADGBE, y en a-t'il d'autres ?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print([...])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Affichez les accordages et le nombre de chansons pour chaque accordage, le tout trié par nombre de chansons décroissant. Pour cela, cous utiliserez la méthode `sorted` ainsi que l'argument mot-clé `key` (des exemples [ici](https://wiki.python.org/moin/HowTo/Sorting#Key_Functions)) :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for tuning in sorted(tunings.keys(), key=[...]):\n", " print([...])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Allez hop un histogramme" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%matplotlib notebook\n", "import matplotlib.pyplot as plt\n", "values = [len(tunings[x]) for x in tunings]\n", "values\n", "plt.bar(range(0, len(values)), values)\n", "plt.xticks(range(0, len(values)), tunings.keys(), rotation=17)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercice 1 : trouvez l'exception**\n", "\n", "Une chanson a un tuning un peu particulier comparé aux autres (indice, c'est une question de taille). Trouvez le tuning qui est différent des autres et donnez la chanson qui lui correspond. _Attention_, \"b\" signifie \"bémol\", il ne s'agit pas d'une note pour l'accordage (contrairement à A, B, C, D, E, F et G) !" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parser de l'xml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nous allons travailler sur un fichier au format [TEI](http://www.tei-c.org/) extrait du corpus *Corpus 14* \n", "PRAXILING - UMR 5267 (PRAXILING) (2014). Corpus 14 [Corpus]. ORTOLANG (Open Resources and TOols for LANGuage) - www.ortolang.fr, https://hdl.handle.net/11403/corpus14/v1. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le fichier se nomme ``josephine-1-150119.xml``. Il s'agit d'une lettre d'une femme de soldat à son époux. \n", "Nous allons extraire du fichier TEI les informations suivantes : \n", "- titre (``/TEI/teiHeader/fileDesc/titleStmt/title``)\n", "- source (``/TEI/teiHeader/fileDesc/sourceDesc/p``)\n", "- contenu de la lettre (``/TEI/text/body``)\n", "\n", "Vous pouvez trouver des indications sur les éléments de la TEI [ici](http://www.tei-c.org/release/doc/tei-p5-doc/fr/html/) (ça pourra être utile pour les questions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Avec lxml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pourquoi `lxml` et pas `xml.etree.ElementTree` ? Parce que : [1](http://lxml.de/intro.html) et surtout [2](http://lxml.de/performance.html) \n", "La bonne nouvelle c'est que votre code sera aussi compatible avec `xml.etree.ElementTree` ou `xml.etree.cElementTree` parce que xml utilise l'API ElementTree. Sauf pour la méthode `xpath` qui est propre à `libxml`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from lxml import etree\n", "tree = etree.parse('josephine-1-150119.xml')\n", "root = tree.getroot()\n", "\n", "# Parcours des enfants de la racine (commentaires et éléments)\n", "for child in root:\n", " print(child.tag)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Avec des requêtes ElementPath" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Le fichier utilise l'[espace de nom](https://fr.wikipedia.org/wiki/Espace_de_noms_XML) TEI : ````, nous devrons l'indiquer dans nos instructions de recherche. \n", "Nous pouvons récupérer un élément particulier qui correspond à un chemin : par exemple, pour récupérer le header TEI de dont le chemin est `/TEI/teiHeader`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# la méthode find renvoie le premier élément qui correspond au chemin argument (ElementPath et non Xpath)\n", "header = root.find(\"./tei:teiHeader\", namespaces={'tei':\"http://www.tei-c.org/ns/1.0\"})\n", "print(header)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Récupérez le titre et affichez son tag XML ainsi que son contenu textuel (`/TEI/teiHeader/fileDesc/titleStmt/title`)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "title = root.find(\"[...]\", namespaces={'tei':\"http://www.tei-c.org/ns/1.0\"})\n", "print(\"Tag : {}\".format(title.tag))\n", "print(\"Texte : {}\".format(title.text))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Idem pour la source (élément `sourceDesc`) :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "source = root.find(\"[...]\", namespaces={'tei':\"http://www.tei-c.org/ns/1.0\"})\n", "print(\"Tag : {}\".format(source.tag))\n", "print(\"Texte : {}\".format(source.text))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Avec des requêtes xpath" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "lxml a aussi une méthode [xpath](https://lxml.de/xpathxslt.html) qui permet d'utiliser directement des [expressions xpath](https://www.w3schools.com/xml/xpath_syntax.asp) (sans oublier les espaces de noms pour notre fichier) :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "source = root.xpath(\"/tei:TEI/tei:teiHeader/tei:fileDesc/tei:sourceDesc/tei:p\", namespaces={'tei':'http://www.tei-c.org/ns/1.0'})\n", "print(type(source)) #xpath retourne une liste\n", "print(source[0].text)\n", "#ou bien\n", "source = root.xpath(\"/tei:TEI/tei:teiHeader/tei:fileDesc/tei:sourceDesc/tei:p/text()\", namespaces={'tei':'http://www.tei-c.org/ns/1.0'})\n", "print(source[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pour le contenu il faut ruser. La difficulté ici tient à l'utilisation d'élements `` de type [milestones](http://www.tei-c.org/release/doc/tei-p5-doc/fr/html/CO.html#CORS5) pour noter les retours à la ligne : \n", "```xml\n", "

\n", "je reponse a ton aimableux lettres\n", "que nous a fait plaisir en naprenas\n", "que tu et enbonne santes car il\n", "anais de maime pour nous\n", "

\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Récupérez dans un premier temps l'ensemble des balises `

` en utilisant la méthode [findall](http://effbot.org/zone/element.htm#searching-for-subelements). la méthode `findall` renvoie une liste avec tous les éléments correspondant au chemin argument." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "body = root.findall(\"./[...]\", namespaces={'tei':\"http://www.tei-c.org/ns/1.0\"})\n", "for elem in body: # tout le texte ne s'affichera pas, c'est normal !\n", " print(elem.text)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ici on ne récupère que les noeuds text précédant les éléments ``.\n", "\n", "Utilisez la fonction `xpath` pour récupérer tous les noeuds text du corps de la lettre. Vous intégrerez dans votre requête la fonction `text` (vue un peu plus haut) dans votre chemin xpath (vous pouvez _aussi_ fouiller [par ici](https://lxml.de/xpathxslt.html) pour avoir de la documentation supplémentaire)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "body = root.xpath(\"[...]\", namespaces={'tei':\"http://www.tei-c.org/ns/1.0\"})\n", "for text in body:\n", " print(text, end=\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercice 2**\n", "\n", "Écrivez une requête xpath pour récupérer tous les éléments raturés de la lettre de Joséphine." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## avec DOM" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "L'API `ElementTree` est propre à Python, `DOM` ([le site officiel](https://www.w3.org/DOM/) et [des informations en français](https://developer.mozilla.org/fr/docs/Web/API/Document_Object_Model)) est une API indépendante d'un langage de programmation. Il existe des implémentations `DOM` dans la plupart des langages de programmation modernes. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from xml.dom import minidom\n", "dom = minidom.parse(\"josephine-1-150119.xml\")\n", "# l'objet Document\n", "dom" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "title = dom.getElementsByTagNameNS(\"http://www.tei-c.org/ns/1.0\", 'title')[0] # un seul élément 'title' dans le document\n", "print(title) # title est un objet Element, pour accèder au contenu textuel il faut récupérer le noeud texte\n", "print(title.lastChild.nodeName)\n", "print(title.lastChild.nodeValue)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "idem pour la source, sauf qu'on ne peut pas se permettre de rechercher tous les éléments `p`. \n", "Il faut trouver l'élément `p` fils de `sourceDesc`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sourceDesc = dom.getElementsByTagNameNS(\"http://www.tei-c.org/ns/1.0\", 'sourceDesc')[0]\n", "for node in sourceDesc.childNodes:\n", " if node.localName == \"p\":\n", " print(node.lastChild.nodeValue)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Et maintenant le contenu et ses éléments milestones. \n", "Pour garder la forme vous réécrirez les boucles `for` suivies de `if` en listes en intension." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "body = dom.getElementsByTagNameNS(\"http://www.tei-c.org/ns/1.0\", 'body')[0]\n", "for node in body.childNodes:\n", " if node.localName == \"p\" or \"opener\":\n", " for in_node in node.childNodes:\n", " if in_node.nodeName == \"#text\":\n", " print(in_node.nodeValue, end=\"\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 1 }