您的位置:首页 > 理论基础 > 计算机网络

网络爬虫 HTML的高级解析 <web scraping with python>第二章

2015-08-06 19:47 531 查看
Chapter 2 .HTML的高级解析

当有人问米开朗琪罗他是怎么雕刻出如同大卫那样绝妙的艺术品时,他说了一句后来著名的话,他说:“这非常的简单啊。你刚才雕刻带走的那块石头只是长的不像大卫啊。”尽管网页爬虫在很多方面和大理石雕刻并相同,但是我们从复杂的页面上提取我们找寻的信息的时候,必须有着相似的态度。有很多技术去分离我们找寻的内容,直到我们获取到我们需要内容。在这一章,我们将看一下解析复杂的HTML页面,仅仅提取出我们需要的信息。

你不是总需要一把锤子

当面对复杂的标签时候,可以深入的使用多行语句去尝试提取你需要的信息时候,总是让人很开心的。但是,请记住,在这里不顾一切的使用分层技术会导致代码难以调试,代码健壮性低,或者两种情况都会有。在开始之前,我们来看一看一些完全能避免这些问题的一些高级HTML解析方式。

比方说,你有一些目标内容。也许是一个名字,一些统计或者是一个文本块。也许它藏在20层的标签才被找到的HTML里面,HTML文本只有一些没有用的标签和HTML属性。比方说,你用下面的代码去进入到里面去尝试提取:

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

这个看起来并没有那么好。除了美观性以外,即使网站管理员最轻微的更改网页也会完全阻挡你的网络爬虫。所以,你有神马办法呢?

①寻找一个“打印此页面”的连接,或者一个移动版本的网页有着更好的HTML格式(更多呈现给自己的是作为一个移动设备和接受移动网站的版本——第十二章)。

②寻找隐藏在JavaScript文件中的信息。记住,你也许需要通过检查导入的JavaScript文件来做到这一点。比如,我曾经收集过街道地址(连同经纬度),关掉了一个格式排列整齐的网站,通过寻找嵌入在谷歌地图中的JavaScript文件精确的显示了每一个地址。

③这种网页标题更常见,但是可用的信息也许在页面的URL本身

④如果因为某些原因,你要找的数据是这个网站独一无二的,那你非常的不走运。如果不是,尝试考虑其他你可以得到你需要信息的资源。是不是其他网站也有相同的数据?网站展示的数据是不是可以从别的网页爬去或者从别的网页整理统计?

特别是当遇到隐藏的或者格式糟糕的数据时,很重要的是不要一开始就去挖掘数据。做一个深呼吸想一想其他替代的方式。如果确定没有其他选择,那么这一章接下来就是为你准备的!

BeautifulSoup的其他功能

在第一章中,我们简单快速的看了一下BeautifulSoup的安装和运行,以及第一次选择对象。在这一部分,我们将讨论通过属性来寻找标签,通过标签列表来工作,以及解析导航树。几乎你遇到的网站都包含着样式。虽然你可能认为专门为浏览器和人去解释专门设计的分层格式是一件坏事,但事实上CSS样式的出现对网页爬虫来说是一件好事。CSS依赖于HTML元素的差异化,他们有精确的相同的标记方式来给他们不同的风格。也就是说,某些标签可以是这样的:

<span class="grenn"></span>

也有看起来是这样的:

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

网页爬虫可以轻易的从他们的类(class)上面分离出这两种不同的标签,例如,他们可能会使用BeautifulSoup来获取所有的的红色文字而不存在绿色文字。因为CSS依赖于这些标签属性让网站有着适合的样式,你可以肯定的是这些类(class)和ID属性将会大量的出现在现在的大部分网站上。

让我们建立一个爬虫示例来爬取位于http://bit.ly/1Ge96Rw的页面。在这个页面,故事中人物讲的话是红色的,人物本身的名字是绿色的,你可以看到span标签,标签引用了合适的CSS类,下面是网页的源代码示例:

"<span calss="red">Heavens! What a virtulent attack</sapn>" replied<span class=

"green">the prince</span>,not in the least disconcerted by this reception.

我们可以使用与第一章第一次使用BeautifulSoup类似抓取整个页面并且创建一个BeautifulSoup对象:

