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

IT技术 javascript python ajax screen-scraping scrapy
2021-01-20 06:09:06

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

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

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

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

刮板只是一个赔率比较引擎。一些网站有 API,但我需要这些 API 没有的。我在 Python 2.7 中使用了 scrapy 库

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

6个回答

这是一个scrapy使用 AJAX 请求的简单示例 让我们看看网站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 文件。

这个肯定更好。
2021-03-30 06:09:06
你好。你能解释一下'url_list_gb_messages'是什么吗?我无法理解。谢谢。
2021-04-03 06:09:06
@polarise 该代码正在使用remodule(正则表达式),它搜索字符串'url_list_gb_messages="(.*)"'并隔离同名变量中括号的内容。这是一个很好的介绍:guru99.com/python-regular-expressions-complete-tutorial.html
2021-04-05 06:09:06

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

在此处输入图片说明

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

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

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

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

如果它甚至没有“scrapy”这个词,这怎么能成为一个可以接受的答案?
2021-03-11 06:09:06
这并不重要。问题是如何使用 Scarpy 来抓取动态网站。
2021-03-27 06:09:06
它有效,并且很容易在 python 中使用 json module进行解析。这是一个解决方案!相比之下,尝试使用selenium或其他人建议的东西,它更令人头疼。如果替代方法更复杂,那么我会把它给你,但事实并非如此@Toolkit
2021-03-29 06:09:06

很多时候我们在爬行时会遇到一些问题,页面上呈现的内容是用 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/

根据他们的网站,Selenium Remote Control 已被 Selenium WebDriver 取代
2021-03-10 06:09:06
在该版本的 selenium 中,您的导入语句将是: from selenium import webdriverchromedriver您碰巧使用的任何内容。文档 编辑:添加文档参考并更改我可怕的语法!
2021-03-18 06:09:06
整洁的解决方案!您有任何有关将此脚本连接到 Firefox 的提示吗?(操作系统是 Linux Mint)。我收到“[Errno 111] 连接被拒绝”。
2021-03-26 06:09:06
此代码不再适用于selenium=3.3.1python=2.7.10,从 selenium 导入 selenium 时出错
2021-04-05 06:09:06

另一种解决方案是实现下载处理程序或下载处理程序中间件。有关下载器中间件的更多信息,请参见scrapy 文档)以下是使用 selenium 和 headless 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_MIDDLEWAREsettings.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 函数中发出一个全新的请求——这是对相同内容的两个请求。

不过我回答这个问题有点晚了>.<
2021-03-13 06:09:06
@pad 没有错。我刚刚发现我的蜘蛛类有一个名为中间件的集合更加清晰。通过这种方式,我可以查看任何蜘蛛类,并准确查看将为其执行哪些中间件。我的项目实现了很多中间件,所以这是有道理的。
2021-03-20 06:09:06
它比我在 SO 上看到的任何其他解决方案都更有效率,因为使用下载器中间件使得它只对页面发出一个请求。公然提出片面的主张。“与scrapy无关”你在抽烟吗?除了实施一些疯狂的复杂、强大和自定义的解决方案之外,这是我见过大多数人使用的方法。唯一的区别是,大多数在蜘蛛中实现了selenium部分,这会导致发出多个请求......
2021-03-29 06:09:06
@ rocktheartsm4l什么不对的只是使用在process_requestsif spider.name in ['spider1', 'spider2']而不是装饰
2021-03-30 06:09:06
这是一个可怕的解决方案。它不仅与scrapy无关,而且代码本身效率极低,而且整个方法总体上违背了scrapy异步Web抓取框架的全部目的
2021-04-07 06:09:06

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

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

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

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

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