Shawn's Blog

一个伪程序员的伪技术博客

0X00 可以被强制转换的自定义类

但凡写过 Python
的人应该都用过int()这个函数了,而且也都知道这个是将其他类型转换成int类型的内置方法,稍微用的多一点的还会知道这个方法如果传入不能被强制转换的数据时会抛出TypeError的异常。那你知道如何让自己定义的类可以被强制转换吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3


class A:
def __int__(self):
return 233


a = A()
print(int(a))

# output
# 233

而且按照官方文档来说的话,如果你的class定义了__int__()方法,则int(your_obj)则会返回__int__()的值,如果定义了__index__()则会返回__index__(),如果定义了__trunc__()也会返回__trunc__()。当然是有优先级的,优先级`int

index > trunc`,可以使用如下代码分别注释这些方法测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3


class A:
def __int__(self):
return 1

def __index__(self):
return 2

def __trunc__(self):
return 3


a = A()
print(int(a))

0X01 int 的第二个参数

那你知道它其实还能接收第二个参数吗?其实 int()
方法可以接受第二个参数的,也就是用于进制转换的参数。换言说就是可以用内置的int()方法将其他进制的字符串数据转换成10进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3


print(int('12345')) # 将字符串格式10进制数字转为整型
print(int('12345', base=10)) # base 默认就是10
print(int('12345', base=8)) # 8进制的12345转成10进制
print(int('FFFFF', base=16)) # 16进制的转成10进制
print(int('0XDEADBEEF', 16)) # 当然可以带对应的0X前缀
print(int('011111', 2)) # 将2进制的数字转成10进制

# output
# 12345
# 12345
# 5349
# 1048575
# 3735928559
# 31

0X02 hex、 bin 等其他转换方法

上面提到了进制转换,这里也就顺便说一下这两个方法好了。其中hex可以将整型数字转成0x开头的16进制字符串,bin可以将整形数字转成0b开头的2进制字符串

1
2
3
4
5
6
7
#!/usr/bin/env python3


print(hex(12345))
print(hex(3735928559))
print(bin(12345))
print(bin(23333))

还有几个之前从来不知道,这次写博客才在官方文档看到的用法,不仅可以控制大小写还能控制是否展示0X这种标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [1]: '%#x' % 255, '%x' % 255, '%X' % 255
Out[1]: ('0xff', 'ff', 'FF')

In [2]: format(255, '#x'), format(255, 'x'), format(255, 'X')
Out[2]: ('0xff', 'ff', 'FF')

In [3]: f'{255:#x}', f'{255:x}', f'{255:X}'
Out[3]: ('0xff', 'ff', 'FF')

In [4]: format(14, '#b'), format(14, 'b')
Out[4]: ('0b1110', '1110')

In [5]: f'{14:#b}', f'{14:b}'
Out[5]: ('0b1110', '1110')

In [6]: '%#o' % 10, '%o' % 10
Out[6]: ('0o12', '12')

In [7]: format(10, '#o'), format(10, 'o')
Out[7]: ('0o12', '12')

In [8]: f'{10:#o}', f'{10:o}'
Out[8]: ('0o12', '12')

官方文档参考:

https://docs.python.org/zh-cn/3/library/functions.html#int

https://docs.python.org/zh-cn/3/library/functions.html#hex

https://docs.python.org/zh-cn/3/library/functions.html#bin

https://docs.python.org/zh-cn/3/library/functions.html#oct

0X00 最常见的持久化方式:挂载出来

相信各位学习使用 Docker 的时候都会出现过好不容易用 Docker 启动了个数据库容器,然后发现只要容器消失之后数据也就一起消失了的情况。然后通常来说会使用这么一个方法来解决问题:将宿主机的某个目录挂载到容器里,这样一来那个数据库容器就可以将数据内容和配置持久化地存储下来了。一般会使用这么一个方法将某个目录挂载到容器内部 docker run -dit --name new_container --mount type=bind,source=/Users/shawn/Downloads/test/test_dir,target=/test_dir alpine

下面图中首先创建了一个名为 this_container 的容器,并将其 /test_dir 目录和宿主机的 /Users/shawn/Downloads/test/test_dir 目录绑定到一起了。然后在容器里向 /test_dir 写入文件之后发现在宿主机的对应位置也是可以看到的,并且文件不会因为容器的生命周期结束而被删除(如果数据在容器内部的话当容器被删除后自然也就不在了)。

