编写可维护的Web scraper的最佳实践是什么?

时间:2014-01-21 08:31:28

标签: python web web-scraping beautifulsoup

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

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

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

特别是,即使原始xpath / css选择器不再有效,是否有任何现有的“智能剪贴板”可以“尽力猜测”?

3 个答案:

答案 0 :(得分:8)

页面有可能发生如此剧烈的变化,因此构建一个非常“智能”的刮刀可能会非常困难;如果可能的话,即使使用机器学习等技术,刮刀也会有些不可预测。制作具有可信度和自动化灵活性的刮刀很难。

可维护性在某种程度上是围绕如何定义和使用选择器的艺术形式。

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

  1. (find)第一阶段非常不灵活,并将页面结构检查为所需元素。如果第一阶段失败,则会抛出某种“页面结构已更改”错误。

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

  3. 这使得刮刀可以通过一定程度的自动检测将自身与激烈的页面更改隔离开来,同时仍保持一定的可靠灵活性。

    我经常使用xpath选择器,它确实令人惊讶,通过一些练习,你可以灵活地使用一个好的选择器,同时仍然非常准确。我确信css选择器是相似的。页面设计的语义和“平面”越多越容易。

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

    1. 您希望在页面上更改什么?

    2. 您希望在页面上保持不变?

    3. 在回答这些问题时,您的选择器越精确越好。

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


      编辑:小例子

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

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

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

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

      我不应该说这是一种“最佳”做法,但它运作良好。

答案 1 :(得分:2)

编辑:哎呀,我现在看到你已经在使用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,但我发现在抓取网站时我经常需要这个。

答案 2 :(得分:2)

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

您可以这样写:

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

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

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