下面我们将介绍三种抓取网页数据的方法,首先是正则表达式,然后是流行的 BeautifulSoup 模块,最后是强大的 lxml 模块。

1. 正则表达式

  如果你对正则表达式还不熟悉,或是需要一些提示时,可以查阅Regular Expression HOWTO 获得完整介绍。

  当我们使用正则表达式抓取国家面积数据时,首先要尝试匹配元素中的内容,如下所示:

>>> import re
>>> import urllib2
>>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
>>> html = urllib2.urlopen(url).read()
>>> re.findall('<td class="w2p_fw">(.*?)</td>', html)
['<img src="/places/static/images/flags/gb.png" />', '244,820 square kilometres', '62,348,447', 'GB', 'United Kingdom', 'London', '<a href="/continent/EU">EU</a>', '.uk', 'GBP', 'Pound', '44', '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA', '^(([A-Z]\\d{2}[A-Z]{2})|([A-Z]\\d{3}[A-Z]{2})|([A-Z]{2}\\d{2}[A-Z]{2})|([A-Z]{2}\\d{3}[A-Z]{2})|([A-Z]\\d[A-Z]\\d[A-Z]{2})|([A-Z]{2}\\d[A-Z]\\d[A-Z]{2})|(GIR0AA))$', 'en-GB,cy-GB,gd', '<div><a href="/iso/IE">IE </a></div>']
>>>

   从上述结果看出,多个国家属性都使用了< td class=”w2p_fw” >标签。要想分离出面积属性,我们可以只选择其中的第二个元素,如下所示:

>>> re.findall('<td class="w2p_fw">(.*?)</td>', html)[1]
'244,820 square kilometres'

   虽然现在可以使用这个方案,但是如果网页发生变化,该方案很可能就会失效。比如表格发生了变化,去除了第二行中的国土面积数据。如果我们只在现在抓取数据,就可以忽略这种未来可能发生的变化。但是,如果我们希望未来还能再次抓取该数据,就需要给出更加健壮的解决方案,从而尽可能避免这种布局变化所带来的影响。想要该正则表达式更加健壮,我们可以将其父元素< tr >也加入进来。由于该元素具有ID属性,所以应该是唯一的。

>>> re.findall('<tr id="places_area__row"><td class="w2p_fl"><label for="places_area" id="places_area__label">Area: </label></td><td class="w2p_fw">(.*?)</td>', html)
['244,820 square kilometres']

  这个迭代版本看起来更好一些,但是网页更新还有很多其他方式,同样可以让该正则表达式无法满足。比如,将双引号变为单引号,< td >标签之间添加多余的空格,或是变更area_label等。下面是尝试支持这些可能性的改进版本。

>>> re.findall('<tr id="places_area__row">.*?<td\s*class=["\']w2p_fw["\']>(.*?)</td>',html)['244,820 square kilometres']

  虽然该正则表达式更容易适应未来变化,但又存在难以构造、可读性差的问题。此外,还有一些微小的布局变化也会使该正则表达式无法满足,比如在< td >标签里添加title属性。 
  从本例中可以看出,正则表达式为我们提供了抓取数据的快捷方式,但是,该方法过于脆弱,容易在网页更新后出现问题。幸好还有一些更好的解决方案,后期将会介绍。

2. Beautiful Soup

  Beautiful Soup是一个非常流行的 Python 模块。该模块可以解析网页,并提供定位内容的便捷接口。如果你还没有安装该模块,可以使用下面的命令安装其最新版本(需要先安装 pip,请自行百度):

pip install beautifulsoup4

  使用 Beautiful Soup 的第一步是将已下载的 HTML 内容解析为 soup 文档。由于大多数网页都不具备良好的 HTML格式,因此 Beautiful Soup 需要对其实际格式进行确定。例如,在下面这个简单网页的列表中,存在属性值两侧引号缺失和标签未闭合的问题。

<ul class=country>
<li>Area
<li>Population
</ul>

  如果 Population 列表项被解析为 Area 列表项的子元素,而不是并列的两个列表项的话,我们在抓取时就会得到错误的结果。下面让我们看一下 Beautiful Soup 是如何处理的。

>>> from bs4 import BeautifulSoup
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
>>> # parse the HTML
>>> soup = BeautifulSoup(broken_html, 'html.parser')
>>> fixed_html = soup.prettify()
>>> print fixed_html
<ul class="country">
<li>
Area
<li>
Population
</li>
</li>
</ul>

  从上面的执行结果中可以看出,Beautiful Soup 能够正确解析缺失的引号并闭合标签。现在可以使用 find() 和 find_all() 方法来定位我们需要的元素了。

