繁体   English   中英

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

[英]Can scrapy be used to scrape dynamic content from websites that are using AJAX?

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

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

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

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

刮板只是一个赔率比较引擎。 有些网站有 API,但我需要为那些没有的网站提供 API。 我将 scrapy 库与 Python 2.7 一起使用

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

这是一个带有 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 文件。

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

在此处输入图像描述

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

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

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

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

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

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

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

3)your_spider.py集成到HTMLResponse中。 解码响应正文将为您提供所需的输出。

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 函数中发出一个全新的请求——这是对相同内容的两个请求。

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

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

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

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

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

从 API 的外部 url 生成的数据调用 HTML 响应作为 POST 方法。

import scrapy
from scrapy.crawler import CrawlerProcess

class TestSpider(scrapy.Spider):
    name = 'test'  
    def start_requests(self):
        url = 'https://howlongtobeat.com/search_results?page=1'
        payload = "queryString=&t=games&sorthead=popular&sortd=0&plat=&length_type=main&length_min=&length_max=&v=&f=&g=&detail=&randomize=0"
        headers = {
            "content-type":"application/x-www-form-urlencoded",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
        }

        yield scrapy.Request(url,method='POST', body=payload,headers=headers,callback=self.parse)

    def parse(self, response):
        cards = response.css('div[class="search_list_details"]')

        for card in cards: 
            game_name = card.css('a[class=text_white]::attr(title)').get()
            yield {
                "game_name":game_name
            }
           

if __name__ == "__main__":
    process =CrawlerProcess()
    process.crawl(TestSpider)
    process.start()

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

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

第一的,

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

第二,

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

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

我认为应该提及 2022 年的一些更现代的替代方案,我想列出该问题更流行的答案中讨论的方法的一些优缺点。

  1. 最佳答案和其他几个讨论使用浏览器dev tools或数据包捕获软件来尝试识别响应url的模式,并尝试重新构造它们以用作scrapy.Request s。

    • 优点:在我看来,这仍然是最好的选择,当它可用时,它比传统方法更快,而且通常比传统方法更简单,即使用xpathcss选择器从 HTML 中提取内容。

    • 缺点:不幸的是,这仅在一小部分动态站点上可用,而且通常网站都有适当的安全措施,这使得使用此策略变得困难。

  2. 使用Selenium Webdriver是之前答案中经常提到的另一种方法。

    • 优点:易于实施,并集成到 scrapy 工作流程中。 此外,还有大量示例,如果您使用scrapy-selenium等 3rd 方扩展,则只需很少的配置

    • 缺点:它很慢。 scrapy 的关键特性之一是它的异步工作流,可以轻松地在几秒钟内抓取数十个甚至数百个页面。 使用 selenium 可以显着减少这种情况。

有两种新方法绝对值得考虑, scrapy-splashscrapy-playwright

刮擦飞溅

  • 一个 scrapy 插件,集成了scrapy开发人员创建和维护的 javascript 渲染服务到 scrapy 工作流中。 该插件可以使用pip3 install scrapy-splash从 pypi 安装,而 splash 需要在它自己的进程中运行,并且最容易从 docker 容器运行。

scrapy 剧作家

  • Playwright 是一种类似于selenium的浏览器自动化工具,但不会像使用 selenium 那样导致速度大幅下降。Playwright 可以毫无问题地适应异步 scrapy 工作流,发送请求与单独使用 scrapy 一样快。 它也比 selenium 更容易安装和集成scrapy-playwright插件也由 scrapy 的开发人员维护,通过 pypi 使用pip3 install scrapy-playwright就像在终端中运行playwright install一样简单。

可以在插件的每个 github 页https://github.com/scrapy-plugins/scrapy-playwrighthttps://github.com/scrapy-plugins/scrapy-splash找到更多详细信息和许多示例。

ps 根据我的经验,这两个项目往往在 linux 环境中工作得更好。 对于 windows 用户,我建议将它与Windows Linux 子系统(wsl)一起使用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM