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

我最近一直在学习 Python,并且正在尝试建立一个 web-scraper。它一点也不花哨; 它的唯一目的是从博彩网站获取数据,并将这些数据输入到 Excel 中。

大多数问题都是可以解决的,而我现在正处于一个小混乱中。然而,我在一个问题上遇到了巨大的障碍。如果一个网站载入一个马的表格,并列出当前的博彩价格,这个信息不在任何源文件。线索是,这些数据有时是实时的,数字显然是从某个远程服务器更新的。我电脑上的 HTML 只是有一个漏洞,在那里他们的服务器推动通过所有有趣的数据,我需要的。

现在我对动态网页内容的经验不足,所以这件事情我很难理解。

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

刮刀只是一个机率比较引擎。有些网站有 API,但我需要这些为那些没有。我正在使用 Python 2.7的 Scrapy 库

如果这个问题太开放了,我很抱歉。简而言之,我的问题是: 如何使用 scrapy 来刮取这些动态数据,以便我可以使用它们?这样我就能实时获取赌博赔率数据了?

145829 次浏览

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

enter image description here

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

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

在分析请求和响应之后,您可以模拟来自 Web 爬虫的这些请求并提取有价值的数据。在许多情况下,获取数据比解析 HTML 更容易,因为数据不包含表示逻辑,而是通过 javascript 代码进行格式化访问。

Firefox 也有类似的扩展名,叫做 纵火犯。有人会说 Firebug 甚至更强大,但我喜欢 webkit 的简单性。

下面是一个带有 AJAX 请求的 scrapy的简单示例。

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

这就是我要找的所有信息。

从现在开始,我必须在 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 文件。

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

Reference: http://snipplr.com/view/66998/

另一种解决方案是实现下载处理程序或下载处理程序中间件。(请参阅 脾气暴躁的医生了解更多关于下载中间件的信息)下面是一个使用 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 函数中发出一个全新的请求——这是对相同内容的两个请求。

我使用 Selenium 和 Firefox web 驱动程序处理 ajax 请求。如果您需要爬虫程序作为守护进程,它并不是那么快,但是比任何手动解决方案都要好得多。

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

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

有一个工作示例 给你,它看起来像这样:

# 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 被称为“ scraper”。如果你把上面提到的代码放在一个名为 handlers.py 的文件中,放在“ scraper”文件夹的根目录下,那么你就可以把它添加到 setings.py:

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

就是这样,JS 解析了 DOM,带有 Scrapy 缓存、重试等功能。

如何使用 Scrapy 来刮取这些动态数据,以便我可以使用 它?

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

请查看 Scrapy 团队的博客文章 。例子废料 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)

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

有两种方法可以清除这类网站。

  1. 您可以使用 splash来呈现 Javascript 代码,然后解析呈现的 HTML。 你可以在这里找到文档和项目 < a href = “ https://github.com/Scrapy-plugins/Scrapy-splash”rel = “ nofollow norefrer”> Scrapy splash,git

  2. 如前所述,通过监视 network calls,是的,您可以找到获取数据的 API 调用,并且 Scrapy Spider 中的 mock 调用可以帮助您获得所需的数据。

从外部 URL (API)生成的数据以 POST 方法调用 HTML 响应。

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

在2022年还有一些更现代的选择,我认为应该提到,我想列出一些利弊的方法讨论在更流行的答案这个问题。

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

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

    • 缺点: 不幸的是,这只能在一小部分动态网站上使用,而且经常有安全措施使得这种策略很难使用。

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

    • 优点: 易于实现,并且可以集成到杂乱的工作流中。此外,还有大量的示例,如果您使用第三方扩展(如 scrapy-selenium) ,则需要的配置非常少

    • 缺点: 太慢了!Scrapy 的一个关键特性是它的异步工作流,使得在几秒钟内抓取几十甚至几百个页面变得非常容易。使用硒可以显著减少这种情况。

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

刮擦-飞溅 :

  • Scrapy 插件集成了 飞溅,一个由 scrapy 开发者创建和维护的 javascript 渲染服务,并将其集成到 Scrapy 工作流中。这个插件可以从带有 pip3 install scrapy-splash的 pypi 安装,而 splash 需要在它自己的进程中运行,并且最容易从 docker 容器运行。

斯卡普里-剧作家 :

  • Playwright 是一个浏览器自动化工具,类似于 selenium,但是没有因为使用硒而导致的速度严重下降。剧作家没有问题适应异步 Scrapy 工作流,使发送请求就像使用 Scrapy 一样快。它也比硒更容易安装和集成。scrapy-playwright插件也是由 Scrapy 的开发者维护的,并且在通过 pypi 安装 pip3 install scrapy-playwright之后,就像在终端中运行 playwright install一样简单。

更多的细节和例子可以在每个插件的 github 页面 https://github.com/scrapy-plugins/scrapy-playwrighthttps://github.com/scrapy-plugins/scrapy-splash中找到。

另外,根据我的经验,这两个项目在 linux 环境中都能更好地工作。对于 windows 用户,我建议使用它与 Linux 的 Windows 子系统(wsl)