Leetao's Blog

Talk is cheap, show me the code

0%

Recently I was working on rss3 SDK. In order to facilitate development, I just make a python version of the reference JavaScript SDK, which means the usage should be pretty similar between both.

Make a Python version

What’s more, to make a clear code,I use type hinting in the new project. However, there is a code snippet in JavaScript version:

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
# index.ts
class RSS3 {
options: IOptions;
persona: Persona;
file: File;
profile: Profile;
items: Items;
item: Item;
links: Links;

constructor(options: IOptions) {
this.options = options;

this.file = new File(this);
this.persona = new Persona(this);
this.profile = new Profile(this);
this.items = new Items(this);
this.item = new Item(this);
this.links = new Links(this);
}
}
# file.ts
import Main from './index';
class File {
private main: Main;
private list: {
[key: string]: RSS3IContent;
} = {};
private dirtyList: {
[key: string]: number;
} = {};

constructor(main: Main) {
this.main = main;
}
...
}

Yes, the two files refer to each other. Actually, it happens on many other files. When I turned this into Python version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# index.py
class RSS3:
...
persona: Persona
file: File_
profile: Profile
items: Items
items: Item
links: Links

# file.py
from .index import RSS3

class File:
rss3: RSS3

def __init__(self, rss3: RSS3):
self.rss3 = rss3

circular import

It didn’t seem any errors, but when I started testing the problems appeared.

1
E   ImportError: cannot import name 'RSS3' from partially initialized module 'rss3.src.index' (most likely due to a circular import) 

how to solve this problem ? Don’t worry, PEP 484 has given a solution.

Solutions

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

So we can modify our code like the following:

1
2
3
4
5
class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

That’s okay already.

Sometimes there’s code that must be seen by a type checker (or other static analysis tools) but should not be executed. For such situations the typing module defines a constant, TYPE_CHECKING, that is considered True during type checking (or other static analysis) but False at runtime.

Modify out code again:

1
2
3
4
5
6
7
8
if TYPE_CHECKING:
from .index import RSS3

class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

What’s more

if we are using Python 3.7+, we can at least skip having to provide an explicit string annotation by taking advantage of PEP 563:

1
2
3
4
5
6
7
8
9
10
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .index import RSS3

class File:
rss3: 'RSS3'

def __init__(self, rss3: 'RSS3'):
self.rss3 = rss3

The from __future__ import annotations import will make all type hints be strings and skip evaluating them.

References

PEP 484 – Type Hints

Python type hinting without cyclic imports

前言

前段时间用 Flutter 做了一个开源的项目 RSSAid,因为需要打包 apk,在此之前一直是在本地签名打包的。后来和别人交流了一下,想起来可以用 Github Action 构建持续化集成,自动打包。然后就研究了一下,最后完成了根据 tag 版本自动生成 apk 的 workflows。

Workflows

自动化构建脚本如下:

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
# main.yml
# 自动构建 Apk
name: Test, Build and Release apk

# 工作流程触发的时机,这里是当一个版本标签推送到仓库时触发
on:
push:
tags:
- v*

# 这个工作流程需要执行的任务
jobs:
process:
name: all process
runs-on: ubuntu-latest
# 这个任务的步骤
steps:
# 拉取项目代码
- uses: actions/checkout@v2
# 建立 java 环境
- name: Setup Java JDK
uses: actions/setup-java@v1.4.3
with:
java-version: "12.x"
# 建立 Flutter 环境
- name: Flutter action
uses: subosito/flutter-action@v1.4.0
with:
channel: "stable"
flutter-version: "1.22.4"
# 下载项目依赖
- run: flutter pub get
- run: echo $ENCODED_KEYSTORE | base64 -di > android/app/keystore.jks
env:
ENCODED_KEYSTORE: ${{ secrets.ENCODED_KEYSTORE }}
# 打包 APK
- run: flutter build apk --release
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
# 发布到 Release
- name: Release apk
uses: ncipollo/release-action@v1.5.0
with:
artifacts: "build/app/outputs/apk/release/*.apk"
token: ${{ secrets.RELEASE_TOKEN }}

使用注意事项

脚本中有很多环境变量,都需要实现定义好,在项目的 secrets 中添加上。

