Strona JavaScript do skrobania sieci w języku Python

178

Próbuję opracować prosty skrobak do sieci. Chcę wyodrębnić tekst bez kodu HTML. W rzeczywistości osiągam ten cel, ale widziałem, że na niektórych stronach, na których ładowany jest JavaScript, nie uzyskałem dobrych wyników.

Na przykład, jeśli jakiś kod JavaScript dodaje jakiś tekst, nie widzę go, ponieważ kiedy dzwonię

response = urllib2.urlopen(request)

Dostaję oryginalny tekst bez dodanego (ponieważ JavaScript jest wykonywany w kliencie).

Dlatego szukam pomysłów na rozwiązanie tego problemu.

mocopera
źródło
2
Wygląda na to, że potrzebujesz czegoś cięższego, spróbuj Selenium lub Watir.
wim
2
Zrobiłem to z powodzeniem w Javie (użyłem zestawu narzędzi Cobra lobobrowser.org/cobra.jsp ) Ponieważ chcesz włamać się do Pythona (zawsze dobry wybór), polecam te dwie opcje: - packtpub.com/article/ web-scraping-with-python-part-2 - blog.databigbang.com/web-scraping-ajax-and-javascript-sites
bpgergo

Odpowiedzi:

203

EDYCJA 30 grudnia 2017 r .: Ta odpowiedź pojawia się w najlepszych wynikach wyszukiwania Google, więc postanowiłem ją zaktualizować. Stara odpowiedź wciąż się kończy.

dryscape nie jest już utrzymywany, a biblioteka zalecana przez programistów dryscape to tylko Python 2. Odkryłem, że korzystanie z biblioteki Pythona Selenium z Phantom JS jako sterownikiem sieciowym jest wystarczająco szybkie i łatwe do wykonania pracy.

Po zainstalowaniu Phantom JS upewnij się, że phantomjsplik binarny jest dostępny w bieżącej ścieżce:

phantomjs --version
# result:
2.1.1

Przykład

Aby dać przykład, utworzyłem przykładową stronę z następującym kodem 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>

bez javascript jest napisane: No javascript supporta z javascript:Yay! Supports javascript

Skrobanie bez obsługi 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>

Skrobanie z obsługą 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'

Możesz także użyć biblioteki Python dryscrape, aby zeskrobać strony internetowe oparte na JavaScript.

Skrobanie z obsługą 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
źródło
16
Niestety, brak obsługi systemu Windows.
Expenzor
1
Jakieś alternatywy dla tych z nas, którzy programują w systemie Windows?
Hoshiko86
2
@ExpenzorPracuję na oknach. PhantomJS działa dobrze.
Aakash Choubey
17
Warto zauważyć, że PhantomJS zostało wycofane i nie jest już aktywnie rozwijane w świetle teraz obsługującego bezgłowe Chrome'a. Sugerowane jest użycie bezgłowego chrome / firefox.
sytech
3
To zarówno wsparcie selenu, jak i sam PhantomJS. github.com/ariya/phantomjs/issues/15344
sytech
73

Nie otrzymujemy poprawnych wyników, ponieważ wszelkie treści generowane przez JavaScript muszą być renderowane w modelu DOM. Kiedy pobieramy stronę HTML, pobieramy początkowy, niezmodyfikowany przez javascript, DOM.

Dlatego przed zaindeksowaniem strony musimy wyrenderować zawartość JavaScript.

Ponieważ selen jest już wielokrotnie wspominany w tym wątku (io tym, jak wolno to robi się czasami wspominano), wymienię dwa inne możliwe rozwiązania.


Rozwiązanie 1: To jest bardzo fajny samouczek na temat używania Scrapy do indeksowania treści generowanych przez javascript i zamierzamy to zrobić.

