Page JavaScript de Web-scraping avec Python

178

J'essaye de développer un simple grattoir Web. Je souhaite extraire du texte sans le code HTML. En fait, j'atteins cet objectif, mais j'ai vu que dans certaines pages où JavaScript est chargé, je n'obtenais pas de bons résultats.

Par exemple, si du code JavaScript ajoute du texte, je ne peux pas le voir, car lorsque j'appelle

response = urllib2.urlopen(request)

J'obtiens le texte original sans celui ajouté (car JavaScript est exécuté dans le client).

Donc, je cherche des idées pour résoudre ce problème.

mocopera
la source
2
On dirait que vous pourriez avoir besoin de quelque chose de plus lourd, essayez Selenium ou Watir.
wim
2
J'ai réussi à faire cela en Java (j'ai utilisé la boîte à outils Cobra lobobrowser.org/cobra.jsp ) Puisque vous voulez pirater en python (toujours un bon choix), je recommande ces deux options: - packtpub.com/article/ web-scraping-with-python-part-2 - blog.databigbang.com/web-scraping-ajax-and-javascript-sites
bpgergo

Réponses:

203

EDIT 30 / Dec / 2017: Cette réponse apparaît dans les premiers résultats des recherches Google, j'ai donc décidé de la mettre à jour. La vieille réponse est toujours à la fin.

dryscape n'est plus maintenu et la bibliothèque que les développeurs de dryscape recommandent est uniquement Python 2. J'ai trouvé que l'utilisation de la bibliothèque python de Selenium avec Phantom JS en tant que pilote Web est assez rapide et facile pour faire le travail.

Une fois que vous avez installé Phantom JS , assurez-vous que le phantomjsbinaire est disponible dans le chemin actuel:

phantomjs --version
# result:
2.1.1

Exemple

Pour donner un exemple, j'ai créé une page d'exemple avec le code HTML suivant. ( lien ):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Javascript scraping test</title>
</head>
<body>
  <p id='intro-text'>No javascript support</p>
  <script>
     document.getElementById('intro-text').innerHTML = 'Yay! Supports javascript';
  </script> 
</body>
</html>

sans javascript il dit: No javascript supportet avec javascript:Yay! Supports javascript

Grattage sans support JS:

import requests
from bs4 import BeautifulSoup
response = requests.get(my_url)
soup = BeautifulSoup(response.text)
soup.find(id="intro-text")
# Result:
<p id="intro-text">No javascript support</p>

Grattage avec support JS:

from selenium import webdriver
driver = webdriver.PhantomJS()
driver.get(my_url)
p_element = driver.find_element_by_id(id_='intro-text')
print(p_element.text)
# result:
'Yay! Supports javascript'

Vous pouvez également utiliser la bibliothèque Python dryscrape pour gratter les sites Web pilotés par JavaScript.

Grattage avec support JS:

import dryscrape
from bs4 import BeautifulSoup
session = dryscrape.Session()
session.visit(my_url)
response = session.body()
soup = BeautifulSoup(response)
soup.find(id="intro-text")
# Result:
<p id="intro-text">Yay! Supports javascript</p>
avi
la source
16
Malheureusement, pas de support Windows.
Expenzor
1
Des alternatives pour ceux d'entre nous qui programment sous Windows?
Hoshiko86
2
@ExpenzorJe travaille sur Windows. PhantomJS fonctionne très bien.
Aakash Choubey
17
Il convient de noter que PhantomJS a été abandonné et n'est plus en cours de développement actif, car Chrome prend désormais en charge sans tête. L'utilisation de chrome / firefox sans tête est suggérée.
sytech
3
C'est à la fois le support du sélénium et PhantomJS lui-même. github.com/ariya/phantomjs/issues/15344
sytech
75

Nous n'obtenons pas les bons résultats car tout contenu généré par JavaScript doit être rendu sur le DOM. Lorsque nous récupérons une page HTML, nous récupérons le DOM initial, non modifié par javascript.

Par conséquent, nous devons rendre le contenu javascript avant d'explorer la page.

Comme le sélénium est déjà mentionné à plusieurs reprises dans ce fil (et la lenteur avec laquelle il devient parfois a également été mentionné), je vais énumérer deux autres solutions possibles.


Solution 1: Ceci est un très bon tutoriel sur la façon d'utiliser Scrapy pour analyser le contenu généré par JavaScript et nous allons suivre exactement cela.

