- 本书赞誉
- 前言
- 目标读者
- 不适合阅读本书的读者
- 本书结构
- 什么是数据处理
- 遇到困难怎么办
- 排版约定
- 使用代码示例
- 致谢
- 第 1 章 Python 简介
- 第 2 章 Python 基础
- 第 3 章 供机器读取的数据
- 第 4 章 处理 Excel 文件
- 第 5 章 处理 PDF 文件 以及用 Python 解决问题
- 第 6 章 数据获取与存储
- 第 7 章 数据清洗:研究、匹配与格式化
- 第 8 章 数据清洗:标准化和脚本化
- 第 9 章 数据探索和分析
- 第 10 章 展示数据
- 第 11 章 网页抓取:获取并存储网络数据
- 第 12 章 高级网页抓取:屏幕抓取器与爬虫
- 第 13 章 应用编程接口
- 第 14 章 自动化和规模化
- 第 15 章 结论
- 附录 A 编程语言对比
- 附录 B 初学者的 Python 学习资源
- 附录 C 学习命令行
- 附录 D 高级 Python 设置
- 附录 E Python 陷阱
- 附录 F IPython 指南
- 附录 G 使用亚马逊网络服务
- 关于作者
- 关于封面
11.5 使用 lxml 读取网页
一个更高级的网页抓取器(其他的高级工具把它作为解析器来使用)是 lxml(http://lxml.de/)。它非常强大而快速,而且有很多很棒的特性,包括生成 HTML 和 XML 以及清洗编写糟糕的网页的能力。除此之外,它有很多用于遍历 DOM 和网页家族关系的工具。
安装 lxml
lxml 有许多不同的 C 依赖,这使得安装它要比安装大多数 Python 库更复杂一点(http://lxml.de/installation.html)。对于 Windows 用户,可查看开源二进制构建版本的 lxml(http://lxml.de/FAQ.html#where-are-the-binary-builds)。对于 Mac 用户,建议安装 Homebrew(http://brew.sh/),这样你可以使用 brew install lxml 安装它。有关高级安装的更多细节,请查看附录 D。
让我们快速地看一下要使用的主要 lxml 特性,先重写 Beautiful Soup 的代码来使用 lxml:
from lxml import html page = html.parse('http://www.enoughproject.org/take_action') ➊ root = page.getroot() ➋ ta_divs = root.cssselect('div.views-row') ➌ print ta_divs all_data = [] for ta in ta_divs: data_dict = {} title = ta.cssselect('h2')[0] ➍ data_dict['title'] = title.text_content() ➎ data_dict['link'] = title.find('a').get('href') ➏ data_dict['about'] = [p.text_content() for p in ta.cssselect('p')] ➐ all_data.append(data_dict) print all_data
❶ 这里使用 lxml 的解析方法,它可以从一个文件名、一个打开的缓冲区或一个合法的 URL 解析。它返回一个 etree 对象。
❷ 因为 etree 对象的方法和属性比 HTML 元素对象少很多,所以这行代码访问根(页面和 HTML 的顶部)元素。根包含所有可能的能够访问的主干(孩子)和细枝(后代)。从根可以向下解析每一个链接或者段落,并且可以返回整个页面的 head 和 body 标签。
❸ 使用根元素,这行代码找到所有的类名称为 views-row 的 div。它使用 cssselect 方法和一个 CSS 选择器字符串,返回一个匹配元素的列表。
❹ 为了抓取标题,使用 cssselect 方法找到 h2 标签。这行代码选择了列表中的第一个元素。cssselect 返回一个所有匹配项的列表,但是我们只想要第一个匹配的元素。
❺ 同 Beautiful Soup 的 get_text 方法类似,text_content 为 lxml HTML 元素对象返回标签(和任何子标签)内的文本。
❻ 这里使用链式方法来从 title 元素中获得锚标签,并且拉取锚标签中的 href 属性。这只返回这一属性的值,类似于 Beautiful Soup 的 get 方法。
❼ 使用列表生成式来从 Take Action div 中的每一个段落中拉取文本,组成完整的文本。
你应该看到与我们使用 Beautiful Soup 时相同的提取数据。不同的是语法和页面加载的方式。Beautiful Soup 使用正则表达式把文档作为一个长字符串解析。lxml 使用 Python 和 C 库来识别页面结构,并且用更加面向对象的方式遍历它。lxml 查看所有标签的结构,(取决于你的计算机和安装它方式的不同)使用最快的方法解析树,并且在一个 etree 对象中返回数据。
我们可以使用 etree 对象本身,或者调用 getroot,这个函数会返回树最顶部的元素——通常为 html。有了这个元素,可以使用很多不同的方法和属性读取和解析页面剩余的部分。我们的解决方案强调了一点:使用 cssselect 方法。这个方法使用 CSS 选择器字符串(类似于 jQuery 示例),并且使用这些字符串来识别 DOM 元素。
lxml 也有 find 和 findall 方法。find 和 cssselect 之间有什么主要区别呢?来看一些示例:
print root.find('div') ➊ print root.find('head') print root.find('head').findall('script') ➋ print root.cssselect('div') ➌ print root.cssselect('head script') ➍
❶ 在根元素上使用 find 方法来找到 div,这返回空。从浏览器的检视来看,我们知道页面充满了 divs !
❷ 使用 find 方法查看头部标签,使用 findall 方法在头部定位脚本元素。
❸ 使用 cssselect 取代 find 正确地定位文档中所有的 divs,它们作为一个大的列表返回。
❹ 使用 cssselect,通过嵌套 CSS 选择器在头部定位脚本标签。使用 head script 返回与从根对象链式调用 find 命令相同的列表。
所以,find 和 cssselect 的操作方式有很大的不同。find 利用 DOM 来遍历元素,并基于祖先和家族关系找到它们,而 cssselect 方法利用 CSS 选择器来寻找页面中所有可能的匹配,或者元素的后继,非常类似于 jQuery。
根据需求的不同,find 或 cssselect 可能更加有用。如果页面的 CSS 类、ID 和其他标识符组织得良好,cssselect 是一个非常棒的选择。但是如果页面没有组织或不使用这些标识符,遍历 DOM 可以帮助你通过家族关系确定内容。
我们想要探索其他有用的 lxml 方法。作为一名开发者,随着不断学习和成长,你可能想要通过 emoji 表情表达进程。出于这个原因,让我们编写一个快速的 emoji 图表解析器(http://www.emoji-cheat-sheet.com/)来保存一个最新的 emoji 表情列表,你可以在 Basecamp、GitHub 和很多其他的技术相关网站上使用它们。下面是做这件事的代码:
from lxml import html import requests resp = requests.get('http://www.emoji-cheat-sheet.com/') page = html.document_fromstring(resp.content) ➊ body = page.find('body') top_header = body.find('h2') ➋ print top_header.text headers_and_lists = [sib for sib in top_header.itersiblings()] ➌ print headers_and_lists proper_headers_and_lists = [s for s in top_header.itersiblings() if s.tag in ['ul', 'h2', 'h3']] ➍ print proper_headers_and_lists
❶ 这段代码使用 requests 库拉取 HTML 文档的主体,之后使用 html 模块的 document_fromstring 方法解析数据为一个 HTML 元素。
❷ 通过查看页面结构,可以看到这是一系列头部的匹配列表。这行代码定位第一个头部,这样我们可以使用家族关系来寻找其他有用的部分。
❸ 这行代码使用列表生成式和 itersiblings 方法(返回一个迭代器)来查看所有的邻居。
❹ 上一个 print 展示了初始的 itersibling 列表生成式返回了远超我们需求的数据,包括一些页面下方带有 div 和 script 元素的部分。使用页面检视,我们确定想要的标签只是 ul、h2 和 h3。这行代码使用列表生成式和一个 if 确保只返回目标内容。
itersiblings 方法和 tag 属性帮助我们轻松地定位想要选择和解析的内容。在这个例子中,我们没有使用任何 CSS 选择器。我们知道,代码不会因为添加一个新部分而损坏,只要页面继续在头部和列表标签中保存内容。
为什么只想使用 HTML 元素构建一个解析器呢?不依赖于 CSS 类的优势是什么?如果一个站点的开发者改变了它的设计或让它变得对移动端更友好,那么很可能他会修改 CSS 和 JavaScript,而不是重新编写页面结构。如果使用基本的页面结构驱动抓取器,它们可能会比那些使用 CSS 的抓取器用得更久,有效期更长。
除了 itersiblings 之外,lxml 对象可以迭代孩子、后继和祖先。使用这些方法遍历 DOM,是熟悉页面组织方式和编写持久代码的很好的方式。你同样可以使用家族关系来编写有意义的 XPath——一种结构化的模式,用于基于 XML 的文档(像 HTML)。尽管 XPath 不是解析网页最简单的方式,但它是一种快速、高效且极度简单的方式。
一个XPath案例
虽然使用 CSS 选择器是一种找到页面上元素和内容的简单方式,也建议你学习和使用 XPath(https://en.wikipedia.org/wiki/XPath)。XPath 是一个标记模式选择器,组合了 CSS 选择器和遍历 DOM 的能力。理解 XPath 是学习网页抓取和网站结构的很好的方式。有了 XPath,你可以访问仅仅使用 CSS 选择器不容易阅读的内容。
XPath 可以用于几乎所有主要的网页抓取库,并且比其他大多数识别和同页面内容交互的方法都快得多。事实上,大多数同页面交互的选择器方法都在库内部转化为 XPath。
为了练习 XPath,你只需要查看浏览器的工具。许多浏览器都能够查看和复制 DOM 中的 XPath 元素。微软也有一篇关于 XPath 的很棒的文章(https://msdn.microsoft.com/en-us/library/ms256086(v=vs.110).aspx),而且 Mozilla 开发者网络上有很多很棒的工具和示例(https://developer.mozilla.org/en-US/docs/Web/XPath),供你更深入地学习 XPath。
XPath 遵循特定的语法来定义元素的类型、在 DOM 中的位置,以及可能拥有什么属性。表 11-2 回顾了可以在网页抓取代码中使用的一些 XPath 语法模式。
表11-2:XPath语法
表达式 | 描述 | 示例 |
//node_name | 在文档中选择所有匹配 node_name 的节点 | //div(选择文档中的所有 div 对象) |
/node_name | 选择当前或前序元素中所有匹配 node_name 的节点 | //div/ul(选择所有 div 内的 ul 对象) |
@attr | 选择一个元素的属性 | //div/ul/@class(选择所有 div 中 ul 对象的 class 属性) |
../ | 选择父元素 | //ul/../(选择所有 ul 元素的父元素) |
[@attr="attr_value"] | 选择有特定属性值的元素 | //div[@id="mylists"](选择 ID 值为“mylists”的 div) |
text() | 从节点或元素中选择文本 | //div[@id="mylists"]/ul/li/text()(选择 ID 为“mylists”的 div 中的列表中元素的文本) |
contains(@attr, "value") | 选择属性具有特定值的元素 | //div[contains(@id, "list")](选择所有 ID 中有“list”的 div) |
* | 通配符 | //div/ul/li/*(选择所有的 div 中 ul 中列表对象的后继) |
[1,2,3...]、[last()] 或 [first()] | 根据在节点中出现的顺序选择元素 | //div/ul/li[3](选择所有 div 中 ul 中的第三个列表对象) |
还有更多的表达式,但是这些已经足够我们开始了。让我们使用 XPath 和本章早些时候创建的非常漂亮的 HTML 页面,研究如何解析 HTML 元素间的家族关系。为了跟随我们,从本书的代码仓库(https://github.com/jackiekazil/data-wrangling)中将其拉取到你的浏览器中(文件:awesome_page.html)。
假设我们想要在页脚部分选择链接。通过使用“检视元素”选项(见图 11-13),可以看到底部栏展示了一个元素列表和它们的祖先。锚链接位于 html 标签内的 body 标签内的 footer 内的一个带有 CSS id 的 div 内的 ul 内的 li 标签里(喔!我觉得快要喘不过来气了!)。
图 11-13:找到页面的元素
怎样编写 XPath 来选择它呢?实际上,有很多种方式。让我们从一个相当明显的方式开始,使用带有 CSS id 的 div 来编写 XPath。用已学到的语法选择 div:
'//div[@id="bottom_nav"]'
可以使用浏览器的 JavaScript 控制台测试这段代码。为了在控制台中测试 XPath,直接将它放在 $x(); 中,这是一个 jQuery 控制台的实现,用于使用 XPath 浏览页面。让我们在控制台中查看一下(见图 11-14)。1
1如果你想要在一个不使用 jQuery 的站点上使用 XPath,需要使用 Mozilla 在文档中描述的不同语法(https://developer.mozilla.org/en-US/docs/Introduction_to_using_XPath_in_JavaScript)。对于这个元素,语法应该是 document.evaluate('//div[@id="bottom_nav"]', document)。
图 11-14:使用控制台编写 XPath
我们有了合法的 XPath 来选择导航栏,因为控制台返回了一个对象(类似于 jQuery 选择器)。但是我们真正想要的是链接。让我们来看一下怎样从这个 div 移到这些链接。我们知道它们是后继,所以编写一个家族关系。
'//div[@id="bottom_nav"]/ul/li/a'
这里我们想要任何具有 id bottom_nav 的 divs,其中包含一个无序列表,然后是匹配项中的列表对象,再然后是这些对象中的锚标签。让我们尝试在控制台中运行它(图 11-15)。
图 11-15:XPath 子元素
可以从控制台的输出看到已经选择了这三个链接。现在,我们只想提取网页地址本身。我们知道每一个锚标签有一个 href 属性。让我们使用 XPath 来为这些属性编写一个选择器:
'//div[@id="bottom_nav"]/ul/li/a/@href'
当在控制台中运行这个选择器时,可以看到我们已经正确地选择了底部链接中的网页地址(见图 11-16)。
图 11-16:寻找 XPath 属性
了解页面结构可以帮助我们得到很难访问的内容,我们可以使用 XPath 表达式取而代之。
由于 XPath 的能力和速度,你需要慢慢学习。比如,如果在类与 ID 之间存在空间,应该使用 contains 模式,而不是 =。元素可以拥有多个类,而 XPath 会假定包含了整个类字符串;使用 contains 将帮助你找到任何包含这个子串的元素。
找到你感兴趣的元素的父元素可能会很有用。假如你对页面上的一个对象列表感兴趣,并且你可以使用 CSS 类或列表中包含的文本轻松地定位一个或多个列表对象。你可以使用这些信息来构建一个 XPath 选择器,定位该元素,之后寻找父元素,让你能够访问整个列表。12.2.1 节探索这些 XPath 选择器类型,因为 Scrapy 利用 XPath 进行快速解析。
使用 XPath 的一个原因是,你通过 CSS 选择器找到的 CSS 类可能并不总能正确地选择元素,特别是使用了不同的驱动处理页面时(例如,Selenium 和许多浏览器)。XPath 天生更加,因而是正确解析网页的一种更加可靠的方式。
如果你已经抓取了一个站点很长时间,并且想要复用相同的代码,XPath 不太可能会由于小段代码的改变和站点的开发而崩溃。更常见的作法是重写一些 CSS 类或样式,而不是修改整个站点和页面结构。因此,XPath 比使用 CSS 更安全(尽管不是万无一失)。
现在你已经学习了一些 XPath 知识,可以尝试使用 XPath 语法重新编写 emoji 处理器,正确地存储每个部分中所有的 emoji 和头部信息。代码类似下面这样。
from lxml import html page = html.parse('http://www.emoji-cheat-sheet.com/') proper_headers = page.xpath('//h2|//h3') ➊ proper_lists = page.xpath('//ul') ➋ all_emoji = [] for header, list_cont in zip(proper_headers, proper_lists): ➌ section = header.text for li in list_cont.getchildren(): ➍ emoji_dict = {} spans = li.xpath('div/span') ➎ if len(spans): link = spans[0].get('data-src') ➏ if link: emoji_dict['emoji_link'] = li.base_url + link ➐ else: emoji_dict['emoji_link'] = None emoji_dict['emoji_handle'] = spans[1].text_content() ➑ else: emoji_dict['emoji_link'] = None emoji_dict['emoji_handle'] = li.xpath('div')[0].text_content() ➒ emoji_dict['section'] = section all_emoji.append(emoji_dict) print all_emoji
❶ 这行代码寻找与 emoji 内容相关的头部信息。它使用 XPath 抓取所有的 h2 和 h3 元素。
❷ 每一个定位到的头部有一个 ul 元素来匹配。这行代码在整个文档中收集所有的 ul 元素。
❸ 使用 zip 方法来打包头部和与之适合的列表,这返回一个元组列表。这行代码之后解包这些元组,使用一个 for 循环拉取每一个部分(头部与列表内容)到独立的变量中。
❹ 这段代码遍历 ul 元素的子元素(li 元素保存着 emoji 表情信息)。
❺ 通过页面检视,我们知道大多数的 li 元素有一个 div,其中包含两个 span 元素。这些 span 包括 emoji 表情的图片链接,以及用来唤起 emoji 表情的文字。这行代码使用 XPath 的 div/span 返回每个子 div 元素下所有的 span 元素。
❻ 为了找到每个元素的链接,这行代码调用第一个 span 的 data-src 属性。如果 link 变量为 None,代码会在我们的数据字典中设置 emoji_link 属性为 None。
❼ 因为 data-src 保存着一个相对 URL,所以这行代码使用 base_url 属性来创建一个完整的绝对 URL。
❽ 为了得到句柄(handle)或唤起 emoji 表情所需的文字,这行代码抓取第二个 span 的文本。不同于链接的逻辑,我们不需要测试这是否存在,因为每一个 emoji 都拥有一个句柄。
❾ 对于包括 Basecamp 声效的页面,对于每一个列表对象,存在一个 div(你可以通过使用浏览器的开发者工具检视页面,轻松地找到它)。这行代码选择 div,并且抓取其中的文本内容。因为这行代码在 else 代码块中,所以我们知道这些只是声音文件,因为它们不使用 spans。
通过重写 emoji 代码来使用 XPath 关系,我们发现标签最后的代码块是声音,并且其中的数据以不同的方式存储。相对于在 span 中保存一个链接,这里只有一个 div 包含唤醒声音的文本。如果只想要 emoji 链接,可以跳过添加它们到列表对象的迭代。取决于你感兴趣的数据,代码会有很大相同,但是你总是可以轻松地利用 if...else 逻辑来确定需要的内容。
通过不超过 30 行的代码,我们创建了一个抓取器来请求页面,通过 XPath 遍历 DOM 关系解析它,同时使用合适的属性或文本内容抓取出需要的内容。这段代码具有很好的扩展性,如果页面的作者添加了更多的数据节,只要页面结构没有大幅度改变,解析器会继续从页面拉取内容,并且我们会拿到不计其数的 emoji 表情!
还有许多其他有用的 lxml 函数。表 11-3 总结了其中一些以及它们的使用场景。
表11-3:lxml特性
方法或属性名称 | 描述 | 文档 |
clean_html | 一个用来清理糟糕格式页面的函数,这样它们可以被正确解析 | |
iterlinks | 一个用来访问页面上每一个锚标签的迭代器 | |
[x.tag for x in root] | 所有的 etree 元素可以作为简单的迭代器使用,支持子元素的遍历 | |
.nsmap | 提供对命名空间的简单访问,如果你愿意使用它们的话 |
现在,当研究页面上的结构化数据和解决如何使用 lxml、Beautiful Soup 和 XPath 从页面中提取内容时,你应该感到很自信。下一章会继续研究其他可以用来做不同类型抓取的库,像基于浏览器的解析和爬虫。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论