notebook notebook
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)

学习笔记

啦啦啦,向太阳~
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)
  • 爬虫

    • 崔庆才 - Python爬虫

      • 爬虫基础
      • 基本库的使用
        • 1. 正则表达式
          • 1.1 match
          • 1.2 search 和 findall
          • 1.3 sub
          • 1.4 compile
        • 2. httpx 的使用
        • 3. XPath 的使用
          • 3.1 XPath 常用规则
          • 3.2 实例引入
          • 3.3 所有节点
          • 3.4 子节点
          • 3.5 文本获取
          • 3.6 属性匹配和属性获取
          • 3.7 按序选择
          • 3.8 节点轴选择
        • 4. pyquery 的使用
        • 5. parsel 的使用
      • JavaScript 动态渲染页面爬取
      • Python 的协程
  • Python 数据分析

  • MySQL

  • Redis

  • Elasticsearch

  • Kafka

  • 数据仓库

  • 数据科学
  • 爬虫
  • 崔庆才 - Python爬虫
yubin
2022-01-11
目录

基本库的使用

# 1. 正则表达式

正则表达式用于字符串的检索、替换和匹配。可以在 菜鸟正则表达式在线测试 (opens new window) 或 oschina正则表达式测试工具 (opens new window) 进行尝试。

Python 的 re 库提供了整个正则表达式的实现,往往选择使用它。第三方模块 regex (opens new window) , 提供了与标准库 re 模块兼容的 API 接口,同时,还提供了更多功能和更全面的 Unicode 支持。

# 1.1 match

re.match(pattern, string, flags=0)

会尝试从字符串的起始位置开始匹配正则表达式,如果匹配,就返回一个相应的匹配对象 。 如果没有匹配,就返回 None ;注意它跟零长度匹配是不同的。

注意即便是 MULTILINE (opens new window) 多行模式, re.match() 也只匹配字符串的开始位置,而不匹配每行开始。

示例:





 



import re

content = 'Hello 123 4567 World_This is a Regex Demo'
pattern = '^Hello\s\d\d\d\s\d{4}\s\w{10}'
result = re.match(pattern, content)
print(result.group())
print(result.span())
1
2
3
4
5
6
7

输出:

Hello 123 4567 World_This
(0, 25)
1
2
  • group 方法返回匹配的内容

  • span 方法返回匹配的范围

# 匹配目标

如果是想从字符串中提取一部分内容,比如从一段文本中提取出 email 地址,该怎么办?可以使用 () 将向提取的子字符串括起来。

示例:




 




import re

content = 'Hello 1234567 World_This is a Regex Demo'
pattern = '^Hello\s(\d+)\sWorld'
result = re.match(pattern, content)
print(result.group())
print(result.group(1))
1
2
3
4
5
6
7

输出:

Hello 1234567 World
1234567
1
2

可以看到 pattern 中数字部分的正则表达式被 () 括了起来,这样调用 group(1) 获取了匹配结果:

  • group() 返回完整的匹配结果
  • group(1) 返回第一个被 () 包围的匹配结果

# 通用匹配

  • * 代表匹配前面的字符无限次
  • . 代表可以匹配任意字符(除换行符)

# 贪婪与非贪婪

有时候通用匹配 .* 匹配到的内容不是我们想要的,这涉及到贪婪模式和非贪婪模式:

  • 贪婪匹配是匹配尽可能多的字符
  • 非贪婪匹配是匹配尽可能少的字符

示例,假如我们仍然向获得目标字符串中间的数字,所以正则表达式中间仍然写 (\d+),两边较为杂乱,所以用 .* 来匹配:

import re

content = 'Hello 1234567 World_This is a Regex Demo'
pattern1 = '^He.*(\d+).*Demo$'  # 贪婪匹配
pattern2 = '^He.*?(\d+).*Demo$' # 非贪婪匹配

result1 = re.match(pattern1, content)
result2 = re.match(pattern2, content)

print(result1.group(1))
print(result1.group(1))
1
2
3
4
5
6
7
8
9
10
11

输出:

