《用Python写网络爬虫》——2.2 三种网页抓取方法
本节书摘来自异步社区《用Python写网络爬虫》一书中的第2章,第2.2节,作者 [澳]Richard Lawson(理查德 劳森),李斌 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.2 三种网页抓取方法现在我们已经了解了该网页的结构,下面将要介绍三种抓取其中数据的方法。首先是正则表达式,然后是流行的BeautifulSoup模块,最后是强大的lxml模块。
2.2.1 正则表达式如果你对正则表达式还不熟悉,或是需要一些提示时,可以查阅https://docs.python.org/2/howto/regex.html 获得完整介绍。
当我们使用正则表达式抓取面积数据时,首先需要尝试匹配
元素中的内容,如下所示。import re url = http://example.webscraping.com/view/United -Kingdom-239 html = download(url) re.findall( td (.*?) /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 标签。要想分离出面积属性,我们可以只选择其中的第二个元素,如下所示。
re.findall( td (.*?) /td , html)[1] 244,820 square kilometres
虽然现在可以使用这个方案,但是如果网页发生变化,该方案很可能就会失效。比如表格发生了变化,去除了第二行中的国土面积数据。如果我们只在现在抓取数据,就可以忽略这种未来可能发生的变化。但是,如果我们希望未来还能再次抓取该数据,就需要给出更加健壮的解决方案,从而尽可能避免这种布局变化所带来的影响。想要该正则表达式更加健壮,我们可以将其父元素
re.findall( tr id="places_area__row" td label for="places_area" id="places_area__label" Area: /label /td td (.*?) /td , html) [244,820 square kilometres]
这个迭代版本看起来更好一些,但是网页更新还有很多其他方式,同样可以让该正则表达式无法满足。比如,将双引号变为单引号,
标签之间添加多余的空格,或是变更area_label等。下面是尝试支持这些可能性的改进版本。re.findall( tr id="places_area__row" .*? td\s* "\]w2p_fw["\] (.*?) /td , html) [244,820 square kilometres]
虽然该正则表达式更容易适应未来变化,但又存在难以构造、可读性差的问题。此外,还有一些微小的布局变化也会使该正则表达式无法满足,比如在
标签里添加title属性。从本例中可以看出,正则表达式为我们提供了抓取数据的快捷方式,但是该方法过于脆弱,容易在网页更新后出现问题。幸好,还有一些更好的解决方案,我们会在接下来的小节中继续介绍。
2.2.2 Beautiful SoupBeautiful Soup是一个非常流行的Python模块。该模块可以解析网页,并提供定位内容的便捷接口。如果你还没有安装该模块,可以使用下面的命令安装其最新版本:
pip install beautifulsoup4
使用Beautiful Soup的第一步是将已下载的HTML内容解析为soup文档。由于大多数网页都不具备良好的HTML格式,因此Beautiful Soup需要对其实际格式进行确定。例如,在下面这个简单网页的列表中,存在属性值两侧引号缺失和标签未闭合的问题。
ul >如果Population列表项被解析为Area列表项的子元素,而不是并列的两个列表项的话,我们在抓取时就会得到错误的结果。下面让我们看一下Beautiful Soup是如何处理的。
from bs4 import BeautifulSoup broken_html = ul # parse the HTML soup = BeautifulSoup(broken_html, html.parser) fixed_html = soup.prettify() print fixed_html html body li Area /li li Population /li /ul /body /html从上面的执行结果中可以看出,Beautiful Soup能够正确解析缺失的引号并闭合标签,此外还添加了和
标签使其成为完整的HTML文档。现在可以使用find()和find_all()方法来定位我们需要的元素了。ul = soup.find(ul, attrs={class:country}) ul.find(li) # returns just the first match li Area /li ul.find_all(li) # returns all matches [ li Area /li , li Population /li ]想要了解全部方法和参数,可以查阅BeautifulSoup的官方文档,其网址为:http://www.crummy.com/software/BeautifulSoup/bs4/doc/。下面是使用该方法抽取示例国家面积数据的完整代码。
from bs4 import BeautifulSoup url = http://example.webscraping.com/places/view/ United-Kingdom-239 html = download(url) soup = BeautifulSoup(html) # locate the area row tr = soup.find(attrs={id:places_area__row}) td = tr.find(attrs={class:w2p_fw}) # locate the area tag area = td.text # extract the text from this tag print area 244,820 square kilometres这段代码虽然比正则表达式的代码更加复杂,但更容易构造和理解。而且,像多余的空格和标签属性这种布局上的小变化,我们也无须再担心了。
2.2.3 LxmlLxml是基于libxml2这一XML解析库的Python封装。该模块使用C语言编写,解析速度比Beautiful Soup更快,不过安装过程也更为复杂。最新的安装说明可以参考http://Lxml.de/installation.html。
和Beautiful Soup一样,使用lxml模块的第一步也是将有可能不合法的HTML解析为统一格式。下面是使用该模块解析同一个不完整HTML的例子。
import lxml.html broken_html = ul tree = lxml.html.fromstring(broken_html) # parse the HTML fixed_html = lxml.html.tostring(tree, pretty_print=True) print fixed_html li Area /li li Population /li /ul同样地,lxml也可以正确解析属性两侧缺失的引号,并闭合标签,不过该模块没有额外添加和
标签。解析完输入内容之后,进入选择元素的步骤,此时lxml有几种不同的方法,比如XPath选择器和类似Beautiful Soup的find()方法。不过,在本例和后续示例中,我们将会使用CSS选择器,因为它更加简洁,并且能够在第5章解析动态内容时得以复用。此外,一些拥有jQuery选择器相关经验的读者也会对其更加熟悉。
下面是使用lxml的CSS选择器抽取面积数据的示例代码。
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 kilometresCSS选择器的关键代码行已被加粗显示。该行代码首先会找到ID为places_area__row的表格行元素,然后选择class为w2p_fw的表格数据子标签。
CSS选择器CSS选择器表示选择元素所使用的模式。下面是一些常用的选择器示例。
选择所有标签:* 选择 a 标签:a 选择所有 的元素:.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规范,其网址为http://www.w3.org/TR`/2011/REC-css3-selectors-20110929/。Lxml已经实现了大部分CSS3属性,其不支持的功能可以参见https://pythonhosted.org/cssselect/#supported-selectors。
需要注意的是,lxml在内部实现中,实际上是将CSS选择器转换为等价的XPath选择器。
2.2.4 性能对比要想更好地对本章中介绍的三种抓取方法评估取舍,我们需要对其相对效率进行对比。一般情况下,爬虫会抽取网页中的多个字段。因此,为了让对比更加真实,我们将为本章中的每个爬虫都实现一个扩展版本,用于抽取国家网页中的每个可用数据。首先,我们需要回到Firebug中,检查国家页面其他特征的格式,如图2.4所示。
从Firebug的显示中可以看出,表格中的每一行都拥有一个以places_起始且以__row结束的ID。而在这些行中包含的国家数据,其格式都和上面的例子相同。下面是使用上述信息抽取所有可用国家数据的实现代码。
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="places_%s__row" .*? td (.*?) /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.cssselect(table tr#places_%s__row td.w2p_fw % field)[0].text_content() return results抓取结果
现在,我们已经完成了所有爬虫的代码实现,接下来将通过如下代码片段,测试这三种方法的相对性能。
import time NUM_ITERATIONS = 1000 # number of times to test each scraper 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)]: # record start time of scrape start = time.time() for i in range(NUM_ITERATIONS): if scraper == re_scraper: re.purge() result = scraper(html) # check scraped result is as expected 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比其他两种方法慢了超过6倍之多。实际上这一结果是符合预期的,因为lxml和正则表达式模块都是C语言编写的,而BeautifulSoup``则是纯Python编写的。一个有趣的事实是,lxml表现得和正则表达式差不多好。由于lxml在搜索元素之前,必须将输入解析为内部格式,因此会产生额外的开销。而当抓取同一网页的多个特征时,这种初始化解析产生的开销就会降低,lxml也就更具竞争力。这真是一个令人惊叹的模块!
2.2.5 结论表2.1总结了每种抓取方法的优缺点。
如果你的爬虫瓶颈是下载网页,而不是抽取数据的话,那么使用较慢的方法(如Beautiful Soup)也不成问题。如果只需抓取少量数据,并且想要避免额外依赖的话,那么正则表达式可能更加适合。不过,通常情况下,lxml是抓取数据的最好选择,这是因为该方法既快速又健壮,而正则表达式和Beautiful Soup只在某些特定场景下有用。
2.2.6 为链接爬虫添加抓取回调前面我们已经了解了如何抓取国家数据,接下来我们需要将其集成到上一章的链接爬虫当中。要想复用这段爬虫代码抓取其他网站,我们需要添加一个callback参数处理抓取行为。callback是一个函数,在发生某个特定事件之后会调用该函数(在本例中,会在网页下载完成后调用)。该抓取callback函数包含url和html两个参数,并且可以返回一个待爬取的URL列表。下面是其实现代码,可以看出在Python中实现该功能非常简单。
def link_crawler(..., scrape_callback=None): links = [] if scrape_callback: links.extend(scrape_callback(url, html) or []) ...在上面的代码片段中,我们加粗显示了新增加的抓取callback函数代码。如果想要获取该版本链接爬虫的完整代码,可以访问https://bitbucket. org/wswp/code/src/tip/chapter02/link_crawler.py。
现在,我们只需对传入的scrape_callback函数定制化处理,就能使用该爬虫抓取其他网站了。下面对lxml抓取示例的代码进行了修改,使其能够在callback函数中使用。
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上面这个callback函数会去抓取国家数据,然后将其显示出来。不过通常情况下,在抓取网站时,我们更希望能够复用这些数据,因此下面我们对其功能进行扩展,把得到的结果数据保存到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)是等价的。如果想要了解更多有关Python特殊类方法的知识,可以参考https://docs.python.org/2/reference/datamodel.html#special-method-names。
下面是向链接爬虫传入回调的代码写法。
link_crawler(http://example.webscraping.com/, /(index|view), max_depth=-1, scrape_callback=ScrapeCallback())现在,当我们运行这个使用了callback的爬虫时,程序就会将结果写入一个CSV文件中,我们可以使用类似Excel或者LibreOffice的应用查看该文件,如图2.5所示。
成功了!我们完成了第一个可以工作的数据抓取爬虫。
Python 网页请求:requests库的使用 requests是 Python 中比较常用的网页请求库,主要用来发送 HTTP 请求,在使用爬虫或测试服务器响应数据时经常会用到,使用起来十分简洁。requests为第三方库,需要我们通过pip方法描述发送 DELETE 请求到指定 url发送 GET 请求到指定 url发送 HEAD 请求到指定 url发送 PATCH 请求到指定 url发送 POST 请求到指定 url发送 PUT 请求到指定 url发送指定的请求方到指定 url每次调用requests请求之后,会返回一个。
Python爬虫网页IP被封的问题分析 如果你在爬虫过程中有遇到“您的请求太过频繁,请稍后再试”,或者说代码完全正确,可是爬虫过程中突然就访问不了,那么恭喜你,你的爬虫被对方识破了,轻则给予友好提示警告,严重的可能会对你的ip进行封禁,所以代理ip那就尤为重要了。今天我们就来谈一下代理IP,去解决爬虫被封的问题。
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
相关文章
- python执行脚本加参数_命令行运行Python脚本时传入参数的三种方式详解以及argparse子命令subparsers()方法
- python魔法方法之-Python __repr__()方法:显示属性
- python - 如何使用 Docker 运行多个 Python 脚本和一个可执行文件?
- Python动态导入模块、类
- 地球引擎初级教程——Python API 语法(内涵JavaScript转python工具包介绍)
- python基础——使用@property
- 87 python高级 - 闭包
- 《python 与数据挖掘 》一1.3 Python开发环境的搭建
- 《python 与数据挖掘 》一 第2章 Python基础入门
- gyp ERR! stack Error: Can‘t find Python executable “python“, you can set the PYTHON env variable.
- Python 字符串小节
- 【Python】【PyPI】twine模块打包python项目上传pypi
- python网络爬虫 - 设定重试次数内反复抓取
- Python编程-基础知识-python项目包和文件的管理以及如何引用相对路径的包和模块
- vscode (Visual Studio Code) 配置Python 虚拟环境
- Python中的join()函数的用法实例分析
- python之获取微信access_token
- python 同名变量引用
- 《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.6 享元模式
- 我的Python程序太慢了。如何加快速度?
- Python 教程之如何使用 matplotlib 在 python 中绘制数学函数
- 使用Python中的HTMLParser、cookielib抓取和解析网页、从HTML文档中提取链接、图像、文本、Cookies(二)(转)
- Python实例---模拟微信网页登录(day2)
- [Spark][Python][DataFrame][SQL]Spark对DataFrame直接执行SQL处理的例子
- python爬虫 requests.get()返回值与html网页不一致
- 2.1 The Python Interpreter(python解释器)
- Python爬虫-Scrapy的item loader