[翻译]<Web Scraping with Python>Chapter 2.高级HTML解析

来源:互联网 发布:java catch return 编辑:程序博客网 时间:2024/06/12 01:05

Chapter 2.高级HTML解析

当米开朗琪罗被问道他怎样雕刻出David这样精湛的作品时,他只是重复的说道:“很简单。你只需要将不需要的部分去掉就行了。”尽管网络爬虫和大理石雕刻在很多方面都不一样,但是我们仍然也可以用相同的态度来看待我们需要从复杂的网页抽取那些我们需要的信息。

你并不是总需要一个锤子

当遇到一个标签的难题的时候(戈尔迪之结,希腊神话中的一个难题),我们常常试图才用多行语句来获取信息。但是需要记住的是,在本章中恣情(with reckless abandon 毫无顾忌地)使用的分层处理技术,可能导致代码难于调试、脆弱,或者两者皆有。在开始之前让我们看一下,在高级HTML解析中一些能避免这些情况的方法。
例如你可能遇到一个深度有20的标签,你写下如此代码:

bsObj.findAll("table")[4].findAll("tr")[2].find("td").findAll("div")[1].find("a")

然并卵,这行代码既不优美,也可能在网站轻微修改的时候就导致爬虫崩溃了。你还有其他选项么?
– 检查”print this page”连接,或许有手机版的网站上有格式更好的HTML(参考12章)
- 检查隐藏在JavaScript脚本中的信息。记住,你或许需要检查每次加载的JavaScript代码。例如,我曾经为了从一个网站收集几乎是数组格式的街道地址(含有纬度和经度)数据,而去参考了Google Map的一个JavaScript脚本,Google Map是对每一个街道地址均以图钉的标记显示的。
- 这对页面标题更加通用,但是这些信息或许在URL本身里面就可以获得。
- 如果出于某些原因,你寻找的信息只能在某网站上用唯一的方式获取,只能说你不走运。如果不是没考虑是否可以从其他地方获取这些信息。有没有另外的网站有相同的信息?这个网站的数据是否是从其他网站爬取或者聚合而得的?
特别是对于隐藏太深或者格式不好的数据,重要的不是马上开始挖掘。深呼吸,想一想有没有替代方案。如果你确定没有任何替代方案存在,本章剩余的部分就是为这种情况而存在的。

BeautifulSoup的另一功能

在第一章我们学习了安装和运行BeautifulSoup,并且通过其tag来获取数据。本章中,我们将讨论通过tag的attribute属性来获取数据,处理tag的列表,解析树状的导航条。
几乎在所有的网站上都使用样式表。尽管你可能认为,一个网站上一层为浏览器或人工解释器来特别设计的样式表是糟糕的,但是层叠样式表(CSS,Cascading Style Sheets)的出现却对爬虫来说是有好处的。CSS依赖于HTML元素的差异,所以即使相同的标记tag,也会呈现不同的样式。例如:

<span class="green"></span><span class="red"></span>

爬虫可以很轻易的从tag中不同的class属性,区分出这两种不同的颜色。例如,爬虫可以简单的用BeautifulSoup来攫取红色的文本而不含任何绿色的文本。

用BeautifulSoup,我们可以用findAll这个非常强大灵活的函数,来从该网页上抽取一个Python专有名词列表。例如,获取那些用标记<span class=”green”></span>限定的文本(这个网页上绿色的是人名,红色的是角色任务的对话)。示例代码:

from urllib.request import urlopenfrom bs4 import BeautifulSouphtml = urlopen("http://pythonscraping.com/pages/warandpeace.html")bsObj = BeautifulSoup(html, 'lxml')nameList = bsObj.findAll("span", {"class":"green"})for name in nameList:    print(name.get_text())

在前一章中,我们用bsObj.tagName来获取在网页中出现的第一个tag中所含有的内容,这里我们用bsObj.findAll(tagName,tagAttribute)来获取网页上所有符合这个tagName的内容的列表,而不仅仅限于第一个。然后才用迭代的方法用name.get_text()来分别输出每一个内容。.get_text()会移除所有的tag,而仅仅返回一个不含任何tag的文本的字符串。要记住的是,从一个BeautifulSoup对象中找寻信息,比从一大块文字中容易得多。.get_text()应当总是你最后执行的一步,紧接着这一步的就是打印、存储或者操作的最终数据。一般来说,在你未完成工作之前,应当尽可能保留这种标记结构。

