Página JavaScript de raspagem da Web com Python

178

Estou tentando desenvolver um raspador de web simples. Quero extrair texto sem o código HTML. De fato, alcancei esse objetivo, mas vi que em algumas páginas onde o JavaScript é carregado, não obtive bons resultados.

Por exemplo, se algum código JavaScript adiciona algum texto, não consigo vê-lo, porque quando ligo

response = urllib2.urlopen(request)

Recebo o texto original sem o adicionado (porque o JavaScript é executado no cliente).

Então, estou procurando algumas idéias para resolver esse problema.

mocopera
fonte
2
Parece que você pode precisar de algo mais pesado, tente Selenium ou Watir.
Wim
2
Fiz isso com sucesso em Java (usei o kit de ferramentas Cobra lobobrowser.org/cobra.jsp ) Como você deseja hackear em python (sempre uma boa escolha), recomendo estas duas opções: - packtpub.com/article/ web raspando-com-pitão-co-2 - blog.databigbang.com/web-scraping-ajax-and-javascript-sites
bpgergo

Respostas:

203

EDIT 30 / Dez / 2017: Esta resposta aparece nos principais resultados das pesquisas no Google, então decidi atualizá-la. A resposta antiga ainda está no fim.

O dryscape não é mais mantido e a biblioteca que os desenvolvedores do dryscape recomendam é apenas Python 2. Descobri que o uso da biblioteca python do Selenium com o Phantom JS como um driver da Web é rápido o suficiente e fácil de realizar o trabalho.

Depois de instalar o Phantom JS , verifique se o phantomjsbinário está disponível no caminho atual:

phantomjs --version
# result:
2.1.1

Exemplo

Para dar um exemplo, criei uma página de amostra com o seguinte código HTML. ( link ):

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

sem javascript diz: No javascript supporte com javascript:Yay! Supports javascript

Raspagem sem suporte a 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>

Raspagem com suporte a 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'

Você também pode usar o dryscrape da biblioteca Python para raspar sites direcionados a javascript.

Raspagem com suporte a 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
fonte
16
Infelizmente, não há suporte para o Windows.
Expenzor 17/04
1
Alguma alternativa para nós programamos no Windows?
precisa saber é o seguinte
2
@ExpenzorEu estou trabalhando no Windows. PhantomJS funciona bem.
Aakash Choubey
17
Vale ressaltar que o PhantomJS foi descontinuado e não está mais em desenvolvimento ativo à luz do Chrome que agora suporta decapitado. É sugerido o uso de chrome / headefox sem cabeça.
Sytech
3
É o suporte ao selênio e o próprio PhantomJS. github.com/ariya/phantomjs/issues/15344
sytech
73

Não estamos obtendo os resultados corretos porque qualquer conteúdo gerado em javascript precisa ser renderizado no DOM. Quando buscamos uma página HTML, buscamos a inicial, sem modificação por javascript, DOM.

Portanto, precisamos renderizar o conteúdo javascript antes de rastrear a página.

Como o selênio já é mencionado várias vezes neste tópico (e o quão lento às vezes também foi mencionado), listarei duas outras soluções possíveis.


Solução 1: este é um tutorial muito bom sobre como usar o Scrapy para rastrear conteúdo gerado por javascript e seguiremos exatamente isso.

Do que precisaremos:

  1. Docker instalado em nossa máquina. Essa é uma vantagem sobre outras soluções até este momento, pois utiliza uma plataforma independente do SO.

  2. Instale o Splash seguindo as instruções listadas para o sistema operacional correspondente.
    Citando a partir da documentação inicial:

    Splash é um serviço de renderização em javascript. É um navegador leve com uma API HTTP, implementado no Python 3 usando Twisted e QT5.

    Basicamente, vamos usar o Splash para renderizar o conteúdo gerado pelo Javascript.

  3. Execute o servidor salpicos: sudo docker run -p 8050:8050 scrapinghub/splash.

  4. Instale o plug- in scrapy-splash :pip install scrapy-splash

  5. Supondo que já tenhamos um projeto Scrapy criado (caso contrário, vamos criar um ), seguiremos o guia e atualizaremos o settings.py:

    Em seguida, vá para o seu projeto scrapy settings.pye defina estes middlewares:

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

    O URL do servidor Splash (se você estiver usando o Win ou OSX, este deve ser o URL da máquina docker: como obter o endereço IP de um contêiner Docker do host? ):

    SPLASH_URL = 'http://localhost:8050'

    E, finalmente, você também precisa definir esses valores:

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

    Em uma aranha normal, você tem objetos de solicitação que podem ser usados ​​para abrir URLs. Se a página que você deseja abrir contiver dados gerados por JS, será necessário usar SplashRequest (ou SplashFormRequest) para renderizar a página. Aqui está um exemplo simples:

    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 renderiza a URL como html e retorna a resposta que você pode usar no método de retorno de chamada (análise).


