字符串

python 中,字符串(str)是一种不可变的序列结构。

字符串创建

定义字符串的方法有以下几种:

  1. 单引号、双引号

    在 python 中,单双引号是完全等价的,在某些语言中,可能会区分字符和字符串,但在 python 中,字符就是长度为 1 的字符串。

    1
    2
    str1 = "Hello, world!"
    str2 = '114.514'
  2. 三引号
    使用三个双引号或单引号可以创建多行字符串。三引号允许一个字符串跨多行,字符串中可以包含换行符、制表符以及其他特殊字符。

    1
    2
    3
    mstr  = """line1,
    line2,
    line3!"""

字符串转义

使用反斜杠可以转义字符。

转义字符 描述 ASCII码
\’ 单引号 39
\" 双引号 34
\\ 反斜杠 92
\n 换行 10
\t 制表符 9
1
2
3
str1 = "line1\nline2"
str2 = "He said, \"Hello, world!\""
str2_better = 'He said, "Hello, world!"'

原始字符串

原始字符串使用一个前缀"r"来标识。原始字符串与普通字符串的区别在于,它不会对反斜杠进行转义,因此可以用来表示含有反斜杠的字符串。例如:

1
2
3
4
5
6
7
# 普通字符串
string1 = "C:\\Users\\Admin\\Desktop\\file.txt"
print(string1) # 打印结果为:C:\Users\Admin\Desktop\file.txt

# 原始字符串
string2 = r"C:\Users\Admin\Desktop\file.txt"
print(string2) # 打印结果为:C:\Users\Admin\Desktop\file.txt

字符串格式化

format 是字符串的一个方法,用来格式化字符串。

format() 方法在字符串中添加变量时使用花括号{}作为占位符,并使用format()将变量传递给它来替换占位符。

1
2
3
4
5
name = "Lucy"
age = 25

text = "我的名字是{},我今年{}岁。".format(name, age)
print(text)

你可能见过这种格式化字符串方法:

1
2
print("%d + %d = %d" % (2,3,5))
# 输出:2 + 3 = 5

这种写法已经过时了。请使用字符串的 format 方法。

格式化字符串

f-string 方法是从 Python 3.6 版本开始引入的,它是一种更简洁,快速的格式化字符串的方法。 它使用花括号{}作为占位符,并直接在变量前加上字母f。

1
2
3
4
5
name = "Lucy"
age = 25

text = f"我的名字是{name},我今年{age}岁。"
print(text)

正则

正则表达式是一种用来描述文本模式的语法规则。它在计算机科学中被广泛应用于文本处理,例如验证、搜索、替换文本等。正则表达式可在基本上所有语言中通用(如python,C++,JavaScript)。

B站弹幕屏蔽词添加界面

在B站中,你也可以通过正则来快速屏蔽一系列弹幕。

在 python 中,正则表达式由内置库 re 实现。在 re 库中,常用的有四个函数:re.matchre.searchre.findallre.sub

引入

考虑如下场景:你是某网站前端,需要识别一个手机号是否合法。

一个手机号被称为合法的,当且仅当

  • 由 11 个数字组成;
  • 开头必须是数字 1;
  • 第二位必须是 3-9 的数字。

精通 python 的你马上写出了如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def phone_isvalid(phone):
if len(phone) != 11:
return False
if phone[0] != '1':
return False
if not "3" <= phone[1] <= "9":
return False
for i in range(2,11):
if not "0"<=phone[i]<="9":
return False
return True


print(phone_isvalid("19198101234")) # True
print(phone_isvalid("11451419195")) # False
print(phone_isvalid("114514")) # False
print(phone_isvalid("这是一个合法的手机号码吗?")) # False

看起来很美好,但是当规则变得复杂起来后(如识别一个合法邮箱地址),这样的代码就似乎有些繁琐了。

事实上,如果用正则表达式来写刚才的代码,仅仅需要一行:

1
2
3
import re
def phone_isvalid(phone):
return re.match("^1[3-9]\d{9}$", phone) is not None

我们来学习如何编写正则表达式(如上代码中 ^1[3-9]\d{9}$ )。

正则表达式的编写

一个简单的正则表达式可以仅仅由字符组成。例如,hello
该表达式只会匹配 hello

字符集

字符集是一系列字符的集合。

表达式 含义
[xyz]
[a-c]
匹配包含在方括号中的任何字符。你可以使用连字符指定字符范围,但如果连字符出现在方括号中的第一个或最后一个字符,则将其视为字面连字符,作为普通字符包含在字符类中。
例如,[abcd-][-abcd] 匹配brisket中的bchop中的cnon-profit中的-(连字符);
[0-9] 匹配所有数字,[a-zA-Z0-9]匹配数字和字母。
[^xyz]
[^a-c]
匹配任何包含在方括号中的字符
例如, [^a-z] 匹配 hello1 中的 1
. 匹配任何字符,在[]内,点失去了它的特殊意义,并与文字点匹配。
\d 匹配任何数字,相当于[0-9]
\w 匹配基本拉丁字母中的任何字母数字字符,包括下划线,相当于[A-Za-z0-9_]
\ 转义字符。例如,\\匹配反斜杠,\*匹配星号。
x|y 析取:匹配 xy 。例如,green|red 匹配 green apple 中的 greenred apple 中的 red