7
1234567
1
2
  • 在贪婪匹配下,.* 会匹配尽可能多的字符,.* 后面是 \d+,也就是至少匹配一个数字,但没有规定几个数字,因此,.* 会尽可能多的匹配,这里也就把 123456 都匹配了,只给 \d+ 留下了一个可满足条件的数字 7
  • 非贪婪匹配的写法是 .*?,也就是多了一个 ? ,这里当 .*? 匹配到 Hello 后面的空白字符时,再往后就是数字了,可以交给 (\d+) 来匹配,于是 .*? 就不再匹配了,最终结果是 (\d+) 匹配了 1234567

所以说,在做匹配的时候、字符串中间尽量使用非贪梦匹配,也就是用 .*?代替 .*,以免出现匹配结果缺失的情况。

但这里需要注意,如果匹配的结果在字符串结尾,.*? 有可能匹配不到任何内容了,因为它会匹配尽可能少的字符。例如:

import re
content ='http://weibo.com/comment/kEraCN'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content)
print('result1',result1.group(1))
print('result2',result2.group(1))
1
2
3
4
5
6

运行结果如下:

result1
result2 kEraCN
1
2

可以观察到,.*?没有匹配到任何结果,而 .* 则是尽量多匹配内容,成功得到了匹配结果。

# 修饰符(标记)

正则表达式的标记用于指定额外的匹配策略。

标记不写在正则表达式里,标记位于表达式之外,格式如下:

/pattern/flags
1

下表列出了正则表达式常用的修饰符:

修饰符 含义 描述
i ignore - 不区分大小写 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
g global - 全局匹配 查找所有的匹配项。
m multi line - 多行匹配 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
s 特殊字符圆点 . 中包含换行符 \n 默认情况下的圆点 . 是 匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。
  • 在 re 库中,是写在 match 方法的第三个参数里,比如 re.match(p, s, re.S)
  • 较为常用的是 re.S 和 re.I

Python 示例:

import re

content = """Hello 1234567 World_This
is a Regex Demo
"""
pattern = '^He.*?(\d+).*Demo$'

result1 = re.match(pattern, content)
print(result1.group(1))
1
2
3
4
5
6
7
8
9

运行后会报错,因为未匹配成功导致 match 返回一个 None,在 None 上调用 group 引发错误。这里 .* 无法匹配换行符,我们再加一个修饰符 re.S 便可以得到解决:

result2 = re.match(pattern, content, re.S)
print(result2.group(1))
1
2

这个 re.S 在网页匹配中经常用到,因为 HTML 节点经常会有换行,加上他,就可以匹配节点与节点之间的换行了。

# 转义匹配

当在目标字符串中遇到用作正则匹配模式的特殊字符时,在此字符前面加上反斜线 \ 转义一下即可。比如 \. 可以匹配 .。

# 1.2 search 和 findall

match 是从字符串的开头开始匹配的,意味着一旦开头不匹配,整个匹配就失败了。

  • search 会在匹配时扫描整个字符串,然后返回第一个匹配成功的结果,如果扫描完都没有找到,那返回 None
  • findall 获取与正则表达式相匹配的所有字符串,其返回结果是列表类型

如果只想获取匹配的第一个字符串,可以用 search 方法,如果需要提取多个内容,可以用 findall 方法。

# 1.3 sub

除了使用正则表达式提取信息,有时候还需要借助它来修改文本。例如,想要把一串文本中的所有数字都去掉,如果只用字符串的 replace 方法,未免太烦琐了,这时可以借助 sub 方法。实例如下:

import re
content = '54aK54yr50iR54ix5L2g'
content = re.sub('\d+', '', content)
print(content)
1
2
3
4

运行结果如下:

aKyroiRixLg
1

这里往 sub 方法的第一个参数中传人 \d+ 以匹配所有的数字,往第二个参数中传入把数字替换成的字符串(如果去掉该参数,可以赋值为空),第三个参数是原字符串。

# 1.4 compile

compile 方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。另外,compile 还可以传入修饰符,例如 re.S 等修饰符,这样在 search、findall 等方法中就不用额外传递了。

import re

content1 = '2019-12-15 12:00'
content2 = '2020-06-12 13:15'
content3 = '2021-09-01 14:20'