from urllib.request import urlopen

from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/warandpeace.html")

bsObj = BeautifulSoup(html)

使用这个BeautifulSoup对象,我们可以使用findAll()函数,来通过<span class="green"></span>标签提取我们需要的内容,findAll()函数返回是一个列表(Python List)(findAll是一个极其灵活的函数,我们会在接下来的内容中大量的使用)

nameList = bsObj.fanAll("span", {"class":"green"})

for name in nameList:

print(name.get_text())

当它开始运行的时候,将会列出所有正确的内容,有条理的而不是混乱的按照规定列出来。所以,这是怎么办到的呢?在此之前,我们调用bsObj.tagName来获取标签在页面上的第一次出现。现在,我们调用bsObj.findAll(tagName, tagAttributes)来用列表返回页面中所有的标签,而不仅仅是第一个。在得到名字的列表之后,程序通过遍历循环列表中所有的名字,并且通过name.get_text()打印出分离出来的标签内容。

什么时候使用get_text()什么时候保留标签呢?

.get_text()提取当前你要求的网页中的所有标签,仅仅返回字符串(string)类型的文本字符串。例如,你正在使用一个很大的文本块,其中包含很多超链接,段落和其他标记,所有的这些都会被剔除,你将会留下所有文字的标签块。请记住,在BeautifulSoup对象中比在一个文本块中更容易找到你需要的。调用.get_text()永远是你在打印,存储或者操纵数据之前最后要做的事情,一般情况下,要尽可能的去尝试

用文本保存标签结构。

BeautifulSoup中的find()以及findAll()

find()和findAll()是BeautifulSoup模块中几乎是你最常用的两个函数。通过他们,你可以通过他们的各种属性很轻易的过滤HTML页面找到需要的多个标签,或者一个标签。这两个函数极其相似,可以通过BeautifulSoup文档中他们的定义证明:

findAll(tag, attributes, recursive, text, limit, keywords)

find(tag, attributes, recursive, text, keywords)

在所有的可能性中,95%的时间,你会发现自己只需要使用前面两个参数:标签和属性。所以,让我们看看所有理由的更多细节。

tag参数我们之前是见过的——你可以通过一个字符串标签或者甚至一个Python的列表存储的标签。例如,下面的代码将会返回一个保存所有标签头的列表①:

.findAll({'h1', 'h2', 'h3', 'h4', 'h5', 'h6'})

其中属性参数使用的是Python的字典属性,并且比配包含在字典中的每一个标签。例如,下面的函数将会返回HTML文档中所有绿色和红色的标签:

.findAll("span", {"class":"green", "class":"red"})

这个递归参数(recursive)是一个布尔值。是如何深入到文档中你想去的地方的呢?如果递归参数设置为True,函数将进入到下一层,下一层的下一层去匹配你提供的参数。如果是False,它只会在你文档的第一层匹配参数。默认情况下,recursive参数为True;除非在你明白你需要什么或者性能问题的时候,离开这个是一个好注意。

这个text参数是不同寻常的,它匹配的是基于标签的文本(text)内容,而不是标签的属性。例如,我们想在包围的标签中找“王子”出现的次数,我们可以通过下面的方式代替前面例子中的.findAll()函数:

nameList = bsObj.findAll(text="the prince")

print(len(nameList))

这个的输出是:7

限制参数仅仅是在findAll办法中使用,当limit参数是1是,find函数就相当于findAll函数。如果你仅仅对从页面中提取第一个X项目有兴趣,你也许会设置limit参数为1.值得注意的是,这是给你的他们在页面上第一次出现的位置,而不是你需要的那一个。这个keyword参数允许你选择包含特殊属性的标签。例如:

allText = bsObj.findAll(id="text")

print(allText[0].get_text())

一个keyword参数的警告:

keyword参数在一些情况下会非常有用。然而,从BeautifulSoup特征来说这个技术是多余的。请记住,任何可以用keyword完成的技术也可以用我们接下来讨论的技术(见正则表达式和Lambda表达式)。例如,下面两行是相同的:

bsObj.findAll(id="text")

bsObj.findAll("", {"id":"text"})

