Ruby爬虫提取BBC GoodFood食谱准备时间的问题求助
精准提取BBC Good Food食谱卡片中的准备时间
我正在构建一个Ruby爬虫解析程序,目标网站为https://www.bbcgoodfood.com/search?q=banana。该网站的食谱卡片中,准备时间与难度信息使用相同类名且无ID,导致提取时会同时获取到无关内容。我仅需提取包含“mins”的准备时间文本,但当前代码会同时获取到难度(如Easy、Hard等),需要解决如何精准提取准备时间的问题。
相关HTML片段
<article class="card text-align-left card--horizontal card--inline card--with-borders" data-item-id="233531" data-item-name="Banana bread" data-variant="horizontal" data-index="1" data-component="Card" data-item-type="recipe" data-subscription-experience="default"> <div class="card__section card__media"> <div class="d-block p-relative"> <div class="p-absolute card__media-buttons pa-xs"></div> </div> <a class="link d-block p-relative card__image-container" href="/recipes/brilliant-banana-loaf" data-component="Link" aria-label="View Banana bread"> <div class="card__overlay-icon p-absolute d-grid justify-content-center align-content-center card__overlay-icon--horizontal"><i class="icon" style="width:24px;min-width:24px;height:24px;min-height:24px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:rgba(255, 255, 255, 1);fill:rgba(255, 255, 255, 1)"><use xlink:href="/static/icons/base/sprite-maps/media-f9e9817ead29e7dce81069945ff5bbe8.svg#play"></use></svg></i></div> <div class="image chromatic-ignore card__img image--fluid image--scaled-up"> <div class="image__container"> <picture class="image__picture" width="93" height="84.51190476190476"> <source type="image/webp" sizes="93px" srcset="https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&webp=true&resize=93,85 93w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&webp=true&resize=186,169 186w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&webp=true&resize=279,254 279w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&webp=true&resize=372,338 372w"> <source sizes="93px" srcset="https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&resize=93,85 93w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&resize=186,169 186w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&resize=279,254 279w,https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&resize=372,338 372w"><img class="image__img" src="https://images.immediate.co.uk/production/volatile/sites/30/2020/06/recipe-image-legacy-id-1273522_8-a6b9246.jpg?quality=90&resize=93,85" alt="Sliced banana bread topped with drizzle and banana chips on a serving plate" data-item-name="Sliced banana bread topped with drizzle and banana chips on a serving plate" title="Banana bread" style="aspect-ratio:1 / 0.9087301587301587;object-fit:cover" loading="lazy" width="93" height="84.51190476190476"></picture> </div> </div> </a> </div> <div class="card__section card__content"><a class="link d-block" href="/recipes/brilliant-banana-loaf" data-component="Link"><h2 class="heading-4" style="color:inherit">Banana bread</h2></a> <div class="card__rating rating"> <div class="rating__values rating__values--no-rate" aria-label="rating"> <div class="rating__stars d-flex"><i class="rating__icon d-inline-flex icon" style="width:15px;min-width:15px;height:15px;min-height:15px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/stars-f9e9817ead29e7dce81069945ff5bbe8.svg#star-fill"></use></svg></i> <i class="rating__icon d-inline-flex icon" style="width:15px;min-width:15px;height:15px;min-height:15px;animation-duration:1000ms"> <svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"> <use xlink:href="/static/icons/base/sprite-maps/stars-f9e9817ead29e7dce81069945ff5bbe8.svg#star-fill"></use> </svg> </i><i class="rating__icon d-inline-flex icon" style="width:15px;min-width:15px;height:15px;min-height:15px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/stars-f9e9817ead29e7dce81069945ff5bbe8.svg#star-fill"></use></svg></i> <i class="rating__icon d-inline-flex icon" style="width:15px;min-width:15px;height:15px;min-height:15px;animation-duration:1000ms"> <svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"> <use xlink:href="/static/icons/base/sprite-maps/stars-f9e9817ead29e7dce81069945ff5bbe8.svg#star-fill"></use> </svg> </i><i class="rating__icon d-inline-flex icon" style="width:15px;min-width:15px;height:15px;min-height:15px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/stars-f9e9817ead29e7dce81069945ff5bbe8.svg#star-fill"></use></svg></i></div> <span class="sr-only">A star rating of 4.5 out of 5.</span><span class="rating__count-text body-copy-small">1883 ratings</span></div> </div> </div> <div class="card__section card__footer"> <ul style="height:22px" class="terms-icons-list d-flex card__footer-item terms-icons-list--single-row list list--horizontal"> <li class="d-flex pb-xs pr-md list-item"> <div class="icon-stack mr-xxs icon-stack--vertical-align" style="height:22px;width:22px"> <div class="icon-stack__icon" style="z-index:2"><i class="card__term-icon icon" style="width:22px;min-width:22px;height:22px;min-height:22px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/core-f9e9817ead29e7dce81069945ff5bbe8.svg#clock-hands"></use></svg></i></div> <div class="icon-stack__icon" style="z-index:1"><i class="card__term-icon-base icon" style="width:22px;min-width:22px;height:22px;min-height:22px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/shapes-f9e9817ead29e7dce81069945ff5bbe8.svg#circle"></use></svg></i></div> </div><span class="terms-icons-list__text d-flex align-items-center" style="height:22px">1 hr 5 mins</span></li> <li class="d-flex pb-xs list-item"> <div class="icon-stack mr-xxs icon-stack--vertical-align" style="height:22px;width:22px"> <div class="icon-stack__icon" style="z-index:2"><i class="card__term-icon icon" style="width:14px;min-width:14px;height:14px;min-height:14px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/food-f9e9817ead29e7dce81069945ff5bbe8.svg#chefs-hat"></use></svg></i></div> <div class="icon-stack__icon" style="z-index:1"><i class="card__term-icon-base icon" style="width:22px;min-width:22px;height:22px;min-height:22px;animation-duration:1000ms"><svg class="icon__svg" aria-hidden="true" focusable="false" style="color:inherit;fill:inherit"><use xlink:href="/static/icons/base/sprite-maps/shapes-f9e9817ead29e7dce81069945ff5bbe8.svg#circle"></use></svg></i></div> </div><span class="terms-icons-list__text d-flex align-items-center" style="height:22px">Easy</span></li> </ul> </div></article>
当前Ruby代码
require 'nokogiri' require 'open-uri' require_relative "recipe" class Scraper def initialize(ingredient) @ingredient = ingredient end def call html = URI.open("https://www.bbcgoodfood.com/search?q=#{@ingredient}").read doc = Nokogiri::HTML(html, nil, "utf-8") results = [] doc.search("article.card.text-align-left.card--horizontal.card--inline.card--with-borders").first(1).each do |element| name = element.search("h2.heading-4").text.strip description = element.search(".region-clamper.line-clamper__content").text.strip prep_time = element.search(".terms-icons-list__text.d-flex.align-items-center").children.text.strip.split("Easy").first #to be continued... end results end end
解决方案
这里提供三种可靠的提取方式,根据网站结构稳定性选择:
方法1:通过关联图标定位(最精准)
观察HTML结构,准备时间对应的li里包含时钟图标(#clock-hands),可以利用这个特征精准定位:
prep_time = element.search('.list-item:has(.icon__svg[href*="#clock-hands"]) .terms-icons-list__text').text.strip
方法2:筛选包含"mins"的文本
直接获取所有目标元素后,筛选文本中包含"mins"的项:
prep_time = element.search('.terms-icons-list__text').find { |node| node.text.include?('mins') }&.text&.strip
方法3:按索引取第一个元素
如果网站中准备时间始终是列表的第一个项,也可以直接取第一个元素:
prep_time = element.search('.terms-icons-list__text').first.text.strip
修改后的完整代码
require 'nokogiri' require 'open-uri' require_relative "recipe" class Scraper def initialize(ingredient) @ingredient = ingredient end def call html = URI.open("https://www.bbcgoodfood.com/search?q=#{@ingredient}").read doc = Nokogiri::HTML(html, nil, "utf-8") results = [] doc.search("article.card.text-align-left.card--horizontal.card--inline.card--with-borders").first(1).each do |element| name = element.search("h2.heading-4").text.strip description = element.search(".region-clamper.line-clamper__content").text.strip # 使用方法1的精准定位 prep_time = element.search('.list-item:has(.icon__svg[href*="#clock-hands"]) .terms-icons-list__text').text.strip results << { name: name, description: description, prep_time: prep_time } end results end end
内容的提问来源于stack exchange,提问作者sas
相关产品推荐
相关产品推荐