Czego będziemy potrzebować:

  1. Docker zainstalowany na naszej maszynie. Jest to plus w stosunku do innych rozwiązań do tego momentu, ponieważ wykorzystuje platformę niezależną od systemu operacyjnego.

  2. Zainstaluj Splash zgodnie z instrukcjami podanymi dla naszego odpowiedniego systemu operacyjnego.
    Cytowanie z dokumentacji splash:

    Splash to usługa renderowania javascript. Jest to lekka przeglądarka internetowa z interfejsem API HTTP, zaimplementowana w Pythonie 3 przy użyciu Twisted i QT5.

    Zasadniczo będziemy używać Splash do renderowania treści generowanych przez Javascript.

  3. Uruchomić serwer ochlapanie: sudo docker run -p 8050:8050 scrapinghub/splash.

  4. Zainstaluj wtyczkę scrapy-splash :pip install scrapy-splash

  5. Zakładając, że mamy już utworzony projekt Scrapy (jeśli nie, zróbmy taki ), będziemy postępować zgodnie z instrukcją i zaktualizować settings.py:

    Następnie przejdź do swojego projektu scrapy settings.pyi ustaw te oprogramowanie pośrednie:

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

    Adres URL serwera Splash (jeśli używasz Win lub OSX, powinien to być adres URL maszyny docker: Jak uzyskać adres IP kontenera Dockera z hosta? ):

    SPLASH_URL = 'http://localhost:8050'

    I na koniec musisz ustawić również te wartości:

    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
  6. Wreszcie możemy użyć SplashRequest:

    W zwykłym pająku masz obiekty Request, których możesz używać do otwierania adresów URL. Jeśli strona, którą chcesz otworzyć, zawiera dane wygenerowane przez JS, musisz użyć SplashRequest (lub SplashFormRequest) do renderowania strony. Oto prosty przykład:

    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 renderuje adres URL jako html i zwraca odpowiedź, której możesz użyć w metodzie callback (parse).


Rozwiązanie 2: Nazwijmy to w tej chwili eksperymentem (maj 2018) ...
To rozwiązanie jest przeznaczone tylko dla wersji 3.6 Pythona (w tej chwili).

Czy znasz moduł zgłoszeń (kto go nie zna )?
Teraz ma małe rodzeństwo indeksujące sieć: request-HTML :

Ta biblioteka ma na celu uczynienie parsowania HTML (np. Skrobania sieci) tak prostym i intuicyjnym, jak to tylko możliwe.

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

  2. Wyślij żądanie do adresu URL strony:

    from requests_html import HTMLSession
    
    session = HTMLSession()
    r = session.get(a_page_url)
  3. Renderuj odpowiedź, aby uzyskać bity wygenerowane przez JavaScript:

    r.html.render()

Wreszcie, moduł wydaje się oferować możliwości skrobania .
Alternatywnie możemy wypróbować dobrze udokumentowany sposób użycia BeautifulSoup z r.htmlwłaśnie wyrenderowanym obiektem.

John Moutafis
źródło
czy możesz rozwinąć, jak uzyskać pełną zawartość HTML, z załadowanymi bitami JS, po wywołaniu .render ()? Po tym punkcie utknąłem. Nie widzę wszystkich ramek iframe, które są wstawiane do strony normalnie z JavaScript w r.html.htmlobiekcie.
anon58192932
@ anon58192932 Ponieważ w tej chwili jest to rozwiązanie eksperymentalne i nie wiem, co dokładnie próbujesz w rezultacie osiągnąć, nie mogę nic zasugerować ... Możesz utworzyć nowe pytanie tutaj na SO, jeśli nie masz wypracował jeszcze rozwiązanie
John Moutafis
2
Pojawił się ten błąd: RuntimeError: Nie można użyć HTMLSession w istniejącej pętli zdarzeń. Zamiast tego użyj AsyncHTMLSession.
HuckIt
1
@HuckIt wydaje się, że to znany problem: github.com/psf/requests-html/issues/140
John Moutafis
47

Może selen to potrafi.

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
niesamowite
źródło
3
Selen jest naprawdę ciężki w tego rodzaju rzeczach, który byłby niepotrzebnie powolny i wymaga głowicy przeglądarki, jeśli nie używasz PhantomJS, ale to zadziała.
Joshua Hedges
@JoshuaHedges Możesz uruchomić inne, bardziej standardowe przeglądarki w trybie bezgłowym.
reynoldsnlp
22

Jeśli kiedykolwiek wcześniej korzystałeś z Requestsmodułu dla Pythona, niedawno dowiedziałem się, że programista stworzył nowy moduł o nazwie, Requests-HTMLktóry teraz ma również możliwość renderowania JavaScript.

Możesz również odwiedzić https://html.python-requests.org/, aby dowiedzieć się więcej o tym module, lub jeśli jesteś zainteresowany renderowaniem JavaScript, możesz odwiedzić https://html.python-requests.org/?#javascript -wsparcie aby bezpośrednio dowiedzieć się, jak używać modułu do renderowania JavaScript za pomocą Pythona.