此外,当你使用keyword时候会遇见一些问题,最显著的是通过他们的class属性搜素时候时,因为class在Python中是一个受保护的关键字。即,class是Python的保留字,是不可以用作变量还活着参数名称的(与前面讨论的BeautifulSoup.findAll()是没有关系的)②。例如,你如果尝试以下调用,你会因为非标准使用class产生一个语法错误:

bsObj.findAll(class="green")

作为代替,你可以使用BeautifulSoup的一个笨拙的解决方案,其中包括了添加下划线:

bsObj.findAll(class_="green")

同样,你可以用引号把class括起来:

bsObj.findAll("", {"class":"green"})

此刻,你可能会问自己,“但是等等,我还没完全弄懂如何通过属性来获取标签列表——通过字典将属性传递给函数?”回想一下,通过列表存储标签用.findAll()函数充当一个属性过滤器(也就是说,它选择具有所有标签,具有标签1,标签2或者标签3..存储在列表中)。如果你有一个长长的标签列表,你可以结束很多你不想要的东西。关键字参数允许你添加一个额外的“过滤器”。findAll函数通过名称和属性寻找标签。但是,如果你需要在文档之中通过他的位置找到标签呢?这时候导航树就派上用场了。在第一章,我们看见了BeautifulSoup中的导航树的一个方向:

bsObj.tag.subTag.anotherSubTag

现在,让我们看看http://bit.ly/1KGe2Qk 的向上导航,对角横跨式的,这种备受质疑的

HTML导航树,作为一个爬取例子(如图2-1):

html

body

div.wrapper

h1

div.content

table#giftList

tr

th

th

th

th

tr.gift#gift1

td

td

span.excitingNote

td

td

img

…table rows continue…

div.footer

我们将会在接下来几部分使用相同的HTML结构作为示例。



















表2-1

列举了一些常用的正则表达式的符号,一个简短的解释和例子。这个列表时不完整的,并且就像前面说的,可能会因为语言产生一些不同。然而,这是在Python中的正则表达式最常用的12个字符,可以用来发现和选择几乎所有的字符串类型。

符号 意义 例子 会匹配到的

* 匹配*之前的字符,字表达式或者括号中的字符0次或者多次 a*b* aaaaaa,aaabbbb,bbbbbb

+ 匹配+之前的字符,子表达式或者括号中的字符至少1次(1次或者多次) a+b+ aaaaab,aaabbb,abbbb

[] 匹配括号中的任意字符(即,挑选其中的任何一个东西) [A-Z]* APPLE,CAPITLALS,QWERTY

() 一组的子表达式(这些都是在正则表达式的命令中首先操作的) (a*b)* aaabaab,abaab,ababaaaab

{m,n} 匹配{}之前的字符,子表达式或者括号中的字符m到n次(包含在内的) a{2,3}b{2,3} aabbbb,aaabb,aabb