>>> ul = soup.find('ul', attrs={'class':'country'})
>>> ul.find('li') # return just the first match
<li>Area<li>Population</li></li>
>>> ul.find_all('li') # return all matches
[<li>Area<li>Population</li></li>, <li>Population</li>]

Note: 由于不同版本的Python内置库的容错能力有所区别,可能处理结果和上述有所不同,具体请参考: https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser。想了解全部方法和参数,可以查阅 Beautiful Soup 的 官方文档

  下面是使用该方法抽取示例国家面积数据的完整代码。

>>> from bs4 import BeautifulSoup
>>> import urllib2
>>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
>>> html = urllib2.urlopen(url).read()
>>> # locate the area row
>>> tr = soup.find(attrs={'id':'places_area__row'})
>>> # locate the area tag
>>> td = tr.find(attrs={'class':'w2p_fw'})
>>> area = td.text # extract the text from this tag
>>> print area
244,820 square kilometres

  这段代码虽然比正则表达式的代码更加复杂,但更容易构造和理解。而且,像多余的空格和标签属性这种布局上的小变化,我们也无需再担心了。

3. Lxml

  Lxml 是基于 libxml2 这一 XML 解析库的 Python 封装。该模块使用 C语言 编写,解析速度比 Beautiful Soup 更快,不过安装过程也更为复杂。最新的安装说明可以参考 http://lxml.de/installation.html .**

  和 Beautiful Soup 一样,使用 lxml 模块的第一步也是将有可能不合法的 HTML 解析为统一格式。下面是使用该模块解析一个不完整 HTML 的例子:

>>> import lxml.html
>>> broken_html = '<ul class=country><li>Area<li>Population</ul>'
>>> # parse the HTML
>>> tree = lxml.html.fromstring(broken_html)
>>> fixed_html = lxml.html.tostring(tree, pretty_print=True)
>>> print fixed_html
<ul class="country">
<li>Area</li>
<li>Population</li>
</ul>

  同样地,lxml 也可以正确解析属性两侧缺失的引号,并闭合标签,不过该模块没有额外添加 < html > 和 < body > 标签。

  解析完输入内容之后,进入选择元素的步骤,此时 lxml 有几种不同的方法,比如 XPath 选择器和类似 Beautiful Soup 的 find() 方法。不过,后续我们将使用 CSS 选择器,因为它更加简洁,并且能够在解析动态内容时得以复用。此外,一些拥有 jQuery 选择器相关经验的读者会对其更加熟悉。

  下面是使用 lxml 的 CSS 选择器抽取面积数据的示例代码:

>>> import urllib2
>>> import lxml.html
>>> url = 'http://example.webscraping.com/view/United-Kingdom-239'
>>> html = urllib2.urlopen(url).read()
>>> tree = lxml.html.fromstring(html)
>>> td = tree.cssselect('tr#places_area__row > td.w2p_fw')[0] # *行代码
>>> area = td.text_content()
>>> print area
244,820 square kilometres

   *行代码首先会找到 ID 为 places_area__row 的表格行元素,然后选择 class 为 w2p_fw 的表格数据子标签。

   CSS 选择器表示选择元素所使用的模式,下面是一些常用的选择器示例:

选择所有标签: *
选择 <a> 标签: a
选择所有 class="link" 的元素: .link
选择 class="link" 的 <a> 标签: a.link
选择 id="home" 的 <a> 标签: a#home
选择父元素为 <a> 标签的所有 <span> 子标签: a > span
选择 <a> 标签内部的所有 <span> 标签: a span
选择 title 属性为"Home"的所有 <a> 标签: a[title=Home]

   W3C 已提出 CSS3 规范,其网址为 https://www.w3.org/TR/2011/REC-css3-selectors-20110929/

  Lxml 已经实现了大部分 CSS3 属性,其不支持的功能可以参见: https://cssselect.readthedocs.io/en/latest/ .

Note: lxml在内部的实现中,实际上是将 CSS 选择器转换为等价的 XPath 选择器。

4. 性能对比

   在以下这段代码中,每个爬虫都会执行 1000 次,每次执行都会检查抓取结果是否正确,然后打印总用时。

# -*- coding: utf-8 -*-

