基于BeautifulSoup库解析HTML页面

BeautifulSoup库,是用于解析、遍历、维护“标签树” 的功能库

约定的引用方式:

1
from bs4 import BeautifulSoup

每一个 BeautifulSoup 类,对应一个HTML/XML的全部内容

Beautiful Soup库解析器

获取 BeautifulSoup 类的方式(重点在下方代码第三行):

1
2
3
4
file = open("./baidu.html", "rb")
htmlFile = file.read()
bs = BeautifulSoup(htmlFile, "html.parser")
# bs = BeautifulSoup('<html>data</data>', 'html.parser')

在构造方法中,除了 html.parser ,还可以传入其他的解析器如:lxml的HTML解析器、lxml的XML解析器(需要额外安装)

HTML格式化打印:bs.prettify()

Tag类(标签)

bs4.element.Tag 类,对应于HTML/XML的标签,是最基本的信息组织单元,用 <></> 标明开头和结尾的。

获取方式:

1
2
bs.title # <title>百度两下,你就知道</title>
bs.a # <a class="mnav" href="http://news.baidu.com" name="tj_trnews"><!--新闻1--></a>

任何存在于HTML语法中的标签,均可通过BeautifulSoup 类 进行访问,即bs.<tag>

当HTML文档存在多个相同的<tag>时,优先返回第一个

Tag.name(标签名称)

每个标签的名称字符串,可通过 <tag>.name 获取

1
2
bs.div.name # div
bs.a.name # a

Tag.attrs(标签属性)

每个标签的属性,如 classhref等,可通过 <tag>.attrs,返回的是字典类型

1
2
bs.a.attrs
# {'class': ['mnav'], 'href': 'http://news.baidu.com', 'name': 'tj_trnews'}

Tag.String

<tag>.string 获取的是标签内非属性字符串

要点:

  1. 注释的文本,返回的是 bs4.element.Comment ;否则,返回的是 bs4.element.NavigableString

    1
    2
    3
    4
    <!--示例1:bs.a.string为"我是注释"-->
    <a href=""><!--我是注释--></a>
    <!--示例2:bs.a.string为"2222"-->
    <a href="">222</a>
  2. 如果指定的<tag>含有多个同级标签(包括注释标签),或者含有更深的层级标签,则 .string 返回为 None

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!--示例1:bs.p.string为None-->
    <p>
    111
    <a href=""><!--我是注释--></a>
    <span>333</span>
    </p>
    <!--示例2:bs.a.string为None-->
    <a href="">我是正文<!--我是注释--></a>
    <!--示例3:bs.article.string为None-->
    <article>
    <a href=""></a>
    </article>

HTML内容遍历

BeautifulSoup 类,是标签树的根节点。

标签树的下行遍历

  • .contents:子节点的列表,即将 <tag> 所有儿子节点存入列表,元素类型是NavigableStringTag等类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    bs.body.contents
    '''注意到,每一儿子节点作为列表中的一个元素
    ['\n',
    <article>
    <a href="">222</a>
    <a href=""><!--我是注释--></a>
    </article>,
    '\n',
    <div id="wrapper"><div id="head"><div class="head_wrapper"><div id="u1"><a class="mnav" href="http://news.baidu.com" name="tj_trnews"><!--新闻1--></a>
    <a class="mnav" href="http://news.baidu.com" name="tj_trnews">新闻</a></div></div></div></div>,
    '\n']
    '''
    bs.body.contents[1]
    '''
    <article>
    <a href="">222</a>
    <a href=""><!--我是注释--></a>
    </article>
    '''
  • .children:子节点的迭代类型,用于循环遍历儿子节点

    1
    2
    for child in bs.body.children:
    print(child)
  • .descendants:子孙节点的迭代类型,包含所有子孙节点(结果类似于DFS),同样用于循环遍历

    1
    2
    for child in bs.body.descendants:
    print(child)

标签树的上行遍历

  • .parent:节点的父亲标签

  • .parents:节点先辈标签的迭代类型,用于循环遍历先辈节点

    1
    2
    3
    4
    5
    for parent in bs.a.parents:
    if parent is None:
    print(parent)
    else:
    print(parent.name)
    1
    2
    3
    4
    article
    body
    html
    [document]

标签树的平行遍历

以下操作均按照原HTML的文本顺序,并且只能遍历同一个父节点下的各节点间

  • .next_sibling:下一个平行节点标签
  • .previous_sibling:上一个平行节点标签
  • .next_siblings:迭代类型,返回后续所有平行节点标签
  • .previous_siblings:同上相反的方向

HTML内容查找

<>.select():类似于CSS选择器,返回的结果为列表。如下:

1
2
3
4
bs.select(".mnav")
bs.select("#u1")
bs.select("a[class='bri']")
bs.select(".mnav ~ .bri")