量词

量词表示要匹配的字符或表达式的数量。

表达式 含义
x* 将前面的项“x”匹配 0 次或更多次。
例如,hello* 可以匹配 hello,helloooooooo,hell
x+ 将前面的项“x”匹配 1 次或更多次。
例如,hello+ 可以匹配 hello,helloooooooo,但不能匹配 hell
x? 将前面的项“x”匹配 0 或 1 次。
例如,hello? 可以匹配 hello,hell
x{n} 与前一项“x”的 n 次匹配
例如,hello{5} 只能匹配 hellooooo
x{n,} 与前一项“x”至少匹配“n”次。
例如,hello{5,} 可以匹配 hellooooo,helloooooo,helloooooooo
x{n,m} 与前一项“x”匹配“n”到“m”次
例如,hello{5,6} 只能匹配 hellooooo,helloooooo
x*?
x+?
x??
x{n}?
x{n,}?
x{n,m}?
默认情况下,像 *+ 这样的量词是“贪婪的”,这意味着它们试图匹配尽可能多的字符串。?量词后面的字符使量词“非贪婪”:意思是它一旦找到匹配就会停止。
例如,给定一个字符串 some <foo> <bar> new </bar> </foo> thing:
<.*> 会匹配 <foo> <bar> new </bar> </foo>
<.*?> 只会匹配 <foo>

断言

断言匹配表达式的边界(简略介绍两个最常用断言)。

表达式 含义
^ 匹配开头。例如 ^a 匹配 apple 中的 a ,不匹配 say 中的 a
$ 匹配开头。例如 a$ 匹配 ga 中的 a ,不匹配 say 中的 a

字符串验证

想要判断某个字符串是否符合指定规则,可以使用 re.match 函数或 re.search

match 函数从开头开始匹配,search 函数可以从中间开始匹配。
它们都返回一个匹配对象 re.Match。若未匹配,返回 None

例如,以下可以判断邮箱地址是否合法:

1
2
3
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
  • ^表示匹配字符串的开始位置
  • [a-zA-Z0-9._%+-]+ 表示匹配至少一个字母、数字、下划线、句点、百分号、加号或减号
  • @ 表示匹配 @ 符号
  • [a-zA-Z0-9.-]+ 表示匹配至少一个字母、数字、句点或连字符
  • \. 表示匹配一个句点
  • [a-zA-Z]{2,} 表示匹配至少两个字母
  • $ 表示匹配字符串的结尾位置

字符串搜索

有一段文本

1
2
3
4
5
6
# 2018年世界GDP排行
1. 美国:20.5万亿美元
2. 中国:13.6万亿美元
3. 日本:4.9万亿美元
4. 德国:4.2万亿美元
5. 英国:2.9万亿美元

你想提取出中国在2018年世界GDP的排位和具体值。

现在问题不是验证,而是提取信息。需要用到另一个功能——捕获组。一个捕获组用 () 定义。

1
2
3
4
5
6
7
8
9
10
11
import re

gdp = """# 2018年世界GDP排行
1. 美国:20.5万亿美元
2. 中国:13.6万亿美元
3. 日本:4.9万亿美元
4. 德国:4.2万亿美元
5. 英国:2.9万亿美元"""

print(re.search("(\d)\. 中国:([\d.]+)万亿美元",gdp).groups())
# 输出:('2', '13.6')

re.search 返回 re.Match 对象,其中 groups 方法返回一个所有捕获组的元组。
还可以使用 group(1) 返回第一个捕获组,以此类推。

re.findall

有一天,你突发奇想,想要找到一篇文章里所有人说的话。

1
2
3
4
5
6
7
8
“你最近感觉怎么样?”小明问道。
“我感觉还不错,谢谢关心。”小红回答。
“你有什么计划吗?”小明再次询问。
“我正在考虑去旅行,发掘更多的地方。”小红回答道。
“听起来很不错,你打算去哪里呢?”小明突然感兴趣了起来。
“我还没有决定呢,可能考虑去亚洲或者欧洲。”小红回答道。
“那你得提前计划好预算和时间。”小明建议道。
“是的,我会的。谢谢你的提醒。”小红感谢道。

你马上编写了如下正则表达式 “.+?”,用来匹配所有引号内的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

word = """“你最近感觉怎么样?”小明问道。
“我感觉还不错,谢谢关心。”小红回答。
“你有什么计划吗?”小明再次询问。
“我正在考虑去旅行,发掘更多的地方。”小红回答道。
“听起来很不错,你打算去哪里呢?”小明突然感兴趣了起来。
“我还没有决定呢,可能考虑去亚洲或者欧洲。”小红回答道。
“那你得提前计划好预算和时间。”小明建议道。
“是的,我会的。谢谢你的提醒。”小红感谢道。"""

