我需要实现一些抓取工具来抓取一些网页(因为该网站没有开放的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选择器不再有效,是否有任何现有的“智能剪贴板”可以“尽力猜测”?
页面有可能发生如此剧烈的变化,因此构建一个非常“智能”的刮刀可能会非常困难;如果可能的话,即使使用机器学习等技术,刮刀也会有些难以预料。制造具有可靠性和自动化灵活性的刮刀很难。
可维护性在某种程度上是以如何定义和使用选择器为中心的艺术形式。
在过去,我已经推出了自己的“两阶段”选择器:
(查找)第一阶段非常不灵活,并检查页面的结构朝向所需的元素。如果第一阶段失败,则会抛出某种“页面结构已更改”错误。
(检索)然后第二阶段有些灵活,并从页面上的所需元素中提取数据。
这使得刮刀可以通过一定程度的自动检测将自身与剧烈的页面更改隔离开来,同时仍然保持一定程度的可靠灵活性。
我经常使用xpath选择器,它真的很令人惊讶,通过一些练习,你可以灵活地使用一个好的选择器,同时仍然非常准确。我确信css选择器是相似的。页面设计的语义和“平面”越多越容易。
要回答的几个重要问题是:
您希望在页面上更改什么?
您希望在页面上保持不变?
在回答这些问题时,您的选择器越精确越好。
最后,您可以选择要承担多少风险,选择器的可信度,在页面上查找和检索数据时,如何制作它们会产生很大的不同;理想情况下,最好从web-api获取数据,希望更多的资源可以开始提供。
编辑:小例子
使用您的场景,您想要的元素所在的位置 .content > .deal > .tag > .price
, 一般 .content .price
选择器对于页面更改非常“灵活”;但是,如果出现假阳性因素,我们可能希望避免从这个新元素中提取。
使用两阶段选择器,我们可以指定一个不太通用,更不灵活的第一阶段 .content > .deal
,然后是第二个更普遍的阶段 .price
使用查询检索最终元素 相对的 到第一个结果。
那么为什么不使用像这样的选择器呢? .content > .deal .price
?
对于我的使用,我希望能够检测大页面更改,而无需单独运行额外的回归测试。我意识到,不是一个大的选择器,我可以编写第一个阶段来包含重要的页面结构元素。如果结构元素不再存在,则第一阶段将失败(或报告)。然后我可以编写第二个阶段,以更优雅地检索与第一阶段结果相关的数据。
我不应该说这是一种“最好”的做法,但它运作良好。
编辑: 哎呀,我现在看到你已经在使用CSS选择器了。我认为他们为您的问题提供了最佳答案。所以不,我不认为有更好的方法。
但是,有时您可能会发现在没有结构的情况下识别数据更容易。例如,如果您想刮价,您可以进行与价格匹配的正则表达式搜索(\$\s+[0-9.]+
),而不是依赖于结构。
就个人而言,我尝试过的开箱即用的网页编写图书馆都留下了一些需要的东西(机械化, Scrapy, 和别的)。
我经常自己动手,使用:
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,但我发现在抓取网站时我经常需要这个。
完全与Python无关,而且不是自动灵活的,但我认为我的模板 Xidel刮刀 拥有最好的维护能力。
你会写它像:
<div id="detail-main">
<del class="originPrice">
{extract(., "[0-9.]+")}
</del>
</div>
模板的每个元素都与网页上的元素进行匹配,如果它们相同,则表示内部的表达式 {}
被评估。
页面上的其他元素将被忽略,因此如果您找到包含元素和已删除元素的正确平衡,则模板将不受所有微小更改的影响。
另一方面,重大更改将触发匹配失败,比xpath / css更好,后者将返回空集。然后,您可以在模板中更改已更改的元素,在理想情况下,您可以直接将旧/已更改页面之间的差异应用于模板。在任何情况下,您都不需要搜索哪个选择器受到影响或更新多个选择器以进行单个更改,因为模板可以包含单个页面的所有查询。