Ce dont nous aurons besoin:

  1. Docker installé dans notre machine. C'est un plus par rapport aux autres solutions jusqu'à présent, car il utilise une plate-forme indépendante du système d'exploitation.

  2. Installez Splash en suivant les instructions répertoriées pour notre système d'exploitation correspondant.
    Citant de la documentation de démarrage:

    Splash est un service de rendu javascript. C'est un navigateur Web léger avec une API HTTP, implémenté dans Python 3 à l'aide de Twisted et QT5.

    Nous allons essentiellement utiliser Splash pour rendre le contenu généré par Javascript.

  3. Exécutez le serveur Splash: sudo docker run -p 8050:8050 scrapinghub/splash.

  4. Installez le plugin scrapy-splash :pip install scrapy-splash

  5. En supposant que nous avons déjà créé un projet Scrapy (sinon, faisons-en un ), nous suivrons le guide et mettrons à jour le settings.py:

    Ensuite, allez dans votre projet scrapy settings.pyet définissez ces middlewares:

    DOWNLOADER_MIDDLEWARES = {
          'scrapy_splash.SplashCookiesMiddleware': 723,
          'scrapy_splash.SplashMiddleware': 725,
          'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
    }

    L'URL du serveur Splash (si vous utilisez Win ou OSX, cela doit être l'URL de la machine docker: Comment obtenir l'adresse IP d'un conteneur Docker auprès de l'hôte? ):

    SPLASH_URL = 'http://localhost:8050'

    Et enfin, vous devez également définir ces valeurs:

    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
  6. Enfin, nous pouvons utiliser un SplashRequest:

    Dans une araignée normale, vous avez des objets Request que vous pouvez utiliser pour ouvrir des URL. Si la page que vous souhaitez ouvrir contient des données générées par JS, vous devez utiliser SplashRequest (ou SplashFormRequest) pour rendre la page. Voici un exemple simple:

    class MySpider(scrapy.Spider):
        name = "jsscraper"
        start_urls = ["http://quotes.toscrape.com/js/"]
    
        def start_requests(self):
            for url in self.start_urls:
            yield SplashRequest(
                url=url, callback=self.parse, endpoint='render.html'
            )
    
        def parse(self, response):
            for q in response.css("div.quote"):
            quote = QuoteItem()
            quote["author"] = q.css(".author::text").extract_first()
            quote["quote"] = q.css(".text::text").extract_first()
            yield quote

    SplashRequest rend l'URL au format HTML et renvoie la réponse que vous pouvez utiliser dans la méthode de rappel (parse).


Solution 2: Appelons cela expérimental pour le moment (mai 2018) ...
Cette solution est pour la version 3.6 de Python uniquement (pour le moment).

Connaissez-vous le module des requêtes (enfin qui ne le sait pas)?
Maintenant, il a un petit frère qui explore le Web: requests-HTML :

Cette bibliothèque a pour but de rendre l'analyse HTML (par exemple le scraping du Web) aussi simple et intuitive que possible.

  1. Installez requests-html: pipenv install requests-html

  2. Faites une demande à l'url de la page:

    from requests_html import HTMLSession
    
    session = HTMLSession()
    r = session.get(a_page_url)
  3. Rendez la réponse pour obtenir les bits générés par Javascript:

    r.html.render()

Enfin, le module semble offrir des capacités de grattage .
Alternativement, nous pouvons essayer la manière bien documentée d'utiliser BeautifulSoup avec l' r.htmlobjet que nous venons de rendre.

John Moutafis
la source
pouvez-vous développer comment obtenir le contenu HTML complet, avec les bits JS chargés, après avoir appelé .render ()? Je suis coincé après ce point. Je ne vois pas tous les iframes qui sont normalement injectés dans la page à partir de JavaScript dans l' r.html.htmlobjet.
anon58192932
@ anon58192932 Puisqu'il s'agit pour le moment d'une solution expérimentale et que je ne sais pas exactement ce que vous essayez de réaliser en conséquence, je ne peux vraiment rien suggérer ... Vous pouvez créer une nouvelle question ici sur SO si vous ne l'avez pas fait a encore
trouvé
2
J'ai eu cette erreur: RuntimeError: Impossible d'utiliser HTMLSession dans une boucle d'événements existante. Utilisez plutôt AsyncHTMLSession.
HuckIt
1
@HuckIl semble être un problème connu: github.com/psf/requests-html/issues/140
John Moutafis
47

Peut-être que le sélénium peut le faire.

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
étonnant ici
la source
3
Le sélénium est vraiment lourd pour ce genre de chose, qui serait inutilement lent et nécessiterait une tête de navigateur si vous n'utilisez pas PhantomJS, mais cela fonctionnerait.
Joshua Hedges
@JoshuaHedges Vous pouvez exécuter d'autres navigateurs plus standard en mode sans tête.
reynoldsnlp le
22

