{ "cells": [ { "cell_type": "markdown", "source": [ "# Scraper « le monde » et construire ton propre corpus 🇫🇷\n", "\n", "[Xiaoou WANG](https://scholar.google.fr/citations?user=vKAMMpwAAAAJ&hl=en)\n", "\n", "## Pourquoi tu aurais besoin de scraper\n", "\n", "Parfois tu te trouves dans la situation où tu dois constituer ton propre corpus pour y mener des tests de classifieur/parseur/etc.\n", "\n", "## Problèmes légaux\n", "\n", "Il est à noter que le scraping peut soulever des problèmes légaux liés surtout aux droits d'auteurs. Je te conseille donc de bien te renseigner avant de faire du scraping. Un bon point de départ serait un site comme [celui-ci](https://soshace.com/responsible-web-scraping-gathering-data-ethically-and-legally/).\n", "\n", "## Scraper le monde\n", "\n", "### Obtenir des liens d'archives\n", "\n", "La première chose à faire est de générer un lien d'archives. A titre d'exemple, le lien `https://www.lemonde.fr/archives-du-monde/01-01-2020/` contient tous les articles publiés le 1er janvier 2020. La fonction `create_archive_link` prend starting ear/month/day et ending year/month/day comme entrées et retourne un dictionnaire sous forme de `year:links`." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [ { "data": { "text/plain": "['https://www.lemonde.fr/archives-du-monde/23-08-2015/',\n 'https://www.lemonde.fr/archives-du-monde/08-07-2015/',\n 'https://www.lemonde.fr/archives-du-monde/13-10-2015/',\n 'https://www.lemonde.fr/archives-du-monde/11-01-2015/',\n 'https://www.lemonde.fr/archives-du-monde/06-07-2015/',\n 'https://www.lemonde.fr/archives-du-monde/22-10-2015/']" }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def create_archive_links(year_start, year_end, month_start, month_end, day_start, day_end):\n", " archive_links = {}\n", " for y in range(year_start, year_end + 1):\n", " dates = [str(d).zfill(2) + \"-\" + str(m).zfill(2) + \"-\" +\n", " str(y) for m in range(month_start, month_end + 1) for d in\n", " range(day_start, day_end + 1)]\n", " archive_links[y] = [\n", " \"https://www.lemonde.fr/archives-du-monde/\" + date + \"/\" for date in dates]\n", " return archive_links\n", "\n", "demo_archives = create_archive_links(2006,2020,1, 12, 1, 31)\n", "\n", "import random\n", "random.sample(demo_archives[2015],6)" ] }, { "cell_type": "markdown", "source": [ "La prochaine étape est de récupérer tous les liens d'article sur la page d'archive. A cet effet t'aurais besoin de 3 modules :\n", "\n", "1. `HTTPError` pour traiter les exceptions\n", "2. `urlopen` pour ouvrir des pages webs\n", "3. `BeautifulSoup` pour parse\n", "\n", "### Quand les exceptions agissent en ta faveur\n", "\n", "Les exceptions sont pratiques dans notre cas car nous avons généré des liens pour des dates comme 02-31, c'est beaucoup facile de catcher ces exceptions que de générer des liens correspondant seulement à des dates existantes. Chaque lien d'article est dans un tag `
` contenant une `class` nommé `teaser`.\n", "\n", "Il est important de filtrer tous les articles non gratuits avec un `span > class icon__premium` (à moins que tu aies un compte premium). Tous les liens contenant le mot-clé `en-direct` sont aussi à filtrer car ce sont des vidéos. Tu aurais compris que web scraping demande des capacités d'analyse des pages webs.\n", "\n", "![](test_images/4404a12e.png)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 6, "outputs": [], "source": [ "from urllib.error import HTTPError\n", "from urllib.request import urlopen\n", "from bs4 import BeautifulSoup\n", "\n", "def get_articles_links(archive_links):\n", " links_non_abonne = []\n", " for link in archive_links:\n", " try:\n", " html = urlopen(link)\n", " except HTTPError as e:\n", " print(\"url not valid\", link)\n", " else:\n", " soup = BeautifulSoup(html, \"html.parser\")\n", " news = soup.find_all(class_=\"teaser\")\n", " # condition here : if no span icon__premium (abonnes)\n", " for item in news:\n", " if not item.find('span', {'class': 'icon__premium'}):\n", " l_article = item.find('a')['href']\n", " # en-direct = video\n", " if 'en-direct' not in l_article:\n", " links_non_abonne.append(l_article)\n", " return links_non_abonne" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "### Sauvegarder les liens d'articles dans un fichier txt\n", "\n", "Je te propose d'écrire une fonction pour stocker les liens existants car il est peu probable qu'ils ne changent. Ici la date de publication est utilisée pour nommer les fichiers.\n", "\n", "```py\n", "def write_links(path, links, year_fn):\n", " with open(os.path.join(path + \"/lemonde*\" + str(year_fn) + \"\\_links.txt\"), 'w') as f:\n", " for link in links:\n", " f.write(link + \"\\n\")\n", "\n", "article_links = {}\n", "\n", "for year,links in archive_links.items():\n", " print(\"processing: \",year)\n", " article_links_list = get_articles_links(links)\n", " article_links[year] = article_links_list\n", " write_links(corpus_path,article_links_list,year)\n", "```\n", "\n", "### Scraper un seul article\n", "\n", "Maintenant tu peux commencer à scraper les textes. Il suffit de scraper tous les éléments `h1, h2 et p`. Mets `recursive=False` car tu veux que le \"scrapeur\" arrête de fouiller dans les structures plus profondes dès qu'il trouve le texte.\n", "\n", "Par souci de modularité, écrivons une fonction scrapant un seul article :\n", "\n", "```py\n", "def get_single_page(url):\n", " try:\n", " html = urlopen(url)\n", " except HTTPError as e:\n", " print(\"url not valid\", url)\n", " else:\n", " soup = BeautifulSoup(html, \"html.parser\")\n", " text_title = soup.find('h1')\n", " text_body = soup.article.find_all([\"p\", \"h2\"], recursive=False)\n", " return (text_title, text_body)\n", "```\n", "\n", "### Classifier les textes par thème\n", "\n", "La classification par thème est très courante comme tâche en machine learning. Je te propose donc une fonction attribuant à chaque article une catégorie. L'information est déjà contenue dans le lien : `https://www.lemonde.fr/politique/article/2020/01/01/reforme-des-retraites-macron-reste-inflexible-aucune-issue-ne-se-profile_6024550_823448.html` contient par exemple le mot-clé `politique`.\n", "\n", "```py\n", "def extract_theme(link):\n", " try:\n", " theme_text = re.findall(r'.fr/.*?/', link)[0]\n", " except:\n", " pass\n", " else:\n", " return theme_text[4:-1]\n", "```\n", "\n", "### Scraper tout\n", "\n", "C'est enfin le moment de tout scraper ! Pour ce faire je te conseille d'utiliser le package `tqdm` car le scraping peut être long et tu voudras savoir où il en est. Voici à quoi ressemble la barre de progression :\n", "\n", "![](test_images/957b6f67.png)\n", "\n", "```py\n", "def scrape_articles(dict_links):\n", " themes = dict_links.keys()\n", " for theme in themes:\n", " create_folder(os.path.join('corpus', theme))\n", " print(\"processing:\", theme)\n", " #### note the use tqdm\n", " for i in tqdm(range(len(dict_links[theme]))):\n", " link = dict_links[theme][i]\n", " fn = extract_fn(link)\n", " single_page = get_single_page(link)\n", " if single_page is not None:\n", " with open((os.path.join('corpus', theme, fn + '.txt')), 'w') as f:\n", " # f.write(dict_links[theme][i] + \"\\n\" * 2)\n", " f.write(single_page[0].get_text() + \"\\n\" * 2)\n", " for line in single_page[1]:\n", " f.write(line.get_text() + \"\\n\" * 2)\n", "```\n", "\n", "Maintenant que tu as tous les articles comme txt rangés dans des répertoires. Voici une fonction qui permet d'extraire n article de chaque répertoire (thème). Il est configuré à 1000 par défaut.\n", "\n", "```py\n", "def cr_corpus_dict(path_corpus, n_files=1000):\n", " dict_corpus = defaultdict(list)\n", " themes = os.listdir(path_corpus)\n", " for theme in themes:\n", " counter = 0\n", " if not theme.startswith('.'):\n", " theme_directory = os.path.join(path_corpus, theme)\n", " for file in os.listdir(theme_directory):\n", " if counter < n_files:\n", " path_file = os.path.join(theme_directory, file)\n", " text = read_file(path_file)\n", " dict_corpus[\"label\"].append(theme)\n", " dict_corpus[\"text\"].append(text)\n", " counter += 1\n", " return dict_corpus\n", "```\n", "\n", "Voilà. Happy Scraping ! Et n'oublie pas de te renseigner pour des problèmes légaux !\n", "\n", "## Gist\n", "\n", "[https://gist.github.com/xiaoouwang/0f054840af560488f2162c110b6045b5#file-lemondescraper-py](https://gist.github.com/xiaoouwang/0f054840af560488f2162c110b6045b5#file-lemondescraper-py)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 0 }