Comment récupérer des données sur le web automatiquement à l’aide du scraping, même lorsque je débute en Python
Pourquoi s'intéresser à l'extraction automatique de données du web
(ou web scraping) ?
- Exercice de programmation motivant
- Projet de recherche
- Data journalisme
- Etre alerté d'une offre intéressante
- Etude de marché et observer la concurrence
- Automatiser sa recherche de mots clés
- Exporter le contenu d'un site en vue de sa migration
CAS D'USAGE
Ai-je le droit de récupérer des données sur le web automatiquement ?
- "Terms & Conditions" ou "Conditions d'utilisation"
- "Privacy Policy" ou "Politique de confidentialité"
- robots.txt
Vérifications d'usage
Rechercher les termes
- Scraper ou Scraping
- Récupération automatique
- Crawler ou Crawling
- Bot ou Robot
- Spider
- Program ou Programme
Conditions d'utilisation
Garder les données de personnes privées hors du contexte de votre automatisation
- Pas de numéros de téléphone privés
- Pas d'adresse emails privées
- Pas d'adresses de privés
- Pas d'identités
N'oubliez pas le RGPD
robots.txt
# robots.txt de developpez.com
Sitemap: https://www.developpez.com/sitemap.xml
Sitemap: https://www.developpez.com/sitemap2.xml.gz
User-agent: *
Disallow: /ws/
Disallow: /telechargements/
Disallow: /redirect/
Disallow: /actu/partager/
robots.txt
# robots.txt de oreilly.com
User-agent: *
Disallow: /images/
Disallow: /graphics/
Disallow: /admin/
Disallow: /promos/
Disallow: /ddp/
Disallow: /dpp/
Disallow: /programming/free/files/
Disallow: /design/free/files/
Disallow: /iot/free/files/
Disallow: /data/free/files/
Disallow: /webops-perf/free/files/
Disallow: /web-platform/free/files/
Disallow: /cs/
Disallow: /test/
Disallow: /*/?ar
Disallow: /*/?orpq
Disallow: /*/?discount=learn
Disallow: /self-registration/*
User-agent: 008
Disallow: /
#ITOPS-10158
Sitemap: https://www.oreilly.com/book-sitemap.xml
#ITOPS-8392
Sitemap: https://www.oreilly.com/radar/sitemap.xml
Sitemap: https://www.oreilly.com/content/sitemap.xml
#ITOPS-10157
Sitemap: https://www.oreilly.com/video-sitemap.xml
Analyser automatiquement un robots.txt
# is_allowed.py
from urllib import robotparser
robot_parser = robotparser.RobotFileParser()
La bibliothèque standard avant tout
# is_allowed.py - pipenv install typer
from urllib import robotparser
from urllib.parse import urlparse
import typer
def prepare_robots_parser(robot_parser, robot_url):
"""Prépare l'analyseur de fichier robots.txt à l'analyse."""
robot_parser.set_url(robot_url)
robot_parser.read()
def is_allowed(robot_parser, url, user_agent='*'):
"""Détermine si une url peut être scrapée."""
return robot_parser.can_fetch(user_agent, url)
def main(robot_url: str, url: str, user_agent: str = "*"):
"""Point d'entrée principal du script."""
# TODO: valider robot_url et url
robot_parser = robotparser.RobotFileParser()
prepare_robots_parser(robot_parser, robot_url)
if is_allowed(robot_parser, url, user_agent):
print(f"L'accès à l'url {url} est autorisé")
else:
print(f"L'accès à l'url {url} n'est pas autorisé")
if __name__ == "__main__":
typer.run(main)
$ python -m is_allowed https://www.developpez.com/robots.txt https://www.developpez.com
L'accès à l'url https://www.developpez.com est autorisé
$
$ python -m is_allowed [OPTIONS] ROBOT_URL URL
Quels outils pour faire du scraping web ?
-
Phase 1 L'extraction
- urllib, requests, scrapy, etc.
-
Phase 2 La transformation
- BeautifulSoup, lxml, parsel, scrapy, etc.
-
Phase 3 L'enregistrement ou loading
- json, xlsx, csv, sqlite3, postgresql, mongodb, etc.
Le web scraping: un processus en 3 Phases
- urllib.request.urlopen()
- requests
- Scrapy (phases 1, 2 et 3)
Phase 1 Télécharger les données
# exemple_urllib.py:
# adapté de pymotw.com/3/urllib.request/
from urllib import request
urllib.request, la bibliothèque standard avant tout
# exemple_urllib.py
# adapté de pymotw.com/3/urllib.request/
from urllib import request
response = request.urlopen('https://python.developpez.com/')
# exemple_urllib.py
# adapté de pymotw.com/3/urllib.request/
from urllib import request
response = request.urlopen('https://python.developpez.com/')
print('RESPONSE:', response)
print('URL :', response.geturl())
headers = response.info()
print('DATE :', headers['date'])
print('HEADERS :')
print('-' * 80)
print(headers)
data = response.read().decode('iso-8859-1')
print('LENGTH :', len(data))
print('DATA :')
print('-' * 80)
print(data)
RESPONSE: <http.client.HTTPResponse object at 0x1019ae6e0>
URL : https://python.developpez.com/
DATE : Thu, 01 Dec 2022 09:18:37 GMT
HEADERS :
--------------------------------------------------------------------------------
Date: Thu, 01 Dec 2022 09:18:37 GMT
Server: Apache/2.4.38 (Debian)
Referrer-Policy: unsafe-url
X-Powered-By: PHP/5.6.40
Set-Cookie: PHPSESSID=t6clh8bnj31joh6rv1ub6pva34; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html, charset=iso-8859-1
LENGTH : 105343
DATA :
--------------------------------------------------------------------------------
<!DOCTYPE html>
<html lang="fr" prefix="og: http://ogp.me/ns#">
<head>
<meta charset="iso-8859-1">
<title>Club des développeurs Python : actualités, cours, tutoriels, faq, sources, forum</title>
<link rel="canonical" href="https://python.developpez.com/">
<meta name="generator" content="developpez-com">
<meta name="description" content="Club des développeurs Python : actualités, cours, tutoriels, faq, sources, forum">
<meta property="og:type" content="website" />
...suite du code html de la page...
$ python -m exemple_urllib
# exemple_urllib.py
# adapté de pymotw.com/3/urllib.request/
from urllib import request
response = request.urlopen('https://python.developpez.com/')
print('RESPONSE:', response)
print('URL :', response.geturl())
headers = response.info()
print('DATE :', headers['date'])
print('HEADERS :')
print('-' * 80)
print(headers)
data = response.read().decode('iso-8859-1')
print('LENGTH :', len(data))
print('DATA :')
print('-' * 80)
print(data)
L'encodage est un problème récurrent lorsqu'on scrape le monde francophone
# exemple_urllib.py
# adapté de pymotw.com/3/urllib.request/
from urllib import request
response = request.urlopen('https://python.developpez.com/')
print('RESPONSE:', response)
print('URL :', response.geturl())
headers = response.info()
print('DATE :', headers['date'])
print('HEADERS :')
print('-' * 80)
print(headers)
data = response.read().decode('utf-8')
print('LENGTH :', len(data))
print('DATA :')
print('-' * 80)
print(data)
L'encodage est un problème récurrent lorsqu'on scrape le monde francophone
Traceback (most recent call last):
File "/opt/homebrew/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/opt/homebrew/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/Users/thierry/dev/wepynaires/2022/django-de-zero/exemple.py", line 16, in <module>
data = response.read().decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 118: invalid continuation byte
Tout ne se passe pas toujours comme on veut
Détecter automatiquement l'encodage
import re
from urllib import request
response = request.urlopen('https://python.developpez.com/')
headers = response.info()
charset = (
re.search(
r"charset=(?P<encoding>\S*)",
headers['Content-Type'],
re.IGNORECASE
)
.group("encoding")
)
Rechercher l'encodage dans les en-têtes HTTP
import re
from urllib import request
response = request.urlopen('https://python.developpez.com/')
raw_data = response.read()
charset = (
re.search(
b"<meta[^>]+charset=['\"](?P<encoding>.*?)['\"]",
raw_data,
re.IGNORECASE
)
.group("encoding")
.decode()
)
Rechercher l'encodage dans la page elle-même
- Guide officiel des expressions rationnelles https://docs.python.org/3/howto/regex.html
- regex101 pour s'entraîner https://regex101.com/
Pas à l'aise avec les expressions rationnelles ou regex ?
BeautifulSoup détecte automatiquement l'encodage
$ pipenv install beautifulsoup4
Installing beautifulsoup4...
Pipfile.lock (d52e37) out of date, updating to (242488)...
Locking [packages] dependencies...
Locking [dev-packages] dependencies...
Updated Pipfile.lock (2e0b0e7b9add9dc3f24d00f495ee7dc1e5687672aee16a577ed934e972242488)!
Installing dependencies from Pipfile.lock (242488)...
from urllib import request
from bs4 import BeautifulSoup
response = request.urlopen('https://python.developpez.com/')
raw_data = response.read()
# html.parser peut être remplacé par lxml
# de meilleures performances d'analyse
soup = BeautifulSoup(raw_content, "'html.parser'")
print(soup)
BeautifulSoup détecte automatiquement l'encodage
Gérer les erreurs avec urllib.request
bit.ly/howto-urllib
from urllib.request import urlopen
from urllib.error import URLError, HTTPError
def get_page(url)
"""Récupère le contenu d'une page et retoure une
urllib.response."""
try:
response = urlopen("https://placepython.fr")
except HTTPError as e:
print("Le serveur n'a pas pu satisfaire la requête.")
print(f"Le code d'erreur est: {e.code}")
except URLError as e:
print("Nous ne sommes pas parvenus à joindre le serveur.")
print(f"Raison de l'échec: {e.reason}")
else:
return response
L'encodage est un problème récurrent lorsqu'on scrape le monde francophone
Récupérer tous les liens d'une pages avec la bibliothèque standard
import re
from urllib.request import urlopen, urljoin
def get_page(url, encoding="utf8"):
"""Récupère une page web avec l'encodage spécifié et
retourne le code html de cette page."""
return urlopen(url).read().decode(encoding)
def extract_links(page):
"""Extrait les liens d'une page HTML et retourne une liste
des urls."""
return re.findall(
'<a[^>]+href=["\'](.*?)["\']', page, re.IGNORECASE
)
def main():
"""Point d'entrée du script."""
url = "https://python.developpez.com"
developpez = get_page(url, encoding="iso-8859-1")
links = extract_links(developpez)
for link in links:
print(urljoin(url, link))
if __name__ == '__main__':
main()
La récupération des données se fait avec urllib.request
import re
from urllib.request import urlopen, urljoin
def get_page(url, encoding="utf8"):
"""Récupère une page web avec l'encodage spécifié et
retourne le code html de cette page."""
return urlopen(url).read().decode(encoding)
def extract_links(page):
"""Extrait les liens d'une page HTML et retourne une liste
des urls."""
return re.findall(
'<a[^>]+href=["\'](.*?)["\']', page, re.IGNORECASE
)
def main():
"""Point d'entrée du script."""
url = "https://python.developpez.com"
developpez = get_page(url, encoding="iso-8859-1")
links = extract_links(developpez)
for link in links:
print(urljoin(url, link))
if __name__ == '__main__':
main()
L'extraction des liens a été faite avec re
https://python.developpez.com
http://www.developpez.net/forums/login.php?do=lostpw
https://www.developpez.net/forums/inscription/
https://www.developpez.com
https://www.developpez.com
https://www.developpez.net/forums/
https://general.developpez.com/cours/
https://general.developpez.com/faq/
https://www.developpez.net/forums/blogs/
https://chat.developpez.com/
https://www.developpez.com/newsletter/
https://emploi.developpez.com/
https://etudes.developpez.com/
https://droit.developpez.com/
https://club.developpez.com/
https://www.developpez.com
https://solutions-entreprise.developpez.com
https://solutions-entreprise.developpez.com
https://abbyy.developpez.com
https://big-data.developpez.com
https://bpm.developpez.com
https://business-intelligence.developpez.com
https://data-science.developpez.com
https://solutions-entreprise.developpez.com/erp-pgi/presentation-erp-pgi/
https://crm.developpez.com
https://sas.developpez.com
https://sap.developpez.com
https://www.developpez.net/forums/f1664/systemes/windows/windows-serveur/biztalk-server/
https://talend.developpez.com
https://droit.developpez.com
https://onlyoffice.developpez.com
https://cloud-computing.developpez.com
https://cloud-computing.developpez.com
https://oracle.developpez.com
https://windows-azure.developpez.com
https://ibmcloud.developpez.com
https://intelligence-artificielle.developpez.com
https://intelligence-artificielle.developpez.com
https://alm.developpez.com
https://alm.developpez.com
https://agile.developpez.com
https://merise.developpez.com
https://uml.developpez.com
https://microsoft.developpez.com
https://microsoft.developpez.com
https://dotnet.developpez.com
https://office.developpez.com
https://visualstudio.developpez.com
https://windows.developpez.com
https://dotnet.developpez.com/aspnet/
https://typescript.developpez.com
https://dotnet.developpez.com/csharp/
https://dotnet.developpez.com/vbnet/
https://windows-azure.developpez.com
https://java.developpez.com
https://java.developpez.com
https://javaweb.developpez.com
https://spring.developpez.com
https://android.developpez.com
https://eclipse.developpez.com
https://netbeans.developpez.com
https://web.developpez.com
https://web.developpez.com
https://ajax.developpez.com
https://apache.developpez.com
https://asp.developpez.com
https://css.developpez.com
https://dart.developpez.com
https://flash.developpez.com
https://javascript.developpez.com
https://nodejs.developpez.com
https://php.developpez.com
https://ruby.developpez.com
https://typescript.developpez.com
https://web-semantique.developpez.com
https://webmarketing.developpez.com
https://xhtml.developpez.com
https://edi.developpez.com
https://edi.developpez.com
https://4d.developpez.com
https://delphi.developpez.com
https://eclipse.developpez.com
https://jetbrains.developpez.com
https://labview.developpez.com
https://netbeans.developpez.com
https://matlab.developpez.com
https://scilab.developpez.com
Et pour récupérer des images ?
import re
from urllib.request import urlopen, urljoin
def get_page(url, encoding="utf8"):
"""Récupère une page web avec l'encodage spécifié et
retourne le code html de cette page."""
return urlopen(url).read().decode('iso-8859-1')
def extract_image_links(page):
"""Extrait les liens d'images d'une page HTML et retourne une
liste des urls."""
return re.findall('<img[^>]+src=["\'](.*?)["\']', page, re.IGNORECASE)
def main():
"""Point d'entrée du script."""
url = "https://python.developpez.com"
developpez = get_page(url, encoding="iso-8859-1")
links = extract_image_links(developpez)
for link in links:
print(urljoin(url, link))
if __name__ == '__main__':
main()
Même usage de urllib.request et re
https://python.developpez.com/template/images/logo-dvp-h55.png
https://www.developpez.com/images/logos/90x90/jetbrains.png?1611920537
https://www.developpez.com/images/logos/90x90/pycharm.png?1553595023
https://www.developpez.com/images/logos/jetbrains.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/programmation.png
https://www.developpez.com/images/logos/jeux.png
https://www.developpez.com/images/logos/programmation.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/programmation.png
https://www.developpez.com/images/logos/jetbrains.png
https://www.developpez.com/images/logos/programmation.png
https://www.developpez.com/images/logos/securite2.png
https://www.developpez.com/images/logos/jetbrains.png
https://www.developpez.com/images/logos/pycharm.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/jetbrains.png
https://www.developpez.com/images/logos/jetbrains.png
https://www.developpez.com/images/logos/python.png
https://www.developpez.com/images/logos/python.png
En apprendre plus sur urllib.request ?
Récupérer des données avec la bibliothèque requests
$ pipenv install requests
Des requêtes HTTP légèrement simplifiées
import requests
def get_page(url, params=None):
"""Récupère une page sur le web et retourne une
request.Response."""
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.ConnectionError:
...
except requests.exceptions.URLRequired:
...
except requests.exceptions.HTTPError:
...
except requests.exceptions.RequestException:
# Tout le reste
...
else:
return response
response.content ou response.text ?
Lorsque requests se trompe dans le décodage
import requests
response = requests.get("https://www.developpez.com")
print(response.encoding)
print("-" * len(response.encoding))
for header, value in response.headers.items():
print(f"{header}: {value}")
ISO-8859-1
----------
Date: Fri, 02 Dec 2022 12:54:23 GMT
Server: Apache/2.4.38 (Debian)
Referrer-Policy: unsafe-url
...
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html, charset=iso-8859-1
ici, requests a tout juste: on pourrait utilise requests.text
from pprint import pprint
import requests
response = requests.get("https://books.toscrape.com")
print(response.encoding)
print("-" * len(response.encoding))
for header, value in response.headers.items():
print(f"{header}: {value}")
ISO-8859-1
----------
Date: Fri, 02 Dec 2022 13:02:10 GMT
Content-Type: text/html
Content-Length: 51294
Connection: keep-alive
Last-Modified: Thu, 26 May 2022 21:15:15 GMT
ETag: "628fede3-c85e"
Accept-Ranges: bytes
Strict-Transport-Security: max-age=0; includeSubDomains; preload
ici, requests se trompe:
ne pas utiliser requests.text !
from bs4 import BeautifulSoup
# La fonction vue précédemment
from .http_tools import get_page
response = get_page("https://developpez.com")
if response:
# ATTENTION: response.content, PAS response.text
soup = BeautifulSoup(response.content, "html.parser")
Utilisation de BeautifulSoup pour détecter automatiquement de l'encodage à partir de response.content
$ pipenv install beautifulsoup4
from urllib import request
from bs4 import UnicodeDammit
import lxml.html
def decode_html(html_bytes):
"""Cherche à décoder des bytes de html à partir des
déclarations dans le code et d'heuristiques."""
converted = UnicodeDammit(html_bytes)
if not converted.unicode_markup:
raise UnicodeDecodeError (
"Echec de la détection de l'encodage. "
"Les jeux de caractères sont %s",
", ".join(converted.tried_encodings)
)
return converted.unicode_markup
response = request.urlopen("https://www.developpez.com")
html_text = decode_html(response.read())
# Permet d'utiliser directement p.ex. lxml qui
# n'est pas aussi doué dans le décodage
html = lxml.html.fromstring(html_text)
Utiliser BeautifulSoup QUE pour détecter l'encodage ?
Phase 2
La transformation
- BeautifulSoup
- lxml
- Parsel
- Scrapy
Disable JavaScript
Un web sans javascript
Analyser le html avec BeautifulSoup
from bs4 import BeautifulSoup
...
def get_page(url):
...
return response
def parse_html(response):
"""Analyse et transforme la réponse reçue de la page
interrogée."""
soup = BeautifulSoup(response.content, "html.parser")
...
$ pipenv install beautifulsoup4
BeautifulSoup:
find et find_all
soup.find('p', id="first-section") # retourne un objet unique
soup.find_all('p', class_="carousel") # retourne une liste
soup.find('img', src='pythons.jpg')
soup.find('img', src=re.compile('\.gif$'))
soup.find_all('p', text="python")
soup.find_all('p', text=re.compile('python'))
for tag in soup.find_all(re.compile('h')):
print(tag.name)
# affiche html, head, hr, h1, h2, hr
# Tous les éléments qui contiennent "Python"
soup.find_all(True, text=re.compile("python"))
BeautifulSoup:
select et select_one
# Source: documentation de BeautifulSoup
# bit.ly/bs4-select
soup.select("title")
# [<title>The Dormouse's story</title>]
soup.select("p:nth-of-type(3)")
# [<p class="story">...</p>]
soup.select("head > title")
# [<title>The Dormouse's story</title>]
soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
soup.select("body > a")
# []
soup.select('a[href]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
La puissance des sélecteurs CSS
MDN le guide de survie des selecteurs CSS
https://mzl.la/3VqdZzN
L'arme fatale de la récupération de données
Utiliser XPath pour récupérer ses éléments ?
lxml ou Parsel ou Scrapy
Parsel:
un wrapper de lxml à la base des sélecteurs de Scrapy
$ pipenv install parsel
https://parsel.readthedocs.io/
<!-- https://parsel.readthedocs.org/en/latest/_static/selectors-sample1.html -->
<html>
<head>
<base href='http://example.com/' />
<title>Example website</title>
</head>
<body>
<div id='images'>
<a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg'/></a>
<a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg'/></a>
<a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg'/></a>
<a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg'/></a>
<a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg'/></a>
</div>
</body>
</html>
Le support CSS et XPath à la base de Scrapy
from bs4 import UnicodeDammit
import requests
from parsel import Selector
def decode_html(html_bytes):
...
url = 'https://parsel.readthedocs.org/en/latest/_static/selectors-sample1.html'
response = requests.get(url)
selector = Selector(text=decode_html(response.content))
parsel.Selector offre une interface de haut niveau à XPath et aux sélecteurs css
>>> selector.xpath('//title/text()')
[<Selector xpath='//title/text()' data='Example website'>]
>>> selector.css('title::text')
[<Selector xpath='descendant-or-self::title/text()' data='Example website'>]
>>> selector.xpath('//title/text()').getall()
['Example website']
>>> selector.xpath('//title/text()').get()
'Example website'
>>> selector.css('title::text').get()
'Example website'
>>> selector.css('img').xpath('@src').getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']
>>> selector.xpath('//div[@id="images"]/a/text()').get()
'Name: My image 1 '
>>> selector.xpath('//div[@id="not-exists"]/text()').get() is None
True
>>> [img.attrib['src'] for img in selector.css('img')]
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']
>>> selector.css('img').attrib['src']
'image1_thumb.jpg'
>>> selector.xpath('//base/@href').get()
'http://example.com/'
>>> selector.css('base::attr(href)').get()
'http://example.com/'
>>> selector.css('base').attrib['href']
'http://example.com/'
>>> selector.xpath('//a[contains(@href, "image")]/@href').getall()
['image1.html',
'image2.html',
'image3.html',
'image4.html',
'image5.html']
>>> selector.css('a[href*=image]::attr(href)').getall()
['image1.html',
'image2.html',
'image3.html',
'image4.html',
'image5.html']
>>> selector.xpath('//a[contains(@href, "image")]/img/@src').getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']
>>> selector.css('a[href*=image] img::attr(src)').getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']
Le mélange des sélecteurs css et de XPath offre des possibilités très expressives
Investissez dans XPath !
Utile avec lxml, Parsel, Scrapy et même avec Selenium pour vos tests Django :)
Scrapy: un framework complet
Le Django de la récupération automatique de données
Pour les petits scripts rapides
- urllib.request et Parsel
Pour les récupérations de données plus avancées
- Scrapy
- Scrapy-playwright (si le site contient du js)
- Scrapy-selenium (si le site contient du js)
Quand utiliser quoi ?
$ scrapy startproject monprojet
$ tree monprojet # windows: tree /F
monprojet
├── monprojet
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
2 directories, 7 files
$ pipenv install scrapy
BOT_NAME = 'monprojet'
SPIDER_MODULES = ['monprojet.spiders']
NEWSPIDER_MODULE = 'monprojet.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'monprojet (+https://placepython.fr)'
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 1
monprojet/settings.py
$ scrapy shell 'https://quotes.toscrape.com/page/1/'
...
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
>>> response.css('title::text').getall()
['Quotes to Scrape']
>>> response.css('noelement')[0].get()
Traceback (most recent call last):
...
IndexError: list index out of range
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'
Se faire la main à l'aide du shell de Scrapy
On se retrouve avec les sélecteurs Parsel
les sélecteurs de response sont un fin wrapper
$ scrapy startproject monprojet
$ tree monprojet # windows: tree /F
monprojet
├── monprojet
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
2 directories, 7 files
Créer des scripts de récupérations des données avec les spiders
import scrapy
class QuotesSpider(scrapy.Spider):
"""Spider responsable de récupérer des citations sur
quotes.toscrape.com. Source: https://docs.scrapy.org/"""
name = "quotes"
def start_requests(self):
urls = [
'https://quotes.toscrape.com/page/1/',
'https://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
# Alternativement, au lieu de définir start_requests
start_urls = [
'https://quotes.toscrape.com/page/1/',
'https://quotes.toscrape.com/page/2/',
]
def parse(self, response):
# Faire quelque chose avec la réponse
...
Créer notre premier Spider pour scraper des données avec Scrapy
monprojet/spiders/quotes_spider.py
class QuotesSpider(scrapy.Spider):
...
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
Extraire des données dans notre spider
$ scrapy crawl quotes -O quotes.json
$ scrapy crawl quotes -o quotes.json
$ scrapy crawl quotes -o quotes.jsonl # ou .jl
$ scrapy crawl quotes -o quotes.csv
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'https://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
Suivre des liens avec notre spider
scrapy.Request ne supporte pas les liens internes
class QuotesSpider(scrapy.Spider):
...
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a').get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
Simplification avec response.follow()
Pas nécessaire d'extraire le href ni d'utiliser urljoin
Stocker ses données en base ?
Créer des pipelines !
Hors scope pour ce WePynaire :)
Que faire si notre page contient des éléments chargés dynamiquement pas Javascript ?
- Selenium
- selenium
- scrapy-playwright
- Playwright
- pytest-playwright
- scrapy-playwright
- Splash
- scrapy-spash
- Puppeteer
- pyppeteer (maintenu ?)
Les options
avec javascript
Scrapy-selenium
monprojet
├── chromedriver
├── geckodriver
├── monprojet
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
$ pipenv install scrapy-selenium
$ pipenv install scrapy-selenium
+ les drivers de navigateurs
from shutil import which
# SELENIUM_DRIVER_NAME = 'firefox'
SELENIUM_DRIVER_NAME = 'chrome'
# SELENIUM_DRIVER_EXECUTABLE_PATH = which('geckodriver')
SELENIUM_DRIVER_EXECUTABLE_PATH = which('chromedriver')
SELENIUM_DRIVER_ARGUMENTS=['--headless']
DOWNLOADER_MIDDLEWARES = {
'scrapy_selenium.SeleniumMiddleware': 800
}
Configuration
import scrapy
from scrapy_selenium import SeleniumRequest
class QuotesSpider(scrapy.Spider):
name = 'quotes'
def start_requests(self):
url = 'https://quotes.toscrape.com/js/'
yield SeleniumRequest(
url=url,
callback=self.parse,
wait_time=10
)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
Mise à jour de notre spider
import scrapy
from scrapy_selenium import SeleniumRequest
class QuotesSpider(scrapy.Spider):
name = 'quotes'
def start_requests(self):
url = 'https://quotes.toscrape.com/js/'
yield SeleniumRequest(
url=url,
callback=self.parse,
wait_time=10
)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
Mise à jour de notre spider
Scrapy-playwright
# settings.py
playwright_handler = (
"scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler"
)
DOWNLOAD_HANDLERS = {
"http": playwright_handler,
"https": playwright_handler,
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
$ pipenv install scrapy-playwright
$ playwright install
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
def start_requests(self):
url = 'https://quotes.toscrape.com/js/'
yield scrapy.Request(url, meta={'playwright': True})
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
Questions ?
Récupérer ses données sur le web automatiquement à l'aide du scraping
By Thierry Chappuis
Récupérer ses données sur le web automatiquement à l'aide du scraping
- 98