Zasadniczo, po poprawnym zainstalowaniu Requests-HTMLmodułu, poniższy przykład, który jest pokazany w powyższym linku , pokazuje, jak możesz użyć tego modułu do zeskrobania strony internetowej i renderowania JavaScript zawartego w witrynie:

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.

Niedawno dowiedziałem się o tym z filmu na YouTube. Kliknij tutaj! aby obejrzeć film na YouTube, który pokazuje, jak działa moduł.

SShah
źródło
3
Należy zauważyć, że ten moduł obsługuje tylko Python 3.6.
nat5142
1
Otrzymałem ten błąd: SSLError: HTTPSConnectionPool (host = 'docs.python-requests.org', port = 443): Przekroczono maksymalną liczbę ponownych prób z adresem URL: / (Spowodowane przez SSLError (SSLError (1, '[SSL: TLSV1_ALERT_INTERNAL_ERROR] alert tlsv1) błąd wewnętrzny (_ssl.c: 1045) ')))
HuckIt Kwietnia
@HuckIt przepraszam Nie znam tego błędu, jednak wygląda na to, że witryna, do której próbujesz się dostać, mogła mieć problem związany z certyfikatem SSL. Przepraszamy, to nie jest rozwiązanie, ale radziłbym zadać nowe pytanie, tutaj w przepełnieniu stosu (jeśli jeszcze nie zostało zadane) i prawdopodobnie podać więcej szczegółów, takich jak adres URL witryny, z której korzystałeś, i kod.
SShah
Wygląda na to, że pod maską zastosowano chrom. Ale dla mnie działa świetnie
Sid
14

Wydaje się, że to również dobre rozwiązanie, zaczerpnięte ze świetnego wpisu na blogu

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
źródło
12

Wygląda na to, że do danych, których naprawdę szukasz, można uzyskać dostęp za pośrednictwem dodatkowego adresu URL wywoływanego przez jakiś kod JavaScript na stronie głównej.

Chociaż możesz spróbować uruchomić javascript na serwerze, aby sobie z tym poradzić, prostszym podejściem może być załadowanie strony za pomocą przeglądarki Firefox i użycie narzędzia takiego jak Charles lub Firebug, aby dokładnie określić, jaki jest ten dodatkowy adres URL. Następnie możesz po prostu zapytać ten adres URL bezpośrednio o dane, które Cię interesują.

Stephen Emslie
źródło
@Kris Na wypadek, gdyby ktoś się na to natknął i chciałby spróbować zamiast czegoś tak ciężkiego jak selen, oto krótki przykład. Spowoduje to otwarcie strony szczegółów części dla nakrętki sześciokątnej w witrynie internetowej McMaster-Carr. Treść ich witryny jest w większości pobierana za pomocą JavaScript i zawiera bardzo mało informacji o natywnej stronie. Jeśli otworzysz narzędzia programistyczne przeglądarki, przejdziesz do karty Sieć i odświeżysz stronę, możesz zobaczyć wszystkie żądania wysłane przez tę stronę i znaleźć odpowiednie dane (w tym przypadku część ze szczegółami html).
SweepingsDemon
Jest to inny adres URL, który można znaleźć na karcie Sieć w przeglądarce Firefox devtool, która, jeśli jest przestrzegana, zawiera kod HTML większości informacji o części i udostępnia niektóre parametry wymagane do łatwego przechodzenia do informacji o innych częściach w celu łatwiejszego przeszukiwania. Ten konkretny przykład nie jest szczególnie przydatny, ponieważ cena jest generowana przez inną funkcję Javascript, ale powinien służyć jako wprowadzenie dla każdego, kto chce zastosować się do rady Stephena.
SweepingsDemon
12

Selen jest najlepszy do skrobania zawartości JS i Ajax.

Zapoznaj się z tym artykułem, aby dowiedzieć się, jak wyodrębniać dane z Internetu za pomocą języka Python

$ pip install selenium

Następnie pobierz sterownik internetowy Chrome.

from selenium import webdriver

browser = webdriver.Chrome()

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

nav = browser.find_element_by_id("mainnav")

print(nav.text)

Łatwe, prawda?

Macnux
źródło
8

Możesz również uruchomić JavaScript za pomocą webdrivera.

from selenium import webdriver

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

lub zapisz wartość w zmiennej

result = driver.execute_script('var text = document.title ; return var')
Serpentr
źródło
lub możesz po prostu skorzystać z driver.titlenieruchomości
Corey Goldberg
8

