浅谈开发PyQt5应用的实践

前言

其实在此之前我早就有接触过PyQt4,但是无奈接触的教程实在不够完整,也没有一条线索能循序渐进地学习,因此才放弃了PyQt,转而使用Electron开发GUI应用(学习Electron的路径实在太好了,有个很详细的例子可以参考)。直至到2019年2月某一天偶然发现packt的一门PyQt5视频教程限免,我就领取了并尝试看,然后就再次正式学习PyQt5。课程地址

开发PyQt应用的核心

在接下来的内容里,我将会谈及几个在开发PyQt应用时重点关注的几个要点,部分内容引用自前面所述课程和我的github repo,一个很简陋的记事本。我所使用的Python版本是Python 3.6.6

核心流程

开发一个复杂的PyQt应用必然不应该完全手写GUI部分的代码,无论是调整还是其他的改动都不太方便(如果真的纯手写我直接用Tkinter不就行了吗)。因此用Qt Designer进行UI设计是最好的,Qt Designer需要手动安装:

1
pip install pyqt5-tools

安装完毕后在你的python安装目录里找到这个路径:~\Python36\Lib\site-packages\pyqt5_tools,并运行designer.exe即可打开。但关于如何使用Qt Designer不是本文的要点。

  1. 在启动Qt Designer后就可以进行UI设计,设计完毕保存为.ui文件,实际上这个是xml结构的文件,可以通过pyuic进行转换,转换为python代码。
  2. 把UI转换为python代码后就可以另开一个文件,请参考我前面提到的记事本demo中的main.py,这个是我编写的程序的入口,经过pyuic自动生成的python文件作为程序的UI渲染器,而我所写的逻辑代码则放在main.py中,实现解耦。如果我们把逻辑混在自动生成的代码里,那么一旦需要在UI的设计上做微调,经过pyuic再次转换的话就会丢失前面所写的内容,因此解耦是很重要的一环。
  3. 完成逻辑编写、测试,就可以使用auto-py-to-exe进行Windows平台可执行文件的打包。

关键知识点

Signal & Slot

这个可谓Qt的核心,如果有接触过jQuery,你就会发现这玩意其实和jQuery的事件处理机制差不多。这里贴一段jQuery的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/javascript" src="/jquery/jquery.js"></script>
<script type="text/javascript">
function onclicked() {
$("p").slideToggle();
}
$(document).ready(function () {
$("button").click(onclicked);
});
</script>
</head>
<body>
<p>这是一个段落。</p>
<button>切换</button>
</body>
</html>

我再贴一段PyQt自动生成的代码:

1
self.actionExit.triggered.connect(MainWindow.close)

可以看到两个代码具有相同的模式:选定对象,检测信号,根据提供的函数名调用函数。

在Qt里,actionExit是sender,被按下的时候就会emit一个triggered signal,使用connect到槽,PyQt这里的槽可以是python函数或者QObject的成员方法。同时你也应该注意到,两个代码都是提供了函数名作为click和connect的参数,若connect的参数是MainWindow.close(),那不用说都会报错。但笔者并不打算在此详细介绍signal & slot,请有需要深入了解的读者自行查阅相关资料。

QThread

如果程序需要执行长时间的任务并且任务与Widgets有交互的话,最好是使用QThread来实现,举个例子,执行一个爬虫并且需要在界面上展示爬取结果的话,就需要使用QThread创建新的线程,然后使用该线程执行相关代码,而如果不用多线程的话,就会阻塞GUI线程直到任务完成,期间用户无法操作GUI,而且Windows可能会提示程序无响应。

所以我们应该继承自QThread然后创建一个类,然后实现run方法,run方法的内容就是我们程序里的耗时任务。

