Python 与 html解析

纵然是瞬间 提交于 2020-02-15 05:47:16

Python 与 html解析

本文由 CDFMLR 原创,收录于个人主页 https://clownote.github.io,并同时发布到 CSDN。本人不保证 CSDN 排版正确,敬请访问 clownote 以获得良好的阅读体验。

正则表达式

正则表达式是一种处理字符串的强大的工具,它有自己特定的语法结构,可以高效地实现字符串的检索、替换、匹配验证等操作。

我们可以利用正则表达式来提取 HTML 中我们感兴趣的信息,正如我们在使用 requests 库爬取知乎发现中的问题时那样。

下表为 正则表达式 的常用匹配规则:

模式 描述
\w 匹配字母数字及下划线
\W 匹配非字母数字及下划线
\s 匹配任意空白字符,等价于 [\t\n\r\f]
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9]
\D 匹配任意非数字
\A 匹配字符串开始
\Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结束
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配字符串的开头
$ 匹配字符串的末尾
. 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符
[...] 用来表示一组字符,单独列出:[amk] 匹配 ‘a’,‘m’ 或 ‘k’
[^...] 不在 [] 中的字符:[^abc] 匹配除了 a,b,c 之外的字符。
* 匹配 0 个或多个的表达式。
+ 匹配 1 个或多个的表达式。
? 匹配 0 个或 1 个由前面的正则表达式定义的片段,非贪婪方式
{n} 精确匹配 n 个前面表达式。
{n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b 匹配 a 或 b
( ) 匹配括号内的表达式,也表示一个组

⚠️【注意】贪婪匹配下,.* 会匹配尽可能多的字符;非贪婪匹配则尽可能匹配少的字符。
⚠️【注意】转义:如果在正则中想表现表中有的那些符号,我们需要转义,即使用 \+ 符号。例如要表现括号(,我们需要写 \(

例如,之前我们抓取「知乎-发现」中的问题时,使用的是如下正则表达式:

explore-feed.*?question_link.*?>(.*?)</

它就可以匹配请求到的 html 中的:

<div class="explore-feed feed-item" data-offset="1">
<h2><a class="question_link" href="/question/311635229/answer/..." target="_blank" data-id="..." data-za-element-name="Title">
......
</a></h2>

RE in Python

Python 内置的 re 库提供了对正则表达式的支持。

下面我们来了解 re 库的使用:

match()

match() 方法,需要我们传入要匹配的字符串以及正则表达式,来检测这个字符串是否符合该正则表达式。

match() 方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。

match() 会从一开始匹配,也就是说,如果第一个字符就匹配不上去,则就不匹配。

我们常可以这样用:

test = '待测字符串'
if re.match(r'正则表达式', test):
    print('Match')
else:
    print('Failed')

提取分组:

>>> text = 'abc 123'

>>> print(re.match(r'\s+\w\d', text))
None

>>> r = re.match(r'\w*? (\d{3})', text)
>>> r
<re.Match object; span=(0, 7), match='abc 123'>
>>> r.group()
'abc 123'
>>> r.group(0)
'abc 123'
>>> r.group(1)
'123'
  • group()、group(0) 会输出完整的匹配结果。
  • group(1),以及此例中没有的group(2),group(3)… 则会输出第一、二、三…个被 () 包围的匹配结果。

修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^$
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

这些修饰符可以作为 re.match 的第三个参数传入,产生上面描述中的效果。

result = re.match('^He.*?(\d+).*?Demo$', content, re.S)

search()

与 match() 不同,search() 在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果,如果一处都没有,则返回 None.

>>> p = '\d+'
>>> c = 'asd123sss'
>>> r = re.search(p, c)
>>> r
<re.Match object; span=(3, 6), match='123'>
>>> r.group()
'123'

findall()

findall() 会搜索整个字符串然后返回匹配正则表达式的所有内容,返回的结果为一个list。

>>> import re
>>> p = '\d+'
>>> c = 'asd123dfg456;;;789'
>>> re.findall(p, c)
['123', '456', '789']

XPath & LXML

XPath (XML Path Language) 是设计来在XML文档中查找信息的语言,它同样适用于HTML。

我们在爬虫时,可以使用 XPath 来做相应的信息抽取。

⚠️【注意】需要安装好 LXML。

XPath常用规则

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

我们常用 // 开头的 XPath 规则来选取所有符合要求的节点。

另外,常用运算符见 XPath 运算符

导入 HTML

从字符串导入 HTML

导入了 LXML 库的 etree 模块,然后声明了一段 HTML 文本,调用 HTML 类进行初始化,这样我们就成功构造了一个 XPath 解析对象。

⚠️【注意】etree 模块可以对 HTML 文本进行修正。

调用 tostring() 方法即可输出修正后的 HTML 代码,结果是 bytes 类型(可以利用 decode() 方法转成 str 类型)

from lxml import etree
text = '''
<div>
    <ul>
         <li class="item-0"><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)
result = etree.tostring(html)
print(result.decode('utf-8'))
从文件导入 HTML
from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

获取节点

获取所有节点

获取一个 HTML 中的所有节点,使用规则 //*

from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())

result = html.xpath('//*')

print(result)

我们得到了一个 由 Element 类型组成的列表。

获取所有指定标签

如果我们想获取所有 li 标签,我们可以把上例中的html.xpath() 中的规则改为 '//li'

from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li')
print(result)

如果无法获取任何匹配结果,html.xpath 将会返回 []

获取子节点

选择 li 节点所有直接 a 子节点,使用规则 '//li/a'

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)

要获取其下所有子孙a节点可以这样://li//a

获取特定属性的节点

用 @ 符号进行属性过滤。
smt[…] 是有 … 限制的smt。

选中 href 是 link4.html 的 a 节点,规则是 '//a[@href="link4.html"]:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]')
print(result)
获取父节点

如果我们想获取上例的父节点, 然后再获取其 class 属性:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/../@class')
# 也可以用“节点轴” '//a[@href="link4.html"]/parent::*/@class'

print(result)

关于节点轴的使用,详见 XPath Axes

获取节点中的的文本

XPath 中的 text() 方法可以获取节点中的直接文本(不包括其子节点中的文本)。

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
print(result)
获取属性
from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)

用上面这种方法只能获取只有一个值的属性,
对于下面这种:

<li class="li li-first"><a href="link.html">first item</a></li>

li 节点的 class 属性有两个值,上面这个方法会失效,我们可以使用 contains() 函数:

from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = e#tree.HTML(text)
result = html.xpath('//li[contains(@class, "li")]/a/text()')
print(result)

这里还可以使用运算符 and 来连接:

from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()')
print(result)

补充

点击链接查看详细的 XPath 教程lxml 库

BeautifulSoup

BeautifulSoup 提供一些简单的、Python式的函数用来处理导航、搜索、修改分析树等功能。它通过解析文档为用户提供需要抓取的数据。利用它我们可以提高解析效率。

BeautifulSoup 拥有完善的官方中文文档,可以查看 BeautifulSoup官方文档

⚠️【注意】需要安装好 BeautifulSoup 和 LXML。

BeautifulSoup 可以使用多种解析器,主要的几种如下:

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, “html.parser”) Python的内置标准库、执行速度适中 、文档容错能力强 Python 2.7.3 or 3.2.2)前的版本中文容错能力差
LXML HTML 解析器 BeautifulSoup(markup, “lxml”) 速度快、文档容错能力强 需要安装C语言库
LXML XML 解析器 BeautifulSoup(markup, “xml”) 速度快、唯一支持XML的解析器 需要安装C语言库
html5lib BeautifulSoup(markup, “html5lib”) 最好的容错性、以浏览器的方式解析文档、生成 HTML5 格式的文档 速度慢、不依赖外部扩展

我们一般使用 LXML 解析器来进行解析,使用方法如下:

from bs4 import BeautifulSoup
soup = BeautifulSoup('<p>Hello</p>', 'lxml')    
print(soup.p.string)

BeaufulSoup对象的初始化

使用如下代码就可以导入HTML,完成BeautifulSoup对象的初始化,并自动更正(如闭合未闭合的标签)。

soup = BeautifulSoup(markup, "lxml")   # markup 是 HTML 的 str

初始化之后我们还可以对要解析的字符串以标准的缩进格式输出:

print(soup.prettify())

节点选择器

选择标签

选择元素的时候直接通过调用节点的名称就可以选择节点元素,
调用 string 属性就可以得到节点内的文本。

from bs4 import BeautifulSoup

html = """
<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>
"""

soup = BeautifulSoup(html, 'lxml')

print(soup.title)           # <title>The Dormouse's story</title>
print(type(soup.title))     # <class 'bs4.element.Tag'>
print(soup.title.string)    # The Dormouse's story
print(soup.head)            # <head><title>The Dormouse's story</title></head>
print(soup.p)               # <p class="title" name="dromouse"><b>The Dormouse's story</b></p>
嵌套选择

我们还可以进行 嵌套选择,即做类似 父.子.孙 的选择:

print(soup.head.title.string)
关联选择

有时候我们难以做到一步就可以选择到想要的节点元素,这时我们可以先选中某一个节点元素,然后以它为基准再选择它的子节点、父节点、兄弟节点等等

获取子孙节点

选取到了一个节点元素之后,如果想要获取它的直接 子节点 可以调用 contents 属性,将返回一个依次列有所有子节点的list。

如p标签之类的节点中可能既包含文本,又包含节点,返回的结果会将他们以列表形式都统一返回。

soup.p.contents     # 注意里面的文字被切成了几部分

'''(result)
[
    'Once upon a time ... were\n',
    <a class="sister" href="..." id="link1"><!-- Elsie --></a>,
    ',\n',
    <a class="sister" href="..." id="link2">Lacie</a>,
    ' and\n',
    <a class="sister" href="..." id="link3">Tillie</a>,
    ';\nand ... well.'
]
'''

同时,查询 子节点,我们还可以使用 children 属性,它将返回一个 list_iterator object,化为 list 之后,就和 contents 一样了:

>>> s.p.children
<list_iterator object at 0x109d6a8d0>
>>> a = list(soup.p.children)
>>> b = soup.p.contents
>>> a == b
True

我们可以逐个编号输出子节点:

for i, child in enumerate(soup.p.children):
    print(i, child)

要得到所有的 子孙节点(所有下属节点)的话可以调用 descendants 属性,descendants 会递归地查询所有子节点(深度优先),得到的是所有的子孙节点,返回结果是一个 <generator object Tag.descendants at 0x109d297c8>

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
print(soup.p.descendants)
for i, d in enumerate(soup.p.descendants):
    print(i, d)
获取父节点和祖先节点

如果要获取某个节点元素的父节点,可以调用 parent 属性,返回一个节点:

>>> soup.span.parent
# 结果是 <p>...</p>

如果我们要想获取所有的祖先节点(一层层向上找,直到整个html),可以调用 parents 属性,返回一个generator:

>>> soup.span.parents
<generator object PageElement.parents at 0x109d29ed0>
>>> list(soup.span.parents)
# 结果是 [<p>...</p>, <div>...</div>, <body>...</body>, <html>...</html>]

⚠️【注意】父是 parent,祖先是 parents

获取兄弟节点

要获取同级的节点也就是兄弟节点,我们可以调用了四个不同的属性,它们的作用不尽相同:

  • next_sibling:获取节点向下一个兄弟节点,返回节点。
  • previous_sibling:获取向上一个兄弟节点,返回节点。
  • next_siblings:获取向下所有兄弟节点,返回一个generator。
  • previous_siblings:获取向上所有兄弟节点,返回一个generator。
>>> from bs4 import BeautifulSoup
>>> html = """
... <html>
...     <body>
...         <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">
...                 <span>Elsie</span>
...             </a>
...             Hello
...             <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>
... """
>>> soup = BeautifulSoup(html, 'lxml')
>>> soup.a
<a class="sister" href="http://example.com/elsie" id="link1">
<span>Elsie</span>
</a>
>>> soup.a.next_sibling
'\n            Hello\n            '
>>> soup.a.previous_sibling
'\n            Once upon a time there were three little sisters; and their names were\n            '
>>> soup.a.next_siblings
<generator object PageElement.next_siblings at 0x1110e57c8>
>>> soup.a.previous_siblings
<generator object PageElement.previous_siblings at 0x1110e5de0>
>>> for i in soup.a.previous_siblings:
...     print(i)
... 

            Once upon a time there were three little sisters; and their names were
            
>>> for i in soup.a.next_siblings:
...     print(i)
... 

            Hello
            
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
 
            and
            
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

            and they lived at the bottom of a well.
        
>>> 


方法选择器

有时难以利用节点选择器直接找到想要的节点时,我们可以利用 find_all()、find() 等方法,传入相应等参数就可以灵活地进行查询,得到想要的节点,然后通过关联选择就可以轻松获取需要的信息。

find()

find() 传入一些属性或文本来得到符合条件的元素,返回第一个匹配的元素。

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

使用实例如下:

from bs4 import BeautifulSoup

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''

soup = BeautifulSoup(html, 'lxml')
print(soup.find(name='ul')
print(soup.find(attrs={'class': 'element'}))
print(soup.find(text=re.compile('.*?o.*?', re.S)))      # 结果会返回匹配正则表达式的第一个节点的文本(结果不是节点)
findall()

find_all,类似于 find,但是 find_all 查询所有符合条件的元素,返回所有匹配的元素组成的列表。

更多

还有诸如find_parents()、find_next_siblings()、find_previous_siblings()等的find,基本使用都差不多,只是搜索范围不同,详见 文档


CSS选择器

BeautifulSoup 还提供了 CSS 选择器。
使用 CSS 选择器,只需要调用 select() 方法,传入相应的 CSS 选择器即可,返回的结果是符合 CSS 选择器的节点组成的列表:

from bs4 import BeautifulSoup

html='''
<div class="panel">
    <div class="panel-heading">
        <h4>Hello</h4>
    </div>
    <div class="panel-body">
        <ul class="list" id="list-1">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
            <li class="element">Jay</li>
        </ul>
        <ul class="list list-small" id="list-2">
            <li class="element">Foo</li>
            <li class="element">Bar</li>
        </ul>
    </div>
</div>
'''

soup = BeautifulSoup(html, 'lxml')
print(soup.select('.panel .panel-heading'))
print(soup.select('ul li'))
print(soup.select('#list-2 .element'))
print(type(soup.select('ul')[0]))

提取信息

获取完整标签

要获取一个标签的完整html代码,只需要写它的节点选择器即可:

soup.title

获取标签类型

利用 name 属性来获取节点的类型(p、a、title、pre 等):

print(soup.title.name)

获取标签内容

正如我们之前所说,调用 string 属性就可以得到节点内的文本:

soup.title.string

⚠️【注意】如果标签下包含其他标签,.string 是不起作用的,它会返回一个 None:

>>> from bs4 import BeautifulSoup
>>> html = '<p>Foo<a href="#None">Bar</a></p>'
>>> soup = BeautifulSoup(html, 'lxml')
>>> print(soup.p.string)
None

获取内容,还可以使用节点的 get_text() 方法:

soup.p.get_text()

利用get_text,可以获取标签下所有文本,包括其子节点中的:

>>> from bs4 import BeautifulSoup
>>> html = '<p>Foo<a href="#None">Bar</a></p>'
>>> soup = BeautifulSoup(html, 'lxml')
>>> print(soup.p.string)
None
>>> print(soup.p.get_text())
FooBar
获取属性

每个节点可能有多个属性,比如 id,class,我们可以调用 attrs 获取所有属性,进而可以通过字典的取值方法(中括号加属性名称,或调用其get()方法)获取特定属性:

print(soup.p.attrs)
print(soup.p.attrs['name'])

'''(results)
{'class': ['title'], 'name': 'dromouse'}
dromouse
'''

也可以直接使用中括号和属性名:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'lxml')
for ul in soup.select('ul'):
    print(ul['id'])
    print(ul.attrs['id'])
    # 循环体的两行代码等效

PyQuery

pyquery: a jquery-like library for python

PyQuery 的使用方法和jQuery基本相同。

⚠️【注意】需要安装好 PyQuery。

初始化

PyQuery 初始化时可以传入多种形式的数据源,如内容是 HTML 的字符串、源的URL、本地的文件名等。

字符串初始化
from pyquery import PyQuery as pq

html = '''
<h1>Header</h1>
<p>Something</p>
<p>Other thing</p>
<div>
    <p>In div</p>
</div>
'''

doc = pq(html)      # 传入HTML字符串
print(doc('p'))     # 传入CSS选择器

'''(results)
<p>Something</p>
<p>Other thing</p>
<p>In div</p>

'''
URL初始化
from pyquery import PyQuery as pq
doc = pq(url='http://www.baidu.com', encoding='utf-8')      # 这里不写encoding可能中文乱码
print(doc('title'))

'''(result)
<title>百度一下,你就知道</title>
'''

CSS选择器

详见 CSS 选择器表

查找节点

  • 查找节点用 children('css-selector') 方法,参数为空则为全部。
  • 查找子孙节点用 find('css-selector') 方法,参数不可为空!
  • 查找节点用 parent('css-selector') 方法,参数为空则为全部。
  • 查找祖先节点用 parents('css-selector') 方法,参数为空则为全部。
  • 查找兄弟节点用 siblings('css-selector') 方法,参数为空则为全部。
>>> p = doc('div')
>>> p
[<div#wrapper>, <div#head>, <div.head_wrapper>, <div.s_form>, <div.s_form_wrapper>, <div#lg>, <div#u1>, <div#ftCon>, <div#ftConw>]
>>> type(p)
<class 'pyquery.pyquery.PyQuery'>
>>> p.find('#head')
[<div#head>]
>>> print(p.find('#head'))
<div id="head"> ... </div> 

遍历

用 PyQuery 选择到的结果可以遍历:

>>> for i in  p.parent():
...     print(i, type(i))
... 
<Element a at 0x1055332c8> <class 'lxml.html.HtmlElement'>
<Element a at 0x105533368> <class 'lxml.html.HtmlElement'>
<Element a at 0x105533458> <class 'lxml.html.HtmlElement'>

注意是lxml的Element了,要用lxml的方法处理。

获取信息

attr() 获取属性
a = doc('a')
print(a.attr('href'))

attr() 必须传入要选择的属性名。
若对象包含多个节点,调用对象的attr(),只会返回第一个对象的对应结果。要返回每一个的需要遍历。

text() 获取文本
a = doc('a')
a.text()

这个会输出所有包含节点的文本join的结果。

节点操作

PyQuery 还可以操作节点,这个不是重点。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!