BeautifulSoup的find()和findAll()

BeautifulSoup中的find()和findAll()这两个函数可能是最常使用的,可以基于tag以及不同attribute轻而易举的过滤HTML网页来找到你想要的所有tags或者一个tag。

findAll(tag, attributes, recursive, text, limit, keywords)find(tag, attributes, recursive, text, keywords)实际上BS4里面的findAll是BS3里面的find_all函数(以下两行代码为译者著):find_all(self, name=None, attrs={}, recursive=True, text=None,                 limit=None, **kwargs)find(self, name=None, attrs={}, recursive=True, text=None,             **kwargs)

在所有可能性中,95%的时候你只需要用前两个参数:tag和attribute。
- tag参数可以接受一个字符串的tag名字,或者字符串的python列表。例如:.findAll({“h1”,”h2”,”h3”,”h4”,”h5”})
- attribute参数接受一个Python字典,只要tag的attribute匹配这个字典中的任何一组属性。例如,以下的函数返回span标记中红色和绿色的对象:.findAll(“span”,{“class”:”green”,”class”:”red”})
- recursive参数设置为True的话,findAll会找寻所有的子节点,子节点的子节点,…….,直到所有的符合条件的tag。如果设置为false,则只会找寻最上层tag。默认情况下是True。
- text参数是基于tag中的文本内容做的匹配,而不是基于tag的属性所做的匹配。例如我们要找寻”the prince”在网页中出现的次数,可以这样写:

nameList=bsObj.findAll(text="the prince")print(len(nameList))
  • limit参数只是在findAll函数中使用。findAll的limit设置为1时,就等价于find函数。如果你只需从网页中找出前x个元素,则你需要设置limit这个函数。注意,这只是按照在网页中出现的顺序给出前x个,不一定是你需要那x个。
  • keyword参数允许你按照包含特别的attribute来选择tags。例如:
allText = bsObj.findAll(id="text")print(allText[0].get_text())

关于Keyword参数的警告

Keyword参数在很多情况下都很有用,但是,用keyword能完成的也能在本章接下来的讨论中用正则表达式(Regular Expression)和Lambda表达式来实现。
另外,你可能在用keyword参数时偶然出现syntax error。尤其是以class这个attribute来找寻元素的时候,因为class在Python中是保留的关键字。在Python中,class不应该被用于变量名或参数名。例如:bsObj.findAll(class=”green”)可能因class的非标准用法出现syntax error。取而代之,你可以用BeautifulSoup的有点笨拙的方法,在class后接一个下划线来解决:bsObj.findAll(class_=”green”),或者可以用引号将class括起来:bsObj.findAll(“”,{“class”:”green”})。此时你不禁问自己:”等等,这不就是我已经知道的,通过传递含有一些attributes的字典来获取一系列tag么?“记住,findAll()是通过attributes列表的”or”来筛选tag的(例如获取tag1 或者 tag2 或者 tag3)。如果你有一列很长的tag,你可用keyword来附加一个”add”的条件来过滤它,以便去除那些你不需要的。

其他的BeautifulSoup对象