这里引用一个网上的非常straight-forward的例子,情看下面的代码:

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
#!/usr/bin/python3
import sys
import tempfile
import subprocess
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
from mainwindow import Ui_MainWindow
class CloneThread(QThread):
signal = pyqtSignal('PyQt_PyObject')
def __init__(self):
QThread.__init__(self)
self.git_url = ""
# run method gets called when we start the thread
def run(self):
tmpdir = tempfile.mkdtemp()
cmd = "git clone {0} {1}".format(self.git_url, tmpdir)
subprocess.check_output(cmd.split())
# git clone done, now inform the main thread with the output
self.signal.emit(tmpdir)
class ExampleApp(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(ExampleApp, self).__init__(parent)
self.setupUi(self)
self.pushButton.setText("Git clone with Thread")
# Here we are telling to call git_clone method when
# someone clicks on the pushButton.
self.pushButton.clicked.connect(self.git_clone)
self.git_thread = CloneThread() # This is the thread object
# Connect the signal from the thread to the finished method
self.git_thread.signal.connect(self.finished)
def git_clone(self):
self.git_thread.git_url = self.lineEdit.text() # Get the git URL
self.pushButton.setEnabled(False) # Disables the pushButton
self.textEdit.setText("Started git clone operation.") # Updates the UI
self.git_thread.start() # Finally starts the thread
def finished(self, result):
self.textEdit.setText("Cloned at {0}".format(result)) # Show the output to the user
self.pushButton.setEnabled(True) # Enable the pushButton
def main():
app = QtWidgets.QApplication(sys.argv)
form = ExampleApp()
form.show()
app.exec_()
if __name__ == '__main__':
main()

这个代码的耗时任务是git clone,因此作者继承自QThread并实现了run方法,这里引用Qt官方文档对run()的描述:

void QThread::run()

The starting point for the thread. After calling start(), the newly created thread calls this function. The default implementation simply calls exec().

You can reimplement this function to facilitate advanced thread management. Returning from this method will end the execution of the thread.

See also start() and wait().

同时run的结尾有个emit,当任务执行完毕后将会发送一个信号,并把结果发送出去。

然后看ExampleApp的代码,实例化CloneThread后,把self.git_clone()和clicked事件连接起来,当按钮被clicked之后,就会通过新的线程执行git clone(调用CloneThread实例的start()方法),然后任务完毕把CloneThread实例发出的信号connect到self.finished()上,并作为run()所emit的内容作为finished()的参数执行收尾工作。

最后我们总结一下使用QThread的流程:

1.继承自QThread创建一个类,实现run方法,根据情况选择emit。

2.在GUI上监听事件,实例化步骤一的类,调用start()方法。

3.根据情况选择connect。

GUI代码与逻辑代码的解耦

在“核心流程”一节已经简单介绍过解耦的重要性,这里简单谈谈如何实践和需要关注的问题。首要的步骤是通过Qt Designer设计出你想要的UI,并可以借助style sheet来美化UI,然后把设计导出为.ui文件。设计是非常重要的工作,因此为了减少以后因为UI更改导致已经写好的逻辑代码出问题,我非常建议在用Designer开发UI前就做好设计,禁止在写逻辑代码的阶段再对UI做大幅度修改。

这里补充个坑:而在开发UI阶段要注意objectName字段,比如Designer里创建的默认窗体的菜单栏,如果你删掉了之前写的menuFile子项,你再创建同名子项的话,objectName字段里得到是自动加了后缀(删掉xxx,再创建就会出现xxx_2)的名字,为了减少不必要的麻烦,应该重做UI。

而.ui文件的备份也很重要,本人试过误操作得到了个惨重教训:对.ui文件执行了PyCharm里配置的autopep8工具,导致格式变化无法再被读取和修改,因此UI设计制作完毕,导出后应该做备份。只要UI文件在,就可以对UI进行修改调整,但是一旦丢失就很麻烦了。

备份好.ui文件后,就可以使用pyuic工具把它们转换为python文件,这里贴我的PyCharm设置:

设置

实际上命令是这样的:pyuic5 xxx.ui -x -o xxx.py

-x选项会自动为生成的python代码添加执行入口,即

1
if __name__ == "__main__":

-o指定输出的文件名,不过要注意不要把External Tools用在错误的文件上搞到格式混乱无法挽回就好了。

用pyuic5转换为py代码之后就可以开一个新文件(比如main.py)来写逻辑,以我的代码为例(dialog.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'dialog.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
...

每个pyuic5转换后的代码都有注释警告不要在此文件中写别的内容,同时每个窗体都有一个以Ui_开头的类,我们只要在main.py里导入此类即可使用:

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
import sys
from PyQt5 import QtWidgets
# when you run you should delete the . before textb
from textb import Ui_MainWindow
from dialog import Ui_Dialog
class MyApp:
def __init__(self):
self.app = QtWidgets.QApplication(sys.argv)
self.main_window = QtWidgets.QMainWindow()
self.ui = Ui_MainWindow()
self.ui.setupUi(self.main_window)
self.dialog = QtWidgets.QDialog()
self.dialog_ui = Ui_Dialog()
self.dialog_ui.setupUi(self.dialog)
self.ui.textEdit.textChanged.connect(self.auto_save)
self.ui.actionOpen.triggered.connect(self.on_open_click)
self.ui.actionSave.triggered.connect(self.on_save_click)
self.ui.actionNew.triggered.connect(self.on_new_click)
self.main_window.show()
sys.exit(self.app.exec_())

这里就导入了Ui_MainWindow和Ui_Dialog并且进行实例化,但是要注意的是,就算mian.py里有多个Ui实例,像

1
self.app = QtWidgets.QApplication(sys.argv)

的代码也只需要写一行,上面的例子里,Dialog被执行setupUi之后并不会直接显示出来,而我们需要这个展示这个dialog的时候就需要执行show()和exec_():

1
2
3
4
5
6
def dialog_alert(self, content):
self.dialog_ui.label.setText(content)
self.dialog_ui.pushButton.clicked.connect(self.dialog.close)
self.dialog.show()
self.dialog.exec_()

这里我们总结一下解耦的原理:

1.设计转换UI为python代码。

2.在主程序入口里import UI转换后的类,实例化并添加逻辑代码,按需显示。

打包exe

虽然这个并不是重点,在Linux环境下直接安装依赖就可以跑起来,但对于我这种常年用Windows的人来说,打包exe肯定比直接在cmder里python xxx.py便捷。这里我觉得auto-py-to-exe这个工具比较好用。

用pip安装后就可以在console里启动这玩意:

转换工具

有趣的是,这玩意的GUI是用一个叫做Eel的库实现的,一个Python+Electron混合程序开发库,有兴趣的读者可以去了解下。

首先是Script Location,以我的代码为例,这里我们选择main.py,即记事本demo的程序入口。

Onefile选项的One Directory是表示打包为一个目录,目录的mian.exe是程序入口;第二个是打包为单个exe文件。就个人感觉,如果你的程序引用了以目录存放的资源(比如图标),或会在相对路径下的进行文件输出,最好是用One Directory,比如我的程序会引用源码包里icon\的文件,如果打包为单个exe,在exe的路径里若没有icon\目录,就无法显示图标。为了组织文件,你肯定需要一个目录把这些东西和其他文件隔离,所以为何不直接打包one directory?还可以做自安装包。

Console Window的第一个选项是程序运行时会带一个cmd黑框,后者则没有,为了美观,一般都是选择后者。

Icon

Icon展开后可以添加ico文件,这个主要是为了打包出来的exe文件的图标,而窗体的icon与此无关,窗体icon需要在Qt Designer里设置。

Addition Files是添加其他依赖资源,比如前面提到的icon\目录,可以用add folder添加,那么在打包出来的包里就会包含icon\目录,记事本demo的窗体icon也能正常显示。

Advance的内容我还没怎么试过,有兴趣的读者可以自行了解。

配置好之后直接按convert .py to .exe即可自动打包,途中应该会报类似的警告:

1
WARNING: lib not found: api-ms-win-crt-heap-l1-1-0.dll dependency of C:\Program Files (x86)\Python\python.exe

我的平台是Win10 1803,实际上打包出来的exe无论在Win7还是Win10上都可以运行,因此直接无视就好了。

总结

这个是我最终做出来的demo:

记事本

说实话,PyQt有Qt非常详细的文档可供参考,只要掌握开发PyQt应用的核心知识,开发GUI应用的难度一点都不比Electron高,甚至更加轻松。我也有意用PyQt重写一个之前用Electron+Vue实现的GUI应用。我还是非常推荐packt那门视频课程的,讲得非常有条理,例子也很好懂,当然Qt的官方文档(重点!是Qt的文档,而非PyQt的文档)也是非常有用,可以去这里看。希望读者看了这篇文章能有所帮助吧。

Qt牛逼!

####################################################################

再推荐一个YouTube大佬的PyQt5系列视频教程

Electron应用开发浅谈

前言

本人在学校经常会控制不住自己的手,一有空就某宝,然后经常乱花钱,搞到生活费月月光,买了iPad之后,看到iOS上有款应用叫Pennis但是要花25元才能使用,反正又不是不会写,便自己写了个带UI的应用,即MMM,Month Money Manager,并基于Apache License 2.0许可证发布,项目地址。本文重点谈谈我在写这个东西时遇到的坑和相关问题。

基本架构

技术栈:bootstrap、vue.js、electron

辅助工具:layoutit,electron-forge

目录结构参考我的Github repo

下面简单讲讲数据的处理流程:

1.每次应用一打开,就会通过nodejs的fs模块检测用户数据文件是否存在,是则读取文件并设置两个属性:exists和data,前者表示用户数据是否存在,后者是数据的具体内容(一个JS Object),当exists为false的时候,通过函数生成一个带初始值的object,否则使用用户数据文件里的源数据object,同时main.js会在特定频道监听来自渲染端的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ipcMain.on('RTOM', (event, arg) => {
switch (arg.head) {
// renderer query current cache every loading time, send a empty body
case 'query': {
event.sender.send('MTOR', {
exists: userDataExists,
data: userDataCache,
});
break;
}
case 'update': {
updateUserData(arg.body);
updateCache();
event.sender.send('MTORUpdateView', {
exists: userDataExists,
data: arg.body,
});
break;
}
default:
// empty
}
});

在主进程里主要是靠ipcMain来收发信息,在RTOM上监听渲染端的数据,数据是一个JS object,当数据头部为query的时候,则把前面读取的文件是否存在和数据object发过去,ipcMain通过event.sender.send() 函数发送数据,并且可以指定频道,这里我用的是MTOR。同时ipcMain也要负责监听来自渲染端的更新请求,然后将渲染端发来的数据object写入本地文件,然后把那些数据通过MTORUpdateView频道再发回渲染端用于更新用户界面。上面的代码均为异步消息。

2.应用首先加载index.html,index.html先通过queryCache() 函数向main.js发送请求,发送请求后进行监听直到获取数据后才初始化创建vue实例:

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
queryCache();
ipcRenderer.on('MTOR', (event, arg) => {
if (arg.exists) {
vueData.exists = true;
vueData.userCache = arg.data;
initalWork();
if (typeof vueData.userCache === 'object') {
const vm0 = new Vue({
el: "#indexView",
data: vueData,
computed: {
imgUrl: function () {
return vueData.needTips ? './localimg/arrow.png' : " ";
}
},
methods: {
appendDayCost: function () {
applyCostExec();
alert("已追加");
},
fixRemain: function () {
fixRemainExec();
alert("已修正");
}
}
});
}
} else {
vueData.exists = false;
vueData.userCache = arg.data;
alert(emptyData);
const allBtn = document.getElementsByClassName('btnBlock');
for (let i = 0; i < allBtn.length; i++) {
allBtn[i].setAttribute('disabled', 'disabled');
}
const vmfake = new Vue({
el: "#indexView",
data: vueData,
computed: {
imgUrl: function () {
return vueData.needTips ? './localimg/arrow.png' : " ";
}
},
methods: {
appendDayCost: function () {
// do nothing
},
fixRemain: function () {
// do nothing
}
}
});
}
});

要分析上面的代码,先从queryCache开始看:

1
2
3
4
function queryCache() {
// 发送请求查询cache,不能直接用sendSync,ipcMain需要设置event.returnValue
ipcRenderer.send('RTOM', generateCommu('query'));
}

ipcRenderer负责为渲染进程收发信息,ipcRenderer发送信息有两种方式:异步和同步,异步用send,同步用sendSync,但是要注意,sendSync发送的数据,ipcMain只有在设置了event.returnValue的值的时候才会响应听同步消息,不设置的话就是渲染页面无法正常显示。

当ipcRenderer接受到来自ipcMain的query应答,就检查数据中的exists为true还是false,若为true,则执行initialWork() ,initialWork的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initalWork() {
const cacheMeta = vueData.userCache.meta;
if (cacheMeta.recordYear === TODAY.year && cacheMeta.recordMonth === TODAY.month) {
if (cacheMeta.recordDay < TODAY.day) {
// 此处满足同年同月但不同日且今天比记录日后
updateCache(vueData.userCache);
writeCache();
cacheTOVueData();
dontNeedTips();
} else if (cacheMeta.recordDay === TODAY.day) {
cacheTOVueData();
dontNeedTips();
} else {
alert(outOfDate);
}
} else {
alert(outOfDate);
}
}

对main.js返回的文件内容进行检查,若同年同月且此刻是记录日期的第二天,则将部分用户数据重新计算并通过writeCache() 函数发送update消息到ipcMain,将数据写回本地,然后把数据更新到先前创建(在queryCache函数执行前)的vueData中,这样就向用户展示来自数据文件的数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let vueData = {
// 基本属性
exists: false,
userCache: {name: 'jack'},
needTips: true, // 判断要不要提醒用户设置预算
// ----交换属性----
newTotal: 0, // 覆盖原来的total,最终同步到userCache
addCost: 0, // 每次输入的额,要加到todayCost
// 使用计算属性 todayCredit
// ----被动属性----
deposit: 0,
todayCredit: 0,
alCond: 'alert-success',
cacheRemain: 0, // 从userCache获取
remain_days: 0, // 从userCache获取
al: 'alert',
alDis: 'alert-dismissable',
};

当用户执行追加本日开支和余额修正时,程序分别通过applyCostExec() 和 fixRemain() 进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function applyCostExec() {
vueData.userCache.user.dayCost += parseFloat(vueData.addCost);
writeCache();
vueData.addCost = 0;
ipcRenderer.on('MTORUpdateView', (event, arg) => {
vueData.exists = arg.exists;
vueData.userCache = arg.data;
initalWork();
});
}
function fixRemainExec() {
vueData.userCache.user.total = vueData.newTotal;
vueData.userCache.user.remain = vueData.newTotal - vueData.userCache.user.deposit;
vueData.userCache.user.dayAverage = vueData.userCache.user.remain / vueData.userCache.user.daysRemain;
writeCache();
vueData.newTotal = 0;
ipcRenderer.on('MTORUpdateView', (event, arg) => {
vueData.exists = arg.exists;
vueData.userCache = arg.data;
initalWork();
});
}

以applyCostExec为例,首先把vueData中v-model的绑定的变量的值同步到userCache中,然后调用writeCache() 写入本地文件,重设v-model对应变量的值为0,然后监听来自ipcMain的数据并再次执行initialWork() ,重复index.html刚加载的时候的界面初始化流程,使用vue来更新用户界面主要是靠双向绑定减少重复工作,若使用jQuery实现同样的功能,代码量将会显著增加,而fixRemain的流程和applyCostExec也基本一致。

回到前面的分析,若main.js在index.html初始化的时候返回给index.html的数据中exists为false,则通过alert提醒用户未设置预算计划,同时通过vue的计算属性在用户界面展示一个箭头提醒用户设置计划。同时通过setAttribute禁用主界面的特定按钮,直到用户设置好计划为止。

至于config.html的逻辑则简单得多,只需要处理用户保存变更和查看about和help的动作就行了。js代码的结构与index.html差不多,一个vue数据对象负责双向绑定,当用户保存变更的时候就读取数据对象的值构造一个object发送给main.js保存在本地文件,然后用户点击“返回”的时候就让index.html执行前面介绍的流程,更新用户界面等。

遇上的坑和关注点

  1. 异步通讯,根据前面描述的逻辑,vue负责更新用户界面,但是前提是本地文件储存的数据要先更新到vueData中。如果创建vue实例的代码在和ipcRenderer监听的代码处于同一命名空间的话,就会导致渲染网页的时候vue获取不了来自异步通讯的数据而是直接读取页面初始化时生成的vueData,从而报错导致页面刷不出来,而且这个阶段只有开发环境版的vue.js能提醒问题出在什么地方。建议在开发阶段使用vue的开发环境版本,并且开启devtools用于调试。vue实例必须在异步数据到达之后才创建,不然会报错。或者使用同步通讯,但是要注意ipcMain的returnValue的设置。

  2. 生成的程序,窗口标题由不同html的title决定,而对话框的标题,由package.json的productName决定。

  3. 建议使用electron-forge初始化一个新的electron项目,免去手动配置boilerplate。

  4. electron-forge的图标设定不会在编译前起效,即你执行electron-forge start的时候,你设置的icon是不会显示的,因为icon的设置是在程序打包的时候才设置的,所以要看到图标的效果只能make完再查看。

  5. Apache License的使用,除了在根目录放LICENSE(通常能通过github直接创建),还要在源码的头部放上一份包含自己信息的声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Copyright [yyyy] [name of copyright owner]
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

    方括号内的内容由自己填写,不保留方括号。关于Apache License的使用可以参考这个:教程

    如果包含第三方库,我参考了Apache Spark项目的做法,在LICENSE后追加说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ------------------------------------------------------------------------------------
    This product bundles various third-party components under other open source licenses.
    This section summarizes those components and their licenses. See licenses/
    for text of these licenses.
    MIT License
    -----------
    local-lib/bootstrap.min.css
    local-lib/bootstrap.min.js
    local-lib/jquery.slim.min.js
    local-lib/popper.min.js
    local-lib/vue.js

    然后在根目录创建一个licenses目录,里面放上对应项目的license原文,并且根目录的NOTICE也要加上相关说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Month Money Manager
    Copyright 2018 Hochikong
    Bootstrap
    Copyright (c) 2011-2018 Twitter, Inc.
    Copyright (c) 2011-2018 The Bootstrap Authors
    jQuery
    Copyright JS Foundation and other contributors, https://js.foundation/
    popper.js
    Copyright © 2016 Federico Zivolo and contributors
    Vue.js
    Copyright (c) 2013-present, Yuxi (Evan) You

    其他人的实践例子:链接

  6. 建议直接用npm处理问题,比如npm install安装依赖,npm run make编译,虽然实际上调用的是electron-forge make。npm install途中可能会报错,删掉node_modules再重试几遍会比较好。

  7. bootstrap的部分界面特效(比如弹窗)无法在electron app中使用。

总结

Electron+Vue+Bootstrap的GUI应用开发难度应该是比Qt等库更加简单,而且具备跨平台的能力,如果熟悉了Electron中的套路,开发第二第三个electron app应该就更容易了。最重要还是要熟悉node.js那些操作,才能实现和本地软件的交互。

在此特别感谢鳗驼螺 ,他的这篇文章算是个非常经典的electron app开发教程,无论是上下文菜单还是窗口菜单、对话框等,都有详细的介绍,感谢他的贡献,我才能顺利开发这个electron 应用。

如何让iPad变成真正的生产力工具

怎么选了iPad 2018?

最近手上多了笔钱,本来之前是考虑买一个1000多的国产Windows平板用来看电子书和作为廉价Surface用,但是看了下淘宝,1500以内的基本都是z8350的处理器,而Android平板主要是App不太行,所以选择加钱买iPad 2018,主要是看中它支持apple pencil。买之前我在知乎上看了下,不少人说2018款的非全贴合屏垃圾,刷新率不及pro,建议加钱入二手pro 9.7或者还在卖的pro 10.5。实际上我入手后(国行,深空灰128G WiFi版),的确是能感受到屏幕上的问题,但是实际使用时对于我影响不大,买的深空灰黑色面板还看不出大黑边。就只有知乎个个年薪百万的随随便便就说加钱买Pro,真的当那2000不是钱啊!iPad 2018 32G版对于学生党来说真的是超高性价比的苹果设备了,当然土豪是不需要性价比的,不过如果去港澳买的话、通过汇率可以将128G版的价格压到2600RMB左右。

我不是苹果全家桶用户,手上使用的设备是一台神舟游戏本+普通21寸IPS面板的显示器、小米6和这台iPad。文章接下来的内容主要是介绍我日常如何在学习上发挥iPad的作用。

基本盘

首先是斧乃木妹妹的亮相 : )

Yay!Peace,Peace

1.基本办公软件:

http://oy30yrqej.bkt.clouddn.com/apps1.png

我主要是使用MS全家桶,Word、Excel、PowerPoint三个,还有Outlook、OneNote和OneDrive,Office Lens主要是方便扫描后导出文档(虽然iPad的摄像头真的不咋样,但是扫描文件、上课拍个PPT还是没问题的),因为我不是苹果全家桶,因此以跨平台的软件优先,To Do是微软出品的一款待办事项工具,支持Android、iOS和Windows,这样我的待办事项能同时在三个平台编辑和查看。而OneDrive主要作为跨平台交换文件的网盘,在国内因为某些原因网页版被DNS污染无法使用,但是客户端可以正常用,而且速度不慢,我一般会在电脑上下载好需要看的PDF然后直接复制到Win 10的OneDrive目录里,然后就可以在iPad上访问,拷贝到PDF阅读器或者保存在Documents(一个免费文件App)里,OneDrive默认只有5G,如果是365用户可以获得1T容量,还可以另外充值每个月1.49刀左右扩容至50G,这里我的选择是去淘宝找扩容服务,通过邀请好友的方式让容量扩张到最高15G,虽然不是太大,但是用于文件交换是足够了。虽然iOS 11提供了“文件”应用,但是Documents更方便,推荐使用。

2.Coding之类:

有时可能需要远程控制自己的PC编辑代码,因此TeamViewer对于我来说是很有用的一个软件,用VNC之类的工具就能搞定远程代码编辑和电脑的维护,所以SSH对于我来说也很重要,有时需要远程连接Linux服务器,我使用的是Termius,免费使用。GoCoEdit是一个类似sublime text的软件,这个软件适合临时在本地写代码,但是不支持自动补全(估计很难提供)。Textcode Viewer是用来获取github上的repo进行code review。

3.其他:

Myscript Calculator可以手写计算,支持解方程之类的,但是写字太难看的人还是别用了…识别率还是比较感人,还不如直接下个正常的计算器app或者直接用CASIO算。

ShadowRocket,不用多说,懂行的人都知道,不过需要搞个美区ID购买, 搞一个美区ID最省事的方法就是某宝。

Chrome、Gmail、Google Drive和Google,因为我手机电脑都是用Chrome,跨设备同步非常方便,所以iPad理所当然是用Chrome,Gmail方便收右键,Google全家桶还不错。

Grammarly,需要英文输入时可以切换这个输入法,支持蓝牙键盘,自动校正英文的输入错误。

Workflow和IFTTT,自动化操作,目前我编辑了一个flow可以直接在浏览器复制url然后一键生成PDF导入笔记软件阅读和写笔记。

AnyRate,计算汇率,但是MIUI自带的计算器也可以计算汇率就是了。

白描,个人非常推荐的一个OCR软件,虽然有人推荐ScannerPro,但是OCR的速度真的非常感人,白描只要6元人民币,识别效果和速度都很好,自带校正窗口,方便修改错误的地方,强烈推荐。

iPad似乎没有预装任何扫二维码的工具,我使用的是卡巴斯基的QR Scanner,毕竟二维码攻击还是存在的,我PC也是用的卡巴斯基KAV,信得过卡巴的软件。

核心

我买iPad的首要目的是阅读和笔记。阅读的主要内容是自己找的PDF和电子书阅读客户端里的内容,翻译工具也是不能缺少的。而笔记主要是将笔记数字化,方便在任何设备上查看和阅读,之前上课是用手写笔记然后下课后回宿舍录入电脑,但是问题在于重复劳动,而且在买数位板之前无法在电脑上画东西(思维导图、流程、构思等),在买iPad之前我还入了一块Wacom 472,方便在电脑上记录笔迹数据。

阅读PDF可以用免费的Adobe Acrobat或者其他,不过我看bundle有折扣就入了PDF Expert与PDF Converter,前者是专门阅读PDF的软件,还有非常强大的PDF批注功能,不过我的垃圾电容笔批注实在不好用,写字写得像翔一样,等迟些入apple pencil再看看效果如何。词典推荐韦氏词典,翻译工具推荐欧路词典和极光词典,欧路词典可以自动跨取词翻译,而极光词典是我尝试过的产品里唯一支持分屏的翻译软件,同时也可以右键复制自动翻译。我买iPad的另一个主要原因是拿来读书,不想买太多纸质书没地方放,正版书可以通过Kindle或者多看获取,这里我使用的是多看阅读,可以购买各种电子书,个人尤其喜欢中信出版社的经济类书。

笔记主要是用GoodNotes与Notes Plus,Notes Plus的防误触比较适合速记,而且有笔迹优化,我的辣鸡电容笔也能写出比较好看的字,但是在PDF Expert和OneNote这种没有手写优化的软件里我的字迹就真的不堪入目。Good Notes比较适合阅读并笔记,可以导入PDF,对手写的优化不错,可以写出小字体,上课的时候也可以即时拍照加入笔记,即时做笔记然后不用等到下课再花时间整理。而Notability对我手上的这种电容笔支持不佳,但不清楚apple pencil的效果如何。MarginNote适合做思维导图,也可以导入PDF、EPUB之类的文件,适合深度思考和整理时用。

OneNote主要是跨平台阅读,我会把iPad上整理的笔记重新导出,添加到OneNote里,然后在OneNote里添加索引方便全文检索,OneNote相当于是核心知识库,被用于查阅,而那些笔记软件是工作台,整理资料被用于录入。一般录入就要回到PC,OneNote 2016可以使用NoteHighlight插入代码并高亮,而另一个Chrome扩展OneNote Web Clipper可以对网页进行完整或者局部截图然后自动导入OneNote,方便截取网页保存。OneNote的优势在于跨平台和自带OCR,这个OCR主要作用在搜索上,对于笔迹和图片上的文字都可以识别出来,这就是我用它做核心知识库的原因。电脑上配合数位板也可以在OneNote上画画写写,但是依然没有手写优化,写的字真的不如我用OpenCanvas这个画画工具写得好。所以最好还是拿来整理查阅比较好。

外设

外设我就买了一个罗技k380和一支30来块的电容笔还送了防误触手套,罗技k380对iPad的兼容性很好,但是!有个问题,默认iOS的智能标点是开的,会导致蓝牙键盘输入全角的引号,而远程写代码的时候总不可能手动复制半角引号吧?此时需要在“设置->通用”里面关闭“智能标点”,然后才能正常地用蓝牙键盘输入半角引号。

电容笔的话,我估计…是估计,apple pencil是最好的选择,或者罗技那个crayon,淘宝上其他品牌的电容笔主要分国产和境外品牌,国产的话主要还是那种笔头是圆盘或球的(30~10元)的被动式和尖头(250~150元)的主动式笔,如果预算不够,买圆盘的那种就最好了,不过要配合前面介绍的一些软件使用才能写得好,而画画的话圆盘笔还是算了,精度不够,总觉得圆盘碍事,还要戴防误触手套画(数位板表示“有电磁感应真的可以为所欲为的”),而贵一点的国产主动式电容笔主要在250以内能提供尖笔头,不过某些淘宝爆款的笔头是硬的,写的时候会很吵,没买过不清楚体验如何。境外品牌有罗技、Wacom、Adonit、Dagi(台湾)等。罗技的笔是crayon,被称为廉价版AP,不过外观真的不怎样,这名字也让人觉得是那种儿童画笔,但400多能搞定,但是某宝上似乎只有一家在卖…Wacom的笔价位主要在700~90以内,不太推荐某支89元(截至写本文时)的笔,实际上还是被动式的,但是是球型笔头,而且贴膜后使用会有debuff。其他的主动式笔都是尖头的,但是体验不知怎样。Adonit在知乎上比较多人说买了,这家提供圆盘、球和尖头三种笔,国内有卖,但是部分笔的价格我觉得可以干脆入AP,但是若不是2018款iPad或者Pro的话可以考虑这家。Dagi达际主要卖的是圆盘笔,不过我不太喜欢圆盘的就是了。说了那么多,我能提供的建议就是:2018款或者Pro用户,需要最好的笔记和绘画体验就apple pencil;其他的iPad用户,可以根据情况买Wacom或者Adonit的主动式笔;缺钱的、简单体验的就国产圆盘或者Wacom低于150的款式;不确定要不要高端笔的,可以入国产主动式;不缺钱的土豪,可以把上面说的厂家的笔全都买一遍,整理个体验报告造福人类;野路子的,薯片袋、电池负极、锡纸等材料自己做一根。

我怎样用iPad作为学习/生产工具

我最近在学Vue.js,我先在iPad的Chrome上打开网页复制Vue文档的url,然后用Workflow一键生成PDF导入GoodNotes,然后课间阅读文档、理解、做笔记。然后在空闲、在宿舍的时候转到电脑上打开IDE写代码实践所学的内容,最后整理代码和写代码途中遇到的问题录入OneNote,并把中途的全部参考资料通过OneNote Web Clipper导入OneNote作为reference,防止以后再查阅的时候网站链接不可用然后丢失这些参考内容。

上课的时候用相机拍老师的PPT,在笔记软件上直接记录老师的讲课内容,相当于纸质笔记本,这个时候主要是要求速记,所以字就写得比较随便,课后在电子版参考资料上整理笔记,整理思维导图之类。

少买/不买实体书了,通过多看阅读或者自己找PDF文件看,同时把笔记直接写在PDF上,节省买实体书的部分成本或者直接白嫖一本书,而且还不需要额外的空间放实体书。

总结

可能是我心态老了,或者就是纯粹的没兴趣,我的iPad买回来就只装了3个游戏,一个GRID Autosport,但是太硬核了,玩不下去,Real Racing 3好歹还能玩;第二个游戏是微软的Solitaire,就是Win10上面的纸牌游戏集合,休闲玩玩,还有个电音弹珠…仅此而已,崩崩崩、FGO那些我都没什么兴趣,玩那些掌上游戏还不如去玩3DS、PSV…对于我来说,iPad真的是无纸化、低成本阅读和外出代替我那2.5 Kg的游戏本处理轻量级任务最好的选择,变成游戏机、视频机?不存在的,游戏机我还少么…iPad于我是真正的生产力工具,也希望本文对读完全文的你有所参考。

评论一篇关于Kotlin与Java的文章

前言

经过一段时间,我已经把Kotlin的学习基本搞定,虽然最近在学Vue.js,但是还是有在关注Kotlin的事。今天偶然看到一篇文章,讲的是Allegro(一家波兰的购物平台,可能是类似淘宝的网站)从Java转到Kotlin又转回Java的事。背景是Allegro在2017年夏天计划写个微服务,为了尝试新的技术然后转到Kotlin上,使用Groovy+Spock做测试,而在2018年他们又把微服务用Java重写,跑在Java10上。

为什么选择Java而不是Kotlin,下面是他们提出的观点

1.Name Shadowing

这个东西我也不太清楚该怎么翻译,但是理解起来就是Kotlin没有做到这一点。在Python里,我们知道函数体内部的同名变量会自动屏蔽外部作用域的同名变量,参考下面的代码:

1
2
3
4
5
6
7
8
9
10
>>> num = 2
>>> def inc(x):
... num = 1
... print(num+x)
...
>>> inc(1)
2
>>> inc(2)
3
>>>

可以看到即使外部定义了num=2,但是在函数体内num=1。而他们给出了一个有点勉强的例子:

1
2
3
4
5
6
7
fun inc(num : Int) {
val num = 2
if (num > 0) {
val num = 3
}
println ("num: " + num)
}

上面的代码执行inc(1),得到的结果是2,意味着只是纯粹的将方法体内第一个num的结果打印了出来,而if的代码块里却又定义了一个同名的变量。他们的意见是:这种代码在Java里是无法通过编译的,因为不能在同一方法作用域里同时定义两个同名变量,但是Kotlin却让这种代码通过了。实际上上面的代码,在IDEA里会提示if语句里的num没有被使用,如果关注方法体内的无用变量的话就会发现这种问题,然而,最重要的一点是,我不清楚谁会写出这样的代码:在同一函数体内定义两个同名变量却指望的是第一个变量的值发生变化。这种错误我觉得纯粹是程序员自己的问题,而不算语言的问题,语言不背这个锅,不写单元测试别赖IDE和语言把你带坑里。

2.类型推导

Java10可以使用类似JavaScript的方式定义变量,不需要显式声明变量类型:

1
var a = "10";

我觉得这不是什么太大的问题,而且为什么你们公司这么快就敢上Java10?没有任何迁移成本?

3.空安全

Kotlin经常强调空安全的问题。但是一旦与Java交互的话,Java可能会毁掉Kotlin的空安全机制。Kotlin里变量无非是两种类型,T与T?,后者表示变量可空,但是有一种特殊类型,来自Java代码的T!,表示类型未确定,可能是T也可能是T?,这样就会导致难以预测的问题。参考下面的代码:

1
2
3
4
5
public class Utils {
static String format(String text) {
return text.isEmpty() ? null : text;
}
}

上面的Java函数可能返回null也可能返回字符串,如果在Kotlin里调用这个函数的话,返回的是T!,如果不经处理直接val f:String = Utils.format(text),就没有考虑到该方法可能会返回null,从而抛出了NPE。或者使用elvis运算符,当format返回null的时候设置返回值为一个空字符串;再或者使用”?.”实现安全调用,或者不指定f的变量类型,但是也会抛出NPE,当然可以使用Kotlin不建议的”!!”,声明f不为T?,但是还是会抛NPE。我觉得这个的确是Kotlin的问题,搞了个T!的平台类型,导致复杂化,但是我个人认为最佳实践是用elvis。希望在后面的版本里Kotlin能着手解决这个问题。

4.类的字面值(Class literals)

在Java里,可以直接通过CLASS.class获取字面值:

1
Gson gson = new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateAdapter()).create();

