Соскребая веб-страницу JavaScript с Python

178

Я пытаюсь разработать простой веб-скребок. Я хочу извлечь текст без кода HTML. На самом деле я достиг этой цели, но я видел, что на некоторых страницах, где загружен JavaScript, я не добился хороших результатов.

Например, если какой-то код JavaScript добавляет текст, я не вижу его, потому что когда я звоню

response = urllib2.urlopen(request)

Я получаю оригинальный текст без добавленного (потому что JavaScript выполняется в клиенте).

Итак, я ищу несколько идей для решения этой проблемы.

mocopera
источник
2
Похоже, вам может понадобиться что-то тяжелее, попробуйте Selenium или Watir.
Вим
2
Я успешно сделал это в Java (я использовал инструментарий Cobra lobobrowser.org/cobra.jsp ) Поскольку вы хотите взломать на python (всегда хороший выбор), я рекомендую эти два варианта: - packtpub.com/article/ web-scraping-with-python-part-2 - blog.databigbang.com/web-scraping-ajax-and-javascript-sites
bpgergo

Ответы:

203

РЕДАКТИРОВАТЬ 30 / Dec / 2017: этот ответ появляется в топ-результатах поиска Google, поэтому я решил обновить его. Старый ответ еще в конце.

Dryscape больше не поддерживается, и разработчики библиотеки Dryscape рекомендуют использовать только Python 2. Я обнаружил, что использование библиотеки Python Selenium с Phantom JS в качестве веб-драйвера достаточно быстрое и простое для выполнения работы.

После установки Phantom JS убедитесь, что phantomjsдвоичный файл доступен по текущему пути:

phantomjs --version
# result:
2.1.1

пример

Чтобы привести пример, я создал образец страницы со следующим HTML-кодом. ( ссылка ):

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

без JavaScript это говорит: No javascript supportи с Javascript:Yay! Supports javascript

Выскабливание без поддержки 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>

Соскоб с поддержкой 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'

Вы также можете использовать библиотеку Python dryscrape для очистки сайтов, управляемых javascript.

Соскоб с поддержкой 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
источник
16
К сожалению, нет поддержки Windows.
Expenzor
1
Какие-нибудь альтернативы для тех из нас, кто программирует в Windows?
Hoshiko86
2
@ExpenzorЯ работаю над окнами. PhantomJS работает отлично.
Аакаш Чуби
17
Стоит отметить, что PhantomJS больше не выпускается и больше не находится в активной разработке в свете Chrome, который теперь поддерживает безголовый. Рекомендуется использовать безголовый Chrome / Firefox.
sytech
3
Это и поддержка селена, и сам PhantomJS. github.com/ariya/phantomjs/issues/15344
sytech
75

Мы не получаем правильных результатов, потому что любой контент, сгенерированный javascript, должен отображаться в DOM. Когда мы выбираем HTML-страницу, мы получаем исходную, не измененную Javascript, DOM.

Поэтому нам нужно визуализировать содержимое javascript перед сканированием страницы.

Поскольку селен уже упоминался много раз в этой теме (и иногда упоминалось также о его медленной скорости), я перечислю два других возможных решения.


Решение 1: Это очень хороший учебник о том, как использовать Scrapy для сканирования контента, созданного на JavaScript, и мы собираемся следовать этому.