Solução 2: Vamos chamar isso de experimental no momento (maio de 2018) ...
Esta solução é apenas para a versão 3.6 do Python (no momento).

Você conhece o módulo de solicitações (bem quem não conhece)?
Agora ele tem uma web que rasteja pouco irmão: orders-HTML :

Esta biblioteca pretende tornar a análise de HTML (por exemplo, raspar a web) o mais simples e intuitiva possível.

  1. Instale orders-html: pipenv install requests-html

  2. Faça uma solicitação para o URL da página:

    from requests_html import HTMLSession
    
    session = HTMLSession()
    r = session.get(a_page_url)
  3. Renderize a resposta para obter os bits gerados pelo Javascript:

    r.html.render()

Finalmente, o módulo parece oferecer recursos de raspagem .
Como alternativa, podemos tentar a maneira bem documentada de usar BeautifulSoup com o r.htmlobjeto que acabamos de renderizar.

John Moutafis
fonte
você pode expandir como obter o conteúdo HTML completo, com os bits JS carregados, depois de chamar .render ()? Estou preso depois desse ponto. Não estou vendo todos os iframes injetados na página normalmente do JavaScript no r.html.htmlobjeto.
anon58192932
@ anon58192932 Como, no momento, esta é uma solução experimental e não sei exatamente o que você está tentando alcançar como resultado, não posso realmente sugerir nada ... Você pode criar uma nova pergunta aqui no SO, se não tiver elaborou uma solução ainda
John Moutafis
2
Eu recebi este erro: RuntimeError: Não é possível usar HTMLSession dentro de um loop de eventos existente. Use AsyncHTMLSession.
HuckIt
1
@HuckEste parece ser um problema conhecido: github.com/psf/requests-html/issues/140
John Moutafis
47

Talvez o selênio possa fazer isso.

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
incrível
fonte
3
O Selenium é realmente pesado para esse tipo de coisa, que seria desnecessariamente lento e requer uma cabeça de navegador se você não usar o PhantomJS, mas isso funcionaria.
Joshua Hedges
@JoshuaHedges Você pode executar outros navegadores mais padrão no modo sem cabeça.
reynoldsnlp
22

Se você já usou o Requestsmódulo para python antes, descobri recentemente que o desenvolvedor criou um novo módulo chamado Requests-HTMLque agora também tem a capacidade de renderizar JavaScript.

Você também pode visitar https://html.python-requests.org/ para saber mais sobre este módulo ou, se estiver interessado apenas em renderizar JavaScript, pode visitar https://html.python-requests.org/?#javascript -suporte para aprender diretamente como usar o módulo para renderizar JavaScript usando Python.

Essencialmente, depois de instalar corretamente o Requests-HTMLmódulo, o exemplo a seguir, mostrado no link acima , mostra como você pode usar esse módulo para raspar um site e renderizar JavaScript contido no site:

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.

Eu aprendi recentemente sobre isso em um vídeo do YouTube. Clique aqui! para assistir ao vídeo do YouTube, que demonstra como o módulo funciona.

SShah
fonte
3
Observe que este módulo oferece suporte apenas ao Python 3.6.
Nat5142 12/12
1
Eu recebi este erro: SSLError: HTTPSConnectionPool (host = 'docs.python-requests.org', porta = 443): Número máximo de tentativas excedidas com url: / (Causado pelo SSLError (SSLError (1, '[SSL: TLSV1_ALERT_INTERNAL_ERROR] alerta tlsv1) Erro interno (_ssl.c: 1045) ')))
HuckIt
@ HuckIt desculpas Não estou familiarizado com esse erro, no entanto, o erro parece que o site que você estava tentando acessar pode ter um problema relacionado à certificação SSL. Desculpe, essa não é uma solução, mas eu recomendo que você faça uma nova pergunta, aqui no estouro de pilha (se ainda não tiver sido solicitada) e, possivelmente, forneça mais detalhes, como o URL do site que você estava usando e seu código.
SShah
Parece estar usando cromo sob o capô. Funciona muito bem para mim
Sid
14

Essa também parece ser uma boa solução, extraída de uma excelente postagem no 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
fonte
12

Parece que os dados que você está realmente procurando podem ser acessados ​​via URL secundário chamado por algum javascript na página principal.

Embora você possa tentar executar o javascript no servidor para lidar com isso, uma abordagem mais simples pode ser carregar a página usando o Firefox e usar uma ferramenta como Charles ou Firebug para identificar exatamente o que é esse URL secundário. Depois, basta consultar esse URL diretamente para os dados que lhe interessam.