RELASE_TOKEN

这个环境变量需要在 Personal access tokens 申请,需要注意的是,申请完成之后,不要着急关闭这个页面,因为一旦关闭就不能再次查看生成的 token 了,这个 token 需要申请 repo 和 workflow 的权限

生成 token 成功后,找到项目的 **Settings => Secrets **选项,新建名为 RELEASE_TOKEN 的 secrets 然后 value 值为刚才生成的 token。这个完成之后,就需要对设置生成 apk 需要的签名进行变量设置了。

签名相关变量

正常 app 签名步骤可以参考 app签名,最终我们会创建一个 key.properties 的文件,文件内容如下:

1
2
3
4
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, e.g. /Users/<user name>/key.jks>

然后在 android/app/build.grade 中配置

1
2
3
4
5
6
7
8
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}

但是将 key.properties 上传到仓库显然是不安全的。所以需要对代码进行修改,将对应的变量添加到 secrets 中,从 secrets 中获取变量。

1
2
3
4
storeFile file(System.getenv("KEYSTORE") ?:"keystore.jks")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")

其中 **KEYSTORE_PASSWORD、KEY_ALIAS、KEY_PASSWORD **直接就可以添加,那么 **KEYSTORE **这个变量怎么处理呢?KEYSTORE 对应着 jks 文件位置。jks 文件显然也不可能上传到仓库,所以我们换种方法,在构建的时候生成 jks 文件。

构建时生成 jks 文件

正常情况下打开生成的 jks 文件多半是乱码,所以我们可以通过 base64 对文件进行编码,然后在构建的时候,再解码重新生成文件。

获取 base64 格式的 keystore

首先获取 base64 格式的 keystore

1
openssl base64 -A -in <jks.文件位置>

然后将输出的结果复制下来

保存 base64 格式的 keystore

将编码后的 keystore 内容,添加到 secrets ,变量名命名为 ENCODED_KEYSTORE,然后在构建过程中就可以将 keystore 文件还原了。

1
echo $ENCODED_KEYSTORE | base64 -di > android/app/keystore.jks

前言

主备同步,也叫主从复制,是 mysql 提供的一种高可用的解决方案,保证主备数据一致性的解决方案。
在生产环境中,会有很多不可控因素,比如数据库服务器宕机等,因此在生产环境中,都会采用主备同步。在应用的规模不大的情况下,一般会采用一主一备。除此之外,采用主备同步还可以:

  • 提高数据库的读并发性,大多数应用都是读比写要多,采用主备同步方案,当使用规模越来越大的时候,可以扩展备库来提升读能力。
  • 备份,主备同步可以得到一份实时的完整的备份数据库。
  • 快速恢复,当主库出错了(比如误删表),通过备库来快速恢复数据。

那么主备同步的原理是什么?

主备同步的实现原理

主备同步模式之所以能够实现,显然是有一种手段可以,将主的 mysql 服务(以下简称 master)执行的 DDL 和 DML 语句传递给 备份的 mysql 服务(以下简称为 slave),这个就是 MySQL 的 binlog,mysql 的 binlog 是 MySQL 最重要的日志,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
所以 binlog 一般会有两个用途:

  • 主备模式下,master 将 binlog 传递给 slave,从而达到数据一致的目的
  • 数据恢复,可以通过 mysqlbinlog 工具来恢复数据

当 master 将 binlog 传递到 slave 的时候,会被传到 slave 的 relay log,relay log 也叫中继日志,是连接 master 和 slave 的核心,relay-log 的结构和 binlog 非常相似,只不过他多了一个 master.info 和 relay-log.info 的文件。

master.info 记录了上一次读取到 master 同步过来的 binlog 的位置,以及连接 master 和启动复制必须的所有信息。
relay-log.info 记录了文件复制的进度,下一个事件从什么位置开始,由 sql 线程负责更新。

数据库同步原理

 说完了原理,接下来说说常见的俩种主备模式以及实践吧。

主备模式与实践

常见的主备模式有俩种分别是 M-S 结构双 M 结构,本篇文章介绍前者 – M-S 结构

M-S结构

