给网站添加https安全证书

众所周知,Chrome的68版会将未使用 https 加密的网站标记为「不安全」。可以说 Google 是在不遗余力地推广传输层安全协议。 响应 Google 的号召。当我得知 https 证书也有一些免费版本可用时,就决定给自己的博客加上 https。 略过各种方案的比较和选择,这里直接记录(我认为)个人博客网站添加 https 证书的最佳实践:

服务器添加安全证书

我参考了这篇:Let’s Encrypt 给网站加 HTTPS 完全指南 但你不用按那篇文章里的步骤来,因为它已经是2年前的文章了,现在可是8102年,事情变得很简单: 只需在服务器上安装 certbot, 它自动帮你完成一系列的证书配置和验证操作。(需要对服务器有足够权限) 网址在:https://certbot.eff.org/ 我选择了 Apache on Ubuntu 16.04 (xenial) ,页面的提示如下: 不一定跟我一样,选择你自己的操作系统服务器软件,certbot 页面上会提供对应的安装方法。 安装后继续按照 certbot 页面的说明 get start ,程序的交互式界面会引导你进行选择和配置。 跟随引导进行操作,完成后网站就被 Let’s Encrypt 提供的 SSL/TLS 证书保护起来了。是不是很有安全感!
(完成证书配置后,certbot 会提示你到 https://www.ssllabs.com/ssltest/index.html 网站去检测 SSL web server 的安全性。最终的评价等级其实取决于你的证书授权机构、它使用的加密协议、能被哪些平台兼容… 作为安全通信方面的小白,我其实不太清楚怎么去确认自己的网站足够安全。这个检测至少提供了一些有效参考信息。) 由于 Let’s Encrypt 提供的免费证书只有3个月有效期,每次到期前需要 renew。certbot 支持自动 renew。 关于 renew 的 config ,你能在 /etc/letsencrypt/renew/ 路径下找到。用下面这个命令测试自己的 renew 设置是否有效:
sudo certbot renew --dry-run

一点清扫工作

现在你应该能通过前缀 https:// 的网址来访问自己的网站了。再回头试一下 http:// 访问,看会不会自动跳转到 https:// 。(如果没有,就要检查网站的重定向设置了) 不过,你也许会遇到,其他的 https 网站在浏览器里都有一把小绿锁显示,为什么我的网站只有小灰锁呢? 在浏览器的小锁(或灰色叹号)上点击,可以看到它对网站潜在隐患的提示。 对于个人博客来说,问题通常出现在站内的图片和链接上。它们的地址可能不是 https 的,没有被 SSL/TLS 保护,所以会被中间人截获。
参考这篇这篇文章,可以编辑当前 WordPress 主题下的 function.php 文件,强制将站内图片的地址转换为 https : 在 function.php 文件的尾部添加以下代码(修改其中两处 YOUR_DOMAIN 为你自己的域名)
/* 替换图片链接为 https */
function my_content_manipulator($content){
    if( is_ssl() ){
        $content = str_replace('http://www.YOUR_DOMAIN.com/wp-content/uploads', 'https://www.YOUR_DOMAIN.com/wp-content/uploads', $content);
    }
    return $content;
}
add_filter('the_content', 'my_content_manipulator');
若还有错,参照这篇文章,利用浏览器的功能来排查错误。 找到错误原因后:如果是通向站外的链接,直接把它们改成 https 就行了(现在几乎所有正经网站都支持https了)。 如果是 WordPress 插件的问题,找找插件设置里能否修改相关地址。插件不支持的话,就看你愿不愿意忍痛寻找替代品了。

Python Tkinter 进阶实践

之前简单介绍过Tkinter:Python 可视化图形界面简单实践
这次来记点进阶心得。


Tkinter实现标签页效果

用 Python 和 Tkinter 写过一些小工具。现在想把它们整合到一起,以类似“标签页”的形式来切换。

具体参考:Tkinter 8.5 reference: a GUI for Python
用到的是 Notebook 控件,这方面网上比较少提,所以记录一下。

举例:

MyNotebook1 = Notebook(top)
MyNotebook1.place(relx=0.022, rely=0.062, relwidth=0.956, relheight=0.876)

之后你就可以将 Notebook 作为 parent 来构建 Frame,承载你的标签页:

Tab1 = Frame(MyNotebook1)
....
MyNotebook1.add(Tab1, text='First tab name')

与普通的 Frame 不同,完成构建的 Tab1 不是通过 pack、grid、place 来布局,而是用 Notebook 的 add 方法,并指定标签名。

再来一个例子:

#about tab
self.TabX = Frame(self.Notebook1)
self.TabXLbl = Label(self.TabX, text="Listen's Swiss Army knife")
self.TabXLbl.pack()
self.TabXLb2 = Label(self.TabX, text="ver 1.3.0")
self.TabXLb2.pack()
self.Notebook1.add(self.TabX, text='About')
#about tab end

上面这个标签页实际效果大致是:

简单来说,你只要先建立了 Notebook 控件,然后往里面 add Frame 就行了,每个 Frame 都会呈现为1个独立的标签页,名字在 add 时指定。


GUI 界面与逻辑分离

如果只是一个功能简单的小工具,随便怎么写都没问题。但我在把多个小工具整合成一套时发现,东西一多了会很难管理。
在网上看到别人的做法,是将界面和逻辑分离开:

class Application_ui(Frame):
#这个类仅实现界面生成功能,具体事件处理代码在子类Application中。
def __init__(self, master=None):
Frame.__init__(self, master)
self.master.title("Listen's Victorinox")
self.master.geometry('880x380')
self.createWidgets()

def createWidgets(self):
....

class Application(Application_ui):
#这个类实现具体的事件处理回调函数。界面生成代码在Application_ui中。
def __init__(self, master=None):
Application_ui.__init__(self, master)
....

上面的做法是从 TK 继承了 Frame,然后重写成自己的 UI (Application_ui)。这个 Application_ui 只生成界面,之后再用 Application 类去继承它,在 Application 里面进行逻辑处理。

单独调试 UI :

if __name__ == "__main__":
top = Tk()
Application_ui(top).mainloop()

这样的好处是,在设计 UI 时,可以只考虑控件和布局。若画面呈现不如预期,就可以直接调整。

而 UI 里的每个控件其实都是空的,它们不实现任何功能,功能都在 Application 类里去添加。
例如:

self.Tab1varWL = StringVar()
self.Tab1varWL.set("WL")
self.TabStrip1__Tab1WLNum['textvariable'] = self.Tab1varWL
self.TabStrip1__Tab1WLNum.bind("Return",self.WL2Page)

self.TabStrip1__Tab1WLNum 这个控件之前是空的,现在我给它设置了 StringVar() ,并且绑定了一个事件是 Return,这个控件收到回车键时会触发 self.WL2Page 方法。
另一个 tab 的实例:
 # Tab4 var
self.Tab4path = StringVar()
self.Tab4pathEntry['textvariable'] = self.Tab4path
self.Tab4ButtonSelect['command'] = self.Tab4selectPath
self.Tab4ButtonConfirm['command'] = self.Tab4ReadFile

相比于把 GUI 各种按钮的函数摆得到处都是,现在则可以按tab分割,都放在 Application 类的私有方法里。

对这部分进行调试:

if __name__ == "__main__":
top = Tk()
Application(top).mainloop()

开发时,可以先在 Application_ui 类里面调试界面,完成后,再到 Application 类给控件们分配变量、绑定方法,验证逻辑功能。
两部分工作分割开,这样就清晰很多。制作复杂界面的 GUI 程序时,界面和逻辑的分离很有必要。


将编译好的程序打包成安装程序来发布

我在上一篇Python 可视化图形界面简单实践里介绍过,用 pyinstaller 简单快速地打包 python 程序,生成不依赖 python 的可执行文件 exe。

当时有说 -F 参数是将程序打包成一个单独的exe文件,不加则会生成一整个目录的文件。 同时也因为速度原因不建议添加这个参数。
可是程序发布时,这么多零散的文件是很不方便的,你只能将目录压缩成zip、发给别人,让别人自己去找里面的 .exe 文件。

一个合理的发布方式是提供安装文件,收到的人执行安装文件就能完成部署。
这里我们用到的工具是 NSIS(Nullsoft Scriptable Install System)。具体步骤参照这篇教程,写的很详细,我就不搬过来了:程序打包成exe文件

原则上任何语言的程序都可以这样发布。NSIS用压缩算法把你的程序那一大堆文件压成一个包,套了个安装向导的壳。
用户拿到后一运行,安装向导像所有程序一样,引导他们选择安装路径等等,然后把压缩包解压到用户路径,再为这个路径创建桌面/开始菜单的快捷方式。

NSIS 可以提供不同语言的安装向导,你甚至可以给它添加奇怪的用户协议~


这篇文章涉及的内容都是我在开发 Victorinox 小工具时遇到的。这是一把全世界只对我1个人有用的瑞士军刀。。。。。。
源码已push到GitHub:https://github.com/MamaShip/Victorinox

Git Flow实践

工作上一直使用git作为代码版本管理工具,但从来没掌握好它。最近我负责的这部分代码管理越来越混乱,掌握一套正确成熟的git工作流程几乎是迫在眉睫的事情。

git 的基础使用就不赘述了。大概每个程序员都会花几个小时掌握 git 的基础操作,再花几周去实践和熟悉吧。
首先推荐两篇文章:

如果你没有遇到过多人协作下代码管理混乱、想要 release 新版时却交不出一份能 work 的代码,读这两篇文章可能不会很有 sense。但你只要遇过一次版本危机,再硬着头皮连续加班到凌晨一两点,回头来看就会很有感受。

上面两篇文章其实讲了2件重要的事:

  • 根据你的项目性质来决定工作流(git flow / github flow / gitlab flow)
  • 在一个固定模式下,git如何规范地工作


由于我在一家制造业企业,RD 只需隔一段时间 Release 一个 FW 版本给产线即可。比起敏捷地迭代,版本的稳定性更重要。传统的 git flow 是适合我们的。

在 git flow 框架下,有2个长期分支:

  • 主分支master
  • 开发分支develop

master名为master,却并不是日常工作的基础分支,而是一个经过验证的稳定分支。随时可以用来发布。
与master平行存在,内容几乎相同但又略有超前的,就是develop分支。平日的开发必须基于develop分支进行。在git flow框架下,当一个新的需求产生时,你基于最新的develop拉出一个 feature/xxx 分支,进行开发,功能完成后,再合并回 develop。(合并后 feature/xxx 分支自然消亡)

3种短期分支(完成开发后,合并进长期分支,然后删除):

  • 功能分支(feature branch)
  • 补丁分支(hotfix branch)
  • 预发分支(release branch)

feature 只能从 develop 拉出,然后合并进 develop。
hotfix 只能从 master 拉出,然后合并进 master 和 develop。
release 则是从 develop 拉出,进行测试,完成验证后合并进 master。

当 git flow 进行 release 时,它实际做了2件事:把从上一次 release 至今的 develop 改动经过验证后合入 master,再反向把 master 合并回 develop 确保两边同步。
为什么要反向 merge 一遍,因为 release 过程中若测试出一些问题,就可以直接在 release/xxx 分支上进行修改,而这个改动是没有发生在 develop 上的。所以 release 分支在删除时会保证这些改动也被同步到develop。

在这样的设计下,master 是稳定的,它受到 release 规则的保护,除非发生 hotfix,就只有经过测验的 release 分支才能合进 master。
而 develop 分支则相对比较敏捷,根据需求拉出 feature branch 后,开发者可以专注于自己的子任务,把兼容性等全局测试交给 release 分支去负责。

使用 git flow 相关工具,可以帮助规范这个流程。例如,工具会禁止你基于 master 拉出一个新的 feature 分支,因为新 feature 总是应该基于 develop 分支来开发,若要紧急修补某个bug,你应该使用 hotfix。
一些git flow工具(如gitflow-toolkit),还可以规范你的commit,确保commit信息清晰有效,能被script解析。这样你就能使用现成的工具(git-chglogconventional-changelog),自动生成change log。这在 release 正式版本的时候是很有用的:


以上是我理解的 git flow 实践。在应用中发现它也对使用者提出了一些要求:

  • 无论是否使用工具辅助,所有人都需要按规范格式进行commit(参考: Angular 社区规范
  • 每一项逻辑上独立的改动,应归属于一次单独的commit
    (不要在一个feature分支下进行多项任务的开发;若有多次commit都是关于同一个修改项目,应先squash再合进develop;尽量让每个 commit 都是一项独立、完整的改动,可以方便debug/release时进行cherry-pick和rebase)
  • git flow tool 大多是基于 Linux 系统。win下的开发者需要自我约束。

git flow 仍在实践中学习。有新的理解会再更新。

对 ML 中 Normal Equation 的理解

机器学习中的回归问题,类似于以往各种工程上的优化问题。定义一个 Cost Function:

(1)   \begin{equation*}  J(\theta) = \frac{1}{2m}\sum_{i=1}^m(h_\theta(x^i)-y^i)^2 \end{equation*}

对这个函数求解最优化问题。

由于J是一个关于\theta的函数,所以目标是求一组最优的\theta,来使J取得最小值。

一个符合直觉的做法是Gradient Descent。也就是迭代地用 J\theta 求导,顺着梯度最大的方向移动,直到收敛于最低点。

另一个方法则是Normal Equation,对我来说这是一个非常强的公式,可以把Gradient Descent花了那么多次迭代才解决的事用一次运算就搞定:

(2)   \begin{equation*}  best \theta = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top \overrightarrow{y} \end{equation*}

这里面的\mathbf{X}是一个矩阵,\overrightarrow{y}是列向量,\theta其实也是列向量。
\mathbf{X}的每一行是一个样本,第i行其实就是 x_i^1, x_i^2, x_i^3, \dots 这样的一组特征(feature),其中的每个值都可以视作特征空间里的一个维度。
对于第i个样本,它对应的(标准)结果就是 y_i,总共m个样本结果组成了向量\overrightarrow{y}
(回顾一下Cost Function:损失函数其实是定义了我们的Hypothesis h_\theta(\mathbf{X})与真实结果\overrightarrow{y}之间的距离。以此衡量训练结果。)
再补充Hypothesis:

(3)   \begin{equation*}  h_\theta(x) = \theta^\top \overrightarrow{x} = \theta_1 x_1 + \theta_2 x_2 + \dots + \theta_n x_n \end{equation*}

Normal Equation 的思路类似于中学数学里求极值:对某个方程求导,然后让式子等于 0,等于 0 的点即为极值点。
但为什么上述思路能被(2)这样一个式子实现呢。


这个问题是我去年初遇到的。当时就想把阳哥哥给我的解答记录下来。拖了这么久,终于给Wordpress装了LaTeX插件,所以来记录一下:

(2)的推导方法有两种,第一种是 cost function J (1)对 \theta 这个向量的每一个分量求偏导

    \[ \begin{cases} \frac{\partial J}{\partial \theta_1} = \frac{1}{m} (h_\theta(x^1)-y^1) x_1\\ \frac{\partial J}{\partial \theta_2} = \frac{1}{m} (h_\theta(x^2)-y^2) x_2\\ \dots \\ \frac{\partial J}{\partial \theta_n} = \frac{1}{m} (h_\theta(x^n)-y^n) x_n \end{cases} \]

假设 \theta 是个n维向量,就得到了n个式子,令这n个式子都等于0,我们就得到了一个线性方程组:

    \[ \begin{cases} \frac{1}{m} (h_\theta(x^1)-y^1) x_1 = 0\\ \frac{1}{m} (h_\theta(x^2)-y^2) x_2 = 0\\ \dots \\ \frac{1}{m} (h_\theta(x^n)-y^n) x_n = 0 \end{cases} \]

这个线性方程组可以写成矩阵相乘的形式,即

(4)   \begin{equation*}  \mathbf{X}^\top\mathbf{X}\theta = \mathbf{X}^\top \overrightarrow{y} \end{equation*}

(4)这个式子就是所谓的Normal Equation。它跟式(2)是等价的。


第二种方法要稍微高级一点儿,写出来会简洁得多。就是直接把cost function写成矩阵的形式,即

    \[ J = (\overrightarrow{y} - \mathbf{X}\theta)^\top (\overrightarrow{y} - \mathbf{X}\theta) \]

然后直接以这个式子对向量 \theta 求导,也会直接得出normal equation。
至于如何直接对向量求导,你可以参考wikipedia的matrix calculus。其实本质上对向量求导就是一种记号,和对向量的每个分量求导没的本质区别。


再回头看一次这个好用的Normal Equation:

    \[ best \theta = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top \overrightarrow{y} \]

对于有 n 个特征维度的 \mathbf{X} 来说,用 Normal Equation 求 \theta 的复杂度是 O(n^3),而 Gradient Descent 的复杂度为 O(kn^2)
所以,当特征维度不多时,都可以用 Normal Equation 快速求解。当 features n > 10000 时,才开始考虑使用 Gradient Descent 。

但是,这个公式显然也不是能够无条件使用的。你可以看到其中有 (\mathbf{X}^\top\mathbf{X})^{-1} ,意味着括号内的矩阵必须是可逆的。
真的是这样吗?——老师说,此处的矩阵并不必须是可逆的。在代码中,对这个矩阵的求逆直接用pinv()函数来做(也就是求伪逆、广义逆)。阳哥哥说,使用广义逆来算 \theta(\mathbf{X}^\top\mathbf{X}) 不可逆时的一种传统的处理方式,可以用的原因是广义逆得出的 \theta 符合一些良好的统计性质。(具体参考:高惠璇的《统计计算》)
不过, (\mathbf{X}^\top\mathbf{X}) 不可逆往往意味着数据模型有问题(Features数量比样本数量还大,或是非常强的collinearity),遇到不可逆的情况还是返回去检查模型比较好。