Stephen Emslie
fonte
@ Kris Caso alguém se depare com isso e queira experimentá-lo em vez de algo tão pesado quanto o selênio, aqui está um pequeno exemplo. Isso abrirá a página de detalhes da peça para uma porca sextavada no site da McMaster-Carr. O conteúdo do site é obtido principalmente usando Javascript e possui muito pouca informação da página nativa. Se você abrir as ferramentas de desenvolvedor do navegador, navegar para a guia Rede e atualizar a página, poderá ver todas as solicitações feitas pela página e encontrar os dados relevantes (neste caso, o html de detalhes da peça).
precisa
Esse é um URL diferente encontrado na guia Rede devtool do Firefox que, se seguida, contém o html para a maioria das informações da peça e expõe alguns dos parâmetros necessários para navegar facilmente para outras informações da peça para facilitar a raspagem. Este exemplo em particular não é particularmente útil, pois o preço é gerado por outra função Javascript, mas deve servir o suficiente como uma introdução para quem deseja seguir o conselho de Stephen.
carregar a
12

O Selenium é o melhor para copiar conteúdo JS e Ajax.

Verifique este artigo para extrair dados da Web usando Python

$ pip install selenium

Faça o download do 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 né?

Macnux
fonte
8

Você também pode executar o javascript usando o webdriver.

from selenium import webdriver

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

ou armazene o valor em uma variável

result = driver.execute_script('var text = document.title ; return var')
Serpentr
fonte
ou você pode simplesmente usar a driver.titlepropriedade
Corey Goldberg
7

Eu, pessoalmente, prefiro usar scrapy e selênio e dockerizing em recipientes separados. Dessa forma, você pode instalar os sites com menos problemas e rastrear sites modernos que quase todos contêm javascript de uma forma ou de outra. Aqui está um exemplo:

Use o scrapy startprojectpara criar seu raspador e escrever sua aranha, o esqueleto pode ser tão simples quanto isto:

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)

A verdadeira mágica acontece no middlewares.py. Substitua dois métodos no middleware do downloader __init__e process_requestda seguinte maneira:

# 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ão se esqueça de ativar esse software intermediário, descomentando as próximas linhas no arquivo settings.py:

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

Avançar para dockerization. Crie a Dockerfilepartir de uma imagem leve (estou usando o python Alpine aqui), copie o diretório do projeto para ele, instale os 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

E, finalmente, junte tudo 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

Corra docker-compose up -d. Se você estiver fazendo isso pela primeira vez, levará um tempo para buscar o último selênio / cromo independente e criar a imagem do seu raspador também.

Uma vez feito, você pode verificar se seus contêineres estão em execução docker pse também se o nome do contêiner de selênio corresponde ao da variável de ambiente que passamos ao nosso contêiner de raspador (aqui estava SELENIUM_LOCATION=samplecrawler_selenium_1).

Digite o contêiner do seu raspador com docker exec -ti YOUR_CONTAINER_NAME sh, o comando para mim foi docker exec -ti samplecrawler_my_scraper_1 sh: cd no diretório certo e execute o seu raspador scrapy crawl my_spider.

A coisa toda está na minha página do github e você pode obtê-la aqui

tarikki
fonte
5

Uma mistura de BeautifulSoup e Selenium funciona muito bem para mim.

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 Você pode encontrar mais condições de espera aqui

Biarys
fonte
4

Você desejará usar urllib, solicitações, driver da web beautifulSoup e selenium em seu script para diferentes partes da página (para citar alguns).
Às vezes, você obtém o que precisa com apenas um desses módulos.
Às vezes, você precisará de dois, três ou todos esses módulos.
Às vezes, você precisará desativar os js no seu navegador.
Às vezes, você precisará de informações de cabeçalho em seu script.
Nenhum site pode ser raspado da mesma maneira e nenhum site pode ser raspado da mesma maneira para sempre, sem precisar modificar seu rastreador, geralmente depois de alguns meses. Mas todos eles podem ser raspados! Onde há vontade, há uma maneira de ter certeza.
Se você precisar coletar dados continuamente no futuro, basta coletar tudo o que precisa e armazená-los em arquivos .dat com pickle.
Continue pesquisando como tentar o que fazer com esses módulos e copiando e colando seus erros no Google.


fonte
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 ..
fonte
1

Estou tentando encontrar respostas para essas perguntas há dois dias. Muitas respostas direcionam você para diferentes questões. Mas a resposta de serpentr acima é realmente objetiva . É a solução mais curta e simples. Apenas um lembrete da última palavra "var" representa o nome da variável , portanto, deve ser usada como:

 result = driver.execute_script('var text = document.title ; return text')
Abd_bgc
fonte
Este deve ser um comentário sobre a resposta da serpentr, não uma resposta separada.
Yserbius 29/01
1
Isso é óbvio. Mas ainda não tenho 50 representantes para comentar a resposta de outra pessoa.
Abd_bgc 02/02
0

Eu tive que lidar com esse mesmo problema em alguns projetos de raspagem de minha própria página. Como eu lidei com isso foi usando a biblioteca de solicitações python para fazer uma solicitação http diretamente para a API, em vez de ter que carregar o JS.

A biblioteca de solicitações python funciona bem para isso, e você pode ver as solicitações http usando o elemento inspecionar e navegando até a guia rede.

Superduperfluous
fonte