但是Kotlin里分了两种class,KClass与Class:

1
2
val kotlinClass : KClass<LocalDate> = LocalDate::class
val javaClass : Class<LocalDate> = LocalDate::class.java

KClass是Kotlin特有的反射API,用kotlin-reflect的时候基本都是基于KClass的。虽然我看到有些资料说建议用Java反射之类的。不过Kotlin的这种字面值的确用起来较为繁琐。

5.相反位置的类型声明

Java里类型声明是放在变量前面的,但是Kotlin是放在变量后面中间还隔了个”:”,他们的意见是:1.为什么要把类型和变量隔开。 2.代码太长的时候(函数签名),函数的返回类型看起来不方便,需要拖动…如果函数签名里的参数一行一行地放,也不方便看返回类型。3.不方便命名变量名…他们想的是让IDE提示去快速书写变量名,但是Kotlin里只能自己手写。

一个看返回类型不太直观的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
fun kafkaTemplate(
@Value("\${interactions.kafka.bootstrap-servers-dc1}") bootstrapServersDc1: String,
@Value("\${interactions.kafka.bootstrap-servers-dc2}") bootstrapServersDc2: String,
cloudMetadata: CloudMetadata,
@Value("\${interactions.kafka.batch-size}") batchSize: Int,
@Value("\${interactions.kafka.linger-ms}") lingerMs: Int,
metricRegistry : MetricRegistry
): KafkaTemplate<String, ByteArray> {
val bootstrapServer = if (cloudMetadata.datacenter == "dc1") {
bootstrapServersDc1
}
...
}

