Centos7编译安装Python

Centos7默认 yum 源中是 Python3.6 版本,目前高版本的 Django 等至少都要求 Python3.8 以上版本。

下文为在 Centos7 上编译安装 Python 的步骤

  1. 安装依赖包

    可以避免编译完成后pip 安装包时报错 No module named '_ctypes'的问题

    1
    yum install libffi-devel
  2. 编译安装 openssl

    1
    2
    3
    4
    5
    wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz
    tar xf openssl-1.1.1w.tar.gz
    cd openssl-1.1.1w
    ./config --prefix=/usr/local/openssl-1.1.1
    make && make install
  3. 下载 Python 源码包并编译(以 3.10.14 版本为例)

    1
    2
    3
    4
    5
    wget https://www.python.org/ftp/python/3.10.14/Python-3.10.14.tar.xz
    tar xf Python-3.10.14.tar.xz
    cd Python-3.10.14
    ./configure --prefix=/usr/local/python310 --with-openssl=/usr/local/openssl-1.1.1 --with-openssl-rpath=auto
    make && make install

EventSource入门

EventSource 接口是 web 内容与服务器发送事件通信的接口。一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件。

WebSocket 不同的是,服务器发送事件是单向的。数据消息只能从服务端到发送到客户端(如用户的浏览器)。这使其成为不需要从客户端往服务器发送消息的情况下的最佳选择。

前端示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let source = new EventSource('/api/hello')
// 连接建立完成
source.onopen = function () {
console.log('Connection was opened.');
}
// 收到消息,event:message
source.onmessage = function (event) {
console.log('on message: ', event.data);
}
// 连接失败/关闭
source.onerror = function (event) {
console.log('on error: ', event)
// 可以手动 close(),禁止默认的自动重连机制
source.close()
}
// 指定 event 的处理,event: ping
source.addEventListener('ping', function (event) {
console.log('ping: ', event.data);
})

Python代码,Django 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.http import StreamingHttpResponse
import json

def hello(request):
# 生成器函数
def handle_data():
for i in range(int(last_id or -1) + 1, 100):
# 触发前端的 onmessage 回调函数,默认 event:message
yield f'data:data is {i}\n\n'
# 返回复杂数据(转JSON字符串)
data = {'a': 1, 'b': 2}
yield f'data:{json.dumps(data)}\n\n'
# 触发前端的 addEventListener('ping'),event:ping
yield f'event:ping\ndata: ping data...\n\n'

response = StreamingHttpResponse(handle_data(), content_type='text/event-stream')
response['Cache-Control'] = 'no-cache'
return response

Python logging入门

Python 内置的日志记录工具

配置

  • Level 日志级别
    • DEBUG
    • INFO
    • WARNING
    • ERROR
    • CRITICAL

基本使用

1
2
3
4
5
6
import logging

# 日志记录到./demo.log文件中,format指定日志格式
logging.basicConfig(filename='./demo.log', format='%(asctime)s %(message)s')

logging.warning('hello')

格式化

1
2
3
4
5
# 基础使用
logging.basicConfig(filename='./demo.log', format='%(asctime)s %(message)s')
# 记录器使用,配合Handler
formatter = logging.Formatter('%(asctime)s %(message)s')
handler.setFormatter(formaater)
格式 描述
%(asctime)s 表示人类易读的 LogRecord 生成时间。 默认形式为 ‘2003-07-08 16:49:45,896’ (逗号之后的数字为时间的毫秒部分)。
%(created)f LogRecord 被创建的时间(即 time.time() 的返回值)。
%(filename)s pathname 的文件名部分。
%(funcName)s 函数名包括调用日志记录.
%(levelname)s 消息文本记录级别('DEBUG''INFO''WARNING''ERROR''CRITICAL')。
%(levelno)s 消息数字的记录级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL).
%(lineno)d 发出日志记录调用所在的源行号(如果可用)。
%(message)s 记入日志的消息,即 msg % args 的结果。 这是在发起调用 Formatter.format() 时设置的。
%(module)s 模块 (filename 的名称部分)。
%(msecs)d LogRecord 被创建的时间的毫秒部分。
%(name)s 用于记录调用的日志记录器名称。
%(pathname)s 发出日志记录调用的源文件的完整路径名(如果可用)。
%(process)d 进程ID(如果可用)
%(processName)s 进程名(如果可用)
%(relativeCreated)d 以毫秒数表示的 LogRecord 被创建的时间,即相对于 logging 模块被加载时间的差值。
%(thread)d 线程ID(如果可用)
%(threadName)s 线程名(如果可用)

文件日志

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

