数据分析师的编程之旅——Python爬虫篇(2)HTML解析器

作者:李禹锋,重庆芝诺大数据分析有限公司数据挖掘工程师。

呈上一篇中发送请求与获取网页源码,本文着重介绍网页源码的解析。主要介绍四种解析方式:正则表达式、CSS选择器、XPATH选择器、模块化选择器。我个人更倾向于XPATH选择器进行解析,所以也着重讲解xpath(选择器会一种即可,待深入时才会考虑每种选择器的优劣)。大部分初识爬虫的教程中以正则表达式来解析,也有使用的模块化选择器(python中主要是BeautifulSoup)

首先提问,网页源码在获取到之后,在python中是什么?——字符串,一段很长很长的字符串,之前也提到过,正则表达式就是一种高级的字符处理方法,任何字符问题均可使用正则表达式,甚至有些特殊的爬虫需求使用正则表达式来解析更为简单,而且不止是正则,使用原生的字符处理函数也可以进行解析,只是既然有汽车,为什么还要走路,除非为了锻炼自己字符处理函数的能力。

同样,网页源码虽然在获取到之后是一个字符串,但其本身也是有结构的。这个结构在前端设计篇中也略微讲到过。首先是由标签对包起来,这就是地基,往上是和两组标签对,网页信息包含在head标签内,网页内容包含在body标签中,接着又是各式的层级结构。如果仅仅是将网页源码当成一个字符串来处理,不免有点自找麻烦,就像明明可以使用因式分解却依旧固执的使用求更公式一样。介绍的方法中后三种均是结构化解析器,也叫做选择器。先介绍xpath选择器,比较推荐使用。

01

xpath

Xpath不是一种独立的语言,而是一种标识路径的方法,是基于XML树状结构的路径查询语言。形似文件系统的路径写法,有着绝对路径和相对路径。首先介绍容易理解一些的绝对路径。例如我现在有如下HTML源码。

HTML解析示例

list1的第一个list

list1的第二个list

list2的第一个list

list2的第二个list

百度链接

展现如下

两个div的id分别为list1和list2,每个div中的第一个li标签class一样,第二个li标签class一样。使用绝对路径来定位到每个li标签的方法如下:

元素

xpath

Div1 li1

/html/body/div[1]/li[1]

Div1 li2

/html/body/div[1]/li[2]

Div2 li1

/html/body/div[2]/li[1]

Div2 li2

/html/body/div[2]/li[2]

绝对路径起始就是从html标签开始,每一层标签挨着往下走,若同一级出现了多个同类标签,使用中括号加数字的方式来选取第几个,注意:xpath是从1开始,不是从0开始。

而相对路径的学问就比较多了,本文只介绍几个最简单的相对路径写法(基本也够用了)。

通用些的相对路径(从可以定位到唯一性的结点开始往后算,方法为在中括号中以@开头,然后跟上对应的属性与属性值)

元素

xpath

Div1 li1

//div[@id=”list1”]/li[1]

Div1 li2

//div[@id=”list1”]/li[2]

Div2 li1

//div[@id=”list2”]/li[1]

Div2 li2

//div[@id=”list2”]/li[2]

同时获取所有li1或li2的相对路径(所有相同结构的均会被选出)

元素

xpath

li1

//li[@class=”li1”]

li2

//li[@class=”li2”]

上面这种方式我比较常用于爬取文章时,直接写//p,然后再来进行筛选,因为p标签就是代表的段落。

那么问题来了,我要获取的不是整个标签,而是详细的标签内容啊,或者说是标签的属性啊,比如innerHTML、href、src等等属性。接下来进行介绍如何获取到详细的属性。

分为两个部分,一个是innerHTML(就是起始标签和终止标签中间的东西),一个是其他属性。InnerHTML使用 节点/text() 来获取;其他属性使用 节点/@属性名 来获取。

例如各个li标签的innerHTML和class。

元素

innerHTML

class

Div1 li1

//div[@id=”list1”]/li[1]/text()

//div[@id=”list1”]/li[1]/@class

Div1 li2

//div[@id=”list1”]/li[2]/text()

//div[@id=”list1”]/li[2]/@class

Div2 li1

//div[@id=”list2”]/li[1]/text()

//div[@id=”list2”]/li[1]/@class

Div2 li2

//div[@id=”list2”]/li[2]/text()

//div[@id=”list2”]/li[2]/@class

获取a标签的href链接

元素

href

a

//a/@href

上面这个方式经常用于从一个网页出发爬取相关链接时的第一步解析。

详细xpath语法可见https://www.w3cschool.cn/xpath/或http://www.runoob.com/xpath/xpath-tutorial.html

在python中可使用lxml模块来将HTML源码转换为XML的树状结构以便进行xpath的路径查询。

以上文中的html代码为例,分别解析出各个li标签的innerHTML和a标签的href,代码如下。

import lxml.etree as e

html = '''

HTML解析示例

list1的第一个list

list1的第二个list

list2的第一个list

list2的第二个list

百度链接

'''

selector = e.HTML(html)