import csv
import time
import urllib2
import re
import timeit
from bs4 import BeautifulSoup
import lxml.html FIELDS = ('area', 'population', 'iso', 'country', 'capital', 'continent', 'tld', 'currency_code', 'currency_name', 'phone', 'postal_code_format', 'postal_code_regex', 'languages', 'neighbours') def regex_scraper(html):
results = {}
for field in FIELDS:
results[field] = re.search('<tr id="places_{}__row">.*?<td class="w2p_fw">(.*?)</td>'.format(field), html).groups()[0]
return results def beautiful_soup_scraper(html):
soup = BeautifulSoup(html, 'html.parser')
results = {}
for field in FIELDS:
results[field] = soup.find('table').find('tr', id='places_{}__row'.format(field)).find('td', class_='w2p_fw').text
return results def lxml_scraper(html):
tree = lxml.html.fromstring(html)
results = {}
for field in FIELDS:
results[field] = tree.cssselect('table > tr#places_{}__row > td.w2p_fw'.format(field))[0].text_content()
return results def main():
times = {}
html = urllib2.urlopen('http://example.webscraping.com/view/United-Kingdom-239').read()
NUM_ITERATIONS = 1000 # number of times to test each scraper
for name, scraper in ('Regular expressions', regex_scraper), ('Beautiful Soup', beautiful_soup_scraper), ('Lxml', lxml_scraper):
times[name] = []
# record start time of scrape
start = time.time()
for i in range(NUM_ITERATIONS):
if scraper == regex_scraper:
# the regular expression module will cache results
# so need to purge this cache for meaningful timings
re.purge() # *行代码
result = scraper(html) # check scraped result is as expected
assert(result['area'] == '244,820 square kilometres')
times[name].append(time.time() - start)
# record end time of scrape and output the total
end = time.time()
print '{}: {:.2f} seconds'.format(name, end - start) writer = csv.writer(open('times.csv', 'w'))
header = sorted(times.keys())
writer.writerow(header)
for row in zip(*[times[scraper] for scraper in header]):
writer.writerow(row) if __name__ == '__main__':
main()

   注意,我们在 *行代码 中调用了 re.purge() 方法。默认情况下,正则表达式会缓存搜索结果,为了公平起见,我们需要使用该方法清除缓存。

下面是我的电脑运行该脚本的结果:

   由于硬件条件的区别,不同电脑的执行结果也会存在一定差异。不过,每种方法之间的相对差异应当是相当的。从结果中可以看出,在抓取我们的示例网页时,Beautiful Soup 比其他两种方法慢了超过 7 倍之多。实际上这一结果是符合预期的,因为 lxml 和正则表达式模块都是 C 语言编写的,而 Beautiful Soup 则是纯 Python 编写的。一个有趣的事实是,lxml 表现的和正则表达式差不多好。由于 lxml 在搜索元素之前,必须将输入解析为内部格式,因此会产生额外的开销。而当抓取同一网页的多个特征时,这种初始化解析产生的开销就会降低,lxml 也就更具竞争力,所以说,lxml 是一个强大的模块。

5. 总结

三种网页抓取方法优缺点:

       抓取方法     性能       使用难度       安装难度
正则表达式 困难 简单(内置模块)
Beautiful Soup 简单 简单(纯Python)
Lxml 简单 相对困难

   如果你的爬虫瓶颈是下载网页,而不是抽取数据的话,那么使用较慢的方法(如 Beautiful Soup)也不成问题。正则表达式在一次性抽取中非常有用,此外还可以避免解析整个网页带来的开销,如果只需抓取少量数据,并且想要避免额外依赖的话,那么正则表达式可能更加适合。不过,通常情况下,lxml 是抓取数据的最好选择,这是因为它不仅速度快,功能也更加丰富,而正则表达式和 Beautiful Soup只在某些特定场景下有用。

转自:https://blog.csdn.net/oscer2016/article/details/70209144

