Можно ли использовать скрап для удаления динамического контента с сайтов, использующих AJAX?

145

Недавно я изучал Python и погружаюсь в создание веб-скребка. Ничего особенного; его единственная цель - получить данные с веб-сайта для ставок и поместить их в Excel.

Большинство проблем решаемы, и у меня есть хороший маленький беспорядок вокруг. Однако я столкнулся с огромным препятствием из-за одной проблемы. Если сайт загружает таблицу лошадей и перечисляет текущие цены на ставки, этой информации нет ни в одном исходном файле. Подсказка заключается в том, что эти данные иногда бывают живыми, причем цифры, очевидно, обновляются с какого-то удаленного сервера. В HTML на моем ПК просто есть дыра, где их серверы проталкивают все интересные данные, которые мне нужны.

Теперь мой опыт работы с динамическим веб-контентом невелик, поэтому у меня возникли проблемы с тем, как мне разобраться.

Я думаю, что Java или Javascript - это ключ, который часто появляется.

Скребок - это просто механизм сравнения шансов. У некоторых сайтов есть API, но мне это нужно для тех, кто этого не делает. Я использую библиотеку Scrapy с Python 2.7

Я извиняюсь, если этот вопрос слишком открытый. Короче говоря, мой вопрос: как можно использовать скрап для очистки этих динамических данных, чтобы я мог их использовать? Чтобы я мог в реальном времени проверить эти данные о коэффициентах ставок?

Джозеф
источник
1
Как я могу получить эти данные, данные, которые являются динамическими и живыми?
Иосиф
1
Если на вашей странице есть JavaScript, попробуйте это
reclosedev
3
Попробуйте некоторые Firefoxрасширения, такие как httpFoxили, liveHttpHeadersи загрузите страницу, которая использует ajax-запрос. Scrapy не идентифицирует автоматически запросы ajax, вам нужно вручную найти соответствующий URL-адрес ajax, а затем выполнить запрос с ним.
Амир Аднан
ура, я сделаю расширения Firefox волшебными
Джозеф
Существует ряд решений с открытым исходным кодом. Но если вы ищете простой и быстрый способ сделать это, особенно для больших рабочих нагрузок, посмотрите SnapSearch ( snapsearch.io ). Он был создан для сайтов JS, HTML5 и SPA, требующих возможности поиска в поисковых системах. Попробуйте демо-версию (если есть пустой контент, это означает, что сайт на самом деле не возвращал основной контент, что может означать перенаправление 301).
CMCDragonkai

Ответы:

74

Браузеры на основе Webkit (например, Google Chrome или Safari) имеют встроенные инструменты разработчика. В Chrome вы можете открыть его Menu->Tools->Developer Tools. NetworkВкладка позволяет увидеть всю информацию о каждом запросе и ответе:

введите описание изображения здесь

Внизу рисунка вы можете видеть, что я отфильтровал запрос до XHR- это запросы, сделанные с помощью кода javascript.

Совет: журнал очищается каждый раз, когда вы загружаете страницу, в нижней части картинки кнопка с черной точкой сохранит журнал.

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

Firefox имеет аналогичное расширение, оно называется firebug . Некоторые утверждают, что firebug еще более мощный, но мне нравится простота webkit.

лыжа
источник
141
Как, черт возьми, это может быть принятым ответом, если в нем даже нет слова «скрап»?
Инструментарий
Это работает, и это легко разобрать, используя модуль json в Python. Это решение! По сравнению с этим, попробуйте использовать селен или другие вещи, которые люди предлагают, это больше головная боль. Если бы альтернативный метод был более замысловатым, я бы дал его вам, но здесь дело не в этом @Toolkit
Arion_Miles
1
Это не очень актуально. Вопрос заключался в том, как использовать scarpy для очистки динамических веб-сайтов.
Э. Эрфан
«Как, черт возьми, это может быть принятым ответом» - потому что практическое использование превосходит политкорректность. Люди понимают КОНТЕКСТ.
Эспрессо
98

Вот простой пример scrapyс запросом AJAX. Пусть увидят сайт rubin-kazan.ru .

Все сообщения загружаются с помощью запроса AJAX. Моя цель - получить эти сообщения со всеми их атрибутами (автор, дата, ...):

введите описание изображения здесь

Когда я анализирую исходный код страницы, я не вижу все эти сообщения, потому что веб-страница использует технологию AJAX. Но я могу с помощью Firebug от Mozilla Firefox (или аналогичного инструмента в других браузерах) проанализировать HTTP-запрос, который генерирует сообщения на веб-странице:

введите описание изображения здесь

Он не перезагружает всю страницу, а только части страницы, которые содержат сообщения. Для этого я нажимаю произвольное количество страниц внизу:

введите описание изображения здесь

И я наблюдаю HTTP-запрос, который отвечает за тело сообщения:

