pyquery:灵活的 html 解析库

楔子

下面我们来介绍一个 html 解析库,名叫 pyquery,如果你用过 jquery,那么 pyquery 会非常容易上手。因为 pyquery 本身就是仿照 jquery 的风格设计的,当然没用过 jquery 也没有关系,因为 pyquery 本身就很容易。

我们使用 requests 下载完网页的 html 之后,肯定要从里面解析出需要的信息,比如:解析出里面所有的图片的路径、获取里面文章的内容等等。下面我们就来看看这个库怎么使用,首先是安装,直接 pip install query 即可。

pyquery 底层使用了 lxml,lxml 也是一个非常强大的库,我们可以使用 xpath 定位任何想要的元素。但是没有 pyquery 方便,灵巧。

PyQuery 对象

我们在下载完 html 之后,会将其传递到 pyquery.PyQuery 中,我们后面会通过操作 PyQuery 对象来获取我们想要的属性。

from pyquery import PyQuery

html = """
<body>
    <p>
        来自东方地灵殿的少女
    </p>
</body>
"""

p = PyQuery(html)  # <class 'pyquery.pyquery.PyQuery'>
print(type(p))
print(p)
"""
<body>
    <p>
        来自东方地灵殿的少女
    </p>
</body>
"""

我们的 html 文本直接丢给 PyQuery 即可,当然打印的话会输出原本的 html,但它是一个 PyQuery 对象。我们除了传递 html 之外,还可以传递一个 url,或者 html 文件。

from pyquery import PyQuery

# 传递一个 url, 会自动调用 urlopen 下载内容
p1 = PyQuery(url="https://www.baidu.com", encoding="utf-8")
# 传递一个 html 文件, 会自动打开并读取
p2 = PyQuery(filename="1.html")

当然后两者其实不是很常用,我们一般还是会搭配 requests 或者 aiohttp,下载完页面之后直接丢给 PyQuery。

CSS 选择器

我们说 pyquery 是模仿 jquery 设计的,显然它也是通过类似于 CSS 选择器的方式进行筛选的,下面介绍一些常用的选择器。

from pyquery import PyQuery

html = """
<body>
    <div class="cls1 cls2">
        <p>你好呀</p>
        <p class="cls3">我不好</p>
        <div class="inner">
            <p id="yoyoyo">
                哼哼
            </p>
        </div>
    </div>

    <div>
        <a href="http://www.baidu.com/hahaha/1.png"></a>
        <p>
            <a href="http://www.baidu.com/gagaga/2.png"></a>
        </p>
    </div>
    
    <div class="cls1">
        <span>嘿嘿嘿</span>
    </div>
</body>
"""
p = PyQuery(html)

我们上面这个 html 为例,来看看相关操作。

选择某个指定标签

# 选择所有的 p 标签
print(p("p"))
"""
<p>你好呀</p>
        <p class="cls3">我不好</p>
        <p id="yoyoyo">
                哼哼
            </p>
        <p>
            <a href="http://www.baidu.com/gagaga/2.png"/>
        </p>
"""

会选择所有指定的标签,并且会包含标签里面的所有内容。

选择多个指定标签

# 选择所有的 p 标签 和 a 标签
print(p("p,a"))
"""
<p>你好呀</p>
        <p class="cls3">我不好</p>
        <p id="yoyoyo">
                哼哼
            </p>
        <a href="http://www.baidu.com/hahaha/1.png"/>
        <p>
            <a href="http://www.baidu.com/gagaga/2.png"/>
        </p>
    <a href="http://www.baidu.com/gagaga/2.png"/>
"""

多个标签之间使用逗号分隔,会将多个标签都筛选出来。注意:筛选的标签之前是独立的,比如第二个 a 标签,它是在 p 标签里面的,我们在筛选 p 标签的时候会将该表签内部的 a 标签筛选出来了,但是在筛选 a 标签的时候又筛选出来一次,因此标签之间是独立的。

选择标签下的所有标签