M-S结构,两个节点,一个当主库、一个当备库,不允许两个节点互换角色。

M-S结构

在状态1中,客户端的读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过来,到本地执行。这样可以保持节点B和A的数据是相同的。
当需要切换的时候,就切成状态2。这时候客户端读写访问的都是节点B,而节点A是B的备库。

实践

修改 master 配置

首先修改 master 中的 my.cnf

1
vi /etc/my.cnf

在 my.cnf 中 [mysqld] 中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[mysqld]
bind-address=192.168.1.100
server-id=1
binlog-ignore-db = "mysql"
binlog-format = mixed
log-bin=mysql-bin
datadir=/var/lib/mysql
innodb_flush_log_at_trx_commit=1
sync_binlog=1

#备注:
# server-id 服务器唯一标识。
# log_bin 启动MySQL二进制日志,即数据同步语句,从数据库会一条一条的执行这些语句。
# binlog_do_db 指定记录二进制日志的数据库,即需要复制的数据库名,如果复制多个数据库,重复设置这个选项即可。
# binlog_ignore_db 指定不记录二进制日志的数据库,即不需要复制的数据库名,如果有多个数据库,重复设置这个选项即可。
# 其中需要注意的是,binlog_do_db和binlog_ignore_db为互斥选项,一般只需要一个即可。

重启 master 的 mysql

保存之后,然后重启 MySQL

1
service restart mysql

创建同步帐号

接着以 root 帐号登录 master 的 mysql,创建一个用于同步的帐号,以下建立同步帐号名为 replication,密码为 password, slave 的 ip 是 192.168.1.101

1
2
3
4
5
mysql> CREATE USER replication@192.168.1.101;
mysql> GRANT REPLICATION SLAVE ON *.* TO replication@192.168.1.101 IDENTIFIED BY 'password';
mysql> flush privileges;
mysql> SHOW MASTER STATUS;
mysql> exit;

数据初始化

到此关于 master 的配置结束了,如果这个时候,要同步的数据库有数据,可以将数据 sql 文件,以便于前期的数据初始化

1
mysqldump –skip-lock-tables –all-databases –user=root –password –master-data > master.sql

修改 slave 的配置

接下来对 slave 的进行修改,首先修改 my.cnf,将下述内容加到 [mysqld] 区域:

1
2
3
4
5
6
[mysqld]
server-id=2
binlog-format=mixed
log_bin=mysql-bin
relay-log=mysql-relay-bin
log-slave-updates=1 # 默认是关闭的,这个时候 salve 从 master 复制的数据不会写入到 log-bin 日志文件中,开启后,则会写入

重启 slave 的 mysql

然后重启 mysql,重启之后,以 root 用户登录 MySQL, 新建要同步的数据库

1
2
3
4
service restart mysql
mysql -uroot -p
mysql> create database database-name;
mysql> exit;

导入 master 的数据

将 master 的导出的 sql 文件,加载到 slave 数据中,进行数据初始化

1
mysql -u root -p database-name < master.sql

设置跟踪主库日志文件

在开启 slave 服务之前,还需要进行一些设置

1
2
3
4
5
6
7
8
mysql> CHANGE MASTER TO 
-> MASTER_HOST='192.168.1.100',
-> MASTER_USER='replication',
-> MASTER_PASSWORD='password',
-> MASTER_LOG_FILE='mysql-bin.000001',
-> MASTER_LOG_POS=500;
mysql> START SLAVE;
mysql> SHOW SLAVE STATUS \G;

每个参数的含义如下:

  • master_host 为主库IP地址

  • master_user 为主库用户名

  • master_password 为主库密码

  • master_log_file 为主库日志文件

  • master_log_pos 为主库日志所占位置

其中 MASTER_LOG_FILE 及 MASTER_LOG_POS 是在 Master 上在 MySQL 执行 “SHOW MASTER STATUS;” 的结果。

启动 slave 服务,并查看slave服务信息

1
2
mysql> START SLAVE;
mysql> SHOW SLAVE STATUS \G;

如果没有错误信息的话,就说明配置成功了。

参考资料

mysql 实现主备同步

MySQL中的binlog和relay-log结构完全详解

MySQL主从复制与双主互备

