可以使用scrapy从使用AJAX的网站中抓取动态内容吗?


145

我最近一直在学习Python,并全力以赴来构建网络抓取工具。一点都不花哨。其唯一目的是从博彩网站上获取数据,并将此数据放入Excel。

大多数问题都是可以解决的,我周围有些混乱。但是,我在一个问题上遇到了巨大的障碍。如果站点加载一张马表并列出当前的投注价格,则此信息不在任何源文件中。线索是这些数据有时是实时的,而数字显然是从某个远程服务器上更新的。我PC上的HTML只是有一个漏洞,他们的服务器正在推送我需要的所有有趣数据。

现在我对动态Web内容的经验很低,所以这件事使我难以理解。

我认为Java或Javascript是关键,这经常弹出。

刮板只是赔率比较引擎。有些网站有API,但对于那些没有的API则需要。我正在使用python 2.7的scrapy库

如果这个问题过于开放,我深表歉意。简而言之,我的问题是:如何使用scrapy来抓取此动态数据,以便可以使用它?这样我就可以实时抓取该赔率赔率数据?


1
我该如何获取这些动态动态的数据?
约瑟夫

1
如果您的网页上有JavaScript,请尝试以下操作
reclosedev

3
尝试使用某些Firefox扩展程序(例如httpFox或),liveHttpHeaders然后加载使用ajax请求的页面。Scrapy不会自动识别ajax请求,您必须手动搜索适当的ajax URL,然后使用该URL进行请求。
阿米尔·阿德南

欢呼声,我将给Firefox扩展一个麻烦
Joseph

有许多开源解决方案。但是,如果您正在寻找一种简便快捷的方法(尤其是对于大型工作负载),请查看SnapSearch(snapsearch.io)。它是为需要搜索引擎可爬网性的JS,HTML5和SPA网站构建的。尝试演示(如果内容为空,则表示该网站实际上未返回任何正文内容,可能意味着301重定向)。
CMCDragonkai 2014年

Answers:


74

基于Webkit的浏览器(例如Google Chrome或Safari)具有内置的开发人员工具。在Chrome中,您可以将其打开Menu->Tools->Developer Tools。该Network选项卡使您可以查看有关每个请求和响应的所有信息:

在此处输入图片说明

在图片的底部,您可以看到我已将请求过滤为XHR-这些是由javascript代码发出的请求。

提示:每次加载页面时都会清除日志,在图片底部,黑点按钮将保留日志。

分析请求和响应后,您可以模拟来自网络爬虫的请求并提取有价值的数据。在许多情况下,获取数据比解析HTML更容易,因为该数据不包含表示逻辑,并且其格式设置为可以由javascript代码访问。

Firefox具有类似的扩展名,称为firebug。有人会说萤火虫功能更强大,但我喜欢webkit的简单性。


141
如果它甚至没有单词“ scrapy”,这怎么会是一个可以接受的答案?
工具包

它有效,并且很容易在python中使用json模块进行解析。这是一个解决方案!相比之下,尝试使用硒或其他人建议的东西,这更让人头疼。如果替代方法更加复杂,那么我将给您,但是这里不是这种情况@Toolkit
Arion_Miles 18-10-21

1
这不是真的相关。问题是如何使用粗俗的方式抓取动态网站。
E. Erfan,

“这到底怎么可能是一个可以接受的答案”-因为实际使用战胜了政治正确性。人类了解上下文。
Espresso

98

这是一个scrapy带有AJAX请求的简单示例 。让我们看看网站rubin-kazan.ru

所有消息都加载了AJAX请求。我的目标是获取这些消息及其所有属性(作者,日期等):

在此处输入图片说明

当我分析页面的源代码时,因为网页使用AJAX技术,所以看不到所有这些消息。但是我可以使用Mozilla Firefox中的Firebug(或其他浏览器中的等效工具)来分析HTTP请求,该请求会在网页上生成消息:

在此处输入图片说明

它不会重新加载整个页面,而是仅重新加载页面中包含消息的部分。为此,我单击底部任意数量的页面:

在此处输入图片说明

我观察到负责邮件正文的HTTP请求:

在此处输入图片说明

完成后,我分析请求的标头(我必须引用将从var部分的源页面中提取的该URL,请参见下面的代码):

在此处输入图片说明

和请求的表单数据内容(HTTP方法为“ Post”):

在此处输入图片说明

以及响应的内容,它是一个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文件。


6
你好 您能否解释一下“ url_list_gb_messages”是什么?我听不懂 谢谢。
极化