# 选择所有 div 标签下的 a 标签
print(p("div a"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
        <a href="http://www.baidu.com/gagaga/2.png"/>
"""

tag1 tag2 tag3,表示筛选所有 tag1 标签下的所有 tag2 标签下的所有 tag3 标签(进制套娃)。

选择标签下的子标签

# 选择所有 div 标签下的 a 标签(只从儿子节点当中选择)
print(p("div>a"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
"""

如果是空格的话,那么会从子孙当中选,但如果是大于号,那么只会从儿子当中选择。

按照 id 选择标签

# 选择 id="yoyoyo" 的标签, 直接通过 #{id} 即可
print(p("#yoyoyo"))
"""
<p id="yoyoyo">
                哼哼
            </p>
"""

id 在一个 html 中具有唯一性,所以有 id 属性的话,那么会非常好定位。

按照 class 选择标签

# 选择 class="cls3" 的标签, 直接通过 .{class} 即可
print(p(".cls3"))
"""
<p class="cls3">我不好</p>
"""

选择所有 class 属性等于 "cls3" 的标签,但是注意,class 属性并不是唯一的,如果有多个标签应用了同一个 class,那么会选择所有应用此 class 的标签。

print(p(".cls1"))
"""
<div class="cls1 cls2">
        <p>你好呀</p>
        <p class="cls3">我不好</p>
        <div class="inner">
            <p id="yoyoyo">
                哼哼
            </p>
        </div>
    </div>

    <div class="cls1">
        <span>嘿嘿嘿</span>
    </div>
"""

我们看到两个 div 都应用了 cls1 这个 class,因此它们都被筛选了出来。而第一个 div 除了 cls1,还应用了 cls2 这个 class。那么问题来了,如果我们希望选择同时应用了 cls1 和 cls2 的标签该怎么做呢?

print(p(".cls1.cls2"))
"""
<div class="cls1 cls2">
        <p>你好呀</p>
        <p class="cls3">我不好</p>
        <div class="inner">
            <p id="yoyoyo">
                哼哼
            </p>
        </div>
    </div>
"""

我们看到此时就只获取了第一个 div,注意:.cls1 和 .cls2 之间不可以有空格,如果加上了空格,那么含义就变成了选择 .cls1 标签下面的 .cls2 标签。没错,无论是 id、class、还是标签都是等价的,它们是可以搭配使用的。比如:div .cls1 表示找到所有 div 标签下的 class="cls1" 的标签;div.cls1 表示找到所有 class="cls1" 的 div 标签;div.cls1 p>.cls2.cls3,它就表示先找到所有 class="cls1" 的 div 标签,然后再从中找到所有的 p 标签,最后再从找到 p 标签的儿子(此时只会找一层)中找到所有class="cls2 cls3" 的标签。

# 找到所有 class="cls1 cls2" 的 div 标签, 再从其儿子中找到所有 class="inner" 的 div 标签
print(p("div.cls1.cls2>div.inner"))
"""
<div class="inner">
            <p id="yoyoyo">
                哼哼
            </p>
        </div>
"""

综上所述,pyquery 还是很强大的。

选择是否具有某个属性的标签

# 选择具有 class 属性的 p 标签
print(p("p[class]"))
"""
<p class="cls3">我不好</p>
"""

# 选择具有 id 属性的 p 标签
print(p("p[id]"))
"""
<p id="yoyoyo">
                哼哼
            </p>
"""

# 选择 class="inner" 的 div 标签, inner 周围可以加上单引号、双引号、也可以不加引号
# 但还是建议加上引号, 因为更清晰, 而且如果包含特殊字符的话, 那么必须加引号
# 等价于 p("div.inner")
print(p("div[class='inner']"))
"""
<div class="inner">
            <p id="yoyoyo">
                哼哼
            </p>
        </div>
"""

# 这些属性除了 id、class 之外, 也可以是其它的任意属性, 比如我们自己随便写一个也是可以的
# 下面选择所有具有 href 属性的 a 标签
print(p("a[href]"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
        <a href="http://www.baidu.com/hahaha/2.png"/>
"""

# 选择以 href 等于某个 url 的 a 标签, 这里的 url 必须要使用引号包起来
print(p("a[href='http://www.baidu.com/hahaha/1.png']"))
# 选择以 ... 开头
print(p("a[href^='http://www.baidu.com']"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
        <a href="http://www.baidu.com/gagaga/2.png"/>
        
"""
print(p("a[href^='http://www.baidu.com/g']"))
"""
<a href="http://www.baidu.com/gagaga/2.png"/>
"""

# 选择以 ... 结尾
print(p("a[href$='1.png']"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
"""

# 选择包含 ...
print(p("a[href*='hahaha']"))
"""
<a href="http://www.baidu.com/hahaha/1.png"/>
"""

选择指定位置的标签

# tag:nth-child(n), 选择第 n 个 tag 标签
print(p.find(".cls1.cls2>p:nth-child(1)"))
"""
<p>你好呀</p>
"""
print(p.find(".cls1.cls2>p:nth-child(2)"))
"""
<p class="cls3">我不好</p>
"""

一些常见的 CSS 选择器我们就说到这里,当然一些内容我们后面会继续补充,比如如何获取标签里面的属性、如何根据文本确定标签等等。

获取标签属性

我们拿到的指定的标签,那么要如何获取里面的属性呢?

获取文本

# 获取了所有的 p 标签
p_tag = p("p")

# 标签有一个 html 方法, 会获取该标签内部的所有内容
# 比如: <p><a></a></p>, 那么调用 html 会打印 <a></a>, 这个不是很常用
# 更常用的是 text 方法, 表示获取文本
print(p_tag.text())  # 你好呀 我不好 哼哼 

获取其它属性

# 获取所有的 a 标签
a_tag = p("a")
print(a_tag.attr("href"))
"""
http://www.baidu.com/hahaha/1.png
"""
# 但是问题来了, 我们貌似有两个 url 啊, 为什么只获取了一个呢?
# 因为 PyQuery 默认只会获取第一个标签的属性, 我们需要进行遍历
for a in a_tag.items():
    print(a.attr("href"))
"""
http://www.baidu.com/hahaha/1.png
http://www.baidu.com/gagaga/2.png
"""
# 此时就全部获取了
# 当然除了 href 属性之外, 我们也可以获取其它的任意属性, 甚至是我们自定义的也可以
for div in p("div").items():
    print(div.attr("class"))
"""
cls1 cls2
inner
None
cls1
"""

个人觉得还是很方便的。

find 和 filter

PyQuery 对象还有两个方法,分别是 find 和 filter,它们的用法举个栗子就一目了然了。

先来看看 find:

# p("div p .cls a p")  等价于 p.find("div p .cls a p")
print(
    str(p("div .inner p")) == str(p.find("div .inner p"))
)  # True

# 另外 p.find("div .inner p") 还可以写成以下几种形式
# p.find("div").find("p").find(".cls") 或者 p.find("div").find("p .cls") 或者 p.find("div p").find(".cls") 等等
print(
    str(p("div .inner p")) ==
    str(p.find("div .inner p")) ==
    str(p.find("div").find(".inner").find("p")) ==
    str(p.find("div .inner").find("p"))
)  # True

但是注意:p("div .inner") 和 p("div")(".inner") 两者不是等价的,因此建议要么不用 find 要么都用 find。

再来看看 filter:

# 所以 filter 就相当于筛选, 先找到所有的 div 标签, 然后从中筛选所以的 class="inner" 的标签
# 如果是多个条件, 那么就写调用多次 filter, 因为整个过程都是在拼接 xpath
print(
    str(p("div.inner")) == str(p.find("div").filter(".inner"))
)  # True

filter 还有一个神奇的用法,那就是可以根据文本进行过滤:

print(p.find("p"))
"""
<p>你好呀</p>
        <p class="cls3">我不好</p>
        <p id="yoyoyo">
                哼哼
            </p>
        <p>
            <a href="http://www.baidu.com/gagaga/2.png"/>
        </p>
"""

# p.find("p") 和 p("p") 均可, 因为这两者是等价的; 
# 但如果再来一个 .inner, 那么只能是 p("p .inner") 不可以是 p("p")(".inner"), 但 p("p").find(".inner") 是可以的, 因为 find 可以多次调用
print(
    p("p").filter(lambda _, this: "我" in PyQuery(this).text())
)  
"""
<p class="cls3">我不好</p>
"""
# 此时只选择了文本包含的 "我" 的 p 标签

总结

总的来说,pyquery 还是相当方便的,相比 bs4 多了更多的灵活性,而且速度也更快一些。

当然我们只介绍了 pyquery 的一部分内容,比如 PyQuery 对象还可以调用 siblings 方法返回所有的兄弟节点、调用 patent 返回父节点、调用 parents 返回祖宗节点等等。只不过对于解析 html 而言,个人觉得不是很常用,有兴趣可以自己去了解一下。

posted @ 2019-11-17 15:59  古明地盆  阅读(813)  评论(0编辑  收藏  举报