Página JavaScript de raspado web con Python

178

Estoy tratando de desarrollar un simple raspador web. Quiero extraer texto sin el código HTML. De hecho, logro este objetivo, pero he visto que en algunas páginas donde se carga JavaScript no obtuve buenos resultados.

Por ejemplo, si algún código JavaScript agrega texto, no puedo verlo, porque cuando llamo

response = urllib2.urlopen(request)

Recibo el texto original sin el agregado (porque JavaScript se ejecuta en el cliente).

Entonces, estoy buscando algunas ideas para resolver este problema.

mocopera
fuente
2
Parece que necesitas algo más pesado, prueba Selenium o Watir.
wim
2
Lo hice con éxito en Java (utilicé el kit de herramientas Cobra lobobrowser.org/cobra.jsp ). Ya que quieres hackear Python (siempre es una buena opción), te recomiendo estas dos opciones: - packtpub.com/article/ web-scraping-with-python-part-2 - blog.databigbang.com/web-scraping-ajax-and-javascript-sites
bpgergo

Respuestas:

203

EDITAR 30 / Dic / 2017: esta respuesta aparece en los principales resultados de las búsquedas de Google, así que decidí actualizarla. La vieja respuesta todavía está al final.

dryscape ya no se mantiene y la biblioteca que recomiendan los desarrolladores de dryscape es solo Python 2. He encontrado que usar la biblioteca de Python de Selenium con Phantom JS como controlador web es lo suficientemente rápido y fácil de hacer el trabajo.

Una vez que haya instalado Phantom JS , asegúrese de que el phantomjsbinario esté disponible en la ruta actual:

phantomjs --version
# result:
2.1.1

Ejemplo

Para dar un ejemplo, creé una página de muestra con el siguiente código HTML. ( enlace ):

<!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>

sin javascript dice: No javascript supporty con javascript:Yay! Supports javascript

Raspado sin soporte 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>

Raspado con soporte 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'

También puede usar la biblioteca Python dryscrape para raspar sitios web controlados por javascript.

Raspado con soporte 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
fuente
16
Lamentablemente, no hay soporte de Windows.
Expenzor
1
¿Alguna alternativa para aquellos de nosotros que programamos en Windows?
Hoshiko86
2
@ExpenzorEstoy trabajando en windows. PhantomJS funciona bien.
Aakash Choubey
17
Vale la pena señalar que PhantomJS ha sido descontinuado y ya no está en desarrollo activo a la luz de Chrome ahora compatible con sin cabeza. Se sugiere el uso de Chrome / Firefox sin cabeza.
sytech
3
Es a la vez soporte de selenio y PhantomJS en sí. github.com/ariya/phantomjs/issues/15344
sytech
73

No estamos obteniendo los resultados correctos porque cualquier contenido generado por JavaScript debe ser procesado en el DOM. Cuando buscamos una página HTML, buscamos la inicial, no modificada por javascript, DOM.

Por lo tanto, debemos procesar el contenido de JavaScript antes de rastrear la página.

Como el selenio ya se menciona muchas veces en este hilo (y también se menciona lo lento que se pone a veces), enumeraré otras dos posibles soluciones.


Solución 1: Este es un tutorial muy bueno sobre cómo usar Scrapy para rastrear contenido generado por JavaScript y vamos a seguir exactamente eso.

Lo que necesitaremos:

  1. Docker instalado en nuestra máquina. Esto es una ventaja sobre otras soluciones hasta este punto, ya que utiliza una plataforma independiente del sistema operativo.

  2. Instale Splash siguiendo las instrucciones enumeradas para nuestro sistema operativo correspondiente.
    Citando de la documentación de bienvenida:

    Splash es un servicio de representación de JavaScript. Es un navegador web liviano con una API HTTP, implementado en Python 3 usando Twisted y QT5.

    Esencialmente vamos a usar Splash para renderizar contenido generado por Javascript.

  3. Ejecutar el servidor de salpicaduras: sudo docker run -p 8050:8050 scrapinghub/splash.

  4. Instale el complemento scrapy-splash :pip install scrapy-splash

  5. Suponiendo que ya tenemos un proyecto Scrapy creado (si no, hagamos uno ), seguiremos la guía y actualizaremos settings.py:

    Luego vaya a su proyecto inestable settings.pyy configure estos middlewares:

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

    La URL del servidor Splash (si está utilizando Win u OSX, esta debería ser la URL de la máquina acoplable: ¿Cómo obtener la dirección IP de un contenedor Docker del host? ):

    SPLASH_URL = 'http://localhost:8050'

    Y finalmente también necesita establecer estos valores:

    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
  6. Finalmente, podemos usar un SplashRequest:

    En una araña normal, tiene objetos Solicitud que puede usar para abrir URL. Si la página que desea abrir contiene datos generados por JS, debe usar SplashRequest (o SplashFormRequest) para representar la página. Aquí hay un ejemplo 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 representa la URL como html y devuelve la respuesta que puede usar en el método de devolución de llamada (análisis).


