openpyxl单元格宽高自适应

默认使用openpyxl导出的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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def adjust_ws(ws):
def get_length(text):
_length = 0
for char in str(text):
if '\u4e00' <= char <= '\u9fff': # 中文字符
_length += 2
else:
_length += 1
return _length

def has_chinese(text):
try:
text.encode('ascii')
return False
except UnicodeEncodeError:
return True

for col in ws.columns:
max_length = 0
column = col[0].column_letter

for cell in col:
if cell.value:
lines = str(cell.value).split('\n')
length = max(get_length(line) for line in lines)
if length > max_length:
max_length = length

# 设置列宽,这里可以根据需要调整比率增加或减少宽度
ws.column_dimensions[column].width = (max_length + 2) * 1

for row in ws.rows:
max_lines = 1
for cell in row:
if cell.value:
lines_count = sum([1.4 if has_chinese(x) else 1 for x in str(cell.value).split('\n')])
if lines_count > max_lines:
max_lines = lines_count
# 设置行高,可根据需要调整比率增加或减少
ws.row_dimensions[row[0].row].height = 15 * max_lines

通过一个实际例子说明如何使用

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
from openpyxl import Workbook
from openpyxl.styles import Alignment

def export_excel():
# 创建工作簿和工作表
wb = Workbook()
ws = wb.active

# 示例数据(包含换行符)
data = [
['姓名', '描述', '备注'],
['张三', '这是一段很长的描述\n需要换行显示', '备注1'],
['李四', '另一段描述内容\n同样需要换行', '备注2'],
]

# 写入数据
for row_idx, row in enumerate(data, start=1):
for col_idx, value in enumerate(row, start=1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
if '\n' in str(value):
cell.alignment = Alignment(
wrap_text=True,
vertical='center',
horizontal='left'
)

# 引用上述函数调整列宽和行高
adjust_ws(ws)

# 保存文件
wb.save('example.xlsx')

export_excel()

注意:通过上述代码可以发现为了自适应宽高对整个excel的内容进行了横向和竖向的遍历,所以其效率肯定不会高,在应用于大量数据的excel时会出现性能问题。

[TOC]

Series

Series是一个一维的标记数组,可以存储任何数据类型。

  • 创建Series

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import pandas as pd

    s = pd.Series(['a', 'b', 'c'])
    print(s)

    # 输出
    0 a
    1 b
    2 c
    dtype: object
  • Series索引

    索引是用于标识每个数据元素的标签,默认情况下pandas会创建从0开始的整数索引,可以通过index参数来指定自定义索引。

    1
    2
    3
    4
    5
    6
    7
    8
    s = pd.Series(['a', 'b', 'c'], index=['V1', 'V2', 'V3'])
    print(s)

    # 输出
    V1 a
    V2 b
    V3 c
    dtype: object
  • 常用操作

    • 通过索引取值

      1
      2
      3
      4
      s = pd.Series(['a', 'b', 'c'], index=['V1', 'V2', 'V3'])
      print(s['V2'])
      # 输出
      b

DataFrame

DataFrame是二维的表格数据结果,可以看作是Series的集合,它有行索引和列名。

  • 创建Dataframe

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import pandas as pd

    df = pd.DataFrame({
    'Name': ['Alice', 'Bob', 'Charlie'],
    'Age': [25, 30, 35],
    'City': ['New York', 'Los Angeles', 'Chicago']
    })
    print(df)
    # 输出
    Name Age City
    0 Alice 25 New York
    1 Bob 30 Los Angeles
    2 Charlie 35 Chicago
  • 查看前N行数据

    1
    2
    3
    4
    5
    print(df.head(2))
    # 输出
    Name Age City
    0 Alice 25 New York
    1 Bob 30 Los Angeles
  • 查看最后N行数据

    1
    2
    3
    4
    5
    print(df.tail(2))
    # 输出
    Name Age City
    1 Bob 30 Los Angeles
    2 Charlie 35 Chicago
  • 获取单列的数据(返回Series)

    1
    2
    3
    4
    5
    6
    print(df['Name'])
    # 输出
    0 Alice
    1 Bob
    2 Charlie
    Name: Name, dtype: object
  • 过滤数据

    • df.iloc[1]

      iloc基于位置取第2行的数据,输出

      1
      2
      3
      4
      Name            Bob
      Age 30
      City Los Angeles
      Name: 1, dtype: object
    • df.iloc[1, 0]

      iloc基于位置取第2行第1列的值,行与列都是从0开始计与索引和列名无关,输出

      1
      Bob
    • df.loc[1]

      loc基于标签取索引为1那一行的数据,输出

      1
      2
      3
      4
      Name            Bob
      Age 30
      City Los Angeles
      Name: 1, dtype: object
    • df.loc[1, 'Name']

      loc基于标签取索引为1(默认索引是整数从0开始)那一行的Name列的值,输出

      1
      Bob
    • df.loc[df['Age'] > 25]

      过滤Age列的值大于25的所有数据,输出

      1
      2
      3
            Name  Age         City
      1 Bob 30 Los Angeles
      2 Charlie 35 Chicago

阿里云全站加速缓存配置

一直对阿里云全站加速(DCDN)的缓存配置比较疑惑,通过提工单及自己的验证算是搞明白了,顺便记录下。

分两种情况来说明下如何配置,及如何验证缓存是否生效

明确网站的静态文件路径

你已经很明确网站的静态文件路径,例如现在比较流行的 react 项目编译后的静态文件路径一般为 /static//assets/

  1. 配置静态路径(注意/static/* 后边的*务必要加)

    image-20240614094051001

  2. 设置缓存时间(可选,默认缓存1小时)

    image-20240614103447299

不确定静态文件路径或非常分散

这时候可以通过指定静态文件类型来配置,会根据请求的文件后缀名来判断是否是静态文件,而不必关心其路径。

  1. 配置静态文件类型(根据实际请求勾选,通常可以全选)

    image-20240614104036351

  2. 设置缓存时间(可选,默认缓存1小时),同上述一致,不再放图了

验证缓存是否生效

打开浏览器的开发者工具控制台切换到 Network 栏,通过查看请求的Response Header 是否包含 X-Cache 判断

image-20240614104605222

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()