对于这点,我受影响不大,保留意见。我也基本没有因为Kotlin和Java的变量类型位置不同导致不便。

6.伴生对象

Kotlin里使用companion object实现”静态对象“,companion object的效果类似Java的static,但是并不是真正的静态对象,要使用真正的静态对象需要通过注解@JvmStatic来声明。他们遇到的问题似乎是Spring需要用”真正的静态对象“启动App:

1
2
3
4
5
6
7
class AppRunner {
companion object {
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(AppRunner::class.java, *args)
}
}
}

不过我个人认为难以判断好坏,因为Kotlin不需要另外声明一个Utils类再写静态方法,我只需要像Python一样直接在一个专门存放公共静态函数的kt文件里写一个全局的函数即可,静态与否意义不大。当然这个是看情况的,需要用Java的static还是要记得加注解。

7.生成collection

他们觉得Kotlin的集合声明不太直观,的确,js、py里声明一个array或者list只需要一个[ ]就能搞定,生成一个object或者dict也是只要一个{ },但是Kotlin里需要用些函数比如listOf()、mapOf()来生成list和map,而且map还是用”to”这种奇葩格式来区分键值对,这一点我倒是挺赞同他们的,为什么非得用些繁琐的手段生成数据?希望后面的迭代能改变这一点。