# 创建记录器对象
logger = logging.getLogger('log1')
# 创建文件处理器,指定日志文件
handler = logging.FileHandler('./demo.log')
# 格式器
formatter = logging.Formatter('%(asctime)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.setLevel('INFO')
logger.error('hello error')

轮换日志(基于文件大小)

1
2
3
4
5
6
7
8
9
10
from logging.handlers import RotatingFileHandler
import logging

logger = logging.getLogger('log1')
# maxBytes 设置单个文件的最大值(单位:字节)
# backupCount 设置最多保留多少个备份文件
# maxBytes 和 backupCount 都必须同时设置才生效
# 以下配置会最多生成 demo.log demo.log.1 demo.log2 demo.log3 4个文件
handler = RotatingFileHandler('./demo.log', maxBytes=1024, backupCount=3)
logger.addHandler(handler)

轮换日志(基于日期)

1
2
3
4
5
6
7
8
9
from logging.handlers import TimedRotatingFileHandler
import logging

logger = logging.getLogger('log1')
# when 设置轮换间隔时间(midnight:午夜0点)
# backupCount 设置最多保留多少个备份文件
# 以下配置会每天生成新文件最多保留7天历史文件
handler = TimedRotatingFileHandler('./demo.log', when='midnight', backupCount=7)
logger.addHandler(handler)

Django ORM入门

表结构如下

1
2
3
4
5
6
7
8
9
10
11
from django.db import models


class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
remarks = models.CharField(max_length=255, null=True)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'persons'

基础操作

filterexclude用法一致

  • 插入

    1
    2
    3
    4
    5
    6
    7
    # 方式一
    Person.objects.create(name='张三', age=22)
    # 方式二
    person = Person(name='李四', age=23)
    person.save()
    # 方式三,查询匹配到更新,否则插入
    Person.objects.update_or_create(name='张三三', defaults={'age': 100})
  • 更新

    1
    Person.objects.filter(name='张三三').update(age=F('age') + 1, remarks='我是备注')
  • 删除

    1
    2
    3
    4
    5
    # 匹配删除
    Person.objects.filter(age=23).delete()
    # 单个删除
    person = Person.objects.get(id=1)
    person.delete()
  • 简单查询

    1
    2
    3
    4
    5
    6
    7
    8
    # 简单查询, 如果未匹配到或匹配到多条则报错
    Person.objects.get(id=1)
    # 查询第一条数据
    Person.objects.first()
    # 查询最后一条数据
    Person.objects.last()
    # 查询所有数据
    Person.objects.all()
  • 条件查询

    1
    2
    3
    4
    5
    # 根据指定字段匹配查询
    Person.objects.filter(name='10号').first()
    # 多条件匹配
    Person.objects.filter(age=30, name__contains='张') # 都满足条件
    Person.objects.filter(Q(age=22) | Q(name__contains='张')) # 满足任意一个
  • 大小比较

    1
    2
    3
    4
    5
    6
    7
    8
    # 大于 
    Person.objects.filter(age__gt=40)
    # 大于等于
    Person.objects.filter(age__gte=40)
    # 小于
    Person.objects.filter(age__lt=40)
    # 小于等于
    Person.objects.filter(age__lte=40)
  • 包含

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 年龄是23, 24, 25的
    Person.objects.filter(age__in=(23, 24, 25))
    # 名字包含字符串‘张’的
    Person.objects.filter(name__contains='张')
    # 名字以‘张’开头
    Person.objects.filter(name__startswith='张')
    # 名字以‘张’结尾
    Person.objects.filter(name__endswith='张')
    # 匹配时忽略大小写,icontains/istartswith/iendswith
    Person.objects.filter(name__istartswith='a')
  • 日期/时间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 匹配年
    Person.objects.filter(created_at__year=2022)
    # 年/月/日/周
    __month
    __day
    __week_day
    __week
    __hour
    __minute
    __second
  • 其他

    1
    2
    3
    4
    # 过滤字段是否为NULL
    Person.objects.filter(remarks__isnull=True)
    # 正则匹配,iregex 忽略大小写
    Person.objects.filter(name__regex=r'[0-9]+')

关联查询

表结构信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.db import models


class Class(models.Model):
name = models.CharField(max_length=30)
remarks = models.CharField(max_length=255, null=True)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'classes'



class Person(models.Model):
cls = models.ForeignKey(Class, on_delete=models.PROTECT, null=True)
name = models.CharField(max_length=30)
age = models.IntegerField()
remarks = models.CharField(max_length=255, null=True)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'persons'
  • 关联查询

    1
    2
    3
    4
    5
    6
    7
    # 通过人查询所属班级信息
    person = Person.objects.first()
    print(person.cls.name)
    # 查询班级下的学生
    cls = Class.objects.filter(name='一一班').first()
    cls.person_set.all() # 班级里的所有人
    cls.person_set.filter(age__in=(22, 23)) # 对属于版本的学生再匹配过滤
  • 关联匹配

    1
    2
    3
    4
    # 匹配属于一一班的学生
    Person.objects.filter(cls__name='一一班')
    # 匹配一年级的学生(班级名称一开头)
    Person.objects.filter(cls__name__startswith='一')
  • 查询优化

    1
    2
    3
    4
    # 关联查询避免访问学生的班级信息时再次查询班级表
    Person.objects.select_related('cls') # sql层面
    Person.objects.prefetch_related('cls')
    Person.objects.annotate(cls_name=F('cls__name')) # 仅关联指定字段查询
  • 其他

HTTP请求参数解析器实现

HTTP请求参数

  • GET / DELETE 查询参数/URL参数
  • POST / PATCH / PUT
    • 请求体(body)application/json www-xxxx form-data
    • request.POST {“key”: “123”}

解析器实现目标

  • 能解析GET/POST参数
  • 参数校验
    • 参数类型,例如必需是int
    • 必填判断
    • 自定义校验,例如该参数只是能(“a”, “b”, “c”)中的一个

代码实现

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import json

body = {
"id": "12",
"url": "https://gitee.com/yooke/User.git",
"type": "c",
}

# request.GET
# request.POST
# application/json json.loads(request.body)


body = json.dumps(body)


class Argument:
def __init__(self, key, required=True, filter=None, type=None, help=None):
self.key = key
self.required = required
self.filter = filter
self.type = type
self.help = help


class Parser:
def __init__(self, *arguments):
self.arguments = arguments

def parse(self, data):
form, error = {}, None
for arg in self.arguments:
value = data.get(arg.key)
if arg.required and value is None: # 判断required属性是否必填
error = arg.help
break
if arg.type: # 判断type属性类型是否正确
if not isinstance(value, arg.type):
try:
value = arg.type(value) # 尝试转换类型
except ValueError:
error = arg.help
break
if arg.filter and not arg.filter(value): # 判断是否符合filter过滤条件
error = arg.help
break
form[arg.key] = value
return form, error


class JSONParser(Parser): # 扩展解析JSON消息体
def parse(self, data):
data = json.loads(data)
return super().parse(data)


class XMLParser(Parser): # 扩展解析XML消息体
def xmltodict(self, data):
# TODO: xml to dict
return data

def parse(self, data):
data = self.xmltodict(data)
return super().parse(data)


def main():
form, error = JSONParser(
Argument('id', type=int, help='ID必须是整数'),
Argument('name', required=False, help='name参数必填'),
Argument('url', help='url参数必填'),
Argument('type', filter=lambda x: x in ('a', 'b', 'c'), help='type参数必须是a,b,c中的一个'),
).parse(body)
if error is None:
print('参数校验通过: ', form)
else:
print('参数校验失败: ', error)


main()

不同路径写法对Rsync的影响

Rsync 同步时需要指定源路径与目标路径,那么路径末尾的 / 会影响同步的结果吗?做了以下测试

同步目录

以源路径 /Downloads/User 目录,远端目录 /data 为例

  • rsync Dowloads/User root@ip:/data
    • /data 存在,/data/User 与源目录一致
    • /data不存在,/data/User与源目录一致
  • rsync Downloads/User root@ip:/data/
    • /data存在,/data/User与源目录一致
    • /data不存在,/data/User与源目录一致
  • rsync Downloads/User/ root@ip:/data
    • /data存在,/data 与源目录一致
    • /data不存在,/data与源目录一致
  • rsync Dowloads/User/ root@ip:/data/
    • /data存在,/data与源目录一致
    • /data 不存在, /data与源目录一致

同步文件

以源路径 /Downloads/User/a.txt文件,远端路径 /data 为例

  • rsync Dowloads/User/a.txt root@ip:/data
    • /data 存在,/data 覆盖内容与a.txt一致
    • /data不存在,/data 创建文件,内容与a.txt一致
  • rsync Downloads/User/a.txt root@ip:/data/
    • /data存在,/data/a.txt 与a.txt一致
    • /data不存在,/data/a.txt 与a.txt一致
  • rsync Downloads/a.txt/ root@ip:/data
    • /data存在,报错:”Downloads/User/a.txt/.” failed: Not a directory (20)
    • /data不存在,报错:”Downloads/User/a.txt/.” failed: Not a directory (20)
  • rsync Dowloads/User/a.txt/ root@ip:/data/
    • /data存在,报错:”Downloads/User/a.txt/.” failed: Not a directory (20)
    • /data 不存在,报错:”Downloads/User/a.txt/.” failed: Not a directory (20)

总结

  • 同步的源路径为目录时
    • 源路径以/结尾时,同步源路径下边的所有文件至目标路径内
    • 源路径非/结尾时,同步源路径自身至目标路径内(源目录会作为目标路径的子目录)
    • 与目标路径是否以/结尾无关
  • 同步的源路径为文件时
    • 源路径以/结尾时报错,无法执行同步
    • 目标路径以/结尾时,同步文件至目标路径下,新建或覆盖目标路径下的同名文件
    • 目标路径非/结尾时,目标路径即为同步之后的文件路径(可理解为把源文件重命名为目标文件)

Xtermjs使用入门

Xterm是一个实现web终端的js库。

使用方法

  1. 安装依赖

    1
    2
    npm install xterm
    yarn add xterm
  2. 引入xterm

    1
    import { Terminal } from 'xterm'
  3. 相关的html代码

    1
    <div id="terminal"/>
  4. 相关的js代码

    1
    2
    3
    let term = new Terminal()
    term.open(document.getElementById('terminal'));
    term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')

常用配置

  • 字体
    • term.options.fontFamily = 'monospace'
  • 字号
    • term.options.fontSize = 12
  • 行号
    • term.options.lineHeight = 1.2
  • 主题配色
    • term.options.theme = {background: '#2b2b2b', foreground: '#A9B7C6', cursor: '#2b2b2b'}

常用插件

  • xterm-addon-fit

    提供terminal内容自适应

    1
    2
    3
    4
    5
    import { FitAddon } from 'xterm-addon-fit'

    const fitAddon = new FitAddon();
    term.loadAddon(fitAddon);
    fitAddon.fit()

Python使用openpyxl读写excel

  • 读取excel的内容

    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
    wb = load_workbook('/Users/aka/Downloads/w_pyxl.xlsx')
    ws = wb['Sheet1']

    for row in ws.rows: # 遍历所有行
    # 按索引取出每行的指定位置的值
    print(row[0].value, row[1].value, row[2].value, row[3].value)


    # 每行组合成字典返回
    def parse_ws(sheet):
    keys = []
    for index, row in enumerate(sheet.rows):
    if index == 0:
    for item in row:
    keys.append(item.value)
    continue

    values = [x.value for x in row]
    yield dict(zip(keys, values))


    wb = load_workbook('/Users/aka/Downloads/w_pyxl.xlsx')
    ws = wb['Sheet1']
    for line in parse_ws(ws):
    print('姓名:', line['姓名']) # 通过表头来取值
  • 生成excel文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from openpyxl import Workbook

    wb = Workbook()
    ws = wb.active

    data = [
    ('张三', '一三班', '100'),
    ('李四', '一三班', '65'),
    ('王武', '一三班', '95')
    ]

    ws.append(('姓名', '班级', '分数'))
    for item in data:
    ws.append(item)

    wb.save('/Users/aka/Downloads/test.xlsx')

使用buildx创建多架构镜像

参考文档1:https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images

参考文档2:https://docs.docker.com/desktop/multi-arch/

需求

步骤

1. 启用 binfmt_misc

1
docker run --privileged --rm tonistiigi/binfmt --install all

2. 创建并切换构建器

1
2
3
docker buildx create --name mybuilder
docker buildx use mybuilder
docker buildx inspect --bootstrap

3. 构建镜像

1
docker buildx build --platform linux/amd64,linux/arm64 -t openspug/spug-service --push .

flex布局元素被挤压

问题

先看个常见的需求,元素A 设置了固定宽度 100px,但当元素B内容过多时会挤压A的宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
.container {
display: flex;
flex-direction: row;
width: 200px;
border: 1px solid #999;
}
.a {
width: 100px;
}
</style>

<div class="container">
<div class="a">leiem.cn</div>
<div>先看个常见的需求,元素A 设置了固定宽度 `100px`,但当元素B内容过多时会挤压A的宽度。</div>
</div>

image-20220511122609088

可以看到总宽度 200px 元素A 设置的宽度 100px 应该占据一半的空间,但展示的效果明显被挤压了。

解决方法

处理办法也很简单就是 flex-shrink 属性,该属性定义了当父元素主轴空间不足时子元素的缩小比例。具体怎么缩小还受其他属性的影响,我们这里就不展开详述了,因为大部分情况下也不会遇到那么复杂场景。

针对上述例子只需要在元素A上添加 flex-shrink: 0 即可解决。

1
2
3
4
.a {
width: 100px;
flex-shrink: 0;
}

添加后的效果,完美解决 😄

image-20220511123334535