Python3 使用 XPath 和 Beautiful Soup4 进行HTML内容解析

一、Python 爬虫HTML知识点

HTML 是一种结构化的标记语言。HTML可以描述一个网页的结构信息。HTML与CSS(Cascading Style Sheets,层叠样式表)、JavaScript一起构成了现代互联网的基石。先以地名为例,来看HTML代码的结构关系:

    <中国>
        <北京>
          <海淀区>
              <五道口>
                  ×x牛肉火锅店​
              </五道口>
          </海淀区>
          <东城区></东城区>
        </北京>
        <陕西​>
          <西安></西安​>
          <咸阳></咸阳​>
        </陕西​>
    </中国>

在这个以地名表示HTML结构的例子中,出现了很多用尖括号括起来的地名,而且这些地名都是成对出现的。有<北京>就有</北京>,有<陕西>就有</陕西>。在HTML中,这叫作标签。一个标签可以表示为:

    <标签名>
        文本
    </标签名>

不加斜杠,表示标签开始;加上斜杠,表示标签结束。它们中间的部分,就是标签里面的元素。标签里面可以是另一个标签,也可以是一段文本。标签可以并列,也可以嵌套。例如<北京>与<陕西>就属于并列关系。而<北京>与<海淀区>就是属于嵌套的关系。不论谁在谁旁边,不论谁包含了谁,通过HTML的这种表示方法,都可以轻易将不同标签的相对关系表现出来。

再来看一段真正的HTML代码的结构:

<html>
<head>
  <title>测试</title>
</head>
<body>
<div class="useful">
  <ul>
    <li class="info">我需要的信息1</li>
    <li class="info">我需要的信息2</li>
    <li class="info">我需要的信息3</li>
  </ul>
</div>
<div class="useless">
  <ul>
    <li class="info">垃圾1</li>
    <li class="info">垃圾2</li>
  </ul>
</div>
</body>
</html>

对比这一段真实的HTML代码和上面地名的例子,可以看到,在结构上面,它们是完全一样的。只不过在真实的HTML代码里面,每个标签除了标签名以外,还有“属性”。一个标签可以有0个、1个或者多个属性,所以一个真正的HTML标签应该是下面这样的:

    <标签名 属性1="属性1的值" 属性2="属性2的值">显示在网页上的文本</标签名>

它可以被表示成一个倒立的树形结构,如图所示。

HTML就是通过这样一种一层套一层的结构来描述一个网页各个部分的相对关系的。这里的<html></html>、<div></div>等都是HTML的标签。如果把HTML最外层的标签<html>当作树根,从树根上面分出了两个树枝<head>和<body>,<body>里面又分出了class分别为useful和useless的两个树枝<div>……正如北京在中国里面,清华大学在北京里面……因此,根据每个树枝独特的标志,一步一步找下去,就可以找到特定的信息。

二、使用 XPath 进行HTML内容解析

2.1 XPath的介绍

XPath(XML Path)是一种查询语言,它能在XML(Extensible Markup Language,可扩展标记语言)和HTML的树状结构中寻找结点。形象一点来说,XPath就是一种根据“地址”来“找人”的语言。用正则表达式来提取信息,经常会出现不明原因的无法提取想要内容的情况。最后即便绞尽脑汁终于把想要的内容提取了出来,却发现浪费了太多的时间。需要寻找的内容越复杂,构造正则表达式所需要花费的时间也就越多。而XPath却不一样,熟练使用XPath以后,构造不同的XPath,所需要花费的时间几乎是一样的,所以用XPath从HTML源代码中提取信息可以大大提高效率。

在Python中,为了使用XPath,需要安装一个第三方库:lxml。

2.2 lxml的安装

1.在Mac OS下安装lxml

如果操作系统为Mac OS,可以直接使用pip 安装lxml 。

pip install lxml

2.在Ubuntu下安装lxml

如果操作系统为Ubuntu,可以使用如下命令安装lxml:

sudo apt-get install python-lxml

3.在Windows下安装lxml

如果操作系统为Windows,那么安装lxml前需要安装好 Python3 和 pip 基础环境。

pip install lxml

4.验证lxml安装是否安装成功

打开Python的交互环境,输入mport lxml,如果不报错,就表示安装成功。

2.3 XPath语法讲解

如果要从上面的HTML代码中提取出以下信息,该怎么办?

    我需要的信息1
    我需要的信息2
    我需要的信息3

如果使用正则表达式应该要写几行代码才能实现。如果使用XPath,代码只有一行:

    info = selector.xpath('//div[@class="useful"]/ul/li/text()')

这一行代码可以直接返回一个列表,列表中就是需要提取的3句话。