8.Optional值的缺失

Java 8 开始,引入了Optional,意味着Stream可能无返回值也可能有返回,Java 8的lambda通过orElse()和ifPresent()处理Optional值。Kotlin则缺乏这方面的支持。我保留意见,从Python来的习惯,一条路走到黑:“当存在多种可能,不要尝试去猜测,而是尽量找一种,最好是唯一一种明显的解决方案”,用elvis就是一把梭(此处应有王守海表情包)!

9.数据类

他们认为数据类Data Class局限多,因为是final,无法继承,所以在某些场合不适用。但他们也承认没有更好的办法,只能手写equals之类的。

10.类默认是final的

Kotlin里所有类默认都是final,想让它能继承需要加open修饰符,或者用一个插件kotlin-allopen。他们认为这是具有争议的,但是从我个人来看这没什么问题,只是写个open而已,而且想玩花样,代理模式、装饰器模式都是能用的…

11.陡峭的学习曲线

这点…你们开心就好,我学习Kotlin也没花太长时间,要比学习曲线你干嘛不跟Scala比?当然相对Groovy的确需要些时间,但是我学它真的没花多久时间…而且我是学了Kotlin才正式、认真地学Java的。

结语

当然,我写这篇东西并不是为了反驳他们,对于他们的工作环境、团队而言,Java 10可能是更好的选择,他们也是再三强调那篇东西并不是为了吹捧Java看衰Kotlin。对于我个人来说,自己做项目更多的还是会考虑Kotlin,毕竟写纯Java我是有点嫌弃的,虽然IDEA聪明得很,但是还是经常要按快捷键生成模板代码,而且我是从Python转过来的,Kotlin在某种程度来说比Java更让人有熟悉的感觉。btw,只有low逼会为了某个OS、技术而和别人争个你死我活,干活的人都是什么适合就用什么,不能赚钱、创造影响力有何意义?