Python爬虫之三种网页抓取方法性能比较的更多相关文章

  1. Python爬虫实战六之抓取爱问知识人问题并保存至数据库

    大家好,本次为大家带来的是抓取爱问知识人的问题并将问题和答案保存到数据库的方法,涉及的内容包括: Urllib的用法及异常处理 Beautiful Soup的简单应用 MySQLdb的基础用法 正则表 ...

  2. Python爬虫实战四之抓取淘宝MM照片

    原文:Python爬虫实战四之抓取淘宝MM照片其实还有好多,大家可以看 Python爬虫学习系列教程 福利啊福利,本次为大家带来的项目是抓取淘宝MM照片并保存起来,大家有没有很激动呢? 本篇目标 1. ...

  3. 一次Python爬虫的修改,抓取淘宝MM照片

    这篇文章是2016-3-2写的,时隔一年了,淘宝的验证机制也有了改变.代码不一定有效,保留着作为一种代码学习. 崔大哥这有篇>>小白爬虫第一弹之抓取妹子图 不失为学python爬虫的绝佳教 ...

  4. Python开发爬虫之动态网页抓取篇:爬取博客评论数据——通过Selenium模拟浏览器抓取

    区别于上篇动态网页抓取,这里介绍另一种方法,即使用浏览器渲染引擎.直接用浏览器在显示网页时解析 HTML.应用 CSS 样式并执行 JavaScript 的语句. 这个方法在爬虫过程中会打开一个浏览器 ...

  5. Python实现简单的网页抓取

    现在开源的网页抓取程序有很多,各种语言应有尽有. 这里分享一下Python从零开始的网页抓取过程 第一步:安装Python 点击下载适合的版本https://www.python.org/ 我这里选择 ...

  6. python爬虫beta版之抓取知乎单页面回答(low 逼版)

    闲着无聊,逛知乎.发现想找点有意思的回答也不容易,就想说要不写个爬虫帮我把点赞数最多的给我搞下来方便阅读,也许还能做做数据分析(意淫中--) 鉴于之前用python写爬虫,帮运营人员抓取过京东的商品品 ...

  7. Python开发爬虫之静态网页抓取篇:爬取“豆瓣电影 Top 250”电影数据

    所谓静态页面是指纯粹的HTML格式的页面,这样的页面在浏览器中展示的内容都在HTML源码中. 目标:爬取豆瓣电影TOP250的所有电影名称,网址为:https://movie.douban.com/t ...

  8. Python爬虫之三种数据解析方式

    一.引入 二.回顾requests实现数据爬取的流程 指定url 基于requests模块发起请求 获取响应对象中的数据 进行持久化存储 其实,在上述流程中还需要较为重要的一步,就是在持久化存储之前需 ...

  9. python爬虫学习:分布式抓取

    前面的文章都是基于在单机操作,正常情况下,一台机器无论配置多么高,线程开得再多,也总会有一个上限,或者说成本过于巨大.因此,本文将提及分布式的爬虫,让爬虫的效率提高得更快. 构建分布式爬虫首先需要有多 ...

随机推荐

  1. Tomcat8.0配置JNDI多数据源

    jndi配置 :此种配置需要在Tomcat的server.xml中和context.xml中配置数据源,在项目中引用. 需要在tomcat下加入数据库连接的jar包,相关包(ojdbc14;c3p0数 ...

  2. BZOJ 4898 [APIO2017] 商旅 | SPFA判负环 分数规划

    BZOJ 4898 [APIO2017] 商旅 | SPFA判负环 分数规划 更清真的题面链接:https://files.cnblogs.com/files/winmt/merchant%28zh_ ...

  3. BZOJ 2527 Meteors | 整体二分

    BZOJ 2527 Meteors 题意 一个圆环上有m个位置,编号为1~m,分别属于n个国家. 有k个时刻,每个时刻都会给圆环上的一个区间中每个位置的值加上一个数. 每个国家有一个目标,问对于每个国 ...

  4. kickstart无人值守安装之实践篇

    1.系统环境准备 涉及的服务有: DHCP服务 TFTP服务 PXE客户端 HTTP服务 [root@ks ~]# cat /etc/redhat-release CentOS release 6.9 ...

  5. 【bzoj3195】 Jxoi2012—奇怪的道路

    http://www.lydsy.com/JudgeOnline/problem.php?id=3195 (题目链接) 题意 一张$n$个点$m$条边的无向图,每个点度数为偶数,一个点只能向标号与它的 ...

  6. Effective C++ 条款08:别让异常逃离析构函数

    1.别让异常逃离析构函数的原因 <Effective C++>第三版中条款08建议不要在析构函数中抛出异常,原因是C++异常机制不能同时处理两个或两个以上的异常.多个异常同时存在的情况下, ...

  7. Luogu P3251 [JLOI2012]时间流逝 期望dp

    题面 题面 题解 期望\(dp\)好题! 今年\(ZJOI\)有讲过这题... 首先因为\(T\)只有\(50\),大力\(dfs\)后发现,可能的状态数最多只有\(20w\)左右,所以我们就可以大力 ...

  8. python【文件操作:读写文件】

    文件读写模式

  9. Django中@login_required用法简介

    我们在网站开发过程中,经常会遇到这样的需求: 用户登陆系统才可以访问某些页面 如果用户没有登陆而直接访问就会跳转到登陆界面,而不能访问其他页面. 用户在跳转的登陆界面中完成登陆后,自动访问跳转到之前访 ...

  10. 豪迈开料锯MDB文件分析

    豪迈CuteRite(简称CR)优化板件后会生成SAW文件.MDB文件,SAW文件用于开料机开料,MDB文件中保存了有限的优化结果记录. 因为CR软件可以根据配置生成不同结构的mdb文件,所以以下内容 ...