Si vous avez déjà utilisé le Requestsmodule pour python auparavant, j'ai récemment découvert que le développeur avait créé un nouveau module appelé Requests-HTMLqui a maintenant également la capacité de rendre JavaScript.

Vous pouvez également visiter https://html.python-requests.org/ pour en savoir plus sur ce module, ou si vous êtes uniquement intéressé par le rendu JavaScript, vous pouvez visiter https://html.python-requests.org/?#javascript -support pour apprendre directement à utiliser le module pour rendre JavaScript en utilisant Python.

Essentiellement, une fois que vous avez correctement installé le Requests-HTMLmodule, l'exemple suivant, qui est affiché sur le lien ci-dessus , montre comment vous pouvez utiliser ce module pour gratter un site Web et rendre le JavaScript contenu dans le site Web:

from requests_html import HTMLSession
session = HTMLSession()

r = session.get('http://python-requests.org/')

r.html.render()

r.html.search('Python 2 will retire in only {months} months!')['months']

'<time>25</time>' #This is the result.

J'ai récemment appris cela à partir d'une vidéo YouTube. Cliquez ici! pour regarder la vidéo YouTube, qui montre le fonctionnement du module.

SShah
la source
3
Il faut noter que ce module prend uniquement en charge Python 3.6.
nat5142
1
J'ai eu cette erreur: SSLError: HTTPSConnectionPool (host = 'docs.python-requests.org', port = 443): Nombre maximal de tentatives dépassé avec url: / (Causé par SSLError (SSLError (1, '[SSL: TLSV1_ALERT_INTERNAL_ERROR] alerte tlsv1 erreur interne (_ssl.c: 1045) ')))
HuckIt
@HuckIt excuses Je ne suis pas familier avec cette erreur, mais l'erreur semble être le suivant: le site Web que vous tentiez d'accéder pourrait avoir eu un problème lié à la certification SSL. Désolé ce n'est pas une solution, mais je vous recommanderais de faire une nouvelle question, ici en débordement de pile (si cela n'a pas déjà été posé) et éventuellement de donner plus de détails tels que l'url du site Web que vous utilisiez et votre code.
SShah
Semble utiliser du chrome sous le capot. Fonctionne très bien pour moi cependant
Sid
14

Cela semble également être une bonne solution, tirée d'un excellent article de blog

import sys  
from PyQt4.QtGui import *  
from PyQt4.QtCore import *  
from PyQt4.QtWebKit import *  
from lxml import html 

#Take this class for granted.Just use result of rendering.
class Render(QWebPage):  
  def __init__(self, url):  
    self.app = QApplication(sys.argv)  
    QWebPage.__init__(self)  
    self.loadFinished.connect(self._loadFinished)  
    self.mainFrame().load(QUrl(url))  
    self.app.exec_()  

  def _loadFinished(self, result):  
    self.frame = self.mainFrame()  
    self.app.quit()  

url = 'http://pycoders.com/archive/'  
r = Render(url)  
result = r.frame.toHtml()
# This step is important.Converting QString to Ascii for lxml to process

# The following returns an lxml element tree
archive_links = html.fromstring(str(result.toAscii()))
print archive_links

# The following returns an array containing the URLs
raw_links = archive_links.xpath('//div[@class="campaign"]/a/@href')
print raw_links
marbel
la source
12

Il semble que les données que vous recherchez vraiment soient accessibles via une URL secondaire appelée par un javascript sur la page principale.

Bien que vous puissiez essayer d'exécuter javascript sur le serveur pour gérer cela, une approche plus simple pourrait être de charger la page à l'aide de Firefox et d'utiliser un outil comme Charles ou Firebug pour identifier exactement ce qu'est cette URL secondaire. Ensuite, vous pouvez simplement interroger cette URL directement pour les données qui vous intéressent.

Stephen Emslie
la source
@Kris Juste au cas où quelqu'un trébucherait là-dessus et voudrait l'essayer au lieu de quelque chose d'aussi lourd que le sélénium, voici un court exemple. Cela ouvrira la page de détail de la pièce pour un écrou hexagonal sur le site Web de McMaster-Carr. Le contenu de leur site Web est principalement récupéré à l'aide de Javascript et contient très peu d'informations sur la page native. Si vous ouvrez les outils de développement de votre navigateur, accédez à l'onglet Réseau et actualisez la page, vous pouvez voir toutes les demandes faites par la page et trouver les données pertinentes (dans ce cas, la partie détail html).
SweepingsDemon
Il s'agit d'une URL différente trouvée dans l'onglet Firefox devtool Network qui, si elle est suivie, contient le html pour la plupart des informations sur les pièces et expose certains des paramètres nécessaires pour naviguer facilement vers d'autres informations sur les pièces pour faciliter le grattage. Cet exemple particulier n'est pas particulièrement utile car le prix est généré par une autre fonction Javascript, mais devrait suffisamment bien servir d'introduction à quiconque souhaite suivre les conseils de Stephen.
SweepingsDemon
12

