【问题标题】:Can scrapy be used to scrape dynamic content from websites that are using AJAX?scrapy 可以用来从使用 AJAX 的网站上抓取动态内容吗?
【发布时间】:2022-11-13 17:24:31
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 我怎样才能得到这些数据,动态和实时的数据?
  • 如果你的页面有javascript,Try this
  • 尝试一些 Firefox 扩展名,如 httpFoxliveHttpHeaders 并加载使用 ajax 请求的页面。 Scrapy 不会自动识别 ajax 请求,你必须手动搜索合适的 ajax 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
  • 它为我检索了一个带有“您需要启用 JavaScript 才能运行此应用程序”的正文。
【解决方案2】:

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

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

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

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

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

【讨论】:

  • 如果它甚至没有“scrapy”这个词,这怎么可能是一个被接受的答案?
  • 它可以工作,并且在 python 中使用 json 模块很容易解析。这是一个解决方案!相比之下,尝试使用硒或人们建议的其他东西,更令人头疼。如果替代方法更复杂,那么我会把它给你,但这里不是@Toolkit
  • 这并不重要。问题是如何使用 scrapy 抓取动态网站。
【解决方案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] 连接被拒绝”。
  • 此代码不再适用于 selenium=3.3.1python=2.7.10,从 selenium 导入 selenium 时出错
  • 在该版本的 selenium 中,您的导入语句将是:from selenium import webdriverchromedriver 或您碰巧使用的任何内容。 Docs 编辑:添加文档参考并更改我可怕的语法!
  • 根据their website,Selenium Remote Control 已被 Selenium WebDriver 取代
【解决方案4】:

另一种解决方案是实现下载处理程序或下载处理程序中间件。 (有关下载器中间件的更多信息,请参阅scrapy docs)以下是一个使用 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)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_requests , if spider.name in ['spider1', 'spider2'] 而不是装饰器有什么问题
  • @pad 这没有错。我只是发现我的蜘蛛类有一个名为中间件的集合更清楚。通过这种方式,我可以查看任何蜘蛛类并准确地查看将为它执行哪些中间件。我的项目实现了很多中间件,所以这是有道理的。
  • 这是一个糟糕的解决方案。它不仅与 scrapy 无关,而且代码本身效率极低,而且整个方法通常都违背了 scrapy 异步网络抓取框架的全部目的
  • 它比我在 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】:

从 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()

【讨论】:

    【解决方案8】:

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

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

    第一的,

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

    第二,

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

        猜你喜欢
        • 2012-01-22
        • 2015-08-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多