将目录挂载到容器内部

这种方式其实更适合宿主机和容器需要共享一些文件的时候使用,例如我本地的开发环境在容器里但是代码又在宿主机上的情况就很适合这种方案。当然也可以将数据文件、配置文件等通过这种方式挂载出来。一般使用 Docker 部署各类服务的时候,都会将配置文件通过这种方式共享出来,方便我们在宿主机上修改配置然后在容器内部生效。

0X01 另一种持久化的方式:卷

说来惭愧,我一直以为只有上面一种数据持久化的方式,直到昨天看书的时候才知道 Docker 还有一种叫做 Volume 的存在。如果说上面说的将数据挂载出来到宿主机的方式有点类似与宿主机与虚拟机共享目录的话,那通过 Volume 持久化文件就类似于给虚拟机挂载一个虚拟磁盘了。

我们首先可以通过 docker volume ls 的方式检查一下当前环境是不是存在已经创建好了的 Volume,通常来说如果你的 Docker 环境已经用过一段时间并且创建过各种不同的容器的话那很有可能已经有存在的 Volume 了。接下来我们通过 docker run -dit --name volatiner --mount source=bizvol,target=/vol alpine 来启动一个容器,后面 mount 的参数就是将名为 bizvol 的 Volume 挂载到容器的 /vol 目录上。这里值得注意的一点是即使你没有手动创建这个名为 bizvol 的 Volume 也是可以正常启动这个容器的,Docker 会为我们自动创建这个 bizvol Volume 的。接下来再使用 docker volume ls 就可以看到这个被创建好了的 Volume 了,我们尝试着向其中写入一点点数据,然后删除这个容器,可以发现这个 Volume 还是健在的,此时我们再启动一个新的容器使用这个 Volume 是没问题的,甚至启动多个容器同时挂载都是可以的,这些容器里都能正常读写该 Volume 的数据。

使用 Volume

关于 Volume 有如下几个命令,这里简单介绍一下

1
2
3
4
5
docker volume create    # 创建 Volume
docekr volume ls # 查看当前存在的 Volume
docker volume inspect # 检查 Volume 的详细信息
docker volume prune # 删除未被容器或服务使用的全部 Volume
docker volume rm # 删除指定 Volume

0X00 前言 & 简介

说起来但凡各位用过 Linux 就应该用过 ssh 了吧,所以怎么使用 ssh 去连接一台服务器、怎么去配置 key 登陆而非密码、怎么允许/禁止 root 用户使用这种问题就不再过多讨论了,这篇文章来介绍一下 ssh隧道 这个东西。

说起来各位在生活工作中肯定遇到过这么一个情况,如图所示:我(A)自己想要访问一台机器(C)但是A和C是不通的,这时候有一台中间的机器(B),A可以访问到B且B和C也是通的(比如说梯子的应用场景)。r当然了你在中间的B机器上开一个 OpenVPN 肯定是可行的,但是一旦你的A连上了 VPN 之后所有流量也就都从B上走了,绝大多数情况下我们其实并不是很需要一个 VPN 而是需要一个轻量化的解决方案。这时候 ssh 隧道就是我们的一个很好的比较轻量化的解决方案,首先它不需要你在客户端和服务器上安装额外的软件(如果你连 ssh 都没装那就不说了);其次它不需要配置文件,为数不多的配置直接写在命令行里;最后它不需要额外的守护进程,你不用了就直接 Ctrl + C 干掉当前进程就行了。

ssh-tunnel

ssh 隧道其实本质上来说就是转发,那我们来依次介绍一下这四种隧道(转发)方法

0X01 动态转发

动态转发是在本地建立一个通往另一台机器的隧道,然后网络监听在本地的某端口,使用 socks5 协议。使用对应的协议和端口的出口流量都会被转发到另一台机器,再由它访问后转发给你。比如你在墙内是无法访问 wikipedia 的,但是你“恰好”有一台在海外的服务器,那么你就可以通过这种方式建立一个简单且临时的小梯子。

命令格式是:ssh -D local-port username@tunnel-host -N,比如说 ssh -D 1080 [email protected] -N

-D 是绑定后面的本地端口
-N 是说非交互环境,不需要执行命令,挂着就好

这样一来就可以在浏览器上配置代理了,配上 socks5://127.0.0.1:1080 之后就可以访问更广阔的互联网了 YOU ARE FREEDOM !!!

这种动态转发其实平时用到的机会并没有很多,因为开发过程中用代理还是比较麻烦的,并不是所有软件/工具都支持独立配置代理,直接配置在浏览器或者操作系统上的话又很容易误伤到其他的进程。如果想要更细致的管理还是需要用下面的本地和远程转发方案。

0X02 本地转发

本地转发也是在本地建立隧道,监听在本地端口,一切发往本地该端口的流量都会根据配置转发到另一台机器上。比如我前几天工作中遇到的一个情景:开发机是 macOS 的,客户用来联调的服务器在他们内网需要 VPN 才行,但是他们用的 VPN 客户端没有 macOS的。所以我在 macOS 上装了个虚拟机,虚拟机起连上了他们的 VPN 也就可以访问联调环境了,但是我的 macOS 还是不能直接访问。这时候就可以用 ssh 隧道来解决问题了。

命令格式是:ssh -L local-port:target-host:target-port username@tunnel-host,比如说 ssh -L 8080:123.123.123.123:80 [email protected]

-L 指的是建立一条本地隧道 local

这样一来我再curl -XGET http://127.0.0.1:8080的时候,流量就会顺着 ssh 的隧道一路直接到最终客户的联调环境了。这种本地转发的情况是非常常用的,这样一来本来没有通的两个机器也就通过这样一条隧道连接起来了。最重要的是我们只占用了本地机器的一个端口,并不影响其他任何的系统配置和任何进程。

0X03 远程转发

远程转发是在中转的服务器上建立隧道,跟本地转发最大的不同是:本地转发只是本地用户自己建立了一条隧道给自己用,但是远程转发可以将端口共享出来给多台设备使用。还是说上面我的那个联调环境,如果说只有我一个人自己联调就用本地转发是刚刚好的,而且不用再登录到远程环境上去,也不需要远程用户比较高的权限。但是说如果我们全公司都在进行这个联调,那就可以用远程转发,在中转服务器上把自己的端口开放出来让所有用户一起使用。

命令格式是:ssh -R local-port:target-host:target-port -N local,例如 ssh -R 8080:123.123.123.123:80 -N local

-R 建立一条远程隧道 remote

这样建立起来的隧道就可以给其他机器使用了,现在所有可以访问中转机的机器都可以通过中转机的 8080 端口访问到客户的联调机。所以说这里的远程转发和本地转发除了命令执行地不同、可用的机器不同以外就没有什么比较大的区别了。

0X04 其它参数

这里再介绍几个额外的参数

-f 后台运行,这样 ssh 隧道就不用占用你当前的 shell 了
-C 压缩内容,原则上就是用 CPU 换带宽
-g 开放本地端口,如果本地转发的时候加了这个参数,那就跟远程转发差不多了

0X00 先胡说两句

兄弟们我的年更文章它又来了~如果你看到这篇文章的时候发现发布时间是 2021-12-31 23:59:59 但你的手机电脑告诉你还没到这个时间,不要慌,一定是你穿越了。

我也不知道为啥莫名其妙就开始写年终总结了,而且今年已经是第三年了。人确实是一种很奇怪的物种,领导老师父母让你总结一年的时候暴躁不堪,根本不想写;但是自己突然来了这个念头之后就非常认真的写了一大堆,告诉我不是我一个人有这种“病”🙄 这次的总结打算换一种方法来写,大概分成了这么几个部分:去年的年度计划情况、年内做得不错的事、没做好的事、可以分享给各位的东西。

0X01 翻车的年度计划

2019 年度总结中我说“取法其上,仅得其中;取法其中,仅得其下”,但是今年就不一样了,今年的年度计划基本可以用四个字来形容:我是废物 😢(其实没这么离谱,听我慢慢道来)

今年就不给大家详细一个个的看我的年度计划了,简单说一下吧:

  1. 阅读两个有兴趣的开源项目源码【完全没看】
  2. 动手写两个自己练手新技能/知识的小项目【几乎没写】
  3. 了解 5 个专业技能【就学到了俩】
  4. 养成 4 个良好习惯【也只坚持了俩】
  5. 读完 24 本专业无关的书【读了 17 本】
  6. 平均每月发布两篇博客【只完成了 1/3】
  7. 了解几个奇奇怪怪领域的业余知识【确实了解了几个】
  8. 多出门走走-拒绝成为肥宅【平均每月一次,完成】
  9. 考一个手动挡驾照【是的我才考驾照】
  10. 买一台 PS5【原价压根买不着】

