Leetao's Blog

Talk is cheap, show me the code

0%

前言

默认情况下 es 是不支持 pdf、doc 等文档的搜索的,但是可以通过安装 Ingest attachment plugin 插件来使得 es 提取通用格式的文件,从而可以实现搜索的功能。

安装与使用

安装 Ingest attachment plugin

安装很简单,通过 elasticsearch-plugin 可以直接进行安装

1
bin/elasticsearch-plugin install ingest-attachment

Ingest attachment plugin 允许 Elasticsearch 通过使用 Apache 文本提取库 Tika 提取通用格式(例如:PPT,XLS 和 PDF)的文件附件。Apache Tika 工具包可从一千多种不同的文件类型中检测并提取元数据和文本。所有这些文件类型都可以通过一个界面进行解析,从而使 Tika 对搜索引擎索引,内容分析,翻译等有用。需要注意的是,源字段必须是 Base64 编码的二进制,如果不想增加在 Base64 之间来回转换的开销,则可以使用 CBOR 格式而不是 JSON,并将字段指定为字节数组而不是字符串表示形式,这样处理器将跳过 Base64 解码。

创建 attachment pipeline

通过 kibana 的开发工具进行请求

1
2
3
4
5
6
7
8
9
10
11
PUT _ingest/pipeline/pdfattachment
{
"description": "Extract attachment information encoded in Base64 with UTF-8 charset",
"processors": [
{
"attachment": {
"field": "file"
}
}
]
}

返回结果:

1
2
3
{
"acknowledged" : true
}

表示创建成功,接下来就是验证上传 pdf 以及搜索功能了。

转换并上传PDF文件的内容到Elasticsearch中

对于 Ingest attachment plugin 来说,它的数据必须是 Base64 的。这里为了快速创建,我们通过一个 bash 脚本去处理用来测试的 pdf

1
2
3
4
5
6
7
8
9
!/bin/bash

encodedPdf=`cat sample.pdf | base64`

json="{\"file\":\"${encodedPdf}\"}"

echo "$json" > json.file

curl -X POST 'http://localhost:9200/pdf-test1/_doc?pipeline=pdfattachment&pretty' -H 'Content-Type: application/json' -d @json.file

上传成功会返回如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "pdf-test1",
"_type" : "_doc",
"_id" : "1oVSxHsB1ubIHqCXIbPU",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

如果出现提示下述提示

1
2
3
4
5
{
"statusCode": 400,
"error": "Bad Request",
"message": "Payload content length greater than maximum allowed: 1048576"
}

说明上传的文件大小超过了 Kibana 默认的上限(默认 1M),修改 kibana 的配置 kibana.yml

1
server.maxPayloadBytes: "209715200"

查看索引并搜索

查看索引

通过下面的命令可以查看 pdf-test1 的索引情况:

1
GET pdf-test1/_search

结果如下图

其中 _source 里有个 content 字段,就是 pdf 的内容,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"_source": {
"file":"...",
"attachment": {
"date": "2021-08-18T07:29:34Z",
"content_type": "application/pdf",
"author": "author",
"language": "lt",
"title": "2021-07(copy)",
"content": "..."
}
}

其中 file 就是 base64 格式的内容,content 则包含了 pdf 的内容,如果不想要 file 则可以通过 remove processor 去除这个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT _ingest/pipeline/pdfattachment
{
"description": "Extract attachment information encoded in Base64 with UTF-8 charset",
"processors": [
{
"attachment": {
"field": "file"
}
},
{
"remove": {
"field": "file"
}
}
]
}

搜索

通过在 kibana 的开发工具执行下述命令,验证一下搜索:

1
2
3
4
5
6
7
8
GET pdf-test1/_search
{
"query": {
"match": {
"attachment.content": "5G"
}
}
}

参考链接

Elasticsearch:如何对PDF文件进行搜索

安装启动 Elasticsearch

1
2
3
docker network create elastic
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.14.0
docker run -d --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.14.0

安装启动 Kibana

1
2
docker pull docker.elastic.co/kibana/kibana:7.14.0
docker run -d --name kib01-test --net elastic -p 5601:5601 -e "ELASTICSEARCH_HOSTS=http://es01-test:9200" docker.elastic.co/kibana/kibana:7.14.0

启动完成之后访问 http://localhost:5601 即可,默认是英文,配置中文可以参考

Kibana 汉化

kibana.yml 中加入

1
i18n.locale: "zh-CN"

然后重启 kibana 即可

配置完成后重启服务,界面如下图:

安装 IK 中文分词

es 中本身自带英文分词,但是实际业务需要存在中文分词的场景,所以手动安装中文分词插件。

下载 IK 分词插件

从 github 上根据不同的 es 版本下载对应的 ik 插件压缩包

https://github.com/medcl/elasticsearch-analysis-ik/releases

在 es 的 plugins 解压

1
2
cd your-es-plugins/plugins/ && mkdir ik
unzip elasticsearch-analysis-ik-7.14.0.zip

然后重新启动 es,这里我是使用 docker 启动 es 的,所以先通过 docker cp 的命令将插件复制到容器中的

1
docker cp es.zip container-id:/usr/share/elasticsearch/plugins/es

重启 es

重启 es 的服务,如果成功加载插件,控制台会有如下的输出:

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

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