Nokogiri 返回空数组

Nokogiri returning empty array

我正在抓取屏幕 http://www.weather.com/weather/hourbyhour/l/INXX0202:1:IN

我尝试选择同时使用 CSS 和 XPath 来获取网站中 table 的降水预报部分。

它们都不能在我的程序中工作,因为它们 return 空数组,但是,它们都可以在 Chrome 开发工具中工作(检查元素 -> 控制台 -> $$ for CSS, Xpath 的 $x).

为什么会这样?它与名称空间有关吗?

require 'open-uri'
require 'nokogiri'
foo = Nokogiri::HTML(open("http://www.weather.com/weather/hourbyhour/l/INXX0202:1:IN"))
foo.remove_namespaces!
p foo.xpath("//section[@data-ng-class]/p[@class='precip weather-cell ng-isolate-scope']/span[@data-ng-if]") # returns []
p foo.css("section[data-ng-class] p[class='precip weather-cell ng-isolate-scope'] span[data-ng-if]")  # returns []

这是我试图从中获取数据的 a screenshot of the website。我要的是"Precip"标题下的数字(例如:图中的85,100,100,95,80,70,45,40)。

我将页面的 HTML 复制到一个本地 HTML 文件中,让我的程序访问那个 file.The 程序然后给了我我需要的输出,但是当我有相同的输出时程序使用 OpenUri 访问网站,它 returned 一个空数组:

require 'open-uri'
require 'nokogiri'
foo = open("http://www.weather.com/weather/hourbyhour/l/INXX0202:1:IN")
nokogirifoo = Nokogiri::HTML(foo)
p nokogirifoo.xpath("//section[@data-ng-class]/p[@class='precip weather-cell ng-isolate-scope']/span[@data-ng-if]") # => empty array

bar = File.open('weather.html') # weather.html is just the html code of the page copied into a local file
nokogiribar = Nokogiri::HTML(bar)
p nokogiribar.xpath("//section[@data-ng-class]/p[@class='precip weather-cell ng-isolate-scope']/span[@data-ng-if]").text # => "85%100%100%95%80%70%45%40%" (this is what I need)

这是 HTML 的片段(显示的部分嵌套在网站的多个标签中):

 <section class="wxcard-hourly summary-view ng-isolate-scope last" data-ng-class="{'last': $last}" data-wxcard-hourly="hour" data-wxcard-hourly-methods="hourlyScope" data-hours-index="hoursDataIndex" data-show-wx-labels="false" data-details-view="false">
    <div class="heading weather-cell" data-ng-switch="dataMethods.checkTime(data.getForecastLocalDate())">
        <h2>

      <span class="wx-dsxdate ng-binding ng-scope" ng-bind-template=" 9:30 am" data-dsxdate="" data-ng-switch-when="min" data-datetime="data.getForecastLocalDate()" data-timezone="locTz" data-format="'h:mm a'"> 9:30 am</span>
        </h2>
    <span class="sub-heading wx-hourly-date wx-dsxdate ng-binding ng-scope" ng-bind-template=" Fri, Nov 20" data-dsxdate="" data-datetime="data.getForecastLocalDate()" data-timezone="locTz" data-format="'EEE, MMM d'"> Fri, Nov 20</span>
    </div>
    <p class="hi-temp temp-1 weather-cell ng-isolate-scope" data-wx-temperature="data.getTemp()" data-show-temp-unit="hoursIndex === 0"> <span data-ng-if="hasValue()" data-ng-bind="temp" class="ng-binding ng-scope">28</span><sup data-ng-if="hasValue()" class="deg ng-scope">°</sup><sup class="temp-unit ng-binding ng-scope" data-ng-if="showTempUnit" data-ng-bind="tempUnit()">C</sup>
</p>
    <p class="feels-like temp-2 weather-cell ng-isolate-scope" data-wx-temperature="data.getFeelsLike()" data-temp-prefix="Feels"><span ng-if="tempPrefix" class="temp-prefix ng-binding ng-scope" data-ng-bind="tempPrefix">Feels</span><span data-ng-if="hasValue()" data-ng-bind="temp" class="ng-binding ng-scope">34</span><sup data-ng-if="hasValue()" class="deg ng-scope">°</sup>