算下来年度计划完成了有一半吧,而且还有一些原本不在年度计划里的重要的事、好玩的事、刺激的事,所以整个年度计划虽然优点拉胯但是也没有完全拉 🤪

阅读全文 »

0X00 前言

如果秉承着「能用就行」的原则,那么这篇文章提到的东西基本都没什么卵用;如果秉承着「写更好的代码」的原则,那么这里提到的东西也许对你有所帮助。

内容主要取材自 Effective Python,主要是作为自己学习后的一个输出而总结的这篇博客

0X01 使用 % 的 C 风格格式化

首先是沿用自 C 风格的使用 % 进行的字符串格式化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> name = 'Shawn'
>>> job = 'developer'
>>> text = 'I am %s, a %s' % (name, job)
>>> text
'I am Shawn, a developer'
>>> height = 123.456
>>> text = 'My height is %4d.' % height
>>> text
'My height is 123.'
>>> text = 'My height is %4f.' % height
>>> text
'My height is 123.456000.'
>>> text = 'My height is %4.8f.' % height
>>> text
'My height is 123.45600000.'

这种写法在写惯了 C 的人身上比较常见,比较熟悉而且也比较简单。不过这种写法有几个问题,首先就是当百分号右侧的变量数量发生变化或者类型发生变化的时候,程序很有可能因为类型转化出现不兼容的情况(当然了,本来是 %s %4d 对应字符串和数字,现在两个都是字符串了当然就出错了)。如果要解决这种问题的话,必须每次修改都要检查百分号左右的占位符和具体数值是否能对应的伤,而且一旦占位符多了之后还很容易看花眼。

还有一个问题就是填充数值的时候通常需要对具体的值进行一些处理,比如保留某几位长度之类的,这样一来表达式可能会很长,从而显得很混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
coffee_price_list = [
('Americano', 15),
('Latte', 25),
('Cappuccino', 30)
]

for index, (name, price) in enumerate(coffee_price_list):
content = '%d. %-12s ----- %.2f' % (index, name, price)
print(content)

# output
0. Americano ----- 15.00
1. Latte ----- 25.00
2. Cappuccino ----- 30.00

我们来看 for 循环里面那行,是不是确实看起来乱乱糟糟的,这还只是三个占位符,如果更多的话就会更混乱了。

第三个问题是如果要用同一个值来填充多个位置,那就需要在右侧重复多次(废话之:你想要几个就得写几个)。我们假设你有一个保证书模板,只需要填入姓名、错误和保证内容就可以生成出例如「我XX再也不YY了,我保证以后ZZ」的十万字长文。但是整篇文章里出现了大量的空位,需要填入这些 XX/YY/ZZ 怎么搞呢?你可能需要在后面写上不计其数的 '--------%s-------%s-------%s-------%s' % (xx, xx, yy, zz, zz, yy) 这种东西(别跟我说你要用 replace 那是另外的内容,这里只讨论字符串格式化😅)。

