【问题标题】:Can scrapy be used to scrape dynamic content from websites that are using AJAX?可以使用 scrapy 从使用 AJAX 的网站中抓取动态内容吗?
【发布时间】:2012-01-22 22:06:29
【问题描述】:

我最近一直在学习 Python,并且正在着手构建一个网络爬虫。这一点都不花哨。其唯一目的是从博彩网站获取数据并将这些数据放入 Excel。

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

现在我对动态网络内容的体验很低,所以这件事让我难以理解。

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

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

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

【问题讨论】:

  • 我怎样才能得到这些数据,动态的和实时的数据?
  • 如果你的页面有javascript,Try this
  • 尝试一些Firefox 扩展如httpFoxliveHttpHeaders 并加载使用ajax 请求的页面。 Scrapy 不会自动识别 ajax 请求,您必须手动搜索适当的 ajax URL,然后使用该 URL 进行请求。
  • 干杯,我会给 Firefox 扩展一个 wizz
  • 有许多开源解决方案。但是,如果您正在寻找一种简单快捷的方法来执行此操作,尤其是对于大型工作负载,请查看 SnapSearch (snapsearch.io)。它是为需要搜索引擎可抓取性的 JS、HTML5 和 SPA 网站构建的。试试这个演示(如果有空内容,这意味着该网站实际上没有返回任何正文内容,可能意味着 301 重定向)。

标签: javascript python ajax screen-scraping scrapy


【解决方案1】:

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

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

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

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

我观察到负责消息正文的 HTTP 请求:

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

以及请求的表单数据内容(HTTP方式为“Post”):

以及响应的内容,是一个JSON文件:

它提供了我正在寻找的所有信息。

从现在开始,我必须在scrapy中实现所有这些知识。让我们为此目的定义蜘蛛:

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 文件。

【讨论】:

  • 嗨。你能解释一下'url_list_gb_messages'是什么吗?我无法理解。谢谢。
  • 这个肯定更好。
  • @polarise 该代码使用re 模块(正则表达式),它搜索字符串'url_list_gb_messages="(.*)"' 并隔离同名变量中括号的内容。这是一个很好的介绍:guru99.com/python-regular-expressions-complete-tutorial.html
【解决方案2】:

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

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

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

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

Firefox 也有类似的扩展名,叫做firebug。有些人会说 firebug 更强大,但我喜欢 webkit 的简单性。

【讨论】:

  • 如果它甚至没有“scrapy”这个词,这怎么可能是一个被接受的答案??
  • 它可以工作,并且在python中使用json模块很容易解析。这是一个解决方案!相比之下,尝试使用硒或人们建议的其他东西,更令人头疼。如果替代方法更复杂,那么我会把它给你,但@Toolkit 不是这种情况
  • 这并不相关。问题是如何使用 Scarpy 抓取动态网站。
【解决方案3】:

很多时候我们在抓取时遇到问题,页面上呈现的内容是用 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 的任何提示吗? (操作系统是 Linux Mint)。我收到“[Errno 111] Connection denied”。
  • 此代码不再适用于 selenium=3.3.1python=2.7.10,从 selenium 导入 selenium 时出错
  • 在该版本的 selenium 中,您的导入语句将是:from selenium import webdriverchromedriver 或您碰巧使用的任何内容。 Docs 编辑:添加文档参考并更改我可怕的语法!
  • Selenium Remote Control 已被 Selenium WebDriver 取代,据their website
【解决方案4】:

另一种解决方案是实现下载处理程序或下载处理程序中间件。 (有关下载器中间件的更多信息,请参阅scrapy docs)以下是使用 selenium 和无头 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() 类添加到settings.py 内的变量DOWNLOADER_MIDDLEWARE

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

优势:
以这种方式而不是在蜘蛛中实现它的主要优点是您最终只会发出一个请求。以 A T 的解决方案为例:下载处理程序处理请求,然后将响应交给蜘蛛。然后蜘蛛在它的 parse_page 函数中发出一个全新的请求——这是对相同内容的两个请求。

【讨论】:

  • 我回答这个问题有点晚了>.
  • @rocktheartsm4l 使用 process_requestsif spider.name in ['spider1', 'spider2'] 代替装饰器有什么问题
  • @pad 这没有错。我只是发现我的蜘蛛类有一个名为中间件的集合更清楚。通过这种方式,我可以查看任何蜘蛛类,并准确查看将为它执行哪些中间件。我的项目实现了很多中间件,所以这是有道理的。
  • 这是一个糟糕的解决方案。它不仅与scrapy无关,而且代码本身效率极低,而且整个方法总体上违背了scrapy异步Web抓取框架的全部目的
  • 它比我在 SO 上看到的任何其他解决方案都要高效得多,因为使用下载器中间件可以做到这一点,因此只对页面发出一个请求.. 如果它太糟糕了,你为什么不上来有更好的解决方案和分享,而不是明目张胆地提出片面的主张。 “与scrapy无关”你在抽烟吗?除了实现一些疯狂的复杂、强大和自定义的解决方案之外,这是我看到大多数人使用的方法。唯一的区别是大多数在蜘蛛中实现硒部分,这会导致发出多个请求......
【解决方案5】:

我使用的是自定义下载器中间件,但对它不太满意,因为我没有设法让缓存与它一起工作。

更好的方法是实现自定义下载处理程序。

有一个工作示例here。它看起来像这样:

# 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,带有scrapy 缓存、重试等等。

【讨论】:

  • 我喜欢这个解决方案!
  • 不错的解决方案。 Selenium 驱动程序仍然是唯一的选择吗?
  • 很好的解决方案。非常感谢。
  • 嗨@ivan,我完全喜欢你的回答。但是,响应没有到达蜘蛛的 parse(callback) 方法。当我检查处理程序内的响应正文时,它符合预期。问题可能出在哪里?你能帮我吗?谢谢。
  • 你好@Vipool,好久没运行这段代码了……最近在用nodejs的sdk.apify.com/docs/examples/crawl-multiple-urls爬取js解析。
【解决方案6】:

如何使用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)

【讨论】:

  • 我们再次面临同样的问题:Scrappy 不是为此目的而制造的,这就是我们面临同样问题的地方。继续使用 phantomJS 或按照其他人的建议,创建自己的下载中间件
  • @rak007 PhantomJS 与 Chrome 驱动程序。你会推荐哪一个?
【解决方案7】:

是的,Scrapy 可以抓取动态网站,通过 javaScript 渲染的网站。

有两种方法可以抓取这类网站。

首先,

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

第二,

正如大家所说,通过监控network calls,是的,您可以找到获取数据的 api 调用,并在您的 scrapy 蜘蛛中模拟该调用可能会帮助您获取所需的数据。

【讨论】:

    【解决方案8】:

    我使用 Selenium 和 Firefox Web 驱动程序处理 ajax 请求。如果您需要爬虫作为守护进程,它并没有那么快,但比任何手动解决方案都要好得多。我写了一个简短的教程here供参考

    【讨论】:

      猜你喜欢
      • 2022-11-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-17
      • 1970-01-01
      • 2013-05-09
      • 2020-10-12
      • 2012-01-01
      相关资源
      最近更新 更多