</p>
    <div class="weather-cell">
        <h3 class="weather-phrase">
            <div class="weather-icon ng-isolate-scope wx-weather-icon" data-wxicon="" data-sky-code="data.getSkyCode()"><div class="svg-icon"><img src="/sites/all/modules/custom/angularmods/app/shared/wxicon/svgz/thunderstorm.svgz?1" aria-hidden="true" alt="thunderstorm"></div></div>

            <span class="phrase ng-binding" data-ng-bind-template="Thunderstorms">Thunderstorms</span>
        </h3>
    </div>
    <!-- The Next Line Is What I Need-->
    <p class="precip weather-cell ng-isolate-scope" data-wx-precip="dataMethods.roundedValue(data.getChanceOfPrecipDay())" data-wx-precip-type="data.getPrecipType()" data-wx-precip-sky-code="data.getSkyCode()"><span aria-hidden="true" class="wx-iconfont-global wx-icon-precip-rain-1"></span><span data-ng-if="!wxPrecipIconOnly" class="precip-val ng-binding ng-scope" data-ng-bind="chanceOfPrecip() | safeDisplay">85%</span></p>

    <p class="humidity-wrapper weather-cell">
      <span data-ng-bind-template="85%" class="humidity ng-binding ng-isolate-scope" data-wx-percentage="data.getHumidity()">85%</span>
    </p>

    <p class="wind-conditions weather-cell">
        <span class="wx-wind ng-binding ng-isolate-scope" data-ng-bind-template="ESE 9 km/h" data-wx-wind-direction="data.getWindDirectionText()" data-wx-wind-speed="data.getWindSpeed()">ESE 9 km/h</span>
    </p>
</section>

问题是您正在使用浏览器查看页面,该页面除了实现 HTML 解析器外,还有一个嵌入式 JavaScript 解释器。浏览器查找并作用于任何 JavaScript <script> 标记,在为用户呈现页面之前加载和调整元素。这就是您想要的页面中发生的事情。解析器,如 Nokogiri,是 NOT 浏览器,并且不关心嵌入式脚本,因为在 HTML 中,脚本只是特定标签内的文本,并且,作为结果,您想要的辅助 HTML 永远不会被检索到。

您说您将 HTML 保存到一个文件中,但是,您没有说 如何 保存它。我猜,因为保存的 HTML 包含您想要的信息,所以它是使用浏览器保存的。

在处理网页时,第一步 是确定页面是使用动态 HTML and/or JavaScript 还是静态 HTML。在浏览器中关闭 JavaScript,然后加载 URL。或者,您可以从命令行使用 wgetcurl 来检索页面并使用编辑器查看它。在任何一种情况下,您是否看到了您想要的内容?如果是这样,那么您很有可能在检索到它后使用像 Nokogiri 这样的解析器来获取它。如果你不这样做,那么你必须使用可以解释 JavaScript 的东西,处理加载的信息,然后将它传递给解析器。

像 PhantomJS 和 Watir 这样的工具可以帮助你,或者,找到一个天气服务,允许你使用 API 来检索数据而不需要抓取,因为抓取总是非常脆弱。

也可以弄清楚 URL JavaScript 正在使用什么来检索数据,然后请求辅助资源并解析它。它 可能 是 HTML,或者它可能 JSON 包含随后由 JavaScript 和整个 table 处理的数据然后即时构建。

Stack Overflow 上有许多问题和答案讨论如何执行上述所有操作。

总而言之,一旦获得所需的 HTML,就可以轻松减少这些值所需的 CSS 选择器。每个值都包含在一个 <style> 标签中,该标签有一个 class,因此请使用 class 来查找该值。