Le sélénium est le meilleur pour gratter le contenu JS et Ajax.

Consultez cet article pour extraire des données du Web à l'aide de Python

$ pip install selenium

Ensuite, téléchargez le pilote Web Chrome.

from selenium import webdriver

browser = webdriver.Chrome()

browser.get("https://www.python.org/")

nav = browser.find_element_by_id("mainnav")

print(nav.text)

Facile, non?

Macnux
la source
8

Vous pouvez également exécuter du javascript à l'aide de webdriver.

from selenium import webdriver

driver = webdriver.Firefox()
driver.get(url)
driver.execute_script('document.title')

ou stocker la valeur dans une variable

result = driver.execute_script('var text = document.title ; return var')
Serpentr
la source
ou vous pouvez simplement utiliser la driver.titlepropriété
Corey Goldberg
8

Personnellement, je préfère utiliser la scrapy et le sélénium et docker les deux dans des conteneurs séparés. De cette façon, vous pouvez installer les deux avec un minimum de tracas et explorer des sites Web modernes qui contiennent presque tous du javascript sous une forme ou une autre. Voici un exemple:

Utilisez le scrapy startprojectpour créer votre grattoir et écrire votre araignée, le squelette peut être aussi simple que ceci:

import scrapy


class MySpider(scrapy.Spider):
    name = 'my_spider'
    start_urls = ['https://somewhere.com']

    def start_requests(self):
        yield scrapy.Request(url=self.start_urls[0])


    def parse(self, response):

        # do stuff with results, scrape items etc.
        # now were just checking everything worked

        print(response.body)

La vraie magie se produit dans le middlewares.py. Remplacez deux méthodes dans le middleware du téléchargeur __init__et process_request, de la manière suivante:

# import some additional modules that we need
import os
from copy import deepcopy
from time import sleep

from scrapy import signals
from scrapy.http import HtmlResponse
from selenium import webdriver

class SampleProjectDownloaderMiddleware(object):

def __init__(self):
    SELENIUM_LOCATION = os.environ.get('SELENIUM_LOCATION', 'NOT_HERE')
    SELENIUM_URL = f'http://{SELENIUM_LOCATION}:4444/wd/hub'
    chrome_options = webdriver.ChromeOptions()

    # chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
    self.driver = webdriver.Remote(command_executor=SELENIUM_URL,
                                   desired_capabilities=chrome_options.to_capabilities())


def process_request(self, request, spider):

    self.driver.get(request.url)

    # sleep a bit so the page has time to load
    # or monitor items on page to continue as soon as page ready
    sleep(4)

    # if you need to manipulate the page content like clicking and scrolling, you do it here
    # self.driver.find_element_by_css_selector('.my-class').click()

    # you only need the now properly and completely rendered html from your page to get results
    body = deepcopy(self.driver.page_source)

    # copy the current url in case of redirects
    url = deepcopy(self.driver.current_url)

    return HtmlResponse(url, body=body, encoding='utf-8', request=request)

N'oubliez pas d'activer cet intermédiaire en décommentant les lignes suivantes dans le fichier settings.py:

DOWNLOADER_MIDDLEWARES = {
'sample_project.middlewares.SampleProjectDownloaderMiddleware': 543,}