введите описание изображения здесь

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

введите описание изображения здесь

И форма данных содержимого запроса (HTTP-метод «Пост»):

введите описание изображения здесь

И содержание ответа, который представляет собой файл JSON:

введите описание изображения здесь

Который представляет всю информацию, которую я ищу.

С сегодняшнего дня я должен применить все эти знания в медицине. Давайте определим паука для этой цели:

class spider(BaseSpider):
    name = 'RubiGuesst'
    start_urls = ['http://www.rubin-kazan.ru/guestbook.html']

    def parse(self, response):
        url_list_gb_messages = re.search(r'url_list_gb_messages="(.*)"', response.body).group(1)
        yield FormRequest('http://www.rubin-kazan.ru' + url_list_gb_messages, callback=self.RubiGuessItem,
                          formdata={'page': str(page + 1), 'uid': ''})

    def RubiGuessItem(self, response):
        json_file = response.body

В parseфункции у меня есть ответ на первый запрос. У RubiGuessItemменя есть файл JSON со всей информацией.

Badarau Petru
источник
6
Здравствуй. Не могли бы вы объяснить, что такое url_list_gb_messages? Я не могу этого понять. Спасибо.
поляризовать
4
Этот, безусловно, лучше.
1a1a11a
1
@polarise Этот код использует reмодуль (регулярные выражения), он ищет строку 'url_list_gb_messages="(.*)"'и изолирует содержимое скобок в переменной с тем же именем. Это хорошее введение: guru99.com/python-regular-expressions-complete-tutorial.html
MGP
42

Много раз при сканировании мы сталкиваемся с проблемами, когда содержимое, отображаемое на странице, генерируется с помощью Javascript, и поэтому scrapy не может сканировать его (например, запросы ajax, сумасшествие jQuery).

Однако если вы используете Scrapy вместе с фреймворком веб-тестирования Selenium, мы можем сканировать все, что отображается в обычном веб-браузере.

Некоторые вещи, на которые стоит обратить внимание:

  • Чтобы это работало, у вас должна быть установлена ​​версия Selenium RC на Python, и вы должны правильно настроить Selenium. Также это просто сканер шаблонов. Вы могли бы стать намного более безумным и более продвинутым с вещами, но я просто хотел показать основную идею. Теперь, когда код стоит, вы будете делать два запроса для любого заданного URL. Один запрос сделан Scrapy, а другой - Selenium. Я уверен, что есть способы обойти это, чтобы вы могли просто заставить Selenium выполнять один-единственный запрос, но я не стал реализовывать это, и, выполнив два запроса, вы также можете сканировать страницу с помощью Scrapy.

  • Это довольно мощно, потому что теперь у вас есть весь DOM, доступный для сканирования, и вы все еще можете использовать все приятные функции сканирования в Scrapy. Конечно, это приведет к медленному сканированию, но в зависимости от того, насколько вам нужен обработанный DOM, это может стоить ожидания.

    from scrapy.contrib.spiders import CrawlSpider, Rule
    from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
    from scrapy.selector import HtmlXPathSelector
    from scrapy.http import Request
    
    from selenium import selenium
    
    class SeleniumSpider(CrawlSpider):
        name = "SeleniumSpider"
        start_urls = ["http://www.domain.com"]
    
        rules = (
            Rule(SgmlLinkExtractor(allow=('\.html', )), callback='parse_page',follow=True),
        )
    
        def __init__(self):
            CrawlSpider.__init__(self)
            self.verificationErrors = []
            self.selenium = selenium("localhost", 4444, "*chrome", "http://www.domain.com")
            self.selenium.start()
    
        def __del__(self):
            self.selenium.stop()
            print self.verificationErrors
            CrawlSpider.__del__(self)
    
        def parse_page(self, response):
            item = Item()
    
            hxs = HtmlXPathSelector(response)
            #Do some XPath selection with Scrapy
            hxs.select('//div').extract()
    
            sel = self.selenium
            sel.open(response.url)
    
            #Wait for javscript to load in Selenium
            time.sleep(2.5)
    
            #Do some crawling of javascript created content with Selenium
            sel.get_text("//div")
            yield item
    
    # Snippet imported from snippets.scrapy.org (which no longer works)
    # author: wynbennett
    # date  : Jun 21, 2011

Ссылка: http://snipplr.com/view/66998/

В
источник
Аккуратное решение! Есть ли у вас какие-либо советы по подключению этого скрипта к Firefox? (ОС Linux Mint). Я получаю "[Errno 111] Соединение отказано".
Андрей
1
Этот код больше не работает для selenium=3.3.1и python=2.7.10, ошибка при импорте селена из селена
benjaminz
1
В этой версии селена ваш оператор импорта будет: from selenium import webdriverили chromedriverили что вы используете. Docs EDIT: Добавление справочной документации и изменить свою ужасную грамматику!
nulltron
Selenium Remote Control был заменен Selenium WebDriver, согласно их веб-сайту
rainbowsorbet
33