Что нам понадобится:

  1. Докер установлен на нашей машине. До этого момента это преимущество перед другими решениями, поскольку оно использует независимую от ОС платформу.

  2. Установите Splash, следуя инструкциям для нашей соответствующей ОС.
    Цитирование из всплеск документации:

    Splash - это сервис рендеринга JavaScript. Это легкий веб-браузер с HTTP API, реализованный в Python 3 с использованием Twisted и QT5.

    По сути, мы будем использовать Splash для визуализации сгенерированного Javascript контента.

  3. Запустите сервер заставки sudo docker run -p 8050:8050 scrapinghub/splash.

  4. Установите плагин scrapy-splash :pip install scrapy-splash

  5. Предполагая, что у нас уже есть проект Scrapy (если нет, давайте создадим его ), мы будем следовать руководству и обновим settings.py:

    Затем перейдите в свой проект Scrapy settings.pyи установите эти промежуточные программы:

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

    URL-адрес сервера Splash (если вы используете Win или OSX, это должен быть URL-адрес докера: как получить IP-адрес контейнера Docker от хоста? ):

    SPLASH_URL = 'http://localhost:8050'

    И, наконец, вам нужно установить эти значения тоже:

    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
  6. Наконец, мы можем использовать SplashRequest:

    У обычного паука у вас есть объекты Request, которые вы можете использовать для открытия URL. Если страница, которую вы хотите открыть, содержит данные, сгенерированные JS, вы должны использовать SplashRequest (или SplashFormRequest) для отображения страницы. Вот простой пример:

    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 отображает URL как html и возвращает ответ, который вы можете использовать в методе обратного вызова (синтаксический анализ).


Решение 2: Давайте назовем этот эксперимент в настоящий момент (май 2018) ...
Это решение только для версии Python 3.6 (на данный момент).

Знаете ли вы модуль запросов (ну, кто не знает)?
Теперь у него есть маленький брат, просматривающий веб: запросы-HTML :

Эта библиотека предназначена для того, чтобы сделать анализ HTML (например, просмотр веб-страниц) максимально простым и интуитивно понятным.

  1. Установить запросы-HTML: pipenv install requests-html

  2. Сделайте запрос на URL страницы:

    from requests_html import HTMLSession
    
    session = HTMLSession()
    r = session.get(a_page_url)
  3. Отобразите ответ, чтобы получить сгенерированные Javascript биты:

    r.html.render()

Наконец, модуль предлагает возможности очистки .
Кроме того, мы можем попробовать хорошо документированный способ использования BeautifulSoup с r.htmlобъектом, который мы только что визуализировали.

Джон Мутафис
источник
Можете ли вы рассказать о том, как получить полный HTML-контент с загруженными битами JS после вызова .render ()? Я застрял после этого момента. Я не вижу всех фреймов, которые обычно вставляются на страницу из JavaScript в r.html.htmlобъекте.
anon58192932
@ anon58192932 Так как на данный момент это экспериментальное решение, и я не знаю, что именно вы пытаетесь достичь в результате, я не могу ничего предложить ... Вы можете создать новый вопрос здесь на SO, если у вас нет разработал решение еще
Джон Moutafis
2
Я получил эту ошибку: RuntimeError: Невозможно использовать HTMLSession в существующем цикле событий. Вместо этого используйте AsyncHTMLSession.
HuckIt
1
@Huck, похоже, это известная проблема: github.com/psf/requests-html/issues/140
Джон Мутафис,
47

Может быть, селен может сделать это.

from selenium import webdriver
import time

driver = webdriver.Firefox()
driver.get(url)
time.sleep(5)
htmlSource = driver.page_source
amazingthere
источник
3
Selenium действительно тяжел для такого рода вещей, это было бы излишне медленно и требует головы браузера, если вы не используете PhantomJS, но это будет работать.
Джошуа Хеджес
@JoshuaHedges Вы можете запустить другие более стандартные браузеры в режиме без головы.
reynoldsnlp
22

Если вы когда-либо использовали Requestsмодуль для Python ранее, я недавно обнаружил, что разработчик создал новый модуль, Requests-HTMLкоторый теперь также имеет возможность визуализации JavaScript.

Вы также можете посетить https://html.python-requests.org/, чтобы узнать больше об этом модуле, или, если вас интересует только рендеринг JavaScript, вы можете посетить https://html.python-requests.org/?#javascript -поддержка непосредственного изучения того, как использовать модуль для рендеринга JavaScript с использованием Python.