pattern = re.compile('\d{2}:\d{2}')

result1 = re.sub(pattern, '', content1)
result2 = re.sub(pattern, '', content2)
result3 = re.sub(pattern, '', content3)

print(result1, result2, result3)
1
2
3
4
5
6
7
8
9
10
11
12
13

输出:

2019-12-15  2020-06-12  2021-09-01
1

更多方法可以参见 Python3 正则表达式 | 菜鸟教程 (runoob.com) (opens new window) 或官方文档 re --- 正则表达式操作 (opens new window)

# 2. httpx 的使用

有些网站强制使用 HTTP/2.0 协议访问,而 urilib 和 requests 只支持 HTTP/1.1,这时可以采用 hyper 或 httpx,后者使用更方便,功能更强大。

官方文档见 HTTPX (python-httpx.org) (opens new window)

# 案例

https://spa16.scrape.center/ 就是一个强制使用 HTTP/2.0 访问的网站,用浏览器打开后查看 Network 可以看到传输全部是通过 h2 方式。

如果我们使用 requests 爬取,会产生错误:

image-20220111155641392

# 安装

见官方文档

# 基本使用

用 httpx 来爬取刚刚的网站:

import httpx

url = 'https://spa16.scrape.center/'
client = httpx.Client(http2=True)
response = client.get(url)
print(response.text)
1
2
3
4
5
6
  • 注意默认不会开启对 HTTP/2.0 的支持,需要手动声明。

注意在客户端的 httpx 上启用对HTTP/2.0的支持并不意味着请求和响应都将通过 HTTP/2.0 传输,这得客户端和服务端都支持 HTTP/2.0 才行。如果客户端连接到仅支持 HTTP/1.1 的服务器,那么它也需要改用HTTP/1.1。

其他用法见官网。

# 3. XPath 的使用

XPath,即 XML 路径语言,是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。

# 3.1 XPath 常用规则

表达式 描述
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。
  • 具体示例可见下面

# 3.2 实例引入

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)  # 此时 result 是 bytes 类型
print(result.decode('utf-8'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 etree.HTML(text) 中,这一步会自动修正 text 所表示的 HTML 文本,转换成 str 后打印结果为:

<html><body>
<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>
     </li></ul>
 </div>
</body></html>
1
2
3
4
5
6
7
8
9
10
11
  • 可见原先 text 中未闭合的 li 节点被补全了,并自动添加了 body、html 节点

另外也可以直接读取文本文件进行解析:html = etree.parse('./text.html', etree.HTMLParser())。

# 3.3 所有节点

一般会用以 // 开头的 XPath 规则来选取所有符合要求的节点。



 


from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
print(result)
1
2
3
4
  • 这里 * 达标匹配所有节点,运行结果是一个 Element 元素的列表。

# 3.4 子节点

  • / 获取直接子节点。比如 //li/a 获取 li 节点的所有直接子节点 a。
  • // 获取子孙节点。比如 //ul//a 获取 ul 节点下的所有子孙节点 a。

# 3.5 文本获取

  • /text() 获取当前节点的内部文本,注意,如果 <li>first<a>second</a></li>,这样 /li/text() 获取的是 first 而不包含 second
  • //text() 获取所有子孙节点的文本,比如上面的 /li//text() 则会获取到 first 和 second。

示例:

from lxml import etree

text = '<li>first<a>second</a></li>'
html = etree.HTML(text)
result1 = html.xpath('//li/text()')
print(result1)
result2 = html.xpath('//li//text()')
print(result2)
1
2
3
4
5
6
7
8

输出结果:

['first']
['first', 'second']
1
2

# 3.6 属性匹配和属性获取

  • 属性匹配—— [@class=‘item-0’] 限制所选取的节点的 class 属性必须为 item-0
  • 属性获取—— @href 表示获取节点的 href 属性

示例:

result1 = html.xpath('//li[@class="item-0"]')  # 属性匹配
result2 = html.xpath('//li/a/@href')  # 属性获取
1
2

# 属性多值匹配

有时候,某些节点的某个属性可能有多个值:

from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[@class="li"]/a/text()')
print(result)
1
2
3
4
5
6
7

这里 li 节点的 class 属性有两个值:li 和 li-first,此时如果还用之前的属性匹配获取节点,就无法进行了,这时需要用到 contains 方法,需要修改为:






 


from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class, "li")]/a/text()')
print(result)
1
2
3
4
5
6
7
  • contains 方法,第一个参数为属性名称,第二个是属性值,只要传入的属性包含传入的属性值,就可以完成匹配。