Mysql log 日志

mysql中binlog_format模式与配置详解

How To Set Up Master Slave Replication in MySQL

前言

scrapy 是 python 中一个优秀的爬虫框架,基于这个框架,用户可以快速构建自己的爬虫程序。框架涉及很多模块,其中有两个核心概念 items 和 item pipelines。

Items: 爬取的主要目标就是从非结构性的数据源提取结构性数据,例如网页。 Scrapy提供 Item 类来满足这样的需求。
Item 对象是种简单的容器,保存了爬取到得数据。 其提供了 类似于词典(dictionary-like) 的API以及用于声明可用字段的简单语法。

Item Pipeline:当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。
每个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。

``

Pipeline

通常情况下 scrapy 会有一个默认的 Pipeline:

1
2
3
class KnowledgeGraphPipeline:
def process_item(self, item, spider):
pass

正常对于传过来的数据,我们通过 item 对应的类型,可以进行判断然后执行逻辑

1
2
if isinstance(item, XXItem):
# 代码逻辑

一旦当我们的 spider 多了,将所有的处理逻辑放到一个 pipeline 显然是不合理的,所以会希望每个 spider 都有一个专门的 pipeline,这个时候我们可以通过复写 spider 的默认配置就可以实现这样的目的

解决办法

1
2
3
4
5
6
7
class XXSpider(scrapy.Spider):
name = 'xx'
custom_settings = {
'ITEM_PIPELINES': {
'knowledge_graph.pipelines.xxxx': 200 # 这个值需要大于默认的 pipeline
}
}

换博客

我竟然又想折腾博客了,我发现每过一段时间我都会想重新折腾一下博客,尽管我知道博客最重要的是内容,但是还是控制不住自己折腾的心。

现在的博客系统是自己基于 Flask 开发的,功能是基本满足日常使用的,原来是从 hexo 迁移过来的,可能料到自己会再次折腾,这个系统可以把自己所有的文章都导出成符合 hexo 格式的 markdown 文件,可能切回 hexo 是最方便的方案?

hexo ?

周六的上午起来就一直在浏览 hexo 的相关主题,比较中意的是 Next 主题,发现随着自己的年龄的增长,对简洁的东西更加中意了,但是出了简洁还希望能有点个性。

wp?

自己现在的博客是部署在服务器上的,如果用上了 hexo,就没有用服务器了,完全可以托管到 github 上,至于为放到服务器上,总觉得把静态博客放到服务器上的行为是对服务器的浪费。

想换 wp,不过不知道自己的小水管能不能撑住 wp,除此之外,网上有不少 hexo 迁移到 wp 的方案但是似乎都有点问题,不是特别想折腾。

end

思前想后,最后还是决定不换了,抽个时间再对自己的博客改造一番。服务器续费了 5 年,至少也让这个博客再运行五年。

读书

最近在看《秋园》,似乎是豆瓣 20 年排名比较靠前的一本书,才读了一点点,提到那个时期,女人还有裹脚布的习惯,“三寸金莲”,从书里对那个场景的描述,就能想象到被裹脚的痛楚,更是难以想象这种恶俗竟然持续了几千年。

《秋园》是自己开始看的第四本书,第三本书是《夜晚的潜水艇》,这篇读书笔记还没有写。。。

综艺

最近几年没事的时候偶尔会看一点综艺,这段时间再看 【奇葩说】,感觉奇葩说里的有的时候讨论的议题还是蛮有意思的,比如昨天再看的 20 期讨论 “父母该不该教孩子让着弟弟妹妹”,正反方都有各自的理由和观点,但是有一句我觉得蛮有道理的,“你是大的,应该让着小的”,说出这句话的时候多半是父母一种偷懒的表现。

BTW,辩论赛真是有意思,当初大学应该去参加的,真是可惜,哈哈

前言

最近使用 flutter 构建 App,涉及到网络请求部分,使用了 dart 自带的 HttpClient 库发现了一个有趣的问题,dart 默认情况下不使用代理,即使电脑开着代理。

解决方案

HttpClient 有个 findProxy 方法,复写这个方法就是设置代理,因此只需要在请求之前设置代理就可以了。

findProxy