Другим решением может быть реализация обработчика загрузки или промежуточного программного обеспечения обработчика загрузки. (см. документацию Scrapy для получения дополнительной информации о промежуточном программном обеспечении загрузчика). Ниже приведен пример класса, использующего селен с веб-драйвером phantomjs без головы:

1) Определите класс в middlewares.pyскрипте.

from selenium import webdriver
from scrapy.http import HtmlResponse

class JsDownload(object):

    @check_spider_middleware
    def process_request(self, request, spider):
        driver = webdriver.PhantomJS(executable_path='D:\phantomjs.exe')
        driver.get(request.url)
        return HtmlResponse(request.url, encoding='utf-8', body=driver.page_source.encode('utf-8'))

2) Добавить JsDownload()класс к переменной DOWNLOADER_MIDDLEWAREвнутри settings.py:

DOWNLOADER_MIDDLEWARES = {'MyProj.middleware.MiddleWareModule.MiddleWareClass': 500}

3) Интеграция HTMLResponseвнутри your_spider.py. Расшифровка тела ответа даст вам желаемый результат.

class Spider(CrawlSpider):
    # define unique name of spider
    name = "spider"

    start_urls = ["https://www.url.de"] 

    def parse(self, response):
        # initialize items
        item = CrawlerItem()

        # store data as items
        item["js_enabled"] = response.body.decode("utf-8") 

Необязательное дополнение:
я хотел сообщить различным паукам, какое промежуточное программное обеспечение использовать, поэтому я реализовал эту оболочку:

def check_spider_middleware(method):
@functools.wraps(method)
def wrapper(self, request, spider):
    msg = '%%s %s middleware step' % (self.__class__.__name__,)
    if self.__class__ in spider.middleware:
        spider.log(msg % 'executing', level=log.DEBUG)
        return method(self, request, spider)
    else:
        spider.log(msg % 'skipping', level=log.DEBUG)
        return None

return wrapper

для работы оболочки все пауки должны иметь как минимум:

middleware = set([])

включить промежуточное ПО:

middleware = set([MyProj.middleware.ModuleName.ClassName])

Преимущество:
Основное преимущество такой реализации, а не паука, состоит в том, что вы в конечном итоге делаете только один запрос. Например, в решении AT: обработчик загрузки обрабатывает запрос, а затем передает ответ пауку. Затем паук делает новый запрос в функции parse_page - это два запроса на один и тот же контент.

rocktheartsm4l
источник
Хотя я немного опоздал на этот вопрос>. <
rocktheartsm4l
@ rocktheartsm4l, что плохого в том process_requests, if spider.name in ['spider1', 'spider2']чтобы просто использовать вместо декоратора
pad
@pad В этом нет ничего плохого. Я просто обнаружил, что для моих классов-пауков более очевидно иметь набор с именем middleware. Таким образом, я мог посмотреть на любой класс пауков и точно определить, какое промежуточное программное обеспечение будет для него выполнено. В моем проекте было реализовано много промежуточного программного обеспечения, так что это имело смысл.
rocktheartsm4l
Это ужасное решение. Мало того, что это не связано с scrapy, но сам код чрезвычайно неэффективен, так как весь подход в целом разрушает всю цель асинхронного фреймворка web-
скрапинга,
2
Это гораздо более эффективно, чем любое другое решение, которое я видел в SO, поскольку использование промежуточного программного обеспечения для загрузчика делает так, что для страницы делается только один запрос ... если это так ужасно, почему бы вам не придумать лучшее решение и поделиться вместо делая явно односторонние претензии. "Не связано со скрапом" ты что-то куришь? Помимо реализации какого-то сумасшедшего сложного, надежного и индивидуального решения, такой подход я видел у большинства людей. Единственное отличие состоит в том, что большинство реализует часть селена в пауке, которая заставляет делать несколько запросов ...
rocktheartsm4l
10

Я использовал пользовательское промежуточное ПО для загрузчика, но мне это не очень понравилось, так как мне не удалось заставить кеш работать с ним.

Лучшим подходом было реализовать пользовательский обработчик загрузки.

Существует рабочий пример здесь . Это выглядит так:

# encoding: utf-8
from __future__ import unicode_literals

from scrapy import signals
from scrapy.signalmanager import SignalManager
from scrapy.responsetypes import responsetypes
from scrapy.xlib.pydispatch import dispatcher
from selenium import webdriver
from six.moves import queue
from twisted.internet import defer, threads
from twisted.python.failure import Failure