使用XPath的代码如下:

  import lxml html
  selector = lxml.fromstring(’网页源代码’)
  info = selector.xpath(’一段XPath语句’)

其中的“网页源代码”可以使用requests来获取。“一段XPath语句”可以按照一定的规则来构造。

1.XPath语句格式

核心思想:写XPath就是写地址。

获取文本:

    //标签1[@属性1="属性值1"]/标签2[@属性2="属性值2"]/..../text()

获取属性值:

    //标签1[@属性1="属性值1"]/标签2[@属性2="属性值2"]/..../@属性n

其中,[@属性="属性值"]不是必需的。它的作用是帮助过滤相同的标签。在不需要过滤相同标签的情况下可以省略。

2.标签1的选取

标签1可以直接从html这个最外层的标签开始,一层一层往下找,这个时候,XPath语句是这样的:

    /html/body/div[@class="useful"]/ul/li/text()

当以html开头的时候,它前面是单斜线。这样写虽然也可以达到目的,但是却多此一举。正如在淘宝买东西时,没有人会把收货地址的形式写为“地球,亚洲,中国,北京,海淀区,××路,××号”一样。地址前面的“地球,亚洲,中国”写了虽然也没错,但却没有必要。谁都知道全世界只有一个北京。而北京必定在中国,中国必定在亚洲,亚洲必定在地球上。所以,写收货地址的时候,直接写北京就可以了,前面的“地球,亚洲,中国”可以省略。XPath也是同样的道理。在XPath里面找到一个标志性的“地标”,然后从这个标志性的“地标”开始往下找就可以了。标志性的“地标”前面的标签都可以省略。那么,如何确定应该从哪个标签开头呢?其原理就是5个字:“倒着找地标”。也就是,从需要提取的内容往上找标签,找到一个拥有“标志性属性值”的标签为止。

上面举例中的HTML代码中,需要的信息所在的标签为<li class="info">,这个标签的class属性的值为“info”。那能不能用它来定位呢?答案是不能,因为在代码里,虽然需要的内容是使用这个标签包起来的,但是不需要的内容也是使用这个标签包起来的。这就说明这个标签的属性值不够独特,不能称为“拥有标志性属性值的标签”。因此,如果使用这个标签开始,就会导致需要的内容和不需要的内容混在一起。

继续往上找,发现<div class="useful">,这个标签很独特。它的class属性的值“useful”独一无二,而且需要提取的内容又都在这个<div>标签里面。所以这个标签可以称得上是“拥有标志性属性值的标签”,可以从这个标签开始来定位。于是定位的XPath就可以写成:

    //div[@class="useful"]/ul/li/text()

3.哪些属性可以省略

来细看下面这个代码片段:

    <div class="useful">
        <ul>
            <li class="info">我需要的信息1</li>
            <li class="info">我需要的信息2</li>
            <li class="info">我需要的信息3</li>
        </ul>
    </div>

<ul>标签本身就没有属性,则写XPath的时候,其属性可以省略。标签有属性,但是如果这个标签的所有属性值都相同,则可以省略属性,例如<li class="info">,所有的<li>标签都有一个class属性,值都为info,所以属性可以省略。

4.XPath的特殊情况

(1)以相同字符串开头有一段如下的HTML代码:

<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <div id="test-1">需要的内容1</div>
    <div id="test-2">需要的内容2</div>
    <div id="testfault">需要的内容3</div>
    <div id="useless">这是我不需要的内容</div>
  </body>
</html>

要抓取“需要的内容1”“需要的内容2”和“需要的内容3”,如果不指定<div>标签的属性,那么就会把“这是我不需要的内容”也提取出来。但是如果指定了<div>标签的属性,就只能提取其中一个。这个时候,就需要用XPath提取所有id以“test”开头的<div>标签。在XPath中,属性以某些字符串开头,可以写为:

    //标签[starts-with(@属性名,"相同的开头部分")]

例如,在上面的代码中可以构造如下XPath:

    //div[starts-with(@id, "test")]/text()

(2)属性值包含相同字符串

寻找属性值包含某些字符串的元素时,XPath的写法格式和以某些字符串开头的写法格式是相同的,只不过关键字从“starts-with”变成了“contains”。例如提取所有属性值中包含“-key”的标签中的文本信息:

    //div[contains(@id, "-key")]/text()

目前,lxml中的XPath不支持直接提取属性值以某些字符串结尾的情况。如果遇到这种情况,建议使用contains代替。

(3)对XPath返回的对象执行XPath

XPath也支持先抓大再抓小。还是以上面中的HTML代码为例,可以通过下面的代码来获取需要的信息:

    //div[@class="useful"]/ul/li/text()