1
2
3
4
HttpClient client = HttpClient();
client.findProxy = (uri) {
return "PROXY localhost:3128;";
};

但是显然在开发过程中我们并不清楚代理具体的 ip 和端口,所以为了提高可用性,需要一个方法帮助我们发现系统代理。

发现系统代理

flutter 有现成的三方 package-system_proxy 可以帮助获取代理。

安装 system-proxy

在 pubspec.yaml 中添加依赖

1
system_proxy: ^0.0.1

使用

1
2
3
4
Map<String, String> proxy = await SystemProxy.getProxySettings();
if (proxy != null) {
print('proxy $proxy');
}

优化代码

成功安装完需要的 package 之后,优化一下代码,使得在请求之前使用系统代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension HttpClientExtension on HttpClient {
Future<HttpClient> autoProxy() async {
Map<String, String> sysProxy = await SystemProxy.getProxySettings();
var proxy = "DIRECT";
if (sysProxy != null) {
proxy = "PROXY ${sysProxy['host']}:${sysProxy['port']}; DIRECT";
print("find proxy $proxy");
}
this.findProxy = (uri) {
return proxy;
};
return this;
}
}

/// 使用方法
var httpClient = await new HttpClient().autoProxy();
/// 逻辑

参考链接

Network calls don’t show in Charles Proxy Debugger

Support proxy configuration in HttpClient

学会描述符之后,不仅有更多的工具集可用,还会对 Python 的运作
方式有更深入的理解,并由衷赞叹 Python 设计的优雅。——Raymond Hettinger
Python 核心开发者和专家

什么是描述符

描述符 (Descriptor) 是 Python 中一个非常重要的特性,在实际应用中我们经常使用到它,但是也最容易被忽略,property、classmethod、staticmethod。那么究竟什么叫描述符呢?看一下官方的定义:

In general, a descriptor is an attribute value that has one of the methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an attribute, it is said to be a descriptor.

一般而言,描述器是一个包含了描述器协议中的方法的属性值。 这些方法有 __get__(), __set__() __delete__()。 如果为某个属性定义了这些方法中的任意一个,它就可以被称为 descriptor

如何使用描述符

除了上面提到的三个内置属性,其实在不少 Python 库中都有关于描述符的应用,比如各种 ORM。

ORM 示例

这里以官方的 ORM 示例做个简单的演示 – 通过描述符来实现简单的 object relational mapping 框架。
其核心思路是将数据存储在外部数据库中,Python 实例仅持有数据库表中对应的的键。描述器负责对值进行查找或更新:

1
2
3
4
5
6
7
8
9
10
11
12
class Field:

def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]

def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()

可以用 Field 类来定义描述了数据库中每张表的模式的 models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()

def __init__(self, key):
self.key = key

class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()

def __init__(self, key):
self.key = key

然后连接数据库验证一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'

>>> Song('Country Roads').artist
'John Denver'

>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams

自定义验证器

官方还提供了一个验证器的例子,同样值得一看。验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from abc import ABC, abstractmethod

class Validator(ABC):

def __set_name__(self, owner, name):
self.private_name = '_' + name

def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)

def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)

@abstractmethod
def validate(self, value):
pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

1
2
3
4
5
6
7
8
class OneOf(Validator):

def __init__(self, *options):
self.options = set(options)

def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

验证结果

1
2
3
4
5
6
class Component:
kind = OneOf('wood', 'metal', 'plastic')
#...
def __init__(self, name, kind, quantity):
self.kind = kind
#...

描述器会阻止无效实例的创建

1
2
3
4
>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

参考

描述器使用指南

前言

通过 Docker 安装 Zabbix Web 界面默认的语言是英文,当切换成中文的时候,有些图标显示的标签就会乱码,不能正常显示,出现这个问题的原因是在于 zabbix 默认的字体对中文支持有问题,所以解决办法就是替换默认的字体。

解决问题

首先找到自己的喜欢的字体,这里我用的是 simkai.ttf。

将字体复制到容器

1
docker cp simkai.ttf zabbix-web-nginx-mysql:/usr/share/zabbix/assets/fonts/

注意:复制到 zabbix-web 的容器而不是 zabbix-server 容器