4
这绝对是更好的。
6

1
@polarise该代码正在使用re模块(正则表达式),它搜索字符串'url_list_gb_messages="(.*)"'并将括号内的内容隔离在同名变量中。这是一个不错的介绍:guru99.com/python-regular-expressions-complete-tutorial.html
MGP

42

进行爬网时,很多时候我们会遇到这样的问题:页面上呈现的内容是使用Javascript生成的,因此scrapy无法为其进行爬网(例如ajax请求,jQuery疯狂)。

但是,如果您将Scrapy与Web测试框架Selenium一起使用,则我们能够对普通Web浏览器中显示的内容进行爬网。

注意事项:

  • 您必须安装Python版本的Selenium RC才能正常工作,并且必须正确设置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的任何提示吗?(OS是Linux Mint)。我收到“ [Errno 111]连接被拒绝”。
安德鲁

1
从硒中导入硒时,此代码不再适用于selenium=3.3.1python=2.7.10,错误
benjaminz

1
在那个版本的selenium中,您的import语句将是: from selenium import webdriverchromedriver您碰巧正在使用的任何东西。文档 编辑:添加文档参考并更改我的可怕语法!
nulltron

根据Selenium WebDriver 网站的介绍
rainbowsorbet

33

另一个解决方案是实现下载处理程序或下载处理程序中间件。(有关下载器中间件的更多信息,请参见scrapy docs)以下是将硒与无头phantomjs webdriver一起使用的示例类:

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

可选的插件:
我希望能够告诉不同的Spider使用哪个中间件,因此我实现了此包装器:

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 2014年

@ rocktheartsm4l什么不对的只是使用在process_requestsif spider.name in ['spider1', 'spider2']而不是装饰

@pad没错。我刚刚发现,对于我的蜘蛛类来说,有一个名为“中间件”的集合更为清晰。这样,我可以查看任何蜘蛛类,并确切查看将为其执行哪些中间件。我的项目中实现了许多中间件,因此这很有意义。
rocktheartsm4l 2014年

这是一个糟糕的解决方案。它不仅与scrapy不相关,而且代码本身效率极低,并且整个方法通常都无法实现对scrapy异步Web抓取框架的整个目的
Granitosaurus

2
它比我在SO上看到的任何其他解决方案(使用下载器中间件)都效率更高,因此它只对页面发出一个请求。如果真是太糟糕了,为什么不提出一个更好的解决方案并共享而不是坦率地提出一个方面的主张。“与刮y无关”是在抽烟吗?除了实现一些疯狂的复杂,​​健壮和自定义解决方案之外,这是我大多数人都看到的方法。唯一的不同是,大多数实施蜘蛛程序中的硒部分会导致发出多个请求……
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()

假设您的刮板称为“刮板”。如果您将提到的代码放在“ scraper”文件夹根目录下的名为handlers.py的文件中,则可以将其添加到settings.py中:

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

还有JS解析DOM的voilà,其中包含抓取的缓存,重试等。


我喜欢这个解决方案!
rocktheartsm4l 2016年

不错的解决方案。Selenium驱动程序仍然是唯一选择吗?
Motheus

很好的解决方案。非常感谢。
CrazyGeek19年

4

如何使用scrapy来抓取此动态数据,以便可以使用它?

我想知道为什么没有人仅使用Scrapy发布解决方案。

查阅Scrapy小组SCRAPING INFINITE SCROLLING PAGES 的博客文章。该示例抓取了使用无限滚动的http://spidyquotes.herokuapp.com/scroll网站。

这个想法是使用浏览器的开发人员工具并注意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)

我们再次面临着同样的问题:并非为此目的而设计,这是我们遇到相同问题的地方。转到phantomJS或按照其他建议,创建自己的下载中间件
rak007

@ rak007 PhantomJS vs Chrome驱动程序。您会建议哪一个?
Chankey Pathak'7

2

是的,Scrapy可以废弃动态网站,即通过javaScript呈现的网站。

抓取此类网站有两种方法。

第一,

您可以splash用来呈现Javascript代码,然后解析呈现的HTML。你可以在这里找到文档和项目Scrapy splash,git

第二,

就像每个人都在说的那样,通过监视network calls,可以的,您可以找到获取数据的api调用,并在您的scrapy spider中模拟该调用可能会帮助您获取所需的数据。


1

我通过使用Selenium和Firefox Web驱动程序来处理ajax请求。如果需要使用爬虫作为守护程序,速度不是很快,但是比任何手动解决方案要好得多。我在这里写了一个简短的教程供参考

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.