Osobiście wolę używać złomu i selenu oraz dokeryzować oba w osobnych pojemnikach. W ten sposób możesz zainstalować zarówno przy minimalnym wysiłku, jak i przeszukiwać nowoczesne strony internetowe, które prawie wszystkie zawierają JavaScript w takiej czy innej formie. Oto przykład:

Użyj, scrapy startprojectaby stworzyć skrobak i napisać pająka, szkielet może być tak prosty:

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)

Prawdziwa magia dzieje się w middlewares.py. Zastąp dwie metody w oprogramowaniu pośredniczącym pobierania __init__i process_requestw następujący sposób:

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

Nie zapomnij włączyć tego oprogramowania pośredniego, odkomentowując kolejne wiersze w pliku settings.py:

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

Dalej do dokeryzacji. Utwórz swój Dockerfilez lekkiego obrazu (używam tutaj Python Alpine), skopiuj do niego katalog projektu, zainstaluj wymagania:

# 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

I wreszcie zbierz to wszystko razem w 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

Biegać docker-compose up -d . Jeśli robisz to za pierwszym razem, pobranie najnowszego selenu / samodzielnego chromu zajmie trochę czasu, a także zbudowanie obrazu skrobaka.

Po zakończeniu możesz sprawdzić, czy Twoje kontenery są uruchomione, docker psa także sprawdzić, czy nazwa kontenera selenu jest zgodna ze zmienną środowiskową, którą przekazaliśmy do naszego kontenera skrobaka (tutaj było toSELENIUM_LOCATION=samplecrawler_selenium_1 ).

Wprowadź swój pojemnik na skrobaki za pomocą docker exec -ti YOUR_CONTAINER_NAME sh, polecenie dla mnie brzmiało docker exec -ti samplecrawler_my_scraper_1 sh, cd do odpowiedniego katalogu i uruchom skrobak za pomocą scrapy crawl my_spider.

Całość jest na mojej stronie github i możesz ją pobrać stąd

tarikki
źródło
5

Mieszanka BeautifulSoup i Selenium działa na mnie bardzo dobrze.

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 Więcej warunków oczekiwania znajdziesz tutaj

Biarys
źródło
4

Będziesz chciał użyć urllib, requestów, sterownika webowego beautifulSoup i selenium w swoim skrypcie dla różnych części strony (żeby wymienić tylko kilka).
Czasami wystarczy jeden z tych modułów.
Czasami będziesz potrzebować dwóch, trzech lub wszystkich tych modułów.
Czasami będziesz musiał wyłączyć js w swojej przeglądarce.
Czasami będziesz potrzebować informacji nagłówka w swoim skrypcie.
Żadnej witryny nie można zeskrobać w ten sam sposób ani żadnej witryny sieci Web nie można zeskrobać w ten sam sposób na zawsze bez konieczności modyfikowania robota indeksującego, zwykle po kilku miesiącach. Ale wszystkie można zeskrobać! Gdzie jest wola, na pewno jest sposób.
Jeśli potrzebujesz nieustannie pobieranych danych w przyszłości, po prostu zeskrob wszystko, czego potrzebujesz i przechowuj je w plikach .dat za pomocą zalewy.
Po prostu szukaj dalej, jak wypróbować te moduły i kopiuj i wklejaj błędy do Google.


źródło
3

Korzystanie z 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 ..
źródło
1

Od dwóch dni staram się znaleźć odpowiedź na to pytanie. Wiele odpowiedzi kieruje cię do różnych kwestii. Ale odpowiedź serpentra powyżej jest naprawdę na temat. To najkrótsze, najprostsze rozwiązanie. Przypominamy, że ostatnie słowo „var” reprezentuje nazwę zmiennej , dlatego powinno być używane jako:

 result = driver.execute_script('var text = document.title ; return text')
Abd_bgc
źródło
To powinien być komentarz do odpowiedzi węża, a nie osobna odpowiedź.
Yserbius
1
To oczywiste. Ale nie mam jeszcze 50 powtórzeń, aby skomentować odpowiedź kogoś innego.
Abd_bgc
0

Miałem do czynienia z tym samym problemem w niektórych moich własnych projektach do skrobania sieci. Sposób, w jaki sobie z tym poradziłem, polegał na użyciu biblioteki żądań Pythona w celu wysłania żądania http bezpośrednio do API, zamiast konieczności ładowania JS.

Biblioteka żądań Pythona działa dobrze w tym celu i możesz zobaczyć żądania http, używając elementu inspect i przechodząc do karty sieci.

Superduperfluous
źródło