《Python网络爬虫权威指南》读书笔记2(第2章:复杂HTML解析)
2.1 不是一直都要用锤子
避免解析复杂HTML页面的方式:
- 寻找“打印此页”的链接,或者看看网站有没有HTML样式更友好的移动版本(把自己的请求头设置成处于移动设备的状态,然后接受网站移动版)。
- 寻找隐藏在JavaScript文件里的信息。要实现这一点,你可能需要查看网页加载的JavaScript文件。
- 虽然网页标题经常会用到,但是这个信息也许可以从网页的URL链接里获取。
- 如果你要找的信息不只限于这个网站,那么你可以找找其他数据源。
2.2 再端一碗BeautifulSoup
下面让我们创建一个网络爬虫来抓取http://www.pythonscraping.com/pages/warandpeace.html这个网页。
在这个网页里,小说人物的对话内容是红色的,人物名称都是绿色的。你可以看到网页源代码里span标签引用了对应的CSS属性,如下所示:
"<span class="red">Heavens! what a virulent attack!</span>" replied <span class="green">the prince</span>, not in the least disconcerted by this reception.
可以使用第1章类似的程序抓取整个页面,然后创建一个BeautifulSoup对象:
from urllib.request import urlopen from bs4 import BeautifulSoup html = urlopen('http://www.pythonscraping.com/pages/warandpeace.html') bs = BeautifulSoup(html.read(),'html.parser') nameList = bs.findAll('span',{'class':'green'}) for name in nameList: print(name.get_text())
find_all函数提取只包含在<span class="green"></span>标签里的文字,这样就会得到一个人物名称的列表。
代码执行后会按照《战争与和平》中的人物出场顺序显示所有人名。调用bs.tagName只能获取页面中指定的第一个标签。调用bs.find_all(tagName,tagAttributes)可以获取页面中所有指定的标签。打印name.get_text(),就可以把标签中的内容分开显示了。
.get_text()会清除你正在处理的HTML文档中的所有标签,然后返回一个只包含文字的Unicode字符串。
通常在你准备打印/存储和操作最终数据时,应该最后才使用.get_text()。一般情况下,你应该尽可能地保留HTML文档的标签结构。
2.2.1 BeautifulSoup的find()和find_all()
BeautifulSoup文档里两者的定义就是这样:
find_all(tag,attributes,recursive,text,limit,keywords)
find(tag,attributes,recursive,text,keywords)
可能你会发现,自己在95%的时间里都只需要使用前两个参数:tag和attributes。
标签tag:你可以传递一个标签的名称或多个标签名称组成的Python列表做标签参数。
属性参数 attributes:用一个Python字典封装一个标签若干属性和对应的属性值。
递归参数recursive:是一个布尔变量。如果recursive设置为True,find_all就会根据你的要求去查找标签参数的所有标签,以及子标签的子标签。如果recursive设置为False,find_all就只查找文档的一级标签。find_all默认是支持递归查找的(默认值为True)
文本参数text:它用标签的文本内容去匹配,而不是用标签的属性。
范围限制参数limit:显然只用于find_all方法。find其实等价于limit等于1时的find_all。
关键词参数keyword:可以让你选择那些具有指定属性的标签。例如:
title = bs.find_all(id='title'.class='text')
页面中的每个id的属性值只能被使用一次。因此在实际情况中,上面的代码可能并不实用,而下面的代码可以达到同样的效果:
title = bs.find(id = 'title')
class是Python语言的保留字,在Python程序里是不能当作变量或参数名使用的。
2.2.2 其他BeautifulSoup对象
NavigableString对象
用来表示标签里的文字,而不是标签本身。
Comment对象
用来查找HTML文档的注释标签<!-- 像这样-->。
2.2.3 导航树
用虚拟的在线购物网站http://www.pythonscraping.com/pages/page3.html作为要抓取的示例网页,演示HTML导航树的纵向和横向导航。
1、处理子标签和其他后代标签
子标签就是父标签的下一级,而后代标签是指父标签下面所有级别的标签。
如果只想找出子标签,可以用.children标签:
from urllib.request import urlopen from bs4 import BeautifulSoup html = urlopen('http://www.pythonscraping.com/pages/page3.html') bs = BeautifulSoup(html,'html.parser') for child in bs.find('table',{'id':'giftList'}).children: print(child)
这段代码会打印giftList表格中所有产品的数据行,包括最开始的列名行。如果你用descendants()函数而不是children()函数,那么就会打印出二十几个标签,包括img标签、span标签,以及每个td标签。
<tr><th> Item Title </th><th> Description </th><th> Cost </th><th> Image </th></tr> <tr class="gift" id="gift1"><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> <tr class="gift" id="gift2"><td> Russian Nesting Dolls </td><td> Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span> </td><td> $10,000.52 </td><td> <img src="../img/gifts/img2.jpg"/> </td></tr> <tr class="gift" id="gift3"><td> Fish Painting </td><td> If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span> </td><td> $10,005.00 </td><td> <img src="../img/gifts/img3.jpg"/> </td></tr> <tr class="gift" id="gift4"><td> Dead Parrot </td><td> This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span> </td><td> $0.50 </td><td> <img src="../img/gifts/img4.jpg"/> </td></tr> <tr class="gift" id="gift5"><td> Mystery Box </td><td> If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span> </td><td> $1.50 </td><td> <img src="../img/gifts/img6.jpg"/> </td></tr>
2、处理兄弟标签
BeautifulSoup的next_siblings()函数使得从表格中收集数据非常简单,尤其是带标题行的表格:
from urllib.request import urlopen from bs4 import BeautifulSoup html = urlopen('http://www.pythonscraping.com/pages/page3.html') bs = BeautifulSoup(html,'html.parser') for sibling in bs.find('table',{'id':'giftList'}).tr.next_siblings: print(sibling)
这段代码会打印产品表格里所有行的产品,第一行表格标题除外(对象不能是自己的兄弟标签)
<tr class="gift" id="gift1"><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> <tr class="gift" id="gift2"><td> Russian Nesting Dolls </td><td> Hand-painted by trained monkeys, these exquisite dolls are priceless! And by "priceless," we mean "extremely expensive"! <span class="excitingNote">8 entire dolls per set! Octuple the presents!</span> </td><td> $10,000.52 </td><td> <img src="../img/gifts/img2.jpg"/> </td></tr> <tr class="gift" id="gift3"><td> Fish Painting </td><td> If something seems fishy about this painting, it's because it's a fish! <span class="excitingNote">Also hand-painted by trained monkeys!</span> </td><td> $10,005.00 </td><td> <img src="../img/gifts/img3.jpg"/> </td></tr> <tr class="gift" id="gift4"><td> Dead Parrot </td><td> This is an ex-parrot! <span class="excitingNote">Or maybe he's only resting?</span> </td><td> $0.50 </td><td> <img src="../img/gifts/img4.jpg"/> </td></tr> <tr class="gift" id="gift5"><td> Mystery Box </td><td> If you love suprises, this mystery box is for you! Do not place on light-colored surfaces. May cause oil staining. <span class="excitingNote">Keep your friends guessing!</span> </td><td> $1.50 </td><td> <img src="../img/gifts/img6.jpg"/> </td></tr>
让标签的选择更具体
如果我们选择bs.table.tr或者直接用bs.tr来获取表格中的第一行,上面的代码也可以获取正确的结果。但是,更完整的代码为:bs.find('table',{'id':'giftList'}).tr
即使页面上只有一个表格(或其他目标标签),只用标签也很容易丢失细节。另外页面布局是不断变化的。如果想让爬虫更稳定,最好还是让标签的选择更加具体。如果有属性,就利用标签的属性。
3、处理父标签
from urllib.request import urlopen from bs4 import BeautifulSoup html = urlopen('http://www.pythonscraping.com/pages/page3.html') bs = BeautifulSoup(html,'html.parser') print(bs.find('img', {'src': '../img/gifts/img1.jpg'}) .parent.previous_sibling.get_text())
$15.00
<tr>
—td
—td ③
—“$15.00" ④
—td ②
—<imgsrc= "../img/gifts/img1.jpg" ①
实现步骤①首先选择图片src=“../img/gifts/img1.jpg”。
②选择图片标签的父标签
③选择td标签的前一个兄弟标签previous_sibling
④选择标签中的文字,“$15.00”。
2.3 正则表达式
正则表达式,可以识别正则字符串;也就是说,它们可以这么定义:”如果你给我的字符串符合规则,我就返回它“,或者是”如果字符串不符合规则,我就忽略它“。
什么是正则字符串?
(1)字母”a“至少出现一次;
(2)后面跟着字母”b“,重复5次;
(3)后面再跟着字母”c“,重复任意偶数次;
(4)最后一位是字母”d“或”e“
正则表达式就是表达这组规则的一种快捷方式。这组规则的正则表达式:aa*bbbbb(cc)*(d|e)
aa*表示”重复任意次a,包括0次“。
(cc)*表示任意偶数个字符都可以编组,这个规则是用括号括住两个c,然后后面跟一个星号,表示可以有任意两个c(也可以是0次)。
(d|e)在两个表达式中间增加一个竖线(|)表示”这个或那个“。
正则表达式在实际中的一个经典应用是识别邮箱地址。
规则 | 正则表达式 |
---|---|
1). 邮箱地址的第一部分至少包括一种内容:大写字母、小写字母、数字0~9、点号(.)、加号(+)或下划线(_) | [A-Za-z0-9._+]+:这个正则表达式简写非常智慧。例如,它用“A-Z”表示“任意A~Z 的大写字母”。把所有可能的序列和符号放在中括号(不是小括号)里表示“括号中的符号里任何一个”。要注意后面的加号,它表示“这些符号都可以出现多次,且至少出现1 次” |
2). 之后,邮箱地址会包含一个@ 符号 | @:这个符号很直接。@ 符号必须出现在中间位置,有且仅有1 次 |
3). 在符合@ 之后,邮箱地址还必须至少包含一个大写或小写字母 | [A-Za-z]+:可能只在域名的前半部分、符号@ 后面用字母。而且,至少有一个字母 |
4). 之后跟一个点号(.) | \.:在域名前必须有一个点号(.)退格在这里用作转义字符 |
5). 最后邮箱地址用com、org、edu、net 结尾(实际上,顶级域名有很多种可能,但是作为示例演示这四个后缀够用了)。 | (com|org|edu|net):这样列出了邮箱地址中可能出现在点号之后的字母序列 |
注意,正则表达式并非处处正则。
2.4 正则表达式和BeautifulSoup
其实,大多数支持字符串参数的函数(比如,find(id='aTagIdHere'))也都支持正则表达式。
from urllib.request import urlopen from bs4 import BeautifulSoup import re html = urlopen('http://www.pythonscraping.com/pages/page3.html') bs = BeautifulSoup(html,'html.parser') images = bs.find_all('img', {'src':re.compile('\.\.\/img\/gifts/img.*\.jpg')}) for image in images: print(image['src'])
../img/gifts/img1.jpg ../img/gifts/img2.jpg ../img/gifts/img3.jpg ../img/gifts/img4.jpg ../img/gifts/img6.jpg
2.5 获取属性
myTag.attrs 获取它的全部属性
myTag.attrs['scr'] 获取图片的源位置scr
2.6 Lambda表达式
Lambda 表达式本质上就是一个函数,可以作为变量传入另一个函数;也就是说,一个函数不是定义成f(x,y),而是定义成f(g(x),y)或f(g(x),h(y))的形式。
BeautifulSoup允许我们把特定类型的函数作为参数传入find_all函数。唯一的限制条件是这些函数必须把一个标签对象作为参数并且返回布尔类型的结果。BeautifulSoup用这个函数来评估它遇到的每个标签对象,最后把评估结果为”真“的标签保留,把其他标签剔除。
例如,下面的代码就是获取有两个属性的所有标签:
bs.find_all(lambda tag: len(tag.attrs) == 2)
作为参数传入的函数是len(tag.attrs) == 2。当该参数为真时,find_all函数将返回tag。
Lambda函数非常实用,甚至可以用它代替现有的BeautifulSoup函数:
bs.find_all(lambda tag:tag.get_text() == 'Or maybe he\'s only resting?')
如果不使用Lambda函数,代码如下:
bs.find_all('', text='Or maybe he\'s only resting?')
输出:
[<span class="excitingNote">Or maybe he's only resting?</span>] ["Or maybe he's only resting?"]
由于Lambda函数可以时任意返回True或者False指的函数,你甚至可以结合使用Lambda函数于正则表达式,来查找匹配特定字符串模式的属性的标签。