HTML高级解析
当Michelangelo(米开朗琪罗)被问及他是如何雕刻出像大卫那样艺术品时,他非常有名的回答是:“这很简单啊,你只要削掉哪些不像大卫那部分的石头就行了。”
即使网络爬虫在大多数方面并不像大理石雕刻,但是,当我们试图从复杂网页中提取信息时,我们一定要持像雕刻一样的态度。这里有许多的技术可以用来去掉哪些看起来并不是我们要寻找的内容,知道我们成功的找到我们想要的信息。在这一章,为了只提取我们想要的信息,我们将要学习解析复杂的HTML页面。
你不总是需要一把锤子(Hammer)
这是具有诱惑力的,当面对一个复杂的(Gordian Knot注释:戈尔迪之结:弗利基亚国王戈尔迪所打的结,按神谕只有将统治亚细亚者才能解开,后被亚历山大大帝用利剑斩开)标签时,可以深入的使用多行语句去尝试提取你需要的信息。但是,请记住,在这里不顾后果的使用分层技术会导致代码难以调试,代码健壮性低,或者两种情况都会有。在开始之前,我们来看一看一些完全能避免这些问题的一些高级HTML解析方式。
比如说你有一些目标内容。也许它是一个名字,统计数值,或文本块。也许是20个标记深埋在一个HTML中,(mush with no helpful tags,怎么翻译?)没有有用的标记或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="green"></span>
另一些看起来是这样的
<span class="red"></span>
网络爬虫可以通过它们的class
轻易的区分这两个标签之间的不同;例如,他们可以使用BeautifulSoup获取所有红色文本但不要绿色文本。因为CSS依赖于这些标签属性让网站有着适合的样式,你几乎可以保证是这些class和ID属性在现在的大部分网站上是极其丰富的。
让我们创建一个网络爬虫的例子来趴着这个连接的页面,在这个页面,故事中人物讲的话是红色的,人物本身的名字是绿色的,你可以看到span标签,标签引用了合适的CSS类,下面是网页的源代码示例:
<span class="red">Heavens! what a virulent attack!</span>" replied
<span class="green">the prince</span>
, not in theleast disconcerted by this reception.
我们可以抓取整个页面并且创建一个BeautifulSoup对象并像在第一章中那样使用它。
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/warandpeace.html")
bsObj = BeautifulSoup(html.read())
使用这个BeautifulSoup对象,我们可以使用findAll()函数,来通过< span class=”green”>< /span>标签提取我们需要的内容,findAll()函数返回是一个列表(Python List)(findAll是一个极其灵活的函数,我们会在接下来的内容中大量的使用)
nameList = bsObj.findAll("span",{"class":"green"})
for name in nameList:
print(name.get_text())
当运行时,会根据在文本在《战争与和平》中出现的顺序列出所有的名词。在这之前,我们调用bsObj.tagName
来获取标签在页面上第一次出现的tagName标签。现在,我们调用bsObj.findAll(tagName, tagAttributes)
来用列表返回页面中所有的标签,而不仅仅是第一个。在得到名字的列表之后,程序通过遍历循环列表中所有的名字,并且通过name.get_text()
打印出分离出来的标签内容。
什么时候使用get_text()什么时候保留标签
.get_text()提取当前你要求的网页中的所有标签,仅仅返回字符串(string)类型的文本字符串。例如,你正在使用一个很大的文本块,其中包含很多超链接,段落和其他标记,所有的这些都会被剔除,你将会留下所有文字的标签块。请记住,在BeautifulSoup对象中比在一个文本块中更容易找到你需要的。调用.get_text()永远是你在打印,存储或者操纵数据之前最后要做的事情,一般情况下,要尽可能的去尝试
BeautifulSoup的find()和findAll()
BeautifulSoup的find()
和findAll()
将是你最常使用的函数。通过它们,你能够轻松的基于标签的多种属性过滤HTML页面而获得你想要的标签的列表或单独的标签。
这两个函数是极其相似的它们在BeautifulSoup文档中的定义可以证明这一点: findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive,text,keywords)
十有八九,95%的时候你会发现自己只需要使用前两个参数:tag
和attributes
。但是让我们更详细的看看所有的这些参数。 tag
参数是我们之前看到的一个,你可以传递一个字符串类型的标签名称或者甚至是一个包含标签名称的字符串Python列表。例如,下面将返回一个文档中所有包含header
标签的列表: .findAll({"h1","h2","h3","h4","h5","h6"})
其中属性参数使用的是Python的字典属性,并且匹配包含在字典中的每一个标签。例如,下面的函数将会返回HTML文档中所有绿色和红色的标签: .findAll("span"",{"class":"green","class":"red"})
递归(recursive)
参数是布尔类型。是如何深入到文档中你想要去的地方?如果recursive
为True
,那么 findAll
函数将进入到下一层,下一层的下一层去匹配你提供的参数。如果是False,它只会在你文档的第一层匹配参数。默认情况下,recursive参数为True;不要管它是一个好主意,除非在你明白你需要什么或者性能问题的时候。
这个text
参数是不同寻常的,它匹配的是基于标签的文本(text)内容,而不是标签的属性。例如,我们想在包围的标签中找the prince
出现的次数,我们可以通过下面的方式代替前面例子中的.findAll()函数:
nameList = bsObj.findAll(text="the prince")
print(len(nameList))
输出结果为'7'
limit
参数只用在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中是一个受保护的关键字。不能用来作为变量或者参数名(这与之前说的BeautifulSoup.findAll()的keyword
参数无关)。例如,如果你试图用下面的调用,你会因为非标准使用class产生一个语法错误:
bsObj.findAll(class="green")
作为代替,你可以使用BeautifulSoup的一个笨拙的解决方案,其中包括了添加下划线:
bsObj.findAll(class_="green")
同样,你可以用引号把class括起来:
bsObj.findAll("",{"class":"green"}
此时,你可能会问你自己,“但是先等等,我还没完全弄懂如何通过属性来获取标签列表——通过字典将属性传递给函数?”
回想一下,通过列表存储标签用.findAll()函数充当一个属性过滤器(也就是说,它选择具有所有标签,具有标签1,标签2或者标签3..存储在列表中)。如果你有一个长长的标签列表,你可以结束很多你不想要的东西。关键字参数允许你添加一个额外的“过滤器”。
其他BeautifulSoup对象
本书到目前为止,你已经见到过BeautifulSoup库的两种类型的对象: bsObj.div.h1
- BeautifulSoup对象:看前面示例代码中的bsObj
- 标签对象:通过调用BeautifulSoup对象的find
和findAll
,获取列表和单独的(内容)
但是,BeautifulSoup库中可不止这两个对象,即使不常用,但对我们来说也非常有必要去了解:
NavigableString对象:
用来描述被标签包裹的文本,而不是标签本身 (some functions operate on,and produce,NavigableStrings,rather than tag objects,这个自己看着理解吧,不好翻译)
The Comment对象(即注释):用来找出HTML中被注释标签包裹的注释内容,<!-- like this one -->
这四个对象是唯一你会在在BeautifulSoup库中遇到的(我写到现在为止)
导航树
findAll
函数的功能是基于name
和attribute
寻找标签。但是,如果你需要基于文档中的位置来寻找标签呢?这正是导航树发挥作用的地方。在第一章中,我们看见了BeautifulSoup中的导航树的一个方向: bsObj.tag.subTag.anotherSubTag
现在,让我们看看https://bit.ly/1KGe2Qk 的向上导航,对角横跨式的,这种备受质疑的HTML导航树,作为一个爬取例子(如图2-1):放不了图~~,自己看书吧
这个页面的HTML绘制出来就是一颗树(为了简洁而省略一些标签),就像下面这样:
- html
- body
-
- div.wrapper
-
-
- h1
-
-
-
- div.content
-
-
-
- table#giftList
-
-
-
-
- tr
-
-
-
-
-
-
- th
-
-
-
-
-
-
-
- th
-
-
-
-
-
-
-
- th
-
-
-
-
-
-
-
- th
-
-
-
-
-
-
- tr.gift#gift1
-
-
-
-
-
-
- td
-
-
-
-
-
-
-
- td
-
-
-
-
-
-
-
-
- span.excitingNote
-
-
-
-
-
-
-
-
- td
-
-
-
-
-
-
-
- td
-
-
-
-
-
-
-
-
- img
-
-
-
-
-
-
-
- …tabel rows continue…
-
-
-
- div.footer
我们会在接下来的几个地方使用这个同样的HTML结构作为例子。
但是任性的我还是决定将网页源码贴出来方便理解和查看,代码如下:
<body>
<div id="wrapper">
<img src="../img/gifts/logo.jpg" style="float:left;">
<h1>Totally Normal Gifts</h1>
<div id="content">Here is a collection of totally normal, totally reasonable gifts that your friends are sure to love! Our collection is
hand-curated by well-paid, free-range * monks.<p>
We haven't figured out how to make online shopping carts yet, but you can send us a check to:<br>
123 Main St.<br>
Abuja, Nigeria
</br>We will then send your totally amazing gift, pronto! Please include an extra $5.00 for gift wrapping.</div>
<table id="giftList">
<tr><th>
Item Title
</th><th>
Description
</th><th>
Cost
</th><th>
Image
</th></tr>
<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>
<tr id="gift2" class="gift"><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 id="gift3" class="gift"><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 id="gift4" class="gift"><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 id="gift5" class="gift"><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>
</table>
</p>
<div id="footer">
© Totally Normal Gifts, Inc. <br>
+234 (617) 863-0736
</div>
</div>
</body>
处理孩子和后代
(注:直接子节点称为children,子节点的子节点称为descendants(后代))
在计算机科学和一些数学分支,你经常会听到关于处理子节点的可怕的事情:移动,存储,删除甚至是’杀死’他们。幸运的是,在BeautifulSoup中,处理子节点的方式有所不同。
在BeautifulSoup库中,就像许多其他的库一样,在孩子(children)和后代(descendants)之间有所区别:和人类家谱极其相似,children就是准确指在父节点下面的一个标签,相反,后代可以指父节点下面任意层的标签。例如,tr
是table
的children
,然而tr
,th
,td
,img
和span
都是table
标签的后代(至少在我们的例子中是这样)。所有的children
都是descendants
,但不是所有的descendants
都是children
。
一般来说,BeautifulSoup函数总是会处理当前被选择标签的descendants
。例如,bsObj.body.h1
选择的第一个h1
标签是body
标签的后代(descendants
)。他不会在body
标签之外进行寻找。
类似的,bsObj.div.findAll("img")
将会寻找文档的第一个div
标签,然后获取div
标签后代中所有img
标签的列表。
如果你只是想要后代中的孩子节点,你可以使用.children
标签:
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html.read())
for child in bsObj.find("table",{"id":"giftList"}).children:
print (child)
这个代码输出的是id
为giftList
的表格中每一行的产品列表。如果你使用descendants
函数代替children
函数,大约会有24个标签会被找到,包括img
,span
,和个别的td
标签。区分children
和descendants
是绝对重要的!
我的理解,HTML如下:
<table>
<tr>
<td>A</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
<td>D</td>
</tr>
</table>
使用
bsObj.find("table").children(这是默认使用的,即不写
.children)的结果为:
bsObj.find(“table”).descendants`的结果为(很像递归啊):
< tr>< td>A< /td>< td>B< /td>< /tr>
< tr>< td>C< /td>< td>D< /td>< /tr>
使用
< tr>< td>A< /td>< td>B< /td>< /tr>
< td>A< /td>
A
< td>B< /td>
B
< tr>< td>C< /td>< td>D< /td>< /tr>
< td>C< /td>
C
< td>D< /td>
D
处理兄弟姐妹
BeautifulSoup的next_siblings()
函数可以很容易的从表格中收集数据,特别是带有标题行的。
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html.read())
for sibling in bsObj.find("talbe",{"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>
这段代码的输出结果为产品表格中除了第一行以外的所有行的产品。为什么标题行被跳过了呢?两个原因:首先,对象不能是自己的兄弟。任何时候你都可以获取一个对象的兄弟,但是对象自己不会被包含在列表中。第二个原因是,这个函数只调用next siblings
即下一个兄弟。如果我们去选择列表中的一行,例如,对列表调用next_siblings
,只有后来的(下一个)兄弟被返回。所以,通过选择标题行和调用next_sibling
,我们可以选择除了标题行自身以外表格中所有行。
使用特定的选择
注意,如果我们选择bsObj.table.tr
或者甚至bsObj.tr
来选表格中的第一行,前面的代码也可以很好的工作。但是,在代码中,I go through all of the trouble of writing everything out in a longer form(在写很长的表单时我经历了所有的麻烦~~不会翻译):bsObj.find("table",{"id":"giftList"}).tr
甚至页面看起来只有一个表格(或者其他目标标签),也很容易遗漏一些东西。另外,页面布局时刻在变化。曾经在页面上出现的第一个标签,也许一些天之后就变成了第二个或第三个标签。为了使你的爬虫更鲁棒,使用越特殊的标签越好。当标签属性可用的时候尽量用它们。
作为next_siblings
的一个组成部分,如果有一个易于选取的标签在兄弟标签列表的末尾那么previous_siblings
函数能够经常发挥作用。
当然,next_sibling
和previous_sibling
函数的表现几乎相同,除了他们返回的是单个标签而不是标签列表。
处理你的父节点
当爬取页面时,你会发现你需要找父节点标签的频率要少于你需要找他们孩子节点或兄弟节点的频率。典型地,当我们呆着爬取目的来看HTML页面时,我们总是从最上层的标签看起,并且找出如何钻到我们想要的准确的数据。但是偶尔你会发现在某些情况下需要BeautifulSoup的parent-finding
函数,.parent
和.parents
。例如:
from urllib.request import urlopen
from bs4 import BeautiflSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautiflSoup(html.read())
print(bsObj.find("img",{"src":"../img/gifts/img1.jpg"}).parent.previous_sibling.get_text())
这个代码将会输出在../img/gifts/img1/jpg
的图片所代码的对象的价格(这里,价格是”$15.00”)
代码是如何工作的呢?下面的图代表了我们工作所需的HTML页面的一部分代码的树结构,带编号的步骤:
<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>
- 首先选择
src="../img/gifts/img1.jpg"
处的图片标签 - 我们选择父标签(这里指,< td>标签)
- 我们选择
< td>
的前一个兄弟标签(previous_sibling)(这里指的是,包含产品价格的< td>标签) - 我们选择标签的文本,”$15.00”
正则表达式
正则表达式的内容较多且繁琐,需要找一本详细介绍正则表达式的书来看一看,在这里我不准备翻译本书关于正则表达式的内容了,博主本人是通过《python 核心编程》这本书来学习的正则表达式的。有兴趣的同学可以去翻一翻。
正则表达式和BeautifulSoup
如果前面的正则表达式部分看起来和本书的任务有点脱节,那么这里将一切联系起来。当爬取网页时将BeautifulSoup和正则表达式结合起来使用。其实,大多数的函数带入一个字符串类型的参数(e.g,find(id=”aTagIdHere”))带入正则表达式也可以很好的工作。
让我们看一些例子,爬取页面:http://www.pythonscraping.com/pages/page3.html。要注意,这里有许多的产品图片在网站中–它们采取以下形式:
<img src="../img/gifts/img3.jpg">
如果我们想要获取所有的产品图片的URL,首先,这似乎看起来相当直接了当:只要使用`.findAll(“img”)抓取所有的图片标签,对吗?但是这里有一个问题。除了明显的”额外“的图片(例如,logos),现代网站经常有隐藏图片,空白图片作为间隔和校准元素,并且你可能没有注意到另一些随机图片标签。确实,你不能认为页面中只有产品图片。
让我们仍然假设页面布局可能在变化,或者因为其他任何原因,我们不想根据图片在页面的位置来找出正确的标签。这可能是当你试图抓取特殊元素或者是随机分散在页面中的数据出现的情况。例如:这里也许有一个有特色的产品图片在页面顶部的特殊位置,但其他的产品图片并不在。
解决方案是寻找标签自己的一些标识。在这种情况下,我们可以看产品图片的文件路径:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html.read())
images = bsObj.findAll("img",{"src":re.compile("\.\.\/img\/gifts/img.*\.jpg")})
for image in images:
print(imgage["src"])
输出结果只有以../img/gitfs/img
开头并以.jpg
结尾为路径的图片:
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg
在BeautifulSoup表达式中,正则表达式可以被作为任何参数插入,为你找到目标元素提供了很大的灵活性。
访问属性
迄今为止,我们学习了如何获取和过滤标签,并且用它们存取内容。但是,在网络爬取时你经常做的不是要寻找标签的内容,而是要找标签的属性。这在像< a>
这样的标签中特别有用,URL被包含在href
属性中,或像< img>
标签,图片的目标地址被包含在src
属性中。
对于标签对象,一个属性的Python列表可以通过调用myTag.attrs
自动存取。注意,它返回的是一个Python字典对象,这使得检索属性变得简单。例如,一个图片的源位置可以通过使用下面的代码找到: myImgTag.attrs['src']
Lambda 表达式
学过Python基础的同学应该都知道Lambda表达式了吧,这里我就不多说了,只举一个例子
下面是获取所有具有两个属性的标签的lambda表达式:
soup.findAll(lambda tag: len(tag.attrs)==2)
获取的结果如下:
< div class=”body” id=”content”>< /div>
< span style=”color:red” class=”title”>< /span>
这些标签都只具有两个属性
在BeautifulSoup中使用Lambda,如果你想要写很少的代码,那么选择器可以作为很好的正则表达式的替代。
The End
啊啊啊啊啊,这一章终于结束了
2016年8月23日 11:03:17
转载请注明出处:http://blog.csdn.net/zwx2445205419/article/details/52266182