三种网页抓取方法
1.正则表达式
(这个真的很难懂,之后会单独写篇笔记)
2.Beautiful Soup
该模块可以解析网页,并提供定位内容的便捷接口。
3.Lxml
lxml是基于libxml2这一lxml解析库的python封装。该模块使用C语言编写,解析速度比Beautiful Soup更快。
lxml也可以正确解析属性两侧缺失的引号,并闭合标签,不过该模块没有额外添加<html>和<body>标签。
解析完输入内容之后,进入选择元素的步骤,此时lxml 有几种不同的方法,比如XPath 选择器和类似Beautiful Soup的find()方法。 不过, 我们将会使用 css选择器, 因为它更加简沽, 并且能够在解析动态内容时得以复用。、
性能对比
FIELDS=('area','population','iso','country','capital','continent ', 'tld','currency code ','currency name','phone','postal_code_format','postal_code_regex','languages' ,'neighbours') import re def re_scraper(html): results={} for field in FIELDs: results[field]=re.search('<tr id="place_%s_row">.*?<td class="w2p_fw">(.*?)<td class="w2p_fw">(.*?)</td>' % field,html).groups()[0] return results from bs4 import BeautifulSoup def bs_scraper(html): soup=BeautifulSoup(html,'html.parser') results={} for field in FIELDS: results[field]=soup.find('table').find('tr',id='places_%s_row' %field).find('td',class_='w2p_fw').text return results import lxml.html def lxml_scraper(html): tree=lxml.html.fromstring(html) results={} for field in FIELDS: results[field]=tree.csselect('table>tr#palce_%s_row>td.w2p_fw' %field)[0].text_content() return results
抓取结果:
import time NUM_ITERATIONS=1000 # 测试每次抓取的时间次数 html=download('http://example.webscraping.com/places/view/United-Kingdom-239') for name,scraper in [('Regular expressions',re_scraper),('BeautifulSoup',bs_scraper),('lxml',lxml_scraper)]: #记录开始抓取的时间 for i in range(NUM_ITERATIONS): if scraper==re_scraper: re.purge() result=scraper(html) #检查期待的抓取结果 assert(result['area']=='244,820 square kilometres') #record end time of scrape and output the total end=time.time() print('%s: %.2f seconds' %(name,end-start))
在这段代码中, 每个爬虫都会执行1000 次, 每次执行都会检查抓取结果 是否正确, 然后打印总用时。 这里使用的 download 函数依然是上一章中定 义的那个函数。 请注意, 我们在加粗的代码行中调用了 re.purge ()方法。 默认情况下, 正则表达式模块会缓存搜索结果,为了与其他爬虫的对比更加 公平, 我们需要使用该方法清除缓存。
下面是在电脑中运行该脚本的结果。
$ python performance.py
Regular expressions: 5.50 seconds
BeautifulSoup : 42.84 seconds
Lxml : 7.06 seconds
结论:
抓取方式 | 性能 | 使用难度 | 安装难度 |
正则表达式 | 快 | 困难 | 简单(内置模块) |
Beautiful Soup | 慢 | 简单 | 简单(内置模块) |
Lxml | 快 | 简单 | 相对困难 |
如果你的爬虫瓶颈是下载网页, 而不是抽取数据的话,那么使用较慢的方 法(如 Beautiful Soup) 也不成问题。
如果只需抓取少量数据, 并且想要避免额外依赖的话, 那么正则表达式可能更加适合。
不过,通常情况下, lxml 是 抓取数据的最好选择,这是因为该方法既快速又健壮,而正则表达式和 Beautiful Soup只在某些特定场景下有用。
为链接爬虫抓取回调
前面我们已经了解了如何抓取数据, 接下来我们需要将其集成到上一章的链接爬虫当中。 要想复用这段爬虫代码抓取其他网站, 我们需要添 加一个callback参数处理抓取行为。callback是一个函数,在发生某 个特定事件之后会调用该函数 (在本例中, 会在网页下载完成后调用 )。该抓取 callback 函数包含 url 和 html 两个参数, 并且可以返回一个待爬 取的 URL 列表。下面是其实现代码,可以看出在Python中实现该功能非常简单。
def link_crawler(...,scraper_callback-None): ... links=[] if scrape_callback: links.extend(scrape_callback(url,html)or []) ...
抓取数据并显示出来:
def scrape_callback(url,html): if re.search('/view/',url): tree=lxml.html.fromstring(html) row=[tree.cssselect('table>tr#places_%s_row >td.w2p_fw' %field)[0].text_content() for field in FIELDS] print url,row
扩展功能,把得到的结果保存到csv表格中:
import csv class ScrapeCallback: def __init__(self): self.writer=csv.writer(open('countries.csv','w')) self.fields=('area','population','iso','country','capital','continent','tld','currency_code', 'currency_name','phone','postal_code_format','postal code regex', 'languages','neighbours') self_writer.writerow(self.fields) def __call__(self,url,html): if re.search('/view/',url): tree=lxml.html.fromstring(html) row=[] for field in self.fields: row.append(tree.cssselect('table>tr#places_{}_row >td.w2p_fw'.format(field))[0].text_content()) self.writer.writerow(row)
为了实现该 callback,我们使用了回调类,而不再是回调函数,以便保 持csv 中 writer 属性的状态。csv 的 writer 属性在构造方法中进行了实例化处理,然后在_call_方法中执行了多次写操作。 请注意,_call_是一个特殊方法,在对象作为函数被调用时会调用该方法,这也是链接爬虫中 cache_callback的调用方法。也就是说,scrape_callback (url, html) 和调用 scrape_callback. _call_(url, html)是等价的。
向链接爬虫传入回调的代码写法:
link_crawler('http://example.webscraping.com/ ','/(Index|view)',max_depth=-1,scrape_callback=ScrapeCallback ())