Kotlin的Constructor总结

前言

最近开始学Kotlin,买了本《Kotlin极简教程》,以后绝对不要在京东阅读上买计算机的电子书,坑爹,我又去买了本实体版。最近看到面向对象这章,本文探讨了下构造函数的一些问题

问题

构造函数简单而言就是在创建类的实例时调用的函数。Kotlin中如果没有可见修饰符或者注解(annotation),就可以忽略不写constructor,反之则要写。修饰符和注解都要写在constructor前面。

Kotlin支持类有一个主构造函数和一到多个次构造函数。使用次构造函数可以根据参数的不同创建不同的实例。比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
class Person constructor(val sex: String, val age: Int, val name: String){
var weight:Double = 0.0
init {
println("The sex is $sex")
println("The age is $age")
}
constructor(sex: String, age: Int, name: String, weight: Double):this(sex,age,name){
this.weight = weight
}
}

我们可以这样调用:

1
2
3
4
5
6
fun main(args: Array<String>){
val p1 = Person("male",23,"jack") //调用的主构造函数
println("All property of p1 is: sex->${p1.sex},age->${p1.age},name->${p1.name}")
val p2 = Person("female",16,"Michelle",45.7) //调用次构造函数
println("The weight of P2 is ${p2.weight}")
}

返回的结果是:

1
2
3
4
5
6
The sex is male
The age is 23
All property of p1 is: sex->male,age->23,name->jack
The sex is female
The age is 16
The weight of P2 is 45.7

次构造函数的函数签名不能有val或者var,而且必须通过this委托给主构造函数。

注意!

1.只能在主构造函数里用init{}

2.这样的代码是错的:

1
2
3
4
5
6
7
8
9
10
class Person constructor(val sex: String, val age: Int, val name: String){
init {
println("The sex is $sex")
println("The age is $age")
}
constructor(sex: String, age: Int, name: String, weight: Double):this(sex,age,name){
val weight: Double = weight
}
}

如果想正确地访问weight变量,只能在主构造函数里创建变量并赋值(不然IDE会提示要不非抽象属性或者要初始化),然后通过this在次构造函数里赋合适的值。无法在次构造函数里创建val或者var

修bug记录

前言

最近修复了stockclib的恶性bug和为TradeServer增加了点功能。把开发途中的重点记录下。

logging配置多个日志文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def generate_logger(name, log_file, level=logging.INFO):
"""
通用的日志记录模块,用于不同功能的日志记录工作
:param name: logger名
:param log_file: 日志文件
:param level: 日志级别,默认INFO
:return: logger对象
"""
handler = logging.FileHandler(log_file)
handler.setFormatter(formatter)
logger = logging.getLogger(name)
logger.setLevel(level)
logger.addHandler(handler)
return logger

通过这个函数可以生产logger对象,并且能指定文件来记录日志内容。交易服务器就是使用日志来记录服务状态变更和撮合成功的信息

参考:

stackoverflow

logging入门

股票平均价格计算

之前一直没搞清楚怎么算摊薄的成本价,然后搞了下搞懂了,主要是三个公式:

假设现在有同一股票的3笔交易,规则手续费和税费都按0.1%即0.001收,买入只收手续费,不满5元收足5元,卖出收手续费(规则与买入一样)和税费:

1
2
3
(1) 5.5¥/2000股/买入;总值=11000;手续费=11000*0.1%=11;平均每股成本(后面均称为‘均价’)用公式1算得=5.50
(2) 5.6¥/300股/买入;总值=1680;手续费=5;均价用公式2算得=5.52
(3) 5.4¥/400股/卖出;总值=2160;手续费+税费=5+2.16=7.16;均价用公式3算得=5.54

