【问题标题】:What is the best practice for writing maintainable web scrapers?编写可维护的网络爬虫的最佳实践是什么?
【发布时间】:2014-02-10 17:38:48
【问题描述】:

我需要实现一些爬虫来抓取一些网页(因为该网站没有开放的 API),提取信息并保存到数据库。我目前正在使用漂亮的汤来编写这样的代码:

discount_price_text = soup.select("#detail-main del.originPrice")[0].string;
discount_price = float(re.findall('[\d\.]+', discount_price_text)[0]);

我猜这样的代码很容易在网页改变时变得无效,即使是轻微的改变。 除了编写回归测试以定期运行以捕获故障之外,我应该如何编写不易受这些变化影响的爬虫?

特别是,即使原始 xpath/css 选择器不再有效,是否有任何现有的“智能抓取工具”可以进行“尽力猜测”?

【问题讨论】:

标签: python web web-scraping beautifulsoup


【解决方案1】:

页面有可能发生如此巨大的变化,以至于构建一个非常“智能”的刮板可能非常困难;如果可能的话,即使使用机器学习等花哨的技术,刮板也会有些不可预测。很难制作一个兼具可信赖性和自动化灵活性的爬虫。

可维护性在某种程度上是一种艺术形式,以如何定义和使用选择器为中心。

过去我推出了自己的“两阶段”选择器:

  1. (find) 第一阶段非常不灵活,它检查页面结构是否朝向所需元素。如果第一阶段失败,则会引发某种“页面结构已更改”错误。

  2. (检索)然后第二阶段有点灵活,从页面上的所需元素中提取数据。

这允许爬虫通过某种程度的自动检测将自己与剧烈的页面更改隔离开来,同时仍保持一定程度的可信赖的灵活性。

我经常使用 xpath 选择器,这真的很令人惊讶,通过一些练习,你可以在使用一个好的选择器的同时保持非常准确的灵活性。我确信css选择器是相似的。页面设计越语义化和“扁平化”,这就越容易。

需要回答的几个重要问题是:

  1. 您希望页面上有什么变化?

  2. 您希望页面上的哪些内容保持不变?

回答这些问题时,您的选择器越准确,您的选择器就越好。

最后,您可以选择要承担多大的风险,选择器的可信度如何,在页面上查找和检索数据时,您如何制作它们会产生很大的不同;理想情况下,最好从 web-api 获取数据,希望更多来源开始提供。


编辑:小例子

使用您的场景,您想要的元素位于.content > .deal > .tag > .price,一般.content .price 选择器在页面更改方面非常“灵活”;但是,如果出现误报元素,我们可能希望避免从这个新元素中提取。

使用两阶段选择器,我们可以指定一个不太通用、更不灵活的第一阶段,例如.content > .deal,然后是第二个更通用的阶段,例如.price,使用查询relative em> 到第一个结果。

那么为什么不直接使用 .content > .deal .price 这样的选择器呢?

就我的使用而言,我希望能够检测大页面更改而无需单独运行额外的回归测试。我意识到我可以编写第一阶段来包含重要的页面结构元素,而不是一个大的选择器。如果结构元素不再存在,第一阶段将失败(或报告)。然后我可以编写第二阶段,以更优雅地检索与第一阶段的结果相关的数据。

我不应该说这是“最佳”做法,但效果很好。

【讨论】:

  • 谢谢!我完全同意选择健壮的选择器是一种艺术形式。我实际上正在考虑编写多个级别的选择器,从非常具体的(如 .content>.deal>.tag>.price)到非常一般的(.content .price),如果当前级别失败,则回退到下一个级别,但我不确定这是一个好主意,因为它可能会引入误报。有时失败总比得到错误的数据好......在你的两阶段模型中,当你说检索可以“有点灵活”时,你是什么意思?当我找到元素时,我只需要提取数据,对吧?
  • 我所说的“有点灵活”是指灵活相对由第一阶段选择器检索到的页面子部分。我在上面添加了一个小例子。
【解决方案2】:

与 Python 完全无关且不具备自动灵活性,但我认为我的 Xidel scraper 的模板具有最好的可维护性。

你可以这样写:

<div id="detail-main"> 
   <del class="originPrice">
     {extract(., "[0-9.]+")} 
   </del>
</div>

模板的每个元素都与网页上的元素进行匹配,如果它们相同,则评估{} 中的表达式。

页面上的其他元素将被忽略,因此如果您在包含的元素和已删除的元素之间找到适当的平衡,则模板将不受所有细微更改的影响。 另一方面,重大更改将触发匹配失败,这比 xpath/css 只会返回一个空集要好得多。然后您可以在模板中仅更改已更改的元素,在理想情况下,您可以直接将旧/更改页面之间的差异应用于模板。在任何情况下,您都不需要搜索受影响的选择器或针对单个更改更新多个选择器,因为模板可以包含单个页面的所有查询。

【讨论】:

    【解决方案3】:

    编辑: 糟糕,我现在看到您已经在使用 CSS 选择器了。我认为他们为您的问题提供了最佳答案。所以不,我认为没有更好的方法。

    但是,有时您可能会发现没有结构更容易识别数据。例如,如果你想抓取价格,你可以做一个匹配价格的正则表达式搜索(\$\s+[0-9.]+),而不是依赖于结构。


    就我个人而言,我尝试过的各种开箱即用的网络爬虫库都有一些不足之处(mechanizeScrapy 等)。

    我通常自己滚动,使用:

    cssselect 允许您使用 CSS 选择器(就像 jQuery 一样)来查找特定的 div、表格等。这被证明是非常宝贵的。

    从 SO 主页获取第一个问题的示例代码:

    import urllib2
    import urlparse
    import cookielib
    
    from lxml import etree
    from lxml.cssselect import CSSSelector
    
    post_data = None
    url = 'http://www.stackoverflow.com'
    cookie_jar = cookielib.CookieJar()
    http_opener = urllib2.build_opener(
        urllib2.HTTPCookieProcessor(cookie_jar),
        urllib2.HTTPSHandler(debuglevel=0),
    )
    http_opener.addheaders = [
        ('User-Agent', 'Mozilla/5.0 (X11; Linux i686; rv:25.0) Gecko/20100101 Firefox/25.0'),
        ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
    ]
    fp = http_opener.open(url, post_data)
    parser = etree.HTMLParser()
    doc = etree.parse(fp, parser)
    
    elem = CSSSelector('#question-mini-list > div:first-child > div.summary h3 a')(doc)
    print elem[0].text
    

    当然,你不需要 cookiejar,也不需要用户代理来模拟 FireFox,但是我发现在抓取网站时我经常需要它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-14
      • 1970-01-01
      • 2015-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-26
      • 1970-01-01
      相关资源
      最近更新 更多