Solución 2: Llamemos a esto experimental en este momento (mayo de 2018) ...
Esta solución es solo para la versión 3.6 de Python (por el momento).

¿Conoces el módulo de solicitudes (bueno, quién no)?
Ahora tiene un pequeño hermano web que rastrea: peticiones-HTML :

Esta biblioteca tiene la intención de hacer que el análisis HTML (por ejemplo, raspar la web) sea lo más simple e intuitivo posible.

  1. Instalar request-html: pipenv install requests-html

  2. Haga una solicitud a la URL de la página:

    from requests_html import HTMLSession
    
    session = HTMLSession()
    r = session.get(a_page_url)
  3. Procese la respuesta para obtener los bits generados por Javascript:

    r.html.render()

Finalmente, el módulo parece ofrecer capacidades de raspado .
Alternativamente, podemos probar la forma bien documentada de usar BeautifulSoup con el r.htmlobjeto que acabamos de renderizar.

John Moutafis
fuente
¿Puedes ampliar cómo obtener el contenido HTML completo, con bits JS cargados, después de llamar a .render ()? Estoy atrapado después de ese punto. No veo todos los iframes que se inyectan en la página normalmente desde JavaScript en el r.html.htmlobjeto.
anon58192932
@ anon58192932 Dado que por el momento esta es una solución experimental y no sé qué es exactamente lo que está tratando de lograr como resultado, realmente no puedo sugerir nada ... Puede crear una nueva pregunta aquí en SO si no lo ha hecho
resolvió
2
Recibí este error: RuntimeError: no se puede usar HTMLSession dentro de un bucle de eventos existente. Utilice AsyncHTMLSession en su lugar.
HuckIt
1
@HuckIt esto parece ser un problema conocido: github.com/psf/requests-html/issues/140
John Moutafis
47

Quizás el selenio pueda hacerlo.

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
asombroso
fuente
3
El selenio es realmente pesado para este tipo de cosas, que sería innecesariamente lento y requiere una cabeza de navegador si no usa PhantomJS, pero esto funcionaría.
Joshua Hedges
@JoshuaHedges Puede ejecutar otros navegadores más estándar en modo sin cabeza.
reynoldsnlp
22

Si alguna vez ha usado el Requestsmódulo para Python antes, descubrí recientemente que el desarrollador creó un nuevo módulo llamado Requests-HTMLque ahora también tiene la capacidad de representar JavaScript.

También puedes visitar https://html.python-requests.org/ para obtener más información sobre este módulo, o si solo está interesado en representar JavaScript, puede visitar https://html.python-requests.org/?#javascript -Soporte para aprender directamente cómo usar el módulo para representar JavaScript usando Python.

Esencialmente, una vez que instala correctamente el Requests-HTMLmódulo, el siguiente ejemplo, que es muestra en el enlace anterior , muestra cómo puede usar este módulo para raspar un sitio web y renderizar JavaScript contenido en el sitio 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.

Recientemente me enteré de esto en un video de YouTube. ¡Haga clic aquí! para ver el video de YouTube, que demuestra cómo funciona el módulo.

SShah
fuente
3
Debe tener en cuenta que este módulo solo admite Python 3.6.
nat5142
1
Recibí este error: SSLError: HTTPSConnectionPool (host = 'docs.python-requests.org', puerto = 443): Reintentos máximos excedidos con url: / (Causado por SSLError (SSLError (1, '[SSL: TLSV1_ALERT_INTERNAL_ERROR] alerta tlsv1) error interno (_ssl.c: 1045) ')))
HuckIt
@HuckIt disculpas No estoy familiarizado con ese error, sin embargo, parece ser que el sitio web al que intentaba acceder podría haber tenido un problema relacionado con la certificación SSL. Lo sentimos, esta no es una solución, pero le recomendaría que haga una nueva pregunta, aquí en el desbordamiento de la pila (si aún no se ha preguntado) y posiblemente brinde más detalles, como la URL del sitio web que estaba usando y su código.
SShah
Parece estar usando cromo debajo del capó. Sin embargo
Sid
14

Esta parece ser una buena solución también, tomada de una gran publicación 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
fuente
12

Parece que se puede acceder a los datos que realmente está buscando a través de una URL secundaria llamada por algunos javascript en la página principal.

Si bien podría intentar ejecutar javascript en el servidor para manejar esto, un enfoque más simple podría ser cargar la página usando Firefox y usar una herramienta como Charles o Firebug para identificar exactamente cuál es esa URL secundaria. Luego, puede consultar directamente en esa URL los datos que le interesan.

Stephen Emslie
fuente
@Kris En caso de que alguien tropiece con esto y quiera probarlo en lugar de algo tan pesado como el selenio, aquí hay un breve ejemplo. Esto abrirá la página de detalles de la pieza para una tuerca hexagonal en el sitio web de McMaster-Carr. El contenido de su sitio web se obtiene principalmente mediante Javascript y tiene muy poca información de la página nativa. Si abre las herramientas de desarrollo de su navegador, navega a la pestaña Red y actualiza la página, puede ver todas las solicitudes realizadas por la página y encontrar los datos relevantes (en este caso, el detalle html de la pieza).
SweepingsDemon
Esta es una url diferente que se encuentra en la pestaña Red de devtool de Firefox que, si se sigue, contiene el html para la mayor parte de la información de la parte y expone algunos de los parámetros necesarios para navegar fácilmente a otra información de parte para un raspado más fácil. Este ejemplo en particular no es particularmente útil ya que el precio es generado por otra función de Javascript, pero debería servir lo suficientemente bien como una introducción a cualquiera que quiera seguir los consejos de Stephen.
SweepingsDemon
12