同时,还可以先抓取useful标签,再对这个标签进一步执行XPath,获取里面子标签的文字。

    useful = selector.xpath('//div[@class="useful"]') #这里返回一个列表
    info_list = useful[0].xpath('ul/li/text()')
    print(info_list)

需要注意的是,在对XPath返回的对象再次执行XPath的时候,子XPath开头不需要添加斜线,直接以标签名开始即可。

(4)不同标签下的文字

有一段如下的HTML代码:

    <! DOCTYPE html>
    <html>
    <head lang="en">
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <div id="test3">
          我左青龙,
          <span id="tiger">
              右白虎,
              <ul>上朱雀,
                  <li>下玄武。</li>
              </ul>
              老牛在当中,
          </span>
          龙头在胸口。
        </div>
    </body>
    </html>

期望把“我左青龙,右白虎,上朱雀,下玄武,老牛在当中,龙头在胸口”全部提取下来。如果直接以下面这个XPath语句来进行提取:

    //div[@id="test3"]/text()

因为只有“我左青龙”和“龙头在胸口”这两句是真正属于这个<div>标签的文字信息。XPath并不会自动把子标签的文字提取出来。在这种情况下,就需要使用string(.)关键字了。首先像先抓大再抓小一样,先获取<div id="test3">这个结点,但是不获取里面的东西。接着对这个结点再使用一次XPath,提取整个结点里面的字符串。核心代码如下:

  data = selector.xpath('//div[@id="test3"]')[0]
  info = data.xpath('string(.)')

通过结果可看到,不仅把所有文字信息都提取了出来,甚至把它们的相对位置也提取了出来。

2.4 使用Google Chrome浏览器辅助构造XPath

在构造XPath语句的过程中,需要寻找“标志性”的标签。但是如果遇到混乱的源代码,就不能单纯靠眼睛来看了。借助Google Chrome浏览器来协助分析网页结构,可以大大提高分析效率。Google Chrome自带的开发者工具可以将网页源代码转换为树状结构,大大提高网页的可读性。在网页上单击右键,在弹出的快捷菜单中选择“检查”命令。

打开开发者工具后,使鼠标指针在开发者窗口中的HTML代码中移动,可以看到页面上不同的地方会高亮,说明当前鼠标指针指向的这个标签,就对应了网页中高亮的这一部分的代码。除了根据代码找网页位置,还可以根据网页位置找代码。单击方框框住的按钮,并将鼠标指针在网页上移动,可以看到开发者工具窗口中的代码随之滚动。

选定要提取的位置以后,此时,开发者工具窗口高亮显示的这一行代码,即为这个提取位置所在的HTML源代码的位置。在上面单击右键,选择“Copy”→“Copy XPath”命令。寻找一个可以输入文字的地方,把结果粘贴下来,可以看到如下的XPath语句:

    //*[@id="thread_list"]/li[2]/div/div[2]/div[1]/div[1]/a

这种写法是可以被lxml解析的。方括号中的数字,表示这是第几个该标签。例如//*[@id="thread_list"]/li[2],表示在id为“thread_list”的标签下面的第2个<li>标签。注意,这里的数字是从1开始的,这和编程语言中普遍的从0开始不一样。

Google Chrome给出的XPath是当前高亮的这一个标签的XPath,被lxml执行以后,也只能得到这一个标签的信息。为了得到一类标签的信息,例如得到所有帖子的标题,就需要将Google Chrome给出的XPath为参考,手动构造范围更大的且更容易读的XPath。例如,Google Chrome给出了一个标志性的id,它的属性值为“thread_list”,那么拥有这个属性的标签就可以作为XPath的起始标签。现在,在Google Chrome给出的这个标签和需要提取的内容之间进行人工分析,可以进一步缩小XPath的范围。

在开发者工具窗口中,每个标签的左边有个小箭头。通过单击小箭头可以展开或者关闭这个标签,通过这个小箭头,可以协助分析页面的HTML结构。

比如,百度贴吧的每一个<li>标签。这些方框中的<li>标签就对应了每一个帖子。所以只要使用XPath先获得每一个方框中的<li>标签,再按照先抓大再抓小的技巧,就可以轻松得到所有帖子的内容。以每个帖子的标题为例,将各个对应的小箭头展开,可以看到方框中的树状结构。构造这样一个虽然很长但是仍可以读懂的XPath:

    //li[@class=" j_thread_list clearfix"]/div[@class="t_con cleafix"]/div[@class="col2_right j_threadlist_li_right "]/div[@class=
    "threadlist_lz clearfix"]/div[@class="threadlist_title pull_left j_th_tit "]/a/text()

这个XPath看起来非常长,但别害怕,它之所以长,仅仅是因为网页的属性值本身就很长,而这些属性值在实际写XPath的时候,直接从网页中复制粘贴下来就可以了。使用lxml执行了这个XPath以后,就可以得到一个列表,这个列表中的内容是本页所有的帖子标题。

