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()
1
2
3
4
5
6
7
8
9
10
11
https://*.githubusercontent.com/*
https://github.githubassets.com/*
https://github.com/*
[
{
"id": 166,
"targets": [
{"id": 1, "name": "hostname1"},
]
}
]

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

Django ORM小记

对一些常用的小技巧记录。

select_for_update

select_for_update 可以做小并发的控制,其只能在事务中使用。符合条件的查询结果会被加锁,其他查询包含加锁的数据时会阻塞,直到事务完成,如果其他查询结果不包含加锁的记录则不受影响。

1
2
3
4
5
6
7
from django.db import transaction

with transaction.atomic():
# 会给所有type = '1' 的记录加锁
tickets = Ticket.objects.select_for_update().filter(type='1')
# 单条记录加锁
ticket = Ticket.objects.select_for_update().get(id=12)

select_related 在查询时通过外键关系将关联的对象一起查询出来,从而避免查询查询以优化查询性能,适合一对一/一对多关系或关联表数据量比较小的情况。

1
Person.objects.select_related('cls')

对应执行的sql语句

1
SELECT `persons`.`id`, `persons`.`name`, `classes`.`id`, `classes`.`name` ... FROM `persons` LEFT OUTER JOIN `classes` ON (`persons`.`cls_id` = `classes`.`id`)

prefetch_related 一样可以用于优化查询性能,但实现方式与select_related不同,其会产生两条sql语句,查询结果会被缓存到内存中,然后进行合并,适合对多关系或关联表数据量比较大的情况。

1
Person.objects.prefetch_related('cls')

对应执行的两条sql语句

1
2
SELECT `persons`.`id`, `persons`.`name` ... FROM `persons`;
SELECT `classes`.`id`, `classes`.`name` ... FROM `classes` WHERE `classes`.`id` IN (2, 3)';

2分钟搞定React服务端渲染

什么是服务端渲染这里就再解释了,网上已经有很多详细的介绍了。

为什么需要服务端渲染

这里再说下为什么需要服务端渲染,最主要解决的问题就是解决 SEO 问题了,因为 React Vue 基于这些框架写出来的项目数据都是浏览器端动态调用后端接口获取的,包括页面的元素结构什么的都是放在 Js 文件里的。当爬虫来访问时只拿到了 <div id="app"></div> 这样一个空的 div 里边什么内容也没,十分不利于 SEO 。

有了上述的 SEO 问题,那么解决问题思路就很简单了,就是如何能让搜索引擎的爬虫爬到页面是包含了完整内容的。

如何实现

实现的方法也很多,大都是要启动个单独的服务来处理这些爬虫的请求。这里使用的是一个开源的解决方案 prerender 使用非常简单,需要有个 nodejs 运行环境,以下上官方的使用文档。

  1. 安装 pretender

    1
    npm install prerender
  2. 创建文件 server.js

    1
    2
    3
    const prerender = require('prerender');
    const server = prerender();
    server.start();
  3. 启动并测试

    1
    2
    3
    node server.js

    curl http://localhost:3000/render?url=https://www.example.com/

其原理就是启动一个 headlesschrome 浏览器,在渲染完成后把结果在返回给客户端。

如何部署

到服务器上部署的时候会有个麻烦的问题,服务器一般都是 Liunx 系统安装 chrome 比较麻烦,那么就可以通过 Docker 来完美解决这个问题了,我已经构建了这样一个镜像,如果要使用可以通过以下步骤使用。

  1. 获取镜像

    1
    docker pull registry.aliyuncs.com/leiem/prerender
  2. 启动容器

    1
    docker run -d --restart=always -p 3000:3000 registry.aliyuncs.com/leiem/prerender
  3. 配置Nginx规则

    1
    2
    3
    4
    5
    6
    7
    8
    location / {
    try_files $uri /index.html;

    if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider") {
    rewrite .* /render?url=$scheme://$host$request_uri break;
    proxy_pass http://127.0.0.1:3000;
    }
    }

    上述规则会匹配常见的搜索引擎的 User-Agent,让这些请求通过我们搭建的服务去处理。

自己构建镜像

可以直接使用我制作好的镜像 registry.aliyuncs.com/leiem/prerender ,如果要自己构建镜像可参考以下文档。

  • Dockerfile

    1
    2
    3
    4
    5
    6
    7
    8
    FROM browserless/chrome
    USER root
    WORKDIR /usr/src/prerender
    COPY server.js package.json ./
    RUN npm install
    USER blessuser
    EXPOSE 3000
    CMD [ "npm", "run", "start" ]
  • server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const prerender = require('prerender')

    const server = prerender({
    followRedirects: true,
    chromeLocation: '/usr/bin/google-chrome',
    chromeFlags: [ '--no-sandbox', '--headless', '--disable-gpu', '--remote-debugging-port=9222', '--hide-scrollbars' ],
    })

    server.use(prerender.blockResources())
    server.use(prerender.removeScriptTags())
    // server.use(require('prerender-memory-cache'))
    server.start()
  • package.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "name": "leiem.cn",
    "version": "1.0.0",
    "license": "MIT",
    "dependencies": {
    "prerender": "5.19.0",
    "prerender-memory-cache": "1.0.2"
    },
    "scripts": {
    "start": "node server.js"
    }
    }