参考:

知道

雪球模拟盈亏

多进程服务器折腾、实现

前言

最近为了写个回测平台真的累成狗,项目地址TradeServer, 目前实现了下单买入卖出、撤单、查看未成交订单、查看用户信息和余额、查看完整操作记录,查看交易记录和收益统计等功能。主要结构为Flask实现的RESTful API和一个包含三个进程的服务器。Flask那个不难,就是设计好json请求结构然后根据情况返回合适的结果即可,但是另外一个含三个进程的服务器就有点麻烦了。

坑1

众所周知,Python的多线程是鸡肋,我之前的爬虫服务器用了rpyc做服务器主体,爬虫函数用了concurrent.futures的ThreadPoolExecutor,本来我是想用多进程实现的,但是发现rpyc做服务主体下你就再也没法用multiprocessing的Pool来执行任务了。现在我的交易服务器需要实现撮合订单或者等待机会撮合,需要多个进程协同操作。

首先是一个进程负责检查数据库某个键的值,如果设置为run,则开始撮合订单,如果设置为stop或其他不是run的值,就会sleep一定的时间然后再检查。那么假设我这个进程能查到这个数据库的键的值为run那么怎样才能让其他进程知道这个消息呢?一般来说最好是使用actor模型,基于消息传递,比如multiprocessing的Queue模块,但是这个交易服务器并不需要那么麻烦,只要一个共享内存就行了。在multiprocessing里可以用Array或者Value来在程序中设置一个进程间都能读写的变量,关于Value或者Array的文档可以看这里,17.2.2.6部分,实现上好像是基于ctypes的共享内存,还有个更高级的Manager,能提供更多数据类型。

这里给一段代码简单地展示下Array的用法pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from multiprocessing import Process
from multiprocessing.sharedctypes import Array
import time
def f():
while True:
time.sleep(2)
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
if __name__ == '__main__':
num = Array('c', b'hello world')
p = Process(target=f, args=())
p.start()
p.join()
print(bytes.decode(num.value))

上面的例子里,首先要从multiprocessing.sharedctypes导入相应的模块,然后创建一个共享变量num,第一个参数是数值类型。上面的例子是char,可以存一定长度的字符类型,即bytes,在python3里可以通过str的encode来把str转换为bytes,也可以用bytes的decode转换为str类型,然后我们就可以把经过encode的字符串放在Array的第二个参数里,如果其他进程想查看这个共享变量的值,直接用num.value就可以获取bytes的值,再转为字符串就不难了。

下面是Array的输入类型对照表:

1
2
3
4
5
6
'c': ctypes.c_char, 'u': ctypes.c_wchar,
'b': ctypes.c_byte, 'B': ctypes.c_ubyte,
'h': ctypes.c_short, 'H': ctypes.c_ushort,
'i': ctypes.c_int, 'I': ctypes.c_uint,
'l': ctypes.c_long, 'L': ctypes.c_ulong,
'f': ctypes.c_float, 'd': ctypes.c_double

总之记住普通的全局变量是无法在进程间共享的。

坑2

解决了共享内存的问题之后,然后是循环任务的问题。一个撮合服务器,应该每隔一段时间获取用户提交的交易请求,然后根据股票现价等信息进行判定是否能交易。我需要引入另外两个进程,一个用来检查订单然后看能不能交易,另一个用来进行损益统计。我相信很多人查multiprocessing的代码范例都是这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print 'Run child process %s (%s)...' % (name, os.getpid())
if __name__=='__main__':
print 'Parent process %s.' % os.getpid()
p = Process(target=run_proc, args=('test',))
print 'Process will start.'
p.start()
p.join()
print 'Process end.'

一看似乎没问题,然而这个程序就是执行一次的,既然要反复检查订单是否能成交就要while循环啊,那么这个while要放在哪?

放在main后面?不行的,如果使用Process来实现多进程,下面的代码pattern是不太好的:

1
2
3
4
5
if __name__ == "__main__":
while True:
p = Process(xxx)
p.start()
p.join()

这种pattern是不行,会提示你创建了多个进程引起冲突:

1
2
3
4
5
6
7
8
if __name__ == "__main__":
p = Process(xxx)
while True:
p.start()
p.join()
WARNING:AssertionError: cannot start a process twice

这个代码意味着你每次循环都创建Process对象然后执行,那么你干嘛不用rpyc写个rpc服务器然后写个普通的while循环的代码来发送请求?

我目前找到的比较ok的多进程服务器代码pattern还是前面的Array例子那个,把while循环放在函数里,main后面就创建Process对象,start()之后这个进程就会一直执行函数里的while循环来打印此刻的时间

下面的pattern在我的撮合服务器代码里也用到:

1
2
3
4
5
6
7
def func():
while True:
exec xxx
if __name__ == "__main__":
p = Process(target=func,args=(xxx,xxx))
p.start()
p.join

不过要注意的是,函数里的while循环最好加上time.sleep(),不然你的的CPU使用率很快就会急剧上升,至少不加sleep我的笔记本CPU风扇没多久就疯转。

有兴趣的可以深入阅读下我的TradeServer的omserver.py代码。

结语

并发、并行是个不简单的话题,不过找到合适的代码pattern就很容易写了。还有,还在用py2的,尽可能还是迁移到3.x吧,至少我现在是用python 3.6了。

参考

官方参考文档

多进程数据共享(好文)

共享内存实现原理

如何共享变量(很老的文章)

别人的学习笔记

多进程编程

Win10 Hyper-V虚拟机折腾记录

Intro

本来我还是在用win7,但是越来越觉得不接收微软的安全更新简直就是要命,然后也想顺便清理下工作环境重新安装一些东西clean下,于是昨天通宵搞启动盘重装系统。装了win10 pro用Microsoft Toolkit激活了。然后安装了Docker for Windows,安装这个会自动帮我配置增加hyper-v的功能。配置完毕之后,如果你启动docker就会启用一个vswitch。这里的vswitch类似VMware的adapter。

配置一个能与宿主机互ping的虚拟机全流程

  1. 创建一个新vswitch

  2. 修改现有的网络适配器,为刚才创建的vswitch提供共享

  3. 配置防火墙的入站规则

下面就具体讲讲是怎么做的

1.创建vswitch

打开Hyper-V管理器,选择“虚拟交换机管理器”:

然后创建一个新的“内部网络”虚拟交换机,随便命名即可,我先前创的是VS NAT:

2.修改网络接口

创建完这个之后,就去控制面目板的“网络连接”选择你现在用的Internet适配器:

右键选择“属性”然后切换到“共享”的页面,允许你刚才创建的vswitch访问:

这样一来就完成了基本配置,这时候点开你创建的vswitch找IPv4属性,可以看到分配的IP地址和掩码:

3.修改防火墙规则

在做了上面的配置之后,使用你刚才创建的NAT vswitch的虚拟机都已经能上网,宿主机也能ping通虚拟机,但是虚拟机无法ping宿主,这就需要你启用下面的防火墙入站规则:

然后也可以自定配置一个规则,允许来自虚拟机对宿主的任何访问:

配置就到此为止,虚拟机和宿主能互ping且虚拟机也能上网了

参考

Ping不通的解决方法

配置NAT

总结

Hyper-V体验还不错,不过如果不是因为要跑docker on windows我也不会装这个东西,还是使用VMware。

11/23更新

放弃了hyper-v和docker on windows,还是用回vmware吧

StockClib开发小结

StockClib更新之际

上个周末花了点时间更新了我的StockClib项目,增加了对富途牛牛网页模拟账户交易操作的功能,同时StockClib更新到0.2。本文将会总结下开发途中的一些问题。

曲线救国

一开始我考虑用的量化平台是BotVS,但是BotVS有两点很难忍:

  1. 文档混乱,看得一脸懵逼
  2. 无法支持第三方库,意味着策略无法与外界交换数据