如果需要的仅仅是帖子的标题,不需要其他内容,XPath还可以进一步缩短为:

    //div[@class="threadlist_title pull_left j_th_tit "]/a/text()

三、Beautiful Soup4

Beautiful Soup4(BS4)是Python的一个第三方库,用来从HTML和XML中提取数据。Beautiful Soup4在某些方面比XPath易懂,但是不如XPath简洁,而且由于它是使用Python开发的,因此速度比XPath慢。

3.1 BS4的安装

使用pip安装Beautiful Soup4:

pip install beautifulsoup4

注意,这里的数字“4”不能省略,因为还有一个第三方库叫作beautifulsoup,但是它已经停止开发了。安装完成以后打开Python的交互环境,输入以下代码并按Enter键:

    from bs4 import BeautifulSoup

如果不报错,表示安装成功。

3.2 BS4语法讲解

使用Beautiful Soup4提取HTML内容,一般要经过以下两步。

(1)处理源代码生成BeautifulSoup对象。

(2)使用find_all()或者find()来查找内容。

1.解析源代码

解析源代码生成BeautifulSoup对象,使用以下代码:

    soup = BeautifulSoup(网页源代码,’解析器’)

这里的“解析器”,可以使用html.parser:

    soup = BeautifulSoup(source, 'html.parser')

如果安装了lxml,还可以使用lxml:

    soup = BeautifulSoup(source, 'lxml')

3.3 查找内容

查找内容的基本流程和使用XPath非常相似。首先要找到包含特殊属性值的标签,并使用这个标签来寻找内容。

假设需要获取“我需要的信息2”,由于这个信息所在<li>标签的class属性的值为“test”,这个值本身就很特殊,因此可以直接通过这个值来进行定位。

    info = soup.find(class_='test')

由于HTML中的class属性与Python的class关键字相同,因此为了不产生冲突,BS4规定,如果遇到要查询class的情况,使用“class_”来代替。在第9行的查询HTML代码中,class属性的属性值为“test”的标签,得到find()方法返回的BeautifulSoup Tag对象。在第11行中,直接通过.string属性就可以读出标签中的文字信息。那如果要获取“我需要的信息1”“我需要的信息2”和“我需要的信息3”,又应该怎么办呢?先抓大再抓小的技巧依然有用:

    useful = soup.find(class_='useful')
    all_content = useful.find_all('li')
    for li in all_content:
        print(li.string)

首先根据标签<div class="useful">查找到有用的内容,然后在这个内容的基础上继续查找<li>标签下面的内容。这里用到了find()方法和find_all()方法。

find()与find_all()的不同点如下:

· find_all()返回的是BeautifulSoup Tag对象组成的列表,如果没有找到任何满足要求的标签,就会返回空列表。

· find()返回的是一个BeautifulSoup Tag对象,如果有多个符合条件的HTML标签,则返回第1个对象,如果找不到就会返回None。

find_all()与find()的参数完全相同,以find_all()为例来说明。

    find_all( name , attrs , recursive , text , **kwargs )

· name就是HTML的标签名,类似于body、div、ul、li。

· attrs参数的值是一个字典,字典的Key是属性名,字典的Value是属性值,例如:

    attrs={'class': 'useful'}

这种写法,class就不需要加下划线。

· recursive的值为True或者False,当它为False的时候,BS4不会搜索子标签。

· text可以是一个字符串或者是正则表达式,用于搜索标签里面的文本信息,因此,要寻找所有以“我需要”开头的信息,还可以使用下面的写法:

    content = soup.find_all(text=re.compile(’我需要’))
    for each in content:
        print(each.string)

· **kwargs表示Key=Value形式的参数。这种方式也可以用来根据属性和属性值进行搜索。这里的Key是属性,Value是属性值。在这里如果需要搜索HTML标签的class属性,就需要写成“class_”。大多数情况下,参数与标签配合使用,但是有时候如果属性值非常特殊,也可以省略标签,只用属性:

    find_all('div', id='test')
   find_all(class_='iamstrange')

这种写法也支持正则表达式。例如对于“我需要的信息3”,它的class属性的属性值为“iamstrange”,因此如果使用正则表达式,就可以写为:

    content = soup.find_all(class_=re.compile('iam'))
    for each in content:
        print(each.string)

除了获取标签里面的文本外,BS4也可以获取标签里面的属性值。如果想获取某个属性值,可以将BeautifulSoup Tag对象看成字典,将属性名当作Key。

--------------------------------------

版权声明:本文为【PythonJsGo】博主的文章,同步在【猿小猴子】WeChat平台,转载请附上原文出处链接及本声明。

--------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值