自学Python第十五天-常用的HTML解析工具:bs4、xpath、re
- BS4
- 安装和引入
- 开始使用
- `find_all()` 方法获取标签
- `find()` 方法获取标签
- `select()` 方法获取标签,css 选择器
- 从标签中获取数据
- XPath
- xpath 基础
- xpath 语法规则
- lxml 模块
- `xpath()` 方法
- RE
- `match()` 方法
- `search()` 方法
- `findall()` 方法
- `finditer()` 方法
- `sub()` 方法
- `subn()` 方法
- `split()` 方法
- `compile()` 方法
- `flags` 参数
- `match` 对象
之前应该写过关于 bs4、xpath、re 的python使用文章,但是找不到了。因为这3种工具在 html 解析中经常用到,所以重新写一遍。
在 python 学习中绕不过去的就是爬虫,学习爬虫绕不过去的就是HTML页面解析,而最常用的解析工具就是 BeautifulSoup4、XPath 和 RE 了。这三个工具的比较如下:
工具 | 解析速度 | 使用难度 | 安装难度 |
---|---|---|---|
bs | 慢 | 最简单 | 简单 |
lxml(xpath) | 快 | 简单 | 一般 |
正则(re) | 最快 | 困难 | 无(内置) |
BS4
BeautifulSoup 4
简称 BS4
,是一个 HTML/XML
的解析器。它是基于 HTML DOM
文档的,会载入整个文档,解析整个 DOM
树,因此时间和内存开销会大很多,性能较低。但是其语法是基于 CSS Selector
的,所以学习和使用非常简单。
BS4中文文档
安装和引入
pip install beautifulsoup4
from bs4 import BeautifulSoup
开始使用
bs4 使用时,首先创建 Beautiful Soup
对象,然后使用该对象的对应方法来解析DOM
获取需要的元素标签对象,最后使用该对象的对应方法获取需要的属性或文本数据。例如:
from bs4 import BeautifulSouphtml = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""# 创建 Beautiful Soup 对象
soup = BeautifulSoup(html, "lxml")print(soup.prettify())
find_all()
方法获取标签
可以使用 find_all(self, name=None, attrs={}, recursive=True, string=None, limit=None, **kwargs)
方法来匹配相应的元素列表。该方法最常用的参数就是 name
,attrs
和 string
。
name
参数可以传递标签名称字符串或列表,以及正则表达式匹配对象
# 根据标签名获取标签元素
ret_a = soup.find_all('a')
ret_img = soup.find_all('img')# 根据标签名列表,返回匹配任一列表元素,即为或的关系
ret = soup.find_all(['a', 'img'])# 根据正则表达式
ret_re = soup.find_all(re.compile('^b'))
attrs
参数可以根据标签的属性来匹配
# 匹配标签中 class 属性
ret_sister_1 = soup.find_all(attrs={'class': 'sister'})
# 简写
ret_sistet_2 = soup.find_all(class_='sister') # 之所以使用 class_ 而不使用 class,是因为 class 是 python 关键字
ret_id = soup.find_all(id='link')
string
参数可以搜索文档中的文本字符串内容。与name
一样,可以接受字符串、列表以及正则表达式
ret_1 = soup.find_all(string='Elsie')
ret_2 = soup.find_all(string=['Tillie', 'Elsie', 'Lacie'])
ret_3 = soup.find_all(string=re.compile('Dormouse'))
当然三个参数可以同时使用,以获取需要的匹配标签元素。
find()
方法获取标签
find
方法与 find_all()
方法一样,区别在于 find()
返回第一个匹配结果,而 find_all()
方法返回所有匹配结果列表。
select()
方法获取标签,css 选择器
bs4 可以直接使用 css 选择器语法作为 select()
方法的参数。需注意的是,返回值也是一个列表
# 选择 title 标签
soup.select('title')
# 选择 img 标签
soup.select('img')
# 类选择器
soup.select('.sister')
# id 选择器
soup.select('#link1')
# 层级选择器
soup.select('p #link1')
# 属性选择器
soup.select('a[class="sister"]')
soup.select('a[href="http://example.com/elsie"]')
从标签中获取数据
获取到标签对象后,可以使用一些方法获取具体需要的数据
get_text()
方法,可以获取文本内容get()
方法,可以获取属性,参数为属性名
for attr in soup.select('a'):print(attr.get('href'))
XPath
XPath (XML Path Language)
即 XML路径语言
,最初时是作为在 XML 文档中查找需要的信息,现在也适用于 HTML 文档。
xpath 作为一种普遍使用的解析语法,有着广泛的作用。xpath 的解析速度不慢,学习和使用起来也算是简单,所以成为解析 html 文档最常用的方法之一。XPath
可以很轻松的选择出想要的数据,提供了非常简单明了的路径选择表达式,几乎想要任何定位功能,XPath
都可以很轻松的实现。
W3School官方文档
xpath 基础
在 xpath 中,每一个标签都称之为节点,最顶层的节点称为根节点。
学习 xpath 可以使用一些浏览器辅助工具:
Chrome
浏览器插件:XPath Helper
Firefox
浏览器插件:XPath Finder
注意: 这些工具是用来学习XPath
语法的,可以在这些工具中测试和联系语法规则,当熟练掌握XPath
的语法后就可以直接在代码中编写XPath
而不一定非要用此工具。
xpath 语法规则
XPath
使用路径表达式来选取文档中的节点或者节点集。
表达式 | 描述 |
---|---|
nodename | 选中该元素 |
/ | 从根节点选取、或者是元素和元素间的过渡 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
text() | 选取文本 |
contains() | 测试是否包含特定字符 |
路径表达式
路径表达式 | 结果 |
---|---|
bookstore | 选择bookstore元素 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠(/ ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置 |
//book/title/@lang | 选择所有的book下面的title中的lang属性的值 |
//book/title/text() | 选择所有的book下面的title的文本 |
查询特定节点
路径表达式 | 结果 |
---|---|
//title[@lang="eng"] | 选择lang属性值为eng的所有title元素 |
/bookstore/book[1] | 选取属于 bookstore 子元素的第1个 book 元素 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后1个 book 元素 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第2个 book 元素 |
/bookstore/book[position()>1] | 选择bookstore下面的book元素,从第2个开始选择 |
/bookstore/book[position()>1 and position()<4] | 选择bookstore下面的book元素,从第2个开始取到第4个元素 |
//book/title[text()='Harry Potter'] | 选择所有book下的title元素,仅仅选择文本为Harry Potter的title元素 |
//book/title[contains(text(), 'arry')] | 选择所有book下的tiile元素中,文本包含 arry 的元素 |
//a[@href[contains(., 'about')]] | 选择所有 href 属性包含 ‘about’ 的 a 元素 |
注意点: 在XPath
中,第一个元素的位置是1
,最后一个元素的位置是last()
,倒数第二个是last()-1
lxml 模块
python 中使用 xpath 最常用的模块就是 lxml 模块。
pip install lxml
from lxml import etree
使用此模块需要先将需要解析的文本转化为 Element
对象,Element
对象有 xpath 的方法
from lxml import etreetext = ''' <div> <ul> <li class="item-1"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> '''html = etree.HTML(text)# 将Element对象转化为字符串
handled_html_str = etree.tostring(html).decode()
print(handled_html_str)
xpath()
方法
Element
对象的 xpath()
方法可以使用 xpath 语法来获取需要的对象或数据。注意返回的是列表,如果是元素对象,则是 Element
对象。所以也可以使用链式调用的方式来多层获取需要的数据。
# 获取数据列表,返回值为字符串列表
href_list = html.xpath("//li[@class='item-1']/a/@href")
title_list = html.xpath("//li[@class='item-1']/a/text()")
# 获取节点列表
li_list = html.xpath("//li[@class='item-1']")
# 从节点列表对象中继续使用 xpath 匹配查询
for li in li_list:item = dict()item["href"] = li.xpath("./a/@href")[0] if len(li.xpath("./a/@href")) > 0 else Noneitem["title"] = li.xpath("./a/text()")[0] if len(li.xpath("./a/text()")) > 0 else Noneprint(item)
RE
RE 模块是python中使用正则语法的模块,正则的语法比较复杂,另外起一篇文章学习。这里只有 re 模块的使用方法。re 模块是 python 的内置模块,所以可以直接引入
import re
re 模块的使用方式一般有两种:
- 直接使用相应的匹配方法,将匹配字符串和待查找文本作为参数传入。
- 将匹配字符串编译为一个
Pattern
对象,并用此对象的相关匹配方法来匹配目标待查找文本。
match
对象是 re 模块方法所返回的默认的匹配对象,大部分匹配方法如果得到匹配结果,就会返回 match
对象。
match()
方法
match(pattern, string, flags=0)
方法可以从字符串开头开始检测是否于模式匹配。如果匹配成功,返回匹配对象,否则返回None
。
# 从开头检测字符串是否匹配
match = re.match(r'\d+', '123abc')
if match:print(match.group()) # 输出 123
search()
方法
search(pattern, string, flags=0)
方法可以在字符串中搜索并返回第一个匹配项。如果匹配成功,返回匹配对象,否则返回None
。
# 使用 search 方法在整个字符串中搜索匹配
search = re.search(r'\d+', 'abc123def')
if search:print(search.group()) # 输出: 123
findall()
方法
findall(pattern, string, flags=0)
方法会返回所有非重叠匹配项的列表。如果匹配模式中有一个或多个捕获组(group),则会返回元组列表。
# 使用 findall 方法找到所有匹配的数字
numbers = re.findall(r'\d+', 'abc123def456')
print(numbers) # 输出: ['123', '456']
finditer()
方法
与 findall()
方法类似,不过返回值为一个迭代器,其中每一个元素都是一个匹配对象。
sub()
方法
sub(pattern, repl, string, count=0, flags=0)
方法可以将匹配项替换为 repl
参数的值,repl
可以是一个字符串或一个函数;如果是函数,每个匹配项都会作为参数传递给这个函数。count
用于指定最大替换次数;默认 0
,替换所有匹配项。
# 使用 sub 方法替换所有的数字为 '#'
replaced = re.sub(r'\d+', '#', 'abc123def456')
print(replaced) # 输出: abc#def#
subn()
方法
与 sub()
方法类似,不过返回值是一个包含新字符串和替换次数的元组。
split()
方法
split(pattern, string, maxsplit=0, flags=0)
方法可以根据匹配项来分割字符串。maxsplit
用于指定最大分割次数;默认 0
,表示分割所有匹配项。
# 使用 split 方法根据数字分割字符串
parts = re.split(r'\d+', 'abc123def456ghi')
print(parts) # 输出: ['abc', 'def', 'ghi']
compile()
方法
compile(pattern, flags=0)
方法实际并不进行匹配,而是返回一个正则表达式匹配模式对象,这个对象可以使用 match
、search
、findall
等方法来进行匹配。常用于同一个正则表达式需要重复的与不同文本进行匹配的情况,避免重复编译相同的模式,提高效率。
p = re.compile(r'\d+')
search = p.search('abc123def')
if search:print(search.group()) # 输出: 123
flags
参数
几乎 re 模块的每种方法都有 flags
参数,该参数可以用于控制正则表达式的匹配方式:
值 | 简写 | 说明 |
---|---|---|
re.IGNORECASE | re.I | 大小写不敏感。 |
re.MULTILINE | re.M | 多行模式,改变 ^ 和 $ 的行为,使它们分别匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。 |
re.DOTALL | re.S | 使 . 特殊字符匹配任何字符,包括换行符。 |
re.UNICODE | re.U | 根据 Unicode 字符集解析字符。这是 Python 3 中的默认行为。 |
re.ASCII | re.A | 使 \w , \W , \b , \B , \d , \D , \s 和 \S 只匹配 ASCII 字符。 |
re.VERBOSE | re.X | 允许在正则表达式中添加空白和注释。 |
import re# 忽略大小写的匹配
case_insensitive = re.findall(r'abc', 'ABCabc', flags=re.IGNORECASE)
print(case_insensitive) # 输出: ['ABC', 'abc']# 多行模式的匹配
multiline = re.search(r'^abc', 'def\nabc', flags=re.MULTILINE)
if multiline:print(multiline.group()) # 输出: abc# 让点号匹配换行符
dotall = re.search(r'a.b', 'a\nb', flags=re.DOTALL)
if dotall:print(dotall.group()) # 输出: a\nb# 使用 ASCII 字符集
ascii_char = re.findall(r'\w+', 'café', flags=re.ASCII)
print(ascii_char) # 输出: ['caf']# 使用 VERBOSE 模式,允许正则表达式分行并添加注释
verbose = re.compile(r"""\b # 单词边界\w+ # 一个或多个字母数字字符\b # 单词边界
""", flags=re.VERBOSE)
print(verbose.findall('Hello, world!')) # 输出: ['Hello', 'world']
match
对象
match
对象有一些常用的属性和方法,来获取需要的数据
属性 | 说明 |
---|---|
string | 返回传递给 match 或 search 等函数的原始字符串。 |
re | 返回用于匹配的正则表达式对象。 |
pos | 返回用于匹配的字符串的起始位置。 |
endpos | 返回用于匹配的字符串的结束位置。 |
lastindex | 返回最后一个被捕获的分组在 Match 对象中的索引。 |
lastgroup | 返回最后一个被捕获的分组的名称。 |
方法 | 说明 |
---|---|
group(num=0) | 返回整个匹配的字符串,或者指定编号的分组。 |
groups(default=None) | 返回一个包含所有捕获组的元组,如果没有匹配则为 default |
groupdict(default=None) | 返回一个字典,包含所有命名的捕获组。 |
start([group]) | 返回指定分组的起始位置。 |
end([group]) | 返回指定分组的结束位置。 |
span([group]) | 返回 (start(group), end(group)) 。 |
import re# 使用 search 方法查找数字
match = re.search(r'\d+', 'User ID: 12345')
if match:print(match.group()) # 输出匹配到的数字: 12345# 使用捕获组
match = re.search(r'User ID: (\d+)', 'User ID: 12345')
if match:print(match.group(1)) # 输出第一个捕获组匹配到的内容: 12345# 使用命名捕获组
match = re.search(r'User ID: (?P<id>\d+)', 'User ID: 12345')
if match:print(match.group('id')) # 输出命名捕获组 'id' 匹配到的内容: 12345# 获取匹配的起始和结束位置
match = re.search(r'ID', 'User ID: 12345')
if match:print(match.span()) # 输出匹配字符串 'ID' 的起始和结束位置: (5, 7)# 获取所有捕获组
match = re.search(r'(\w+) (\w+)', 'Hello World')
if match:print(match.groups()) # 输出所有捕获组的内容: ('Hello', 'World')# 获取所有命名捕获组
match = re.search(r'(?P<first>\w+) (?P<second>\w+)', 'Hello World')
if match:print(match.groupdict()) # 输出所有命名捕获组的内容: {'first': 'Hello', 'second': 'World'}