后面我换了聚宽,聚宽的教程和文档和ricequant的都差不多也很清晰,聚宽还支持用requests来交换数据,但是问题又出现了,我要加密传输交易信号才行,本来问了朋友考虑用AES加密但是后面实际玩了下,聚宽的策略运行必须先回测,但我那套策略思路要搞回测很困难,而且回测的意义不大,因此决定放弃一切量化平台(虽然前期学习必须在那些平台上练手),自己做交易接口,不做回测了。

万得终端有套支持excel、matlab和python等语言的本地交易API,但是文档像吃了屎一样,富途牛牛之类的客户端无法控制意义也不大。遂找网页版的模拟交易账户,一开始是找了同花顺的,但是太简陋,没用。后来看了下新浪爱财iTrade,也不算满意,最后找到了富途牛牛模拟交易的网页版入口,用手机注册即可。最重要的是一个网页同时支持A、H和美股的交易,可玩性就强了很多了。

这么看来,以后要是实际跑策略实盘,估计直接用浏览器自动操作就可以了。前提是券商提供网页版下单入口。

分析研究

通过firefox抓包,我发现了富途牛牛网页交易时发送的数据结构,但是cookie有些内容搞不明白怎么生成的,于是选择了headless浏览器+splinter的方案

在windows上直接用firefox测试,基本把登录、下单的测试代码弄出来了(下面是部分测试实例):

In [31]: f = b.find_by_name('stockCode')

In [32]: f
Out[32]:
[<splinter.driver.webdriver.WebDriverElement at 0x6d546a0>,
 <splinter.driver.webdriver.WebDriverElement at 0x6d54128>]

In [33]: f[1].fill("000858")

In [34]:

In [34]: g = b.find_by_name("price")

In [35]: g
Out[35]: [<splinter.driver.webdriver.WebDriverElement at 0x6a25780>]

In [36]: g[0].fill(76.20)

In [45]: amount = b.find_by_name("qty_str")

In [46]: amount
Out[46]: [<splinter.driver.webdriver.WebDriverElement at 0x6a22320>]

In [47]: amount[0].fill("400")

In [48]: buy = b.find_by_text("模拟买入")

In [49]: buy
Out[49]: [<splinter.driver.webdriver.WebDriverElement at 0x69fa358>]

In [50]: buy[0].click()

In [52]: bid
Out[52]: [<splinter.driver.webdriver.WebDriverElement at 0x69361d0>]

In [53]: bid[0].click()

In [54]: confirm = b.find_by_text("确定")

In [55]: confirm
Out[55]:
[<splinter.driver.webdriver.WebDriverElement at 0x6a2d7b8>,
 <splinter.driver.webdriver.WebDriverElement at 0x6a2d160>]

confirm[1].click()

但是存在一个问题,无法指定撤单,只能先撤最新的单:

问题存在的原因主要是splinter不支持根据class找元素,而我那时候还没搞懂xpath,结果就放弃了基于splinter的代码(实际上只要用xpath,没有任何元素是找不到的)

我的splinter的初步方案是使用click_link_by_text(“撤单”)来做撤单,但是这个就无法撤前面的单,只能撤了最新的单再撤旧单。

后面基于“根据class找元素”的想法,选择了selenium,这个库是splinter的底层,splinter只是简化了使用(也相当于减少了部分功能),后面不知咋的找到了xpath的资料翻了下,发现提取xpath简单到死,直接对着元素选择检查然后右键复制xpath即可,不过chrome和firefox的xpath格式不同,前者的往往是很简短的,比如:

//*[@id='confirmDialog']/div[3]/span[2]/button[2]

即使格式不同,但是都能被selenium正确识别。

最后撤单逻辑通过位置来确定不同的单,然后输入一个从1数起的坐标即可选择性撤单,最新的单位置为1,旧的单位置值递增,比如上图特锐德的位置是1,中车的位置是2,只要输入准确的位置就可以撤单,但是要时刻维护一个“待成交单”的数据,不然无法撤单。

因此我在StockClib里增加了ftTrader库,一个无状态的基于firefox的自动交易接口库,预计0.3会增加一个函数简化交易数据的维护,而0.4开始会逐渐增加对美股港股的支持。

不过有点小问题,虽然我已经显式给selenium加了waits,但是如果网络不好,网页加载未完的话就执行交易函数的话会出现下单失败的问题,这个后面0.3会尝试修复。

headless

我一开始想用chromeless,但是不支持python,弃。然后找headless chrome,无奈我windows的chrome是58,windows版要60才支持headless模式。后来在虚拟机里测试,但是selenium总是启动不了headless chrome,报错,而且安装贼麻烦,最后直接换headless firefox了。如果ftTrader选择debug模式,则会启动GUI版火狐,否则默认headless。headless模式下必须指定geckodriver的路径才能正常启动,刚好我的火狐更新到了57,Windows版可以测试headless模式,而linux下安装也不麻烦,apt就一句命令的事。FF最新版对比旧版的确快不少,体验较好。

关于headless firefox的资料可以看这里:Here

总结

完成这个交易API之后就可以着手搞策略和对应的策略执行引擎了,当然我也不希望又搞套DSL出来,策略直接是python程序就好,托管者就像supervisor一样监控运行状态就够了。不过在搞这些之前,我还是得看看传统的量化知识。

参考

Selenium的Waits

正则表达式快速入门参考

Intro

虽然在为VyOS写python-vyos-mgmt的时候已经被提醒过一定要学正则表达式,不过一直没空去学,然后今天准备去看看BotVS的资料的时候就看到有则关于正则的资料,遂趁开例会时的空闲时间快速入门了一把。虽然用bash的时候经常用*来rm,但是更复杂的基本不会用了。

资料

这个cheat sheet包含了绝大多数的使用方式:

更详细的文档可以参考这里:

cheatsheet

然后为了方便测试自己的正则表达式是否正确,推荐一个国人编写的运行在.Net的图形化工具Regester,最后更新在2017年6月:

Regester

他也推出了一个30分钟入门教程(虽然可能不止30分钟):

30分钟入门

不过我觉得如果不借助任何工具去学的话还是这个人的教程比较适合,他把能匹配的字符串都按代码格式显示方便理解:

入门

进阶

我觉得比较重要的内容

  1. 一个”.”只能匹配一个字符,N个”.”能匹配N个

  2. “\”各领域都很常见的转义字符

  3. “K[abc]T”只能匹配一个长度为3的,含abc任意“一个”字符的字符串,”KabT”无法被匹配,而使用这个模式可以匹配”KabT”:K[\w]*T

  4. “{m}”匹配前一个字符串m次,比如”a{2}b”,如果目标字符串为”aaab”,则只能匹配出”aab”,若目标为”ab”,则无法匹配

  5. “{m,n}”中,m<n

  6. “[A-Za-z0-9]”匹配任何字母和数字,”\w”则匹配[A-Za-z0-9_](多一个下划线),”\s”是空白字符,大写”\w”和”\s”的规则都是原来的字符的对立规则

  7. 后面加问号,比如”*?”可将匹配符变成非贪婪的

  8. “^”在中括号内为否定,中括号/字符串外为头部边界,”$”则用作尾部边界

Python的re模块的通用代码patten

import re
pattern = re.compile("K[\w]*T")
# 无法匹配时将返回None
res = pattern.match("K[\w]*T")
if res:print(res.group())

结果:

In [10]: if res:print(res.group())
KabvT

如果匹配失败:

In [6]: res = pattern.match("K T")

In [7]: res.group()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-09a435ad28f6> in <module>()
----> 1 res.group()

结语

和Vi一样我也不能完全记住全部快捷键,正则还是用到再具体看吧


Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2020 HOCHIKONG's WAPORIZer All Rights Reserved.

访客数 : | 访问量 :