Página de JavaScript de raspado web con Python

Estoy tratando de desarrollar un raspador web simple. Quiero extraer texto sin el código HTML. De hecho, logro este objective, 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 algún texto, no lo puedo ver, porque cuando llamo

response = urllib2.urlopen(request) 

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

Entonces, estoy buscando algunas ideas para resolver este problema.

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

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

Una vez que haya instalado Phantom JS , asegúrese de que el binario de phantomjs 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 ):

     Javascript scraping test   

No javascript support

sin javascript dice: No javascript support y con javascript: Yay! Supports 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: 

No javascript support

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 de 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: 

Yay! Supports javascript

Tal vez el selenium pueda hacerlo.

 from selenium import webdriver import time driver = webdriver.Firefox() driver.get(url) time.sleep(5) htmlSource = driver.page_source 

No estamos obteniendo los resultados correctos porque cualquier contenido generado por javascript debe representarse en el DOM. Cuando obtenemos una página HTML, obtenemos la inicial, sin modificar por javascript, DOM.

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

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


Solución 1: Este es un muy buen tutorial sobre cómo usar Scrapy para rastrear el contenido generado en javascript y lo seguiremos.

Lo que necesitaremos:

  1. Docker instalado en nuestra máquina. Esta 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 desde la documentación de bienvenida:

    Splash es un servicio de renderizado javascript. Es un navegador web ligero con una API HTTP, implementado en Python 3 utilizando Twisted y QT5.

    Esencialmente, vamos a utilizar Splash para renderizar el contenido generado en Javascript.

  3. Ejecute el servidor de bienvenida: sudo docker run -p 8050:8050 scrapinghub/splash .

  4. Instala el plugin scrapy-splash : pip install scrapy-splash

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

    Luego, vaya a settings.py de su proyecto de scrapy y 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 necesitas establecer estos valores también:

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

    En una araña normal tienes objetos de solicitud que puedes usar para abrir URL. Si la página que desea abrir contiene datos generados por JS, debe usar SplashRequest (o SplashFormRequest) para renderizar 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 presenta la URL como html y devuelve la respuesta que puede utilizar en el método de callback (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 (en este momento).

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

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

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

  2. Hacer una solicitud a la url de la página:

     from requests_html import HTMLSession session = HTMLSession() r = session.get(a_page_url) 
  3. Renderiza la respuesta para obtener los bits generados en 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 objeto r.html que acabamos de representar.

Esto también parece ser una buena solución, tomada de una excelente 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 

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

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

Esencialmente, una vez que haya instalado correctamente el módulo de Requests-HTML , el siguiente ejemplo, que se muestra en el enlace anterior , muestra cómo puede usar este módulo para raspar un sitio web y representar el código JavaScript que contiene 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'] '' #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.

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

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

Selenium es el mejor para raspar el contenido de JS y Ajax.

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

 $ pip install selenium 

A continuación, descargue 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?

También puedes 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') 

Querrá usar urllib, drivers, beautifulSoup y selenium web driver en su script 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ás dos, tres o todos estos módulos.
A veces tendrás que apagar el js en tu navegador.
A veces necesitarás información del encabezado en tu script.
Ningún sitio web puede ser raspado de la misma manera y ningún sitio web puede ser raspado de la misma manera para siempre sin tener que modificar su rastreador, generalmente después de unos pocos meses. ¡Pero todos pueden ser raspados! Donde hay una voluntad hay una manera segura.
Si necesita datos raspados continuamente en el futuro, simplemente raspe todo lo que necesita y guárdelo en archivos .dat con pickle.
Simplemente siga buscando cómo probar qué con estos módulos y copiando y pegando sus errores en Google.

Una mezcla de BeautifulSoup y Selenium funciona muy bien para mí.

 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í.

Personalmente, prefiero usar scrapy y selenium y desgasificar ambos en recipientes separados. De esta manera, puede instalar ambos con problemas mínimos y rastrear sitios web modernos que casi todos contienen javascript de una forma u otra. Aquí hay un ejemplo:

Usa el scrapy startproject de scrapy startproject para crear tu raspador y escribe tu 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 sucede en el middlewares.py. Sobrescriba dos métodos en el middleware de descarga, __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 software intermedio sin comentar las siguientes líneas en el archivo settings.py:

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

Siguiente para la dockerización. Cree su Dockerfile desde una imagen liviana (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 docker-compose.yaml 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 recuperar el último selenium / cromo independiente y también construir la imagen de su raspador.

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

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

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

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