这里我们已经了解了BeautifulSoup对象,以及通过对BeautifulSoup对象使用find或者findAll来获取的Tag对象。除此之外,在BeautifulSoup库中,还有两个不太常用的对象,但是仍然很重要
- NavigableString对象(用带文本和外层的tag字符串来表示,而不是用tag对象)
- Comment对象(通过注释的tag来找到HTML中的注释部分,例如<!--like this one-->
在本书中,你就会用到这四个对象。

导航树(Navigating Tree)

findAll函数是通过tag的name和attribute来寻找tag的。但是如果你希望通过位置来寻找tag呢?这就是导航树(Navigation Tree)出现的原因,其很方便的用于按照位置寻找tag。在Chapter 1 中,我们是按照单一方向来看待BeautifulSoup树的,比如:bsObj.tag.subTag.anotherSubTag 。
现在,让我们来从另外的方面来看待HTML树(从下至上,跳跃式的,甚至是对角线方式的搜寻),在这个网上购物网页上做实验。(http://www.pythonscraping.com/pages/page3.html)
这个HTML可以对应如下树状结构(为了简洁省略了一些),例如:(此处略去一个不好用markdown实现的树状图,可以参考英文版page 43)

处理子节点以及其他后裔节点

在我们的例子的BeautifulSoup对象中,例如tr是table的子节点,同样tr,th,td,img和span都是table的后裔节点。所有的子节点都是后裔节点,但不是所有的后裔节点都是子节点。例如,bsObj.body.h1是body的后裔节点,不会取到不是body的后代节点。相似的,bsObj.div.findAll(“img”)将会找到第一个div,然后在找到这div下面的所有的img。如果你只想得到子节点,可以用.children:

from urllib.request import urlopenfrom bs4 import BeautifulSouphtml = urlopen("http://www.pythonscraping.com/pages/page3.html")bsObj = BeautifulSoup(html, 'lxml')for child in bsObj.find("table", {"id":"giftList"}).children:    print(child)

以上代码将会打印giftList表下所有的产品行,如果你使用descendants()代替children()函数,将会在table下找到大约二十多个tag(实际上包括tag中的content也算作独立的对象,这里大概有接近50个)并且打印出来(包括img,span,td 等)

from urllib.request import urlopenfrom bs4 import BeautifulSouphtml = urlopen("http://www.pythonscraping.com/pages/page3.html")bsObj = BeautifulSoup(html, 'lxml')for child in bsObj.find("table", {"id":"giftList"}).descendants:    print(child)

从以上可以看出子节点和后代节点的区别(即使用children和descendants的区别)

处理兄弟节点(siblings)

BeautifulSoup中的next_siblings()函数用于从tables中提取一行一行的数据,其每行顺序都和标题行对应。

from urllib.request import urlopenfrom bs4 import BeautifulSouphtml = urlopen("http://www.pythonscraping.com/pages/page3.html")bsObj = BeautifulSoup(html, 'lxml')for sibling in bsObj.find("table", {"id":"giftList"}).tr.next_siblings:    print(sibling)

以上代码输出产品表格中除了第一行(标题行)之外的所有行。为什么标题行被跳过了?有两个原因:第一,任何时候,对象本身都不是自己的兄弟节点。第二,这个函数是叫做next_siblings,如果从list的中间任意选取一行,然后调用,只能得到按顺序的下一个(next)兄弟节点。所以,如果选取标题行并且调用next_siblings,我们可以得到除了标题行之外的整个table的所有其他行。

指定选择细节

注意,如果为了选中table的第一行,我们使用bsObj.table.tr或者甚至仅仅用bsObj.tr,上面的代码仍然能运行的很好。但是,我在代码中考虑了所有可能遇到的问题,而将其写成一个较长的格式:bsObj.find(“table”, {“id”:”giftList”}).tr 。尽管该网页只有一个table,但是仍然很容易错过一些东西。另外,因为页面的格式可能不断变化。曾经在页面上第一行显示的,可能在某一天会变成第二行甚至第三行。所以,为了让爬虫更加健壮,最好在选择的时候尽可能详细而明确指定其tag。这时候,就可以好好利用tag的attribute属性来达成这一点了。
作为next_siblings的补充,previous_siblings函数在某些情况下也很有用,比如说在你期望获取的兄弟tag列表的最后有容易可选中的tag。当然,和刚才提到的next_siblings以及previous_siblings函数极其相似,next_sibling以及previous_sibling函数返回单一的tag而不是一个tag的列表。

处理父节点

当爬取网页的时候,你可能会发现,查找父节点远没有查找他们的子节点以及兄弟节点频繁。但是,偶尔我们也可以用BeautifulSoup中的父节点查找函数 .parent 或者 .parents 来达成那些临时需求。

from urllib.request import urlopenfrom bs4 import BeautifulSouphtml = urlopen("http://www.pythonscraping.com/pages/page3.html")bsObj = BeautifulSoup(html, 'lxml')print(bsObj.find("img", {"src":"../img/gifts/img1.jpg"}).parent.previous_sibling.get_text())

让我们对照下面的图解,看看这是怎么得来的:
1,首先选中img的tag;
2,选中父节点<td>tag;
3,选中之前的兄弟节点(在本例总是包含美元和价格的<td>tag);
4,选中这个tag中的内容(”$15.00”)

<tr id="gift1" class="gift"><td>Vegetable Basket</td><td>This vegetable basket is the perfect gift for your health conscious (or overweight) friends!<span class="excitingNote">Now with super-colorful bell peppers!</span></td><td>$15.00</td><td><img src="../img/gifts/img1.jpg"></td></tr>

正则表达式

一个计算机科学的老笑话这样说:“有个问题,你准备用正则表达式来解决,现在,你有两个问题了。”很多看上去毫无意义的字符组成的正则表达式是难以学习但是却很强大,有的时候很多复杂的搜索和过滤语句,实际上只需要一行正则表达式就可以解决了。(关于正则表达式,这段我就不翻译了,多练习多看文档,和正则表达式厮混才是王道。记住了,只有和正则表达式一起厮混,才能搞清楚它们怎么工作的,管它们调戏你,还是你调戏它们呢。^_^)
表2-1列出了常见的12个正则表达式符号,并给出简单的解释和例子。但这绝不是全部正则表达式,在其他语言中,你也可能遇到和python中稍有不同的正则表达式。
表2-1,常用正则表达式符号

符号 含义 示例 示例可能匹配的字符串 * 匹配之前的字符、子表达式、或者用方括号[]括起来的其中一个字符,匹配0或者多次 a*b* aaaaaaaa, aaabbbbb, bbbbbb + 匹配之前的字符、子表达式、或者用方括号[]括起来的其中一个字符,匹配1或者多次 a+b+ aaaaaaab, aaabbbbb, abbbbb [] 匹配方括号[]内的任意一个字符 [A-Z]* APPLE, CAPITALS, QWERTY () 匹配一组子表达式(被圆括号括起来的正则表达式的运算次序优先级最高,不能被拆开看待) (a*b)* aaabaab, abaaab, ababaaaaab {m,n} 匹配之前的字符、子表达式、或者用方括号[]括起来的其中一个字符,匹配m到n次(包含m和n次) a{2,3}b{2,3} aabbb, aaabbb, aabb, aaabb [^] 匹配不在方括号[]内的任一单字符 [^A-Z]* apple, lowercase, qwerty | 匹配由|分割的任一字符、字符串或者子表达式 b(a|i|e)d bad, bid, bed . 匹配任意单字符(包括符号、数字、空格等) b.d bad, bzd, b$d, b d ^ 表明以一个字符或者子表达式开头的字符串 ^a apple, asdf, a \ 转义字符(在此之后的特殊字符将会按照字面意思解释) .\|\ .|\ $ 经常用于正则表达式结尾,指明正则匹配到此为止。如果一个正则表达式没有$,则在正则表达式所能匹配字符串之后,还允许字符串剩余的字符去匹配 .* 格式的正则表达式。可以类推到 ^ 符号。 [A-Z]*[a-z]*$ ABCabc, zzzyx, Bob ?! 这个在一个字符或者正则表达式之前的奇怪的字符对,是代表”不包含”的意思。表明紧接着?!出现的字符不应该在更大字符串的特定位置出现。这用法有点诡异,毕竟,这个字符或许能在整个字符串的其他位置出现。如果要完全消除一个字符,请使用^和$连接其首尾。 ^((?![A-Z]).)*$ 不含大写字母。 例如 $ymb0lsa4e f!ne

正则表达式,并不总是一致通用的

标准版的正则表达式(例如我们在这本书中用到的,Python和BeautifulSoup使用的)是基于Perl语法的正则表达式。很多现代编程语言使用的是标准版的正则表达式,或者使用的是非常相近的。但是请注意,如果你在另一语言中使用正则表达式,你或许会遇到问题。例如一些现代编程语言,比如说Java,其使用的正则表达式和本书讲解的有些许不同。当你有疑问,记得最好的解释是参考文档。

正则表达式和BeautifulSoup

如果之前的关于正则表达式的章节,看上去和这本书的目的(爬虫)有些脱节的话。那么这里将会将其连接起来。BeautifulSoup和正则表达式将会在网络爬虫中联合使用。实际上,很多函数中的参数是一个字符串(比如find(id=”aTagIdHere”)),接受一个正则表达式仍然会很好的工作。让我们再来看,爬取这个网页(http://www.pythonscraping.com/pages/page3.html)的例子。在这个网页上有很多产品的图片,我们想取得下面这个图片。
<img src=”../img/gifts/img3.jpg”>
如果我们向攫取所有产品的图片,或许我们会直接这样做:用 findAll(“img”),对吧?但是这里有个问题。除了很明显的”额外的”图片(比如logo),现代网页经常含有隐藏的图片,比如说:用于占位或对齐的空白图片,其他随机的而基本不会引起人注意的图片。所以,你不能通过这种方式,只考虑抓取到产品图片。
让我们假定网页的布局可能改动,或者不管出于什么原因,我们不想依赖于图片的位置去获取正确的tag。这或许是个典型的例子,更一般的是你想获取一些随机分散在网站上的元素或者图片的数据。例如,有一个特别的产品的图片,只出现在某些页面的顶端,而不出现在其他页面上。解决方法是找到能够确定这个tag的标志。在这个例子中,我们查看了产品图片的这个文件的路径。

from urllib.request import urlopenfrom bs4 import BeautifulSoupimport rehtml = urlopen("http://www.pythonscraping.com/pages/page3.html")bsObj = BeautifulSoup(html, 'lxml')images = bsObj.findAll("img", {"src":re.compile("\.\.\/img\/gifts/img.*\.jpg")})for image in images:    print(image["src"])

只输出了以 ../img/gifts/img 开始,以 .jpg 结尾的图片地址。
../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg
正则表达式可以添加到BeautifulSoup接受的任何参数中,以便于查找目标元素。

访问属性(Attributes)

到目前为止,我们已经了解了如何获取和过滤tag,以及获取他们的内容。但是,在网络中爬取,你很多时候并非是查找tag的内容,而是查找tag的某些属性。这在某些时候特别有用,比如说tag <a> 中的目标url是含在href这个属性中的,还比如说tag <img> 中目标图片是含在src这个属性中的。
对于tag对象来说,属性的Python列表对象可以很容易通过 myTag.attrs来访问。记住,myTag.attrs这个会按照字面解释返回一个Python的字典对象,myTag.attrs用来检索和操作这些细琐的属性。比如说,图片的地址,能够通过以下一行代码来获取: myImgTag.attrs[‘src’]

Lambda表达式

即使你是计算机科班出生,也有可能只是在学校里面学过Lambda表达式,然后就再也没有用过了。本质上,lambda表达式是一个函数,其被当作一个变量传递给另一个函数。换而言之,用来替代定义一个普通的函数 f(x,y) ,你可以定一个一个函数比如说 f(g(x), y) ,甚至 f(g(x), h(x)) 。BeautifulSoup函数允许我们将特定类型的函数作为参数传递给findAll函数。仅有的限制是这些特定类型的函数,必须接受tag对象作为参数,并且返回一个布尔类型(True / False)。每个BeautifulSoup含有的所有tag对象,都被在此函数中被评估,tags被评估为true的就会返回,评估为false的就被会丢弃。
例如,下面的代码检索所有有两个attributes的tag对象:
bsObj.findAll(lambda tag: len(tag.attrs) == 2)
也就是说,这会命中如下形式的tag:
<div class=”body” id=”content”></div>
<span style=”color:red” class=”title”></span>
如果你乐意写点代码的话,在BeautifulSoup中使用lambda函数,可以作为正则表达式的替代方法。

BeautifulSoup之外

尽管BeautifulSoup贯穿全书(也是Python中最受欢迎的HTML库之一),但是需要注意的是,并不是只有BeautifulSoup库可用。如果BeautifulSoup不满足你的需求,可以考虑以下使用很广泛的库:

lxml库:

这个库被用来解析HTML和XML文档,其是由很底层的C语言书写的。这需要用一段时间来学习,但是lxml库在解析大多数的HTML文档的时候相当快。

HTML Parser库:

这是个Python内置的解析库,不需要另外安装,所以使用很方便。

1 0
原创粉丝点击