По сути, после правильной установки Requests-HTMLмодуля в следующем примере, показанном по вышеуказанной ссылке , показано, как можно использовать этот модуль для очистки веб-сайта и рендеринга JavaScript, содержащегося на веб-сайте:

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.

Я недавно узнал об этом из видео на YouTube. Кликните сюда! смотреть видео на YouTube, которое демонстрирует, как работает модуль.

SShah
источник
3
Следует отметить, что этот модуль поддерживает только Python 3.6.
nat5142
1
Я получил эту ошибку: SSLError: HTTPSConnectionPool (host = 'docs.python-requests.org', port = 443): превышено максимальное количество попыток с URL: / (вызвано SSLError (SSLError (1, '[SSL: TLSV1_ALERT_INTERNAL_ERROR] tlsv1 alert) внутренняя ошибка (_ssl.c: 1045) ')))
HuckIt
@HuckIt Извинения Я не знаком с этой ошибкой, однако, похоже, что ошибка, веб-сайт, на который вы пытались попасть, мог иметь проблему, связанную с сертификацией SSL. Извините, это не решение, но я бы порекомендовал вам задать новый вопрос здесь, в переполнении стека (если он еще не задавался), и, возможно, предоставить более подробную информацию, такую ​​как URL-адрес веб-сайта, который вы использовали, и ваш код.
SShah
Кажется, использовать хром под капотом. Работает отлично для меня, хотя
Сид
14

Это также кажется хорошим решением, взятым из отличного поста в блоге.

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
источник
12

Похоже, к данным, которые вы действительно ищете, можно получить доступ через вторичный URL, который вызывается каким-то javascript на основной странице.

Хотя вы можете попробовать запустить javascript на сервере, чтобы справиться с этим, более простой подход может состоять в том, чтобы загрузить страницу с помощью Firefox и использовать такой инструмент, как Charles или Firebug, чтобы точно определить, что это за вторичный URL. Затем вы можете просто запросить этот URL непосредственно для данных, которые вас интересуют.

Стивен Эмсли
источник
@ Kris На всякий случай, если кто-то наткнется на это и захочет попробовать его вместо чего-то такого тяжелого, как селен, вот короткий пример. Это откроет страницу детали детали для шестигранной гайки на сайте МакМастер-Карр. Их веб-сайт в основном выбирается с использованием Javascript и содержит очень мало информации о нативных страницах. Если вы откроете инструменты разработчика своего браузера, перейдете на вкладку Сеть и обновите страницу, вы сможете увидеть все запросы, сделанные этой страницей, и найти соответствующие данные (в данном случае html деталей).
SweepingsDemon
Это другой URL-адрес, который можно найти на вкладке «Сеть» Firefox devtool, которая, если она используется, содержит html для большей части информации о деталях и предоставляет некоторые параметры, необходимые для простой навигации к другой информации о деталях для упрощения очистки. Этот конкретный пример не особенно полезен, поскольку цена генерируется другой функцией Javascript, но он должен послужить хорошим введением для тех, кто хочет следовать совету Стивена.
SweepingsDemon
12

Селен лучше всего подходит для очистки содержимого JS и Ajax.

Проверьте эту статью для извлечения данных из Интернета, используя Python

$ pip install selenium

Затем загрузите веб-драйвер Chrome.

from selenium import webdriver

browser = webdriver.Chrome()

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

nav = browser.find_element_by_id("mainnav")

print(nav.text)

Легко, правда?

Macnux
источник
8

Вы также можете выполнить JavaScript с помощью веб-драйвера.

from selenium import webdriver

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

или сохранить значение в переменной

result = driver.execute_script('var text = document.title ; return var')
Serpentr
источник
или вы можете просто использовать driver.titleсобственность
Кори Голдберг
8

Лично я предпочитаю использовать скрап и селен, а также докеризацию в отдельных контейнерах. Таким образом, вы можете установить как с минимальными хлопотами, так и сканировать современные веб-сайты, которые почти все содержат JavaScript в той или иной форме. Вот пример:

Используйте, scrapy startprojectчтобы создать свой скребок и написать свой паук, скелет может быть таким простым:

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)

Настоящая магия происходит в middlewares.py. Перезаписать два метода в промежуточном программном обеспечении загрузчика, __init__и process_request, следующим образом:

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

Не забудьте включить эту промежуточную программу, раскомментировав следующие строки в файле settings.py:

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

Далее для докеризации. Создайте свой Dockerfileиз облегченного образа (я использую Python Alpine здесь), скопируйте в него каталог вашего проекта, установите требования:

# 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

И, наконец, свести все это в 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

Беги docker-compose up -d. Если вы делаете это в первый раз, потребуется некоторое время, чтобы он извлек последнюю версию селен / автономный хром, а также построил ваш скребковый образ.

Как только это будет сделано, вы можете проверить, что ваши контейнеры работают, docker psа также убедиться, что имя контейнера селена совпадает с именем переменной среды, которую мы передали нашему контейнеру скребка (здесь это было SELENIUM_LOCATION=samplecrawler_selenium_1).

Введите свой контейнер скребка с docker exec -ti YOUR_CONTAINER_NAME shпомощью команды для меня docker exec -ti samplecrawler_my_scraper_1 sh, перейдите в правильный каталог и запустите свой скребок с помощью scrapy crawl my_spider.

Все это на моей странице GitHub, и вы можете получить его здесь

tarikki
источник
5

Сочетание BeautifulSoup и Selenium работает очень хорошо для меня.

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 Вы можете найти больше условий ожидания здесь

Biarys
источник
4

Вы захотите использовать в своем скрипте веб-драйвер urllib, запросы, beautifulSoup и selenium для разных частей страницы (и многие другие).
Иногда вы получите то, что вам нужно, только с одним из этих модулей.
Иногда вам понадобятся два, три или все эти модули.
Иногда вам нужно отключить JS в вашем браузере.
Иногда вам понадобится информация заголовка в вашем скрипте.
Ни один веб-сайт не может быть удален одним и тем же способом, и ни один веб-сайт не может быть удален одним и тем же способом навсегда без необходимости изменения вашего сканера, обычно через несколько месяцев. Но все они могут быть очищены! Там, где есть воля, есть способ наверняка.
Если в будущем вам понадобятся данные в будущем, просто соберите все, что вам нужно, и сохраните их в файлах .dat с помощью pickle.
Просто продолжайте искать, как попробовать эти модули, копировать и вставлять свои ошибки в Google.


источник
3

Использование 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)
Аш-Ishh ..
источник
1

Я пытался найти ответ на этот вопрос в течение двух дней. Многие ответы направляют вас на разные вопросы. Но ответ змея выше, действительно, в точку. Это самое короткое и простое решение. Просто напоминание, что последнее слово «var» представляет имя переменной , поэтому должно использоваться как:

 result = driver.execute_script('var text = document.title ; return text')
Abd_bgc
источник
Это должен быть комментарий к ответу змея, а не отдельный ответ.
Исербий
1
Это очевидно. Но у меня пока нет 50 повторений, чтобы комментировать чужой ответ.
Abd_bgc
0

Мне приходилось сталкиваться с этой же проблемой в некоторых моих собственных веб-проектах. Я справился с этим, используя библиотеку запросов Python для выполнения http-запроса непосредственно к API, вместо того, чтобы загружать JS.

Для этого хорошо работает библиотека запросов python, и вы можете просмотреть http-запросы, используя элемент inspect и перейдя на вкладку network.

Superduperfluous
источник
Это не дает ответа на вопрос. Как только у вас будет достаточно репутации, вы сможете комментировать любой пост ; вместо этого предоставьте ответы, которые не требуют разъяснений от автора . - Из обзора
Джефф