# 多属性匹配

还有一种情况是根据多个属性来确定一个节点,这时需要同时匹配多个属性。运算符 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)
1
2
3
4
5
6
7

除此之外,还有很多其他运算符:

运算符 描述 实例 返回值
| 计算两个节点集 //book | //cd 返回所有拥有 book 和 cd 元素的节点集
+ 加法 6 + 4 10
- 减法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等于 price=9.80 如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。
!= 不等于 price!=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
< 小于 price<9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
<= 小于或等于 price<=9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
> 大于 price>9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
>= 大于或等于 price>=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。
or 或 price=9.80 or price=9.70 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。
and 与 price>9.00 and price<9.90 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。
mod 计算除法的余数 5 mod 2 1

# 3.7 按序选择

选择节点时,可能匹配了多个节点,但我们只想要其中第一个或最后一个,怎么办?

可以使用向中括号传入索引的方法获取特定次序的节点,但要注意,xpath 里面与写代码不同,序号是以 1 开头,而不是 0。

示例:

  • //li[1] 表示选取第一个 li 节点;
  • //li[last()] 表示选取最后一个 li 节点;
  • //li[position()<3] 表示选取位置小于 3 的 li 节点,即序号 1 和 2 的节点;
  • //li[last()-2] 表示选取了倒数第三个节点,因为 last() 代表最后一个,再减 2 就是倒数第三个了。

# 3.8 节点轴选择

轴可定义相对于当前节点的节点集。

轴名称 结果
ancestor 选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute 选取当前节点的所有属性。
child 选取当前节点的所有子元素。
descendant 选取当前节点的所有后代元素(子、孙等)。
descendant-or-self 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following 选取文档中当前节点的结束标签之后的所有节点。
namespace 选取当前节点的所有命名空间节点。
parent 选取当前节点的父节点。
preceding 选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling 选取当前节点之前的所有同级节点。
self 选取当前节点。

示例:

from lxml import etree

text = '''
<html><body>
<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>
     </li></ul>
 </div>
</body></html>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/ancestor::*')  # 获取第一个 li 节点的所有祖先节点
print(result)  # 包括 html、body、div 和 ul
result = html.xpath('//li[1]/ancestor::div')
print(result)
result = html.xpath('//li[1]/attribute::*')  # 第一个 li 节点的所有属性值
print(result)
result = html.xpath('//li[1]/child::a[@href="link1.html"]')  # 所有直接子节点中的符合属性条件的 a 节点
print(result)
result = html.xpath('//li[1]/descendant::span')  # 所有子孙节点中的 span 节点
print(result)
result = html.xpath('//li[1]/following::*[2]')  # 当前节点之后的第二个节点
print(result)
result = html.xpath('//li[1]/following-sibling::*')  # 所有的后续同级节点
print(result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 4. pyquery 的使用

占坑...

# 5. parsel 的使用

parsel 这个库可以解析 HTML 和 XML,并同时支持使用 XPath 和 CSS 选择器对内容进行提取和修改同时还融合了正则表达式的提取功能。parsel 灵活且强大,同时也是 Python 最流行的爬虫框架 Scrapy 的底层支持。

网页解析利器 parsel (opens new window)

编辑 (opens new window)
上次更新: 2022/09/14, 16:01:03
爬虫基础
JavaScript 动态渲染页面爬取

← 爬虫基础 JavaScript 动态渲染页面爬取→

最近更新
01
Deep Reinforcement Learning
10-03
02
误删数据后怎么办
04-06
03
MySQL 一主多从
03-22
更多文章>
Theme by Vdoing | Copyright © 2021-2024 yubincloud | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×