[^] 匹配任何不在括号内的单个字符 [^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

?! “不包含”。这是个奇怪的配对符号,紧接着一个字符(或者正则表达式)

表明这个字符不应该出现的那里。这个 no-caps-here,用起来可能会非常棘手。

字符可能会在字符串的不同部分 $ymb0lsa4ef!ne被找到。如果试图完全消除

一个字符,使用在两端使用^和$ ^((?![A-Z]).)*$ $ymb0lsa4ef!ne

正则表达式;不总是规则

正则表达式的标准版本(我们在书是通过Python和BeautifulSoup中使用的)是根据Perl的语法。大多数现代编程语言使用这种或者是非常相似的。请注意,如果你在其他语言中使用正则表达式,你也许将会遇到问题。即使一些现代编程语言,如java,也有一些轻微的差异。当有疑问时候,请阅读文档。

正则表达式和BeautifulSoup:

如果上一节的正则表达式看起来和这本书的任务有点脱节,这里就将他们联系起来。当它开始爬取页面时BeautifulSoup和正则表达式一起使用。事实上,大多数函数(例如:find(id="aTagIdHere"))的string 参数使用正则表达式将会更好。我们通过一些列子来看看,爬取http://bit.ly/1KGe2Qk页面。注意这里有很多产品图片在网站上——他们采用以下组成:

<img src="../img/gifts/img3.jpg">

如果我们想抓取网站的所有产品的图片,他们看起来相当简单:仅仅使用.findAll("img")来抓取所有的图像标签,对吗?但是这里有个问题,除了明显的额外的图像(如,徽标),现代的网站经常有隐藏的图像,用来间隔和对齐的空白图像,以及其他你可能不知道的随机图像。当然,你统计的页面图片不仅仅是产品图片了。让我们假设页面布局会发生变化。或者说,无论出于何种原因,我们不希望依赖于页面中图片的位置找到正确的标签。这可能是你试图抓取整个网站中的随机分布的一些元素或者片数据。例如,有可能是一个页面顶部的特殊布局的特别产品图片,而不是其他。解决办法是去寻找一些关于标签本身的识别。在这种情况下,我们可以看产品的文件路径:

from urllib.request import urlopen

from bs4 import BeautifulSoup

import re

html = urlopen("http://www.pythonscraping.com/pages/page3.html")

bsObj = BeautifulSoup(html)

images = bsObj.fandAll("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中的任何参数使用,为你在寻找目标元素时提供了极大地灵活性。

访问属性:

到目前为止,我们已经看了如何访问并且过滤标签和访问内容。然而,很多时候在网页抓取中你不需要寻找一个标签的内容,你找的是他的属性。这个变的特别的有用,比如说标签<a>,当URL是包含在herf属性中或者<img>标签中,其中目标图像包含src属性。调用Python的列表属性就可以轻易的浏览标签对象:

myTag.attrs

请注意,这个从字面上返回一个Python的字典对象,这使得检索和操纵这些属性很容易,例如,可以按照下面的方式使用:

myImgTag.attrs['src']

Lambda表达式:

如果你受过正规的计算机科学的教育,你可能在学校第一次了解过Lambda表达式,然后就再也没有使用过。如果不是这样,那么你可能不熟悉这个(或者说熟悉只是在某个时间了解过)。在这一部分,我们不会深入到这些非常有用的功能中去,但是我们会看一下他们在网页爬虫中非常有用的功能。

从本质上讲,一个Lambda表达式是传递到另外一个函数作为一个函数的变量的,就是说,不是定义一个像f(x,y)这样的函数,你也许会定义一个像f(g(x),y)或者f(g(x),h(y))这样的函数。

BeautifulSoup提供了使得我们能够通过某种类型的参数带入到findAll()的功能。唯一的限制就是这些函数必须有一个标签对象作为参数并且返回一个布尔值。BeautifulSoup遇到的每一个标签都会被这个函数评估,如果评估结果是“True”的将会返回,否则会丢弃。

例如,以下检索具有两个属性的标签:

soup.findAll(lambda tag: len(tag.attrs) == 2)

这个是将会找具有以下特征的标签:

<div clsaa="body" id="content"></div>

<span style="color:red" class="title"></span>

在BeautifulSoup中使用lambda函数,可以作为正则表达式的一个很棒的替代,而且更简洁。

除了BeautifulSoup之外:

虽然BeautifulSoup是贯穿本书(并且是最流行的一种用于Python的HTML库),请记住,这不是唯一的选择。如果BeautifulSoup不能满足你的需要,看一下其他广泛使用的库:

lxml:

该库基于解析HTML和XML文档,是非常著名的低层次的和大量基于C语言的库。尽管需要花费一段时间来学习(一个陡峭的学习路线意味着你学习非常快),这在解析大多数HTML文档时非常的快。

HTML解析器:

这是Python的内置解析库。因为它不需要安装(相比于其他,显然,安装Python时就被安装),它也是使用非常方便。

①如果你希望得到文档中所有的h<some_level>标签,还有更多简洁的方式写这段代码来完成同样的事情。我们来看看BeautifulSoup和正则表达式中其他接近这种类型的办法。

②Python语言参考提供保护关键字

③你可能会问自己,有“非正则表达”吗?这种非正则表达超出了本书的范围,但是他们包括

“写一个素数数量的字符串a,刚好是字符串b的两倍数目”或者“写一个回文”。幸运的是,我在网络爬虫中从来没有遇到过需要识别这种字符串的情况。
转载请注明出处http://blog.csdn.net/qq_15297487
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息