当然了这个问题也不是无解,我们使用 dict 来替换平时用的 tuple 就可以了,就是类似下面这种用法(虽然我从来没真的在代码里见过谁这么写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
value_a = 'aaaaaaaaaaaaaaaa'
value_b = 'bbbbbbbbbbbbbbbb'
value_c = 'cccccccccccccccc'

content = '''
%(val_a)s, %(val_a)s, %(val_a)s
%(val_b)s, %(val_b)s, %(val_b)s
%(val_c)s, %(val_c)s, %(val_c)s
%(val_a)s, %(val_b)s, %(val_c)s
''' % {
'val_a': value_a,
'val_b': value_b,
'val_c': value_c
}

print(content)

# output

aaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbb, bbbbbbbbbbbbbbbb, bbbbbbbbbbbbbbbb
cccccccccccccccc, cccccccccccccccc, cccccccccccccccc
aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc

虽然这种写法解决了多次重复使用的问题,但是加重了第二点也就是代码更冗长了,因为不仅要给变量做格式化,还要给每个占位符再设定一个 key 且为其匹配好。

最后就是因为每次都需要把 key 至少写两次(占位符那里一次,后面的字典里一次),甚至因为 value 过长还可能再把变量提出去单独定义一下,就会导致整个表达式非常长,比较容易出现 bug 且定位 bug 比较复杂。

0X02 另一种方法:format函数与方法

format 是我平时用的最多的一种方法了,比较常规的方法是调用str 对象的 format() 方法,例如下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
name_1 = 'shawn'
name_2 = 'bluce'

print('hello {}, hello {}.'.format(name_1, name_2))
print('hello {:<10}, hello {:<20}.'.format(name_1, name_2))
print('hello {1}, hello {0}.'.format(name_1, name_2))
print('hello {1}, hello {0}, hello {0}, hello {1}.'.format(name_1, name_2))

# output
hello shawn, hello bluce.
hello shawn , hello bluce .
hello bluce, hello shawn.
hello bluce, hello shawn, hello shawn, hello bluce.

第二种可能比较少见,不过规则比较简单,就是在花括号里写一个冒号,冒号右边可以用 C 方法格式化变量。第三四种就比较常见了,可以通过 index 来规定位置。

这种方法还有一些更高级的用法,例如在花括号里访问字典的 key 或者访问列表中的下标(好像也没见人这么用过)

1
2
3
4
5
6
7
8
9
data = [
{'name': 'shawn', 'gender': 'M'},
{'name': 'bluce', 'gender': 'F'}
]

print('{data[0][name]} and {data[1][name]} is {data[0][gender]} and {data[1][gender]}'.format(data=data))

# output
shawn and bluce is M and F

0X03 更好的方法 f-string 插值格式字符串

在 Python 3.6 中引入的这个特性可以解决上述提到的问题,语法要求格式化的字符串前面加上一个f做前缀,就类似于之前的b/r这种。这里也同样支持前面 format 那里用到的格式化方法,例如 f'{name_1:<10}, {value:.2f}' 这种。

一个简单的例子

1
2
3
4
5
6
7
name_1 = 'shawn'
name_2 = 'bluce'

print(f'hello {name_1}, hello {name_2}')

# output
hello shawn, hello bluce

我们现在再回过头来看一下最开始提到的四个问题:第一个,如果需要调整顺序,那么百分号左侧的正文要改,右侧的值也要改,就要改两次。现在没有百分号也就不再区分左右了,如果调整顺序那么就只调整一次就行,方便了很多。第二个,如果对填进去的值稍作处理可能会导致整个表达式变得很长。现在因为省略了百分号右边的内容,所以整个表达式还是精简了不少的。第三个,当某个变量/值要用多次的时候就需要左右共写两次那么多。用 f-string 方式的话,如果确实需要调用多次且每次都要进行修改(例如保留小数或是转成大写之类的),则可以考虑将其提取出去单独赋值,然后在格式化的时候用新值来代替,还能更加符合字符串格式化的语义。第四个是说如果使用 dict 的话会使代码变多,现在不用字典了当然也就没有这个问题了。

下面是几种用法,看起来 f-string 并没有代码量少很多,是因为这个例子并不能很明显的体现出代码量少的优势,但是已经体现出可读性和维护性的优势了。如果一眼看过去,明显是 f-string 的用法最简单清晰明了。

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
52
53
54
data = [
{'name': 'shawn', 'score': 78.5},
{'name': 'ami', 'score': 89.0},
{'name': 'jack', 'score': 92.0},
{'name': 'amber', 'score': 99.5}
]

print('------------- style_1 -------------')
for item in data:
style_1 = 'name: %-10s score: %2.2f' % (item['name'], item['score'])
print(style_1)

print('\n\n------------- style_2 -------------')
for item in data:
style_2 = 'name: {:10s} score: {:2.2f}'.format(item['name'], item['score'])
print(style_2)

print('\n\n------------- style_3 -------------')
for item in data:
style_3 = 'name: {name:10s} score: {score:2.2f}'.format(name=item['name'], score=item['score'])
print(style_3)

print('\n\n------------- f-string -------------')
for item in data:
f_string = f'name: {item["name"]:10s} score: {item["score"]:2.2f}'
print(f_string)

# output
------------- style_1 -------------
name: shawn score: 78.50
name: ami score: 89.00
name: jack score: 92.00
name: amber score: 99.50


------------- style_2 -------------
name: shawn score: 78.50
name: ami score: 89.00
name: jack score: 92.00
name: amber score: 99.50


------------- style_3 -------------
name: shawn score: 78.50
name: ami score: 89.00
name: jack score: 92.00
name: amber score: 99.50


------------- f-string -------------
name: shawn score: 78.50
name: ami score: 89.00
name: jack score: 92.00
name: amber score: 99.50
0%