Ensuite pour l'ancrage. Créez votre à Dockerfilepartir d'une image légère (j'utilise python Alpine ici), copiez-y le répertoire de votre projet, installez les exigences:

# Use an official Python runtime as a parent image
FROM python:3.6-alpine

# install some packages necessary to scrapy and then curl because it's  handy for debugging
RUN apk --update add linux-headers libffi-dev openssl-dev build-base libxslt-dev libxml2-dev curl python-dev

WORKDIR /my_scraper

ADD requirements.txt /my_scraper/

RUN pip install -r requirements.txt

ADD . /scrapers

Et enfin, rassemblez tout cela dans docker-compose.yaml:

version: '2'
services:
  selenium:
    image: selenium/standalone-chrome
    ports:
      - "4444:4444"
    shm_size: 1G

  my_scraper:
    build: .
    depends_on:
      - "selenium"
    environment:
      - SELENIUM_LOCATION=samplecrawler_selenium_1
    volumes:
      - .:/my_scraper
    # use this command to keep the container running
    command: tail -f /dev/null

Courez docker-compose up -d. Si vous faites cela la première fois, cela prendra un certain temps pour récupérer le dernier sélénium / chrome autonome et créer votre image de grattoir.

Une fois que c'est fait, vous pouvez vérifier que vos conteneurs fonctionnent avec docker pset également vérifier que le nom du conteneur de sélénium correspond à celui de la variable d'environnement que nous avons passée à notre conteneur de racleur (ici, c'était le cas SELENIUM_LOCATION=samplecrawler_selenium_1).

Entrez votre conteneur de grattoir avec docker exec -ti YOUR_CONTAINER_NAME sh, la commande pour moi était docker exec -ti samplecrawler_my_scraper_1 sh, cd dans le bon répertoire et exécutez votre grattoir avec scrapy crawl my_spider.

Le tout est sur ma page github et vous pouvez l'obtenir d' ici

tarikki
la source
5

Un mélange de BeautifulSoup et de Selenium fonctionne très bien pour moi.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup as bs

driver = webdriver.Firefox()
driver.get("http://somedomain/url_that_delays_loading")
    try:
        element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "myDynamicElement"))) #waits 10 seconds until element is located. Can have other wait conditions  such as visibility_of_element_located or text_to_be_present_in_element

        html = driver.page_source
        soup = bs(html, "lxml")
        dynamic_text = soup.find_all("p", {"class":"class_name"}) #or other attributes, optional
    else:
        print("Couldnt locate element")

PS Vous pouvez trouver plus de conditions d'attente ici

Biarys
la source
4

Vous voudrez utiliser urllib, requests, beautifulSoup et le pilote Web sélénium dans votre script pour différentes parties de la page (pour n'en nommer que quelques-unes).
Parfois, vous obtiendrez ce dont vous avez besoin avec un seul de ces modules.
Parfois, vous aurez besoin de deux, trois ou tous ces modules.
Parfois, vous devrez désactiver les js sur votre navigateur.
Parfois, vous aurez besoin d'informations d'en-tête dans votre script.
Aucun site Web ne peut être gratté de la même manière et aucun site Web ne peut être gratté de la même manière pour toujours sans avoir à modifier votre robot d'exploration, généralement après quelques mois. Mais ils peuvent tous être grattés! Là où il y a une volonté, il y a un moyen sûr.
Si vous avez besoin de données récupérées en continu dans le futur, il vous suffit de gratter tout ce dont vous avez besoin et de le stocker dans des fichiers .dat avec pickle.
Continuez simplement à chercher comment essayer quoi avec ces modules et copiez et collez vos erreurs dans Google.


la source
3

Utilisation de PyQt5

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEnginePage
import sys
import bs4 as bs
import urllib.request


class Client(QWebEnginePage):
    def __init__(self,url):
        global app
        self.app = QApplication(sys.argv)
        QWebEnginePage.__init__(self)
        self.html = ""
        self.loadFinished.connect(self.on_load_finished)
        self.load(QUrl(url))
        self.app.exec_()

    def on_load_finished(self):
        self.html = self.toHtml(self.Callable)
        print("Load Finished")

    def Callable(self,data):
        self.html = data
        self.app.quit()

# url = ""
# client_response = Client(url)
# print(client_response.html)
Ash-Ishh.
la source
1

J'essaye de trouver réponse à ces questions depuis deux jours. De nombreuses réponses vous orientent vers différents problèmes. Mais la réponse de Serpentr ci-dessus est vraiment pertinente . C'est la solution la plus courte et la plus simple. Juste un rappel, le dernier mot "var" représente le nom de la variable , il doit donc être utilisé comme:

 result = driver.execute_script('var text = document.title ; return text')
Abd_bgc
la source
Cela devrait être un commentaire sur la réponse de serpentr, pas une réponse séparée.
Yserbius le
1
Cela est évident. Mais je n'ai pas encore 50 représentants pour commenter la réponse de quelqu'un d'autre.
Abd_bgc le
0

J'ai dû faire face à ce même problème sur certains projets de web scraping. La façon dont je l'ai géré était en utilisant la bibliothèque de requêtes python pour faire une requête http directement à l'API, au lieu d'avoir à charger le JS.

La bibliothèque de requêtes python fonctionne bien pour cela, et vous pouvez voir les requêtes http en utilisant l'élément inspect et en accédant à l'onglet réseau.

Superduperflu
la source