require 'nokogiri'
doc = Nokogiri::HTML(<<EOT)

    <section class="wxcard-hourly summary-view ng-isolate-scope last" data-ng-class="{'last': $last}" data-wxcard-hourly="hour" data-wxcard-hourly-methods="hourlyScope" data-hours-index="hoursDataIndex" data-show-wx-labels="false" data-details-view="false">
        <div class="heading weather-cell" data-ng-switch="dataMethods.checkTime(data.getForecastLocalDate())">
            <h2>

          <span class="wx-dsxdate ng-binding ng-scope" ng-bind-template=" 9:30 am" data-dsxdate="" data-ng-switch-when="min" data-datetime="data.getForecastLocalDate()" data-timezone="locTz" data-format="'h:mm a'"> 9:30 am</span>
            </h2>
        <span class="sub-heading wx-hourly-date wx-dsxdate ng-binding ng-scope" ng-bind-template=" Fri, Nov 20" data-dsxdate="" data-datetime="data.getForecastLocalDate()" data-timezone="locTz" data-format="'EEE, MMM d'"> Fri, Nov 20</span>
        </div>
        <p class="hi-temp temp-1 weather-cell ng-isolate-scope" data-wx-temperature="data.getTemp()" data-show-temp-unit="hoursIndex === 0"> <span data-ng-if="hasValue()" data-ng-bind="temp" class="ng-binding ng-scope">28</span><sup data-ng-if="hasValue()" class="deg ng-scope">°</sup><sup class="temp-unit ng-binding ng-scope" data-ng-if="showTempUnit" data-ng-bind="tempUnit()">C</sup>
    </p>
        <p class="feels-like temp-2 weather-cell ng-isolate-scope" data-wx-temperature="data.getFeelsLike()" data-temp-prefix="Feels"><span ng-if="tempPrefix" class="temp-prefix ng-binding ng-scope" data-ng-bind="tempPrefix">Feels</span><span data-ng-if="hasValue()" data-ng-bind="temp" class="ng-binding ng-scope">34</span><sup data-ng-if="hasValue()" class="deg ng-scope">°</sup>
    </p>
        <div class="weather-cell">
            <h3 class="weather-phrase">
                <div class="weather-icon ng-isolate-scope wx-weather-icon" data-wxicon="" data-sky-code="data.getSkyCode()"><div class="svg-icon"><img src="/sites/all/modules/custom/angularmods/app/shared/wxicon/svgz/thunderstorm.svgz?1" aria-hidden="true" alt="thunderstorm"></div></div>

                <span class="phrase ng-binding" data-ng-bind-template="Thunderstorms">Thunderstorms</span>
            </h3>
        </div>
        <!-- The Next Line Is What I Need-->
        <p class="precip weather-cell ng-isolate-scope" data-wx-precip="dataMethods.roundedValue(data.getChanceOfPrecipDay())" data-wx-precip-type="data.getPrecipType()" data-wx-precip-sky-code="data.getSkyCode()"><span aria-hidden="true" class="wx-iconfont-global wx-icon-precip-rain-1"></span><span data-ng-if="!wxPrecipIconOnly" class="precip-val ng-binding ng-scope" data-ng-bind="chanceOfPrecip() | safeDisplay">85%</span></p>

        <p class="humidity-wrapper weather-cell">
          <span data-ng-bind-template="85%" class="humidity ng-binding ng-isolate-scope" data-wx-percentage="data.getHumidity()">85%</span>
        </p>

        <p class="wind-conditions weather-cell">
            <span class="wx-wind ng-binding ng-isolate-scope" data-ng-bind-template="ESE 9 km/h" data-wx-wind-direction="data.getWindDirectionText()" data-wx-wind-speed="data.getWindSpeed()">ESE 9 km/h</span>
        </p>
    </section>
EOT

从简单的搜索开始:

doc.at('.precip-val').text # => "85%"

at 找到第一个匹配的节点并 returns 它。 text 检索其文本节点。

你想要多个节点 class,所以这样的事情应该有所帮助:

doc.search('.precip-val').map(&:text) # => ["85%"]

search 找到所有匹配的节点和 returns 一个 NodeSet,它就像一个数组,可以使用 map.

进行迭代

他们不太可能将 .precip-val 用于非沉淀标签包装值,但是,如果他们这样做了,请尝试:

doc.search('span.precip-val').map(&:text)

看看你得到了什么。