#!/usr/bin/env python # coding: utf-8 # Dans ce billet, nous allons [comme il y a quelques semaines](http://flothesof.github.io/podcast-rendez-vous-avec-X-fr.html), construire un flux RSS pour mon émission radio préférée : *Sur les épaules de Darwin*, animée par l'extraordinaire Jean Claude Ameisen. # # Afin de faire ceci, nous allons partir du [travail de Clément Grimal](http://clementgrimal.fr/darwin/), qui maintient une liste à jour des émissions avec des liens de téléchargement. # # Une question, avant de commencer. Pourquoi faire ce travail, alors que le site de Clément permet déjà d'écouter à volonté ces émissions ? Parce que son site ne permet pas de faire une recherche plein texte sur le contenu des émissions et également parce que j'aime écouter les émissions dans un logiciel de podcast afin de garder un historique de mes écoutes (j'utilise [Podcast Addict](https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en)). # # # **TL;DR** # Le lien vers le flux RSS généré à partir du présent notebook est ici : *https://raw.githubusercontent.com/flothesof/posts/master/files/podcast_Sur_les_epaules_de_Darwin.xml* # Date de dernière mise à jour : *avril 2020*. # # Récupération des liens vers les émissions # Comme je le disais, notre point de départ est le site de Clément Grimal. Nous analysons sa source avec `beautifulsoup4` pour en extraire les éléments qui nous intéressent : # # # - titre de l'émission # - date de diffusion # - lien de téléchargement # - lien vers la page du site France Inter # In[1]: import requests from bs4 import BeautifulSoup base_url = "http://clementgrimal.fr/darwin/" r = requests.get(base_url) r.encoding = 'utf-8' soup = BeautifulSoup(r.text, 'html.parser') # L'objet `soup` nous permet d'extraire les différents éléments de la page web, de manière structurée. On obtient la liste de toutes les émissions de cette manière : # In[2]: all_shows = ([item for item in (li.find('table') for li in soup.findAll('li')) if item]) len(all_shows) # A partir du tag `li` (item de liste en HTML), on peut obtenir le lien vers le fichier mp3 ainsi que le site France Inter, de même que le titre et la date de diffusion. # # Par exemple avec le dernier épisode : # In[3]: show = all_shows[-1] show.find('a', class_='download-link').attrs['href'].strip() # In[4]: show.find('span').find('a').attrs['href'] # In[5]: show.find('span').find('a').text # In[6]: str(show.find('span').find('a').next_sibling)[14:] # Ecrivons maintenant une boucle pour extraire ces informations pour l'intégralité des épisodes : # In[7]: show_data = [] for show in all_shows: download_link = show.find('a', class_='download-link') description_link = show.find('span').find('a').attrs['href'] title = show.find('span').find('a').text date = str(show.find('span').find('a').next_sibling)[14:] if download_link: if download_link.attrs['href'].strip().startswith('.'): download_link = "http://clementgrimal.fr/darwin" + download_link.attrs['href'].strip()[1:] else: download_link = download_link.attrs['href'].strip() show_data.append([download_link, description_link, title, date]) else: print(f"lien de téléchargement indisponible pour l'émission {show.find('a').attrs['href']}") # Construisons une dataframe `pandas` avec ces données : # In[8]: import pandas as pd df = pd.DataFrame(show_data, columns=['lien mp3', 'lien description', 'titre', 'date']) df.head(10) # In[9]: df.describe() # Nous voilà maintenant en possession des liens vers 497 épisodes publiés avec lien de téléchargement sur le site de Clément Grimal. On remarque que certaines d'entre elles sont des rediffusions (394 titres uniques sur 497). # Téléchargeons maintenant les descriptions des émissions, afin de construire un fil RSS avec celles-ci. # # Téléchargement des descriptions à partir du site de France Inter # Nous avons les liens vers la page de chaque émission : si on lit celle-ci, on peut en extraire le contenu que nous cherchons, c'est-à-dire la description de l'épisode. # # Essayons de faire cela avec le dernier épisode en date. # In[10]: link = df['lien description'].iloc[-1] link # In[11]: soup = BeautifulSoup(requests.get(link).text, 'html.parser') # In[12]: article_tag = soup.find('article') article_tag # Il se trouve que des balises script sont contenues dans les épisodes (elles affichent de la publicité sur le site de France Inter). # Si on reformate les tags HTML, on obtient le résultat suivant : # In[13]: soup.find_all('section')[0].text # In[14]: def clean(soup, tags_to_clean=['script']): """Nettoie le HTML de l'émission en enlevant les balises inutiles.""" for tag in tags_to_clean: for item in soup.find_all(tag): item.decompose() return soup.article clean(soup) # On peut afficher un aperçu de notre description : # In[15]: from IPython.display import HTML HTML(str(clean(soup))) # Ceci étant fait, on peut écrire une boucle pour extraire toutes les descriptions d'épisode : # In[16]: from functools import lru_cache @lru_cache(maxsize=None) def link_getter(description_link): """Récupère la page de l'émission en question, stocke le résultat pour un accès ultérieur plus rapide.""" return requests.get(description_link) # In[17]: import tqdm descriptions = [] for description_link in tqdm.tqdm(df['lien description']): soup = BeautifulSoup(link_getter(description_link).text, 'html.parser') descriptions.append(clean(soup)) # On peut compléter notre `dataframe` avec les descriptions obtenues : # In[18]: df['description'] = descriptions df['description_text'] = [d.text for d in descriptions] # Afin de vérifier que tout va bien, nous allons faire un pré-affichage rapide avec `ipywidgets`. # In[19]: from ipywidgets import interact @interact def preview_html(n=(0, df.shape[0] - 1)): return HTML(f"lien" + str(df.description.iloc[n])) # # Rajouter d'autres sections # Comme on peut le voir sur cet épisode [https://www.franceinter.fr/emissions/sur-les-epaules-de-darwin/sur-les-epaules-de-darwin-16-mai-2015], parfois les références sont cachées dans un tag `section`, avec le nom de l'équipe de production. Comptons le nombre de tag `section` : # In[20]: sections = [] for description_link in tqdm.tqdm(df['lien description']): soup = BeautifulSoup(link_getter(description_link).text, 'html.parser') sections.append(len(soup.find('div', class_='main-content').find_all('section'))) # Et regardons le comptage : # In[21]: pd.Series(sections).value_counts() # On peut donc proposer un deuxième algorithme : on va chercher les sections supplémentaires en-dessous de `main-content` et on les nettoie elles-aussi. # In[22]: def clean2(soup, tags_to_clean=['script']): """Nettoie le HTML de l'émission en enlevant les balises inutiles.""" for tag in tags_to_clean: for item in soup.find_all(tag): item.decompose() return str(soup.find('div', class_='main-content').article) + "\n".join([str(item) for item in soup.find('div', class_='main-content').find_all('section', class_='content-body-more-details')]) # In[23]: HTML(clean2(soup)) # On met à jour les descriptions. # In[24]: descriptions = [] for description_link in tqdm.tqdm(df['lien description']): soup = BeautifulSoup(link_getter(description_link).text, 'html.parser') descriptions.append(clean2(soup)) # In[31]: df['description'] = descriptions df['description_text'] = [BeautifulSoup(desc, 'html.parser').text for desc in descriptions] # Et on vérifie qu'on a le résultat attendu. # In[32]: @interact def preview_html(n=(0, df.shape[0] - 1)): return HTML(f"lien" + str(df.description.iloc[n])) # Un dernier test : # In[27]: soup = BeautifulSoup(link_getter(df['lien description'].iloc[230]).text, 'html.parser') HTML(clean2(soup)) # Il nous manque encore la récupération des tailles de fichier mp3, qui sera inscrite dans le flux RSS : # # Taille des fichiers mp3 # In[28]: byte_lengths = [] for mp3_link in tqdm.tqdm(df['lien mp3']): r = requests.head(mp3_link) byte_lengths.append(r.headers['content-length']) # In[29]: df['byte_length'] = byte_lengths df.head() # # Ecriture du fichier RSS # Maintenant que nous avons collecté toutes les émissions, nous pouvons écrire un ficher RSS, comme nous l'avions déjà fait [dans le cas de l'émission Rendez vous avec X](http://flothesof.github.io/podcast-rendez-vous-avec-X-fr.html). # L'un des points importants est que nous voulons écrire du HTML dans la description du podcast. Pour ce faire, nous devons utiliser un champ dédié : `` alors que le champ `` ne doit contenir que du texte (source ici : [https://podnews.net/article/how-podcast-show-notes-display](https://podnews.net/article/how-podcast-show-notes-display)). # In[34]: import dateparser import xml.etree.cElementTree as ET rss = ET.Element("rss", version="2.0") channel = ET.SubElement(rss, "channel") title = ET.SubElement(channel, "title") title.text = 'Podcast Sur les épaules de Darwin' description = ET.SubElement(channel, "description") description.text = "Podcast inofficiel de l'émission Sur les épaules de Darwin, tiré du site http://clementgrimal.fr/darwin/ et https://www.franceinter.fr/emissions/sur-les-epaules-de-darwin" for index, row in df.iterrows(): item = ET.SubElement(channel, "item") item_title = ET.SubElement(item, "title") item_title.text = row['titre'] item_description = ET.SubElement(item, "description") item_description.text = row['description_text'].strip() item_content_encoded = ET.SubElement(item, "content:encoded") item_content_encoded.text = row['description'] item_pubdate = ET.SubElement(item, "pubDate") item_pubdate.text = dateparser.parse(row.date).date().strftime('%a, %d %b %Y 11:00:00') item_enclosure = ET.SubElement(item, "enclosure", url='{}'.format(row['lien mp3']), length=row.byte_length, type="audio/mpeg") tree = ET.ElementTree(rss) tree.write("files/podcast_Sur_les_epaules_de_Darwin.xml", encoding='utf-8') # Le lien vers le fichier généré est ici : **https://raw.githubusercontent.com/flothesof/posts/master/files/podcast_Sur_les_epaules_de_Darwin.xml**. # *Ce billet a été écrit à l'aide d'un notebook Jupyter. Son contenu est sous licence BSD. Une vue statique de ce notebook peut être consultée et téléchargée ici : [20170908_PodcastEpaulesDeDarwin.ipynb](http://nbviewer.ipython.org/urls/raw.github.com/flothesof/posts/master/20170908_PodcastEpaulesDeDarwin.ipynb).*