<>.find_all(name, attrs, recursive, string, **kwargs):查找与字符串完全匹配的内容,并存放到列表类型

  • name:按标签名称进行检索,如:

    1
    2
    3
    4
    bs.find_all('a')
    bs.find_all(['a', 'b'])
    bs.find_all(re.compile('b')) # 支持正则表达式,返回 body、b 等标签
    bs.find_all('a', limit=3) # 结果最多只取limit个元素
  • attrs:按标签属性值进行检索

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bs.find_all('a', 'mnav') 
    '''
    [<a class="mnav" href="http://news.baidu.com" name="tj_trnews"><!--新闻1--></a>, <a class="mnav" href="http://news.baidu.com" name="tj_trnews">新闻</a>]
    '''
    bs.find_all(id='u1')
    '''
    [<div id="u1">
    <a class="mnav" href="http://news.baidu.com" name="tj_trnews"><!--新闻1--></a>
    <a class="mnav" href="http://news.baidu.com" name="tj_trnews">新闻</a>
    </div>]
    '''
    bs.find_all(id=re.compile('link'))
  • recursive:是否对子孙全部检索,默认为 True

  • string:按<>...</> 中字符串区域进行检索

    1
    2
    bs.find_all(string='新闻')
    bs.find_all(string=re.compile('产品'))

基于Re(正则表达式)库提取页面关键信息

引入方式:

1
import re

常用操作符

操作符说明实例
.任何单个字符
[]单个字符给出取值范围(字符集)[abc] 表示a,b,c[a-z] 表示 az 单个字符
[^ ]对单个字符给出排除范围
*前一个字符任意次拓展(0次或无限次)abc* 表示:ab,abc,abcc,abccccc
+前一个字符至少拓展1次(>=1)abc+ 表示:abc,abcccc
?前一个字符要么拓展0次,要么拓展1次abc? 表示:ab,abc
``左右表达式任意一个
{m}前一个字符拓展m次ab{2}c 表示:abbc
{m,n}前一个字符拓展[m, n]次ab{1,2}c 表示:abc,abbc
^匹配字符串开头^abc 表示 abc 且在一个字符串开头
$匹配字符串结尾abc$ 表示abc 且在一个字符串的结尾
()分组标记,内部只能用 ``
\d匹配数字,等价于 [0-9]
\w匹配字母、数字、下划线,等价于[A-Za-z0-9_]

语法实例:

正则表达式对应字符串
`P(YYT
PYTHON+‘PYTHON’、‘PYTHONN’、‘PYTHONNN’ …
PY[TH]ON‘PYTON’、‘PYHON’
PY[^TH]?ON‘PYON’、‘PYaON’、‘PYbON’、‘PYcON’…
PY{:3}N‘PN’、‘PYN’、‘PYYN’、‘PYYYN’

匹配IP地址的正则表达式:

满足0-99:[1-9]?\d;又满足100-199:1\d{2};又满足200-249:2[0-4]\d;又满足250-255:25[0-5],故精确的写法如下:

(([1‐9]?\d|1\d{2}|2[0‐4]\d|25[0‐5]).){3}([1‐9]?\d|1\d{2}|2[0‐4]\d|25[0‐5])

表示类型

re 库建议采用 raw string 类型(不包含对转义符再次转义的字符串)表示正则表达式,即 r'text',如 r'[1-9]\d{5}'r'\d{3}-\d{8}|\d{4}-\d{7}'

具体用法

re.search()re.match() 等方法:

1
2
3
4
5
6
7
#下面两种等价用法:
# 函数式用法:一次性操作
rst = re.search(r'[1-9]\d{5}', 'BIT 100081')

# 面向对象用法:编译后的多次操作
pat = re.compile(r'[1-9]\d{5}') # 将正则表达式串编译为正则表达式【对象】
rst = pat.search('BIT 100081')

以下方法均可用上述的面向对象方法,即将正则表达式编译至正则表达式对象后,再调用。

但更加常用匹配方法为:re.findall(pattern, string, flags=0),搜索字符串,以列表类型返回全部能匹配的子串。其中,pattern:正则表达式的字符串;string:待匹配字符串;flags:正则表达式使用时的控制标记

注意区分 bs4 的 find_all() 用于找到所在标签及内容,findall() 匹配具体字符串

另外,re库默认采用贪婪匹配,即输出匹配最长的子串

1
2
3
4
re.findall(r'[A-Z]', 'ASDdfsDSfSDFHIH') 
# ['A', 'S', 'D', 'D', 'S', 'S', 'D', 'F', 'H', 'I', 'H']
re.findall(r'[A-Z]+', 'ASDdfsDSfSDFHIH') # 最长连续匹配串
# ['ASD', 'DS', 'SDFHIH']

匹配并替换的方法:re.sub(pattern, repl, string, count=0, flags=0),其中,pattern :正则表达式的字符串,repl替换匹配字符串的字符串,string匹配字符串,count:匹配的最大替换次数

1
2
3
4
re.sub(r"aaa", "A", "WOCAaaaAdfsfa")
# WOCAAAdfsfa
re.sub(r"[1-9]\d{5}", ":zipcode", "BIT100081 TSU100084")
# BIT:zipcode TSU:zipcode