class PhantomJSDownloadHandler(object):

    def __init__(self, settings):
        self.options = settings.get('PHANTOMJS_OPTIONS', {})

        max_run = settings.get('PHANTOMJS_MAXRUN', 10)
        self.sem = defer.DeferredSemaphore(max_run)
        self.queue = queue.LifoQueue(max_run)

        SignalManager(dispatcher.Any).connect(self._close, signal=signals.spider_closed)

    def download_request(self, request, spider):
        """use semaphore to guard a phantomjs pool"""
        return self.sem.run(self._wait_request, request, spider)

    def _wait_request(self, request, spider):
        try:
            driver = self.queue.get_nowait()
        except queue.Empty:
            driver = webdriver.PhantomJS(**self.options)

        driver.get(request.url)
        # ghostdriver won't response when switch window until page is loaded
        dfd = threads.deferToThread(lambda: driver.switch_to.window(driver.current_window_handle))
        dfd.addCallback(self._response, driver, spider)
        return dfd

    def _response(self, _, driver, spider):
        body = driver.execute_script("return document.documentElement.innerHTML")
        if body.startswith("<head></head>"):  # cannot access response header in Selenium
            body = driver.execute_script("return document.documentElement.textContent")
        url = driver.current_url
        respcls = responsetypes.from_args(url=url, body=body[:100].encode('utf8'))
        resp = respcls(url=url, body=body, encoding="utf-8")

        response_failed = getattr(spider, "response_failed", None)
        if response_failed and callable(response_failed) and response_failed(resp, driver):
            driver.close()
            return defer.fail(Failure())
        else:
            self.queue.put(driver)
            return defer.succeed(resp)

    def _close(self):
        while not self.queue.empty():
            driver = self.queue.get_nowait()
            driver.close()

Предположим, ваш скребок называется «скребок». Если вы поместите упомянутый код в файл с именем handlers.py в корне папки «scraper», то вы можете добавить его в файл settings.py:

DOWNLOAD_HANDLERS = {
    'http': 'scraper.handlers.PhantomJSDownloadHandler',
    'https': 'scraper.handlers.PhantomJSDownloadHandler',
}

И вуаля, JS анализировал DOM с кэш-памятью, повторными попытками и т. Д.

Иван Чаер
источник
Мне нравится это решение!
rocktheartsm4l
Хорошее решение. Драйвер Selenium по-прежнему единственный вариант?
Мотеус
Отличное решение. Большое спасибо.
CrazyGeek
4

Как можно использовать скрап для очистки этих динамических данных, чтобы я мог их использовать?

Интересно, почему никто не опубликовал решение, используя только Scrapy.

Проверьте сообщение в блоге от команды Scrapy СКРЕПКА БЕСКОНЕЧНЫХ СТРАНИЦ . Пример записок http://spidyquotes.herokuapp.com/scroll сайт котором используется бесконечная прокрутка.

Идея состоит в том, чтобы использовать Developer Tools вашего браузера и замечать запросы AJAX, а затем на основе этой информации создавать запросы для Scrapy .

import json
import scrapy


class SpidyQuotesSpider(scrapy.Spider):
    name = 'spidyquotes'
    quotes_base_url = 'http://spidyquotes.herokuapp.com/api/quotes?page=%s'
    start_urls = [quotes_base_url % 1]
    download_delay = 1.5

    def parse(self, response):
        data = json.loads(response.body)
        for item in data.get('quotes', []):
            yield {
                'text': item.get('text'),
                'author': item.get('author', {}).get('name'),
                'tags': item.get('tags'),
            }
        if data['has_next']:
            next_page = data['page'] + 1
            yield scrapy.Request(self.quotes_base_url % next_page)
Чанки патхак
источник
Мы снова сталкиваемся с той же проблемой: Scrappy не создан для этой цели, и здесь мы сталкиваемся с той же проблемой. Перейдите к phantomJS или, как предлагали другие, создайте свое собственное промежуточное ПО для загрузки
rak007
@ rak007 PhantomJS против драйвера Chrome. Какой из них вы бы предложили?
Chankey Pathak
2

да, Scrapy может удалять динамические веб-сайты, веб-сайты, которые отображаются с помощью javaScript.

Существует два подхода к поиску подобных веб-сайтов.

Первый,

Вы можете использовать splashдля рендеринга кода Javascript, а затем проанализировать визуализированный HTML. вы можете найти документ и проект здесь Scrapy splash, git

Во-вторых,

Как утверждают все, отслеживая network calls, да, вы можете найти вызов API, который извлекает данные, и имитация вызова в вашем пауке-скрапе может помочь вам получить нужные данные.

ThunderMind
источник
1

Я обрабатываю запрос ajax с помощью Selenium и веб-драйвера Firefox. Это не так быстро, если вам нужен сканер как демон, но намного лучше, чем любое ручное решение. Я написал короткий учебник здесь для справки

Narko
источник