#获取li标签

li = selector.xpath('//li/text()')

print(li)

#获取a标签

a = selector.xpath('//a/@href')

print(a)

可以看出xpath提取后的结果均为列表(如果没有最后提取的属性,那么结果为节点,而该节点可以继续往后写xpath,不过这里最顶层的节点为当前节点,下一个例子中详细见代码)

以百度新闻python爬虫为例,需要提取第一页中的每一块内容的标题和链接,每一个标题和链接需要进行匹配(不能提取出20个标题和链接后挨个来进行匹配,很容易错位)代码如下。

from urllib.request import Request,urlopen

from lxml.etree import HTML

req = Request('http://news.baidu.com/ns?cl=2&rn=20&tn=news&word=python%E7%88%AC%E8%99%AB')

req.add_header('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36')

html = urlopen(req).read().decode('utf-8')

selector = HTML(html)

nodes = selector.xpath('//div[@id="content_left"]/div[3]/div')

for node in nodes:

result = {

'title':node.xpath('h3/a/text()'),

'link':node.xpath('h3/a/@href')

}

print(result)

代码中首先定位到id为content_left的div,然后从该div往下找到每一个新闻内容的版块(每个版块的结果几乎是一模一样的),此时的节点已定位到了20个新闻内容的版块,故返回的nodes节点内有20个选择器(因还未提取出其属性,故该节点依旧还是选择器)。接下来使用一个循环,将每个新闻版块的标题和链接提取出来,放入同一个字典中(这样做就保证了一个标题对应一个链接,大大的降低了错位的可能性)。当然,获取到的结果是一个列表并不理想,还需要进行进一步的处理(其实使用字符串的拼接即可,不再赘述)

02

CSS选择器

接下来介绍CSS选择器。CSS选择器顾名思义其实就是CSS粉刷匠定位元素的方式,写法和xpath有相似之处,功能也相似,但差别也很大(使用css选择器的最好是有前端基础的童鞋)。例如还是以上文中写的HTML代码,css选择器采用pyquery模块(其实无论是请求还是解析方法都太多太多了,前期到中期只需要挑一个喜欢的习惯的写就可以了)。

from pyquery import PyQuery as pq

html='''

HTML解析示例

list1的第一个list

list1的第二个list

list2的第一个list

list2的第二个list

百度链接

'''

doc = pq(html)

#获取所有li标签的innerHTML

li1 = doc('li').text()

print(li1)

#获取id为list1的div下的所有li标签的innerHTML

li2 = doc('#list1 > li').text()

print(li2)

#获取class为li1的li标签的innerHTML

li3 = doc('.li1').text()

print(li3)

#获取a标签的href

href = doc('a').attr('href')

print(href)

在css选择器中选择下一节点时可不写 > ,可直接打一个空格代表下一层,id属性直接用#来表示,class属性用一个 . 来表示。

详细手册参考http://www.w3school.com.cn/cssref/css_selectors.asp

03

正则表达

其实写到这里不是太想写正则,因为我个人爬虫的时候真的很难用到,但还是不能忘了本,勉强写一写吧(其实就是不想写正则而已T_T)。

打滚。。代码如下

import re

html=r'''

HTML解析示例

list1的第一个list

list1的第二个list

list2的第一个list

list2的第二个list

百度链接

'''

#获取每个li的innerHTML

li1 = re.findall(r'(.+)', html,flags=0)

for li in li1:

print(li)

print(li[1])

print('==============================')

#获取id为list1的div下的li的innerHTML

div = re.findall(r'

(.+)

',re.sub(r'[\n\t]','',html),flags=0)[0][1]

li2 = re.findall(r'(.+)',div)

for li in li2:

print(li[1])

print('==============================')

#获取a标签的href

a = re.findall(r'(.+)',html,flags=0)

print(a[0][1])

04

bs4

使用bs4模块中的BeautifulSoup来进行解析。需求同上。

from bs4 import BeautifulSoup

html=r'''

HTML解析示例

list1的第一个list

list1的第二个list

list2的第一个list

list2的第二个list

百度链接

'''

bs = BeautifulSoup(html)

#获取每个li的innerHTML

li1 = bs.findAll(name='li')

for li in li1:

print(li.getText())

print('=============================')

#获取id为list1的div下的li的innerHTML

li2 = bs.findAll(name='div',id='list1')[0].findAll(name='li')

for li in li2:

print(li.getText())

print('=============================')

#获取a标签的href

a = bs.findAll(name='a')[0].attrs['href']

print(a)

每种解析方法都有其市场,都有人用,只是每个工程师都有着自己的偏好和特长而已,我最开始是使用的beautifulsoup,然后使用CSS选择器,再之后正则也写了几个月,现在更喜欢用xpath,还是那句青菜萝卜各有所爱,都能够达到目标,这就足够了。

关于解析方法本文主要做了一个列举,若想深入学习某种解析方法的朋友敬请留言,畅所欲言,倘若有需求会针对某种解析方法进行详细的讲解。人生苦短,我选python。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180206G1B6A800?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券