El selenio es el mejor para raspar contenido JS y Ajax.

Consulte este artículo para extraer datos de la web usando Python

$ pip install selenium

Luego descarga Chrome webdriver.

from selenium import webdriver

browser = webdriver.Chrome()

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

nav = browser.find_element_by_id("mainnav")

print(nav.text)

Fácil, verdad?

Macnux
fuente
8

También puede ejecutar javascript usando webdriver.

from selenium import webdriver

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

o almacenar el valor en una variable

result = driver.execute_script('var text = document.title ; return var')
Serpentr
fuente
o simplemente puede usar la driver.titlepropiedad
Corey Goldberg
8

Personalmente prefiero usar scrapy y selenio y dockerizar ambos en contenedores separados. De esta forma, puede instalar ambos con una molestia mínima y rastrear sitios web modernos que casi todos contienen JavaScript de una forma u otra. Aquí hay un ejemplo:

Use el scrapy startprojectpara crear su raspador y escribir su araña, el esqueleto puede ser tan simple como esto:

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 verdadera magia ocurre en middlewares.py. Sobrescriba dos métodos en el middleware del descargador __init__y process_request, de la siguiente manera:

# 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)

No olvide habilitar este middlware descomentando las siguientes líneas en el archivo settings.py:

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

Siguiente para la dockerización. Cree su imagen a Dockerfilepartir de una imagen ligera (estoy usando python Alpine aquí), copie el directorio de su proyecto, instale los requisitos:

# 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

Y finalmente reúne todo en 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

Ejecutar docker-compose up -d. Si está haciendo esto la primera vez, le llevará un tiempo obtener el último selenio / cromo independiente y también construir su imagen de rascador.

Una vez hecho esto, puede verificar que sus contenedores se estén ejecutando docker psy también verificar que el nombre del contenedor de selenio coincida con el de la variable de entorno que pasamos a nuestro contenedor de raspador (aquí estaba SELENIUM_LOCATION=samplecrawler_selenium_1).

Ingrese su contenedor de raspador con docker exec -ti YOUR_CONTAINER_NAME sh, el comando para mí fue docker exec -ti samplecrawler_my_scraper_1 sh, cd en el directorio correcto y ejecute su raspador con scrapy crawl my_spider.

Todo está en mi página de Github y puedes obtenerlo desde aquí.

tarikki
fuente
5

Una mezcla de BeautifulSoup y Selenium me funciona muy bien.

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")

PD Puedes encontrar más condiciones de espera aquí

Biarios
fuente
4

Querrá usar urllib, peticiones, beautifulSoup y el controlador web de selenio en su secuencia de comandos para diferentes partes de la página, (por nombrar algunas).
A veces obtendrá lo que necesita con solo uno de estos módulos.
A veces necesitará dos, tres o todos estos módulos.
A veces necesitará apagar el js en su navegador.
A veces necesitarás información de encabezado en tu script.
Ningún sitio web se puede eliminar de la misma manera y ningún sitio web se puede eliminar de la misma manera para siempre sin tener que modificar su rastreador, generalmente después de unos meses. ¡Pero todos pueden ser raspados! Donde hay voluntad hay una manera segura.
Si necesita datos raspados continuamente en el futuro, simplemente raspe todo lo que necesita y almacénelos en archivos .dat con pickle.
Simplemente siga buscando cómo probar qué con estos módulos y copiando y pegando sus errores en Google.


fuente
3

Usando 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 ..
fuente
1

He estado tratando de encontrar respuesta a estas preguntas durante dos días. Muchas respuestas lo dirigen a diferentes problemas. Pero la respuesta anterior de serpentr es realmente al punto. Es la solución más corta y simple. Solo un recordatorio de que la última palabra "var" representa el nombre de la variable , por lo que debe usarse como:

 result = driver.execute_script('var text = document.title ; return text')
Abd_bgc
fuente
Esto debería ser un comentario sobre la respuesta de serpentr, no una respuesta separada.
Yserbius
1
Eso es obvio. Pero todavía no tengo 50 repeticiones para hacer comentarios sobre la respuesta de otra persona.
Abd_bgc
0

He tenido que lidiar con este mismo problema en algunos proyectos propios de scraping web. La forma en que lo traté fue mediante el uso de la biblioteca de solicitudes de Python para hacer una solicitud http directamente a la API, en lugar de tener que cargar el JS.

La biblioteca de solicitudes de Python funciona bien para esto, y puede ver las solicitudes http utilizando el elemento de inspección y navegando a la pestaña de red.

Superdufluo
fuente