替换默认字体文件

进入到容器中然后替换默认的字体文件

1
2
3
4
docker exec -it  zabbix-web-nginx-mysql bash
cd /usr/share/zabbix/assets/fonts/
mv DejaVuSans.ttf DejaVuSans.ttf.bak # 不放心的话,可以先备份一下原来的字体文件
mv simkai.ttf DejaVuSans.ttf

然后刷新页面,乱码的问题就解决了

前言

前几天将 IntelliJ IDEA 升级到 2020.3 版本,然后在编译项目的时候发现原本正常运行的项目无法编译成功了,提示下面的错误:

java: You aren’t using a compiler supported by lombok, so lombok will not work and has been disabled. 

在网上检索寻找问题解决办法,发现在 github 上已经有人反馈了这个问题 [BUG] Lombok Does not work with IntelliJ EAP 2020.3 Build 203.4203.26 并给出了解决办法。

解决办法

总共发现两种解决办法,两种办法均有效。

方法一

设置编译器(compiler),打开设置(File -> Settings)

然后选择编译器(compiler),在 Shared build process VM options 填入: -Djps.track.ap.dependencies=false

**点击 apply,这个时候就可以重新编译成功了。

方法二

第二种方法就比较简单了,将你的 Lombok 升级就可以解决了,升级到 1.18.14

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.14</version>
</dependency>

结尾

本次出现这个问题,IDEA 方拒绝背锅,说是 Lombok 的问题。完整内容见:Build project with lombok - IntelliJ Ultimate 2020.3 EAP

前言

在实际过程开发中,有时候会有在启动 Web 服务的同时,希望启动一个后台任务去运行其他的任务。显然后台的任务,都是借由线程去完成的,所以在什么地方运行线程是需要我们解决的问题。

解决方案

请求时启动任务

这种方式显然时最简单的,配合 flask 自带的 api 我们可以在第一次请求时,甚至在每次请求启动线程。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading
import time
from flask import Flask
app = Flask(__name__)

# app.before_request etc
@app.before_first_request
def activate_job():
def run_job():
while True:
print("执行后台任务...")
time.sleep(3)

thread = threading.Thread(target=run_job)
thread.start()

@app.route("/")
def hello():
return "Hello World!"


if __name__ == "__main__":
app.run()

运行一下代码看一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
(venv) D:\workspace\python\test>python test_flask.py
* Serving Flask app "test_flask" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
执行后台任务...
127.0.0.1 - - [24/Nov/2020 16:02:22] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Nov/2020 16:02:22] "GET /favicon.ico HTTP/1.1" 404 -
执行后台任务...
执行后台任务...

显然这个有一点局限性,在没有任何请求的时候线程不会执行。我们可以对代码进行优化,让线程这部分独立于 Flask 应用。

和 Flask 应用一起启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import threading
import time
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"


def start_runner():
def start_loop():
while True:
print("执行后台任务...")
time.sleep(3)

thread = threading.Thread(target=start_loop)
thread.start()

if __name__ == "__main__":
start_runner()
app.run()

同样的,运行一下代码看一下结果:

1
2
3
4
5
6
7
8
9
10
(venv) D:\workspace\python\test>python test_flask.py
执行后台任务...
* Serving Flask app "test_flask" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
执行后台任务...
执行后台任务...

这个时候,后台任务和 Flask 应用相互独立,功能算是基本实现。但是后台任务和 Flask 应用完全分离,实现方式不够优雅,我们对代码进行重构一下。

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
import requests
import threading
import time
from flask import Flask
import os

class FlaskApp(Flask):
def __init__(self, *args, **kwargs):
super(FlaskApp, self).__init__(*args, **kwargs)
self._activate_background_job()

def _activate_background_job(self):
def run_job():
while True:
print('执行后台任务')
time.sleep(3)

t1 = threading.Thread(target=run_job)
t1.start()


app = FlaskApp(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

拓展思维

到这里程序算是基本上完成了,但是如果在运行过程中,停止应用的话,显然会让后台任务直接粗暴的退出,那么有没有一种优雅的方式,可以让应用程序在退出时,让后台任务收拾完成之后,优雅的退出呢?