print(re.findall("“.+?”",word))
# 输出:['“你最近感觉怎么样?”', '“我感觉还不错,谢谢关心。”', '“你有什么计划吗?”', '“我正在考虑去旅行,发掘更多的地方。”', '“听起来很不错,你打算去哪里呢?”', '“我还没有决定呢,可能考虑去亚洲或者欧洲。”', '“那你得提前计划好预算和时间。”', '“是的,我会的。谢谢你的提醒。”']

我们看到,返回了一个字符串的列表,就是每次匹配的内容。
这时候,你想:能不能不要引号?用捕获组即可,若pattern中只有一个捕获组,会返回捕获组中内容的列表。

如下:

1
2
print(re.findall("“(.+?)”",word))
# 输出:['你最近感觉怎么样?', '我感觉还不错,谢谢关心。', '你有什么计划吗?', '我正在考虑去旅行,发掘更多的地方。', '听起来很不错,你打算去哪里呢?', '我还没有决定呢,可能考虑去亚洲或者欧洲。', '那你得提前计划好预算和时间。', '是的,我会的。谢谢你的提醒。']

可以看到,确实只返回了捕获组里的内容。

如果一个表达式里有超过一个捕获组,则会返回一个字符串元组的列表。

1
2
print(re.findall("“(.+?)”(.{2})",word))
# 输出[('你最近感觉怎么样?', '小明'), ('我感觉还不错,谢谢关心。', '小红'), ('你有什么计划吗?', '小明'), ('我正在考虑去旅行,发掘更多的地方。', '小红'), ('听起来很不错,你打算去哪里呢?', '小明'), ('我还没有决定呢,可能考虑去亚洲或者欧洲。', '小红'), ('那你得提前计划好预算和时间。', '小明'), ('是的,我会的。谢谢你的提醒。', '小红')]

这样就可以同时匹配话和人的名字。

字符串替换

你又突发奇想,想要把引号改成书名号,可以使用 re.sub 编写代码。

1
2
conversation = re.sub(r'“(.*?)”', '《\g<1>》', conversation)
print(conversation)

这个表示对所有满足 “(.*?)” 的内容,都替换成第二个参数。其中 \g<1> 表示第一个捕获组。

输出:

1
2
3
4
5
6
7
8
《你最近感觉怎么样?》小明问道。
《我感觉还不错,谢谢关心。》小红回答。
《你有什么计划吗?》小明再次询问。
《我正在考虑去旅行,发掘更多的地方。》小红回答道。
《听起来很不错,你打算去哪里呢?》小明突然感兴趣了起来。
《我还没有决定呢,可能考虑去亚洲或者欧洲。》小红回答道。
《那你得提前计划好预算和时间。》小明建议道。
《是的,我会的。谢谢你的提醒。》小红感谢道。

练习

网页处理

荷塘月色_链接

这是一篇注音后的荷塘月色节选。要求分析网页结构,使用爬虫,将注音转化为纯文本形式。

即得到:

月光(yuè guāng)如流水(liú shuǐ)一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳(niú rǔ)中洗过一样;又像笼着轻纱的梦。虽然是满月(yuè),天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生(cóng shēng)的灌木,落下参差的斑驳(bān bó)的黑影,峭(qiào)楞楞如鬼(guǐ)一般;弯弯(wān wān)的杨柳(yáng liǔ)的稀疏(xī shū)的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲(fàn é líng)上奏着的名曲。

知识库:
html中,ruby标签可为文字注音。rt标签内为文字的发音。

1
<ruby><rt></rt></ruby>

歌词处理

残酷な天使のテーゼ(残酷天使的行动纲领) - 高橋洋子 - 单曲 - 网易云音乐

你想获取这首歌的歌词。你通过爬虫(已帮你写好)从该网页上获取了歌词的内容,接下来要进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import json

def get_lyric(song_id):
"""Get lyric from netease music.

Args:
song_id (str|int): id of the song

Returns:
tuple[str]: a tuple with japanese version of the
lyric and the translated version of the lyric
"""
url = "http://music.163.com/api/song/lyric?id={}+&lv=1&tv=-1"
r = requests.get(url.format(song_id))
jsonObj = json.loads(r.text)
return jsonObj["lrc"]["lyric"],jsonObj["tlyric"]["lyric"]

jlyric, tlyric = get_lyric(657666)

请自己运行代码,并分析返回的两个字符串。然后编写代码,处理数据,要求能得到如下几个变量。

1
2
3
4
5
6
7
8
9
10
11
12
lyricist = # 作词人名 ("及川眠子")
composer = # 作曲人名 ("佐藤英敏")
arranger = # 编曲人名 ("大森俊之")

lyric = [
# (日文歌词 , 中文歌词),
# 例如
("残酷な天使のように","就像那残酷的天使一样"),
("少年よ 神話になれ","少年哟!变成神话吧"),
...
...
]