百元改造家庭网络拓扑,享受极致网络体验
自从我去年购买了软路由和 NAS 后就开始折腾家庭网络,到目前为止已经 1 年多了。如果说去年还只是装装 openwrt,搞搞智能家居中枢家庭服务器这种小打小闹的玩意,今年则是彻底给家庭网络拓扑来了个大换血。同时我今年兼顾了成本,在去年软路由的基础上仅用了一个额外的网管交换机(成本约 100 元)解决了日常网络使用过程中的诸多痛点,包括单线复用、千兆 LAN 汇聚、IPTV 单播等等。虽然过程一直磕磕绊绊,有碰到 Mac 下奇怪的网络栈问题的,有碰到 IPTV 离奇卡死的,甚至动用反编译分析一个 CLI 工具参数的,但到今天总算有一个比较满意的结果了。
我个人同样在这次网络配置过程中受益匪浅,参考了很多国内外论坛的内容,其中也有一些是我自己研究出来的。因此我打算将整个网络方案分多篇博文一一介绍。一方面是希望能和更多人共同交流,另一方面则是为了避免对整个网络的配置过程产生遗忘,以免今后发生任何故障时难以下手。
另外,由于个人的拖延症,在写这篇博客的时候已经离完成整个家庭网络的建设过去了大约一个月的时间,部分内容可能有所遗忘或修改,但我也会尽量覆盖到。
我家的网络来源于上海电信的 1000Mbps 光纤接入,包含两路 IPTV。弱电箱中存放了光猫、软路由和无线路由器。光猫拨号上网后接入 Openwrt 软路由中,软路由不负责拨号上网,而是另外起了网段用于路由。家庭中的所有设备都通过 Openwrt 路由上网,其中一台无线路由器华硕 RT-AC86U 设置为有线 ap 模式供所有无线设备上网。光猫的三口和四口带有 IPTV 功能,直接接入两个房间的 IPTV 盒子用于观看 IPTV。
整套上网方案看起来挺清晰的,但是随着设备的增多和需求的变化,目前这套架构主要有以下几个痛点亟待解决:
最后,预算有限,自然是希望成本越低越好!
总结一下其实就两大需求:
结合一下去年做的 PVE 套 Openwrt 的方案,整个架构图就出来了
软路由的 4 个口其中 3 个 (eth1-eth3) 直通给 Openwrt,另一个网口 (eth0) 组网桥分别给 PVE Host 和 Openwrt 上网。eth0 和 eth1 直接接在光猫的两个 LAN 口上,这两个 LAN 口直接在 Openwrt 中做 WAN 聚合,达到突破千兆瓶颈的目的。
PVE 开一个 CPU 网桥同时共享给 Openwrt 和其他虚拟机,这样其他虚拟机就能通过 Openwrt 的 LAN 上网了。另外两个直通口 (eth2, eth3) 连到 TL-SG2008D 8口千兆网管交换机上,后者支持 VLAN 转发和链路聚合,并且售价只要 100 出头,完美满足我的需求。这样 Openwrt 的 WAN 侧和 LAN侧 经过网口链路聚合都具有了 2Gbps 的转发能力。
下面来解决 IPTV 的问题,将光猫的 IPTV 口接到交换机上后,交换机就可以转发 VLAN 包了。于是做这么几个配置:
来到客厅那边,本来应该是需要再来一个网管交换机的。不过华硕的路由器刷了梅林之后直接可以做 VLAN 转发了,因此 ap 就和交换机二合一了,好评!
结局令人满意,上面提到的几个需求都完美的得到了解决,随便测个速
突破 1100 Mbps,上天!再测测 NAS 传输
1 | ➜ ~ iperf3 -c openwrt.lan -t 5 |
单线程突破 1Gbps,多线程接近 2Gbps,简直爽快!千兆网口也能体验超千兆网速啦!
最后解答一个问题,为什么要用光猫拨号而不直接桥接 Openwrt 上网,不是还能少一层 NAT 吗?
首先,我这台光猫的性能已经足够千兆上网了,并且我给光猫设置了 DMZ 到 Openwrt,因此实测下来并没有什么性能的损失。其次,我的 Openwrt 是套在 PVE 里面的虚拟机,如果由 Openwrt 上网,那么连上 PVE 宿主机就需要通过 Openwrt 了,这样如果 Openwrt 在配置的时候搞坏了,或者某天 Openwrt 突然炸了,那么我就彻底失去了对 PVE 的控制,到时候的重置以及恢复将是灾难性的。现在 PVE 和 Openwrt 通过网桥同时通过光猫上网,能够保证 PVE 不需要借助 Openwrt 就能直接连接,这样即使 Openwrt 哪天突然炸了,我也能通过有线或者无线连接光猫来操控 PVE 去控制 Openwrt。
那么就写到这里了,详细配置请参考后续的文章。
]]>快速编写一个装饰类的装饰器类 (a decorator class to decorate a class),调用方便、扩展性强、内附代码实现
在我们会编写函数装饰器用于装饰函数、类装饰器用于装饰函数后,我们很自然会想到一个问题,能否编写类装饰器装饰一个类?我们能否通过仅仅对类装饰,却能 Hook 掉这个类的所有成员函数以达到方便扩展的目的?本文将快速回顾前几种装饰器,并最终得到一个装饰类的全能装饰器类。
我们先来回顾一下前几种装饰器的基本形式,并且假设我们现在需要记录函数执行的日志、包括函数的输入和输出
不再赘述,直接看代码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
27from functools import wraps
def Logger(func):
def wrapper(*args, **kwargs):
print('call %s() with args: %s, kwargs: %s' % (func.__name__, args, kwargs))
ret = func(*args, **kwargs)
print('%s() return %s' % (func.__name__, ret))
return ret
return wrapper
def NamedLogger(name):
def decorator(func):
def wrapper(*args, **kwargs):
print('%s: call %s() with args: %s, kwargs: %s' % (name, func.__name__, args, kwargs))
ret = func(*args, **kwargs)
print('%s: %s() return %s' % (name, func.__name__, ret))
return ret
return wrapper
return decorator
这里我们编写了两个函数用作装饰器。对于无参数的装饰器,我们直接返回一个 wrapper
对函数进行修饰。而对于无参数的装饰器,我们返回一个返回 wrapper
的函数,这个函数对原函数进行修饰。
调用代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
def add(a, b):
return a + b
def sub(a, b):
return a - b
if __name__ == '__main__':
print(add(1, 2))
print(sub(1, 2))
输出结果1
2
3
4
5
6call add() with args: (1, 2), kwargs: {}
add() return 3
3
myLogger: call sub() with args: (1, 2), kwargs: {}
myLogger: sub() return -1
-1
我们能否编写一个类用于装饰函数呢,答案是肯定的,上代码
1 | class Logger: |
注意带参数类 Logger
和不带参数的类 NamedLogger
中 __init__
的区别,在不带参数的类中,__init__
函数实际上是把原函数作为参数传入了,而在带参数的类中,__init__
函数的参数作为装饰器本身。同时两者都实现了 __call__
函数,用于模拟函数的行为,但其中的实现大不相同。Logger
中初始化已经得到了函数,因此在 __call__
中直接调用即可,而 NamedLogger
中 __init__
仅初始化了装饰器本身,因此 __call__
需要返回一个原函数的包装 wrapper
。
使用和函数形式相同的调用代码,输出结果和上述一致。
介绍完了函数装饰器,下面我们来介绍类装饰器。根据前面的经验,既然函数装饰器需要返回一个函数,那么类装饰器我们返回一个类就好了。因此我们可以快速编写这样一段代码
1 | def NamedLogger(name): |
注意这里 __call__
等于执行了原类的构造函数。能不能再给力一点呢?如果我们想在修饰类的同时顺便把他的成员函数也修饰了呢?
当然可以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
27def NamedLogger(name):
class ClassWrapper:
def __init__(self, cls):
self.old_class = cls
def __call__(self, *args, **kwargs):
self.old_object = self.old_class(*args, **kwargs)
return self
def add(self, *args, **kwargs):
print(f'{name}: call {self.old_class.__name__}.add() with args: {args}, kwargs: {kwargs}')
ret = self.old_object.add(*args, **kwargs)
print(f'{name}: {self.old_class.__name__}.add() return: {ret}')
return ret
return ClassWrapper
class Adder:
def add(self, a, b):
return a + b
if __name__ == '__main__':
adder = Adder()
print(adder.add(1, 2))
我们在这里通过 __call__
返回了 ClassWrapper
本身,这样在 adder = Adder()
调用时本质是实际 adder
是拿到了 ClassWrapper
这个对象,然后我们通过直接定义一个 add()
函数来接管原类的 add
函数。
但是问题又来了,这个实现直接定义了一个 add
函数,但原类的函数名不应该暴露给装饰器,如果原类是一个黑盒呢?能不能再给力一点呢?
额,有点复杂,不过还是可以做到的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from functools import wraps
def NamedLogger(name):
class ClassWrapper:
def __init__(self, cls):
self.old_class = cls
def __call__(self, *args, **kwargs):
self.old_object = self.old_class(*args, **kwargs)
return self
def __getattr__(self, func_name):
target = getattr(self.old_object, func_name)
def wrapper(*args, **kwargs):
print(f"{name}: call {func_name}() with args: {args}, kwargs: {kwargs}")
result = target(*args, **kwargs)
print(f"{name}: {func_name}() return: {result}")
return result
return wrapper
return ClassWrapper
我们重载 __getattr__
函数,然后通过 getattr
得到函数目标 target
,接下来返回一个 wrapper
对 target
进行包装即可。
好了,我们差不多完成了类装饰器的雏形,这个装饰器可以对一个类的所有成员函数进行修饰,非常方便。我们最后再补充一点细节,比如我们的类装饰器能否同时兼容有参数形式和无参数形式?如果原类调用了成员变量怎么办?按照现在的实现同样会返回一个 wrapper
。
最终的代码如下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
52from functools import wraps, partial
def NamedLogger(cls=None, name='default Logger'):
if cls is None:
return partial(NamedLogger, name=name)
# see https://stackoverflow.com/a/65470430/7620214
class ClassWrapper:
def __init__(self, *cls_args, **cls_kwargs):
self.old_object = cls(*cls_args, **cls_kwargs)
def __getattr__(self, func_name):
target = getattr(self.old_object, func_name)
if not callable(target):
return target
def wrapper(*args, **kwargs):
print(f"{name}: call {func_name}() with args: {args}, kwargs: {kwargs}")
result = target(*args, **kwargs)
print(f"{name}: {func_name}() return: {result}")
return result
return wrapper
return ClassWrapper
class Adder:
def add(self, a, b):
return a + b
class Adder2:
def __init__(self):
self.last = 0
def add(self, a, b):
self.last = a + b
return self.last
if __name__ == '__main__':
adder = Adder()
print(adder.add(1, 2))
adder2 = Adder2()
print(adder2.add(1, 2))
print(adder2.last)
代码的实现变得更简洁了一点,__call__
也被干掉了,并且同时支持带参数和不带参数两种不同形式的装饰方法。此外,我们还用 callable
判断目标属性是否是函数。至于其余剩下的改动,就留给读者思考了。
最后,你可以在 这里 下载到最终的源代码。
]]>使用先序遍历的节点访问顺序重新给左右儿子编号,大大节省了空间
自古以来,实现线段树所需要的数组空间大小有着比较激烈的争论。一种基于递归的线段树实现认为至少需要$\mathcal{O}(4n)$的空间,而另一种基于直接循环的方法认为其实只需要$2n-1$的空间。关于为什么基于递归的方法需要$\mathcal{O}(4n)$的严格证明可以去网上搜索,或者这里提供了一个简易的证明方法。总的来说,基于递归的线段树实现起来更加直观,但由于dummy node的存在造成了空间的浪费。而基于直接循环的方法对实现的要求更高。本文介绍了一个基于递归的线段树实现,通过改变每个节点的左右儿子的编号高效的利用了空间,使得基于递归的线段树也只需要$2n-1$的空间。另外,关于基于循环的线段树实现方法,可以参考这篇博客。
要回答为什么dummy node的存在导致了基于递归的线段树需要$\mathcal{O}(4n)$的空间,我们得先了解一下传统的实现方式。
我们考虑一个恰好为$2^k$的序列,由于长度恰好为2次幂,因此线段树的每个节点的长度恰好是它父亲节点的一半,因此最后形成的线段树是一颗满二叉树。比如下图是一颗长度为8的序列组成的线段树,维护了区间$min$的查询。
我们还可以注意到,如果根节点的编号是1的话,那么对于每个非叶子节点$x$,它的左儿子是$2x$,右儿子是$2x+1$。整个线段树的节点个数是$2n-1$。我们的build()
函数可以大致这么实现。
1 | template<typename Container> |
那么很自然地我们就会想到,如果对于一个长度不是$2^k$的序列,上述结论是否依然成立呢?答案是显然的,很自然地,我们依然可以将每个非叶子节点的左儿子设置成$2x$,右节点设置成$2x+1$。那么对于一颗长度为10的序列组成的线段树,就会变成下图所示
我们大致可以得出两个结论。第一,如果你去统计这颗线段树的节点个数,你会发现它依然是$2n-1$。这个是很容易理解的,因为任何一个$[1,n]$的序列,它一定有$n-1$个分割点,那么要全部变成长度为1的叶子节点,它必然要经历$n-1$次分割,加上叶子节点的个数$n$,总节点自然就是$2n-1$了。因此我们可以得出一个重要结论
长度为$n$的序列会形成$2n-1$个节点个数的线段树。
这个结论非常重要,之后我们还会用到,因此需要暂时把它记住。至于第二个结论,就是从图中我们发现有些节点是不存在的,比如18-23号节点,另外24和25号节点的编号实际上已经超过了$2n-1$,这就是为什么本文开头提到基于递归的线段树需要$\mathcal{O}(4n)$的空间的原因。
那么既然线段树的总节点树就是$2n-1$,我们是否可以通过某种办法,把这些节点的编号重新映射一下,使得线段树中每个节点的编号都落在$2n-1$内,这样我们就不需要开$\mathcal{O}(4n)$的空间了,并且每个空间也能充分得到利用了,岂不美哉?
答案是肯定的,比如我们可以按照整棵树的节点访问顺序,也就是先序遍历的顺序给节点重新编号。
我们不再使用$2x$和$2x+1$给左右儿子编号了,而是使用从根节点开始,实际访问每个节点的顺序,也就是先序遍历的顺序给每个节点编号,或者再换一个说法,就是dfs的访问顺序。这样每个节点在数组空间中的排列就是紧密的了,那我们就只需要开$2n-1$的数组大小就能完成整颗线段树的构建了。
那么说了那么多,问题来了。我在递归遍历的时候怎么求出左右儿子的编号呢?本来只要$2x$和$2x+1$就能很容易的求出了,现在这么一搞,我怎么知道左右儿子的编号是多少呢?难不成我还要搞一个全局计数器去记录节点编号吗?
并不需要。首先我们来看左儿子的编号,因为现在的编号顺序就是实际的访问顺序了,而左儿子恰恰就是要访问的下一个节点,那么显然,左儿子的编号就是$x+1$了。那么右儿子呢?其实也很简单,我们注意到,如果左儿子划分了$[l,r]$这个区间,那么实际上左儿子就是一颗划分了$[l,r]$序列的子线段树。还记得之前的结论吗?$[l,r]$的区间长度是$r-l+1$,划分了$r-l+1$的长度会形成$2(r-l+1)-1$个节点个数。因此如果当前节点是$x$,它的左子树就有$2(r-l+1)-1$个节点,而右儿子恰好就是左子树全部遍历完之后的下一个节点,因此右儿子的节点编号就是$x+2(r-l+1)-1+1$,也就是$x+2(r-l+1)$了。而$l,r$这些值在我们遍历的时候都是自然维护了,所以计算左右儿子都不需要引入任何额外的变量。build()
代码如下所示1
2
3
4
5
6
7
8
9
10
11
12
13
14template<typename Container>
T build(const Container &a, int cur, int left, int right) {
if (left == right) {
return tree[cur] = a[left];
}
int mid = (left + right) >> 1;
- int lc = cur << 1;
- int rc = cur << 1 | 1;
+ int lc = cur + 1 ;
+ int rc = cur + ((mid - left + 1) << 1);
T l = build(a, lc, left, mid);
T r = build(a, rc, mid + 1, right);
return tree[cur] = combine(l, r);
}
注意到相比之前的代码,我们仅仅是修改了左右儿子的节点编号lc
和rc
而已。
虽然开$4n$的空间实际上并不会有什么太大的影响,但使用新的节点编号方法并不会增加任何代码的复杂度,相反还能节省一半的空间,因此在我看来这种方式更加优雅。从另一个角度来说,将线段树的数组空间排列紧密化可以更好地利用局部性原理,在某种意义上对性能有一定的提升。
上述思想参考了tourist大神的线段树代码,在此默默表示感谢。
]]>探索折腾博客的终极意义
折腾博客的意义是什么?这两天看到群里有个人说的很对:虽然明知没人来看自己博客,但是折腾起来就是很爽。就像虽然看起来我像是博客3个多月没更新了,但实际上实际上我网站已经折腾了好几个月了,只是没有新写文章而已。总的来说,我把我原来对NexT主题的修改重新集成为一个npm插件,方便其他人使用。另外我添加了更多自动化的操作,不但以后只需要push新的文章,CI就能自动更新网站。并且,Travis的定时运行任务功能可以方便地自动拉取主题的最新代码,自动更新主题版本,从此再也没有主题过时的烦恼,简直爽歪歪。
当然写这篇文章的最终目的,那当然就是来推广我的nppm插件的。所以下面让我来介绍一下这个插件hexo-disqus-php-api。
总的来说,这是一个集成Disqus服务的评论插件。我一直使用disqus-php-api来解决国内无法直接访问Disqus的问题,它依赖一个PHP后端来转发Disqus API请求,从而实现了直接评论的功能,并且这个项目还实现了很多Disqus评论框支持的额外功能比如表情投票、相关文章推荐等等。简直可以媲美原生的Disqus评论框了,相比那些只能看不能评论的反向代理插件不知道高到哪里去了。
这一段我会介绍原来的集成方法,如果只想了解使用方法可以跳过这一部分。由于NexT主题本身没有这个项目的相关支持,因此在此之前,我是手动修改主题中的相关swig代码,那个时候NexT还是7.1版本,NexT对于第三方评论插件的集成方式还比较混乱,有多混乱?看下面这段代码就差不多知道了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18{% if theme.disqus.enable %}
{% include 'disqus.swig' %}
{% if theme.disqusapi.enable %}
{% include 'disqusapi.swig' %}
{% elif ... and ... and ... %}
{% include ... %}
{% elif ... and ... and ... %}
{% include ... %}
{% endif %}
{% if page.comments %}
{% if theme.livere_uid %}
{% include 'livere.swig' %}
{% elif ... %}
{% include 'gitment.swig' %}
......
{% endif %}
{% endif %}
这还只是其中的一部分,你可以在这里和这里找到更多的类似这样的丑陋代码。这些代码导致如果想要集成一个新的评论集成,你就得再补一个elif
上去,估计这些代码也是经过一次次的elif
变的越来越臃肿的。同时这也导致另一个问题,每次主题更新的时候,你需要合并这些你自己的修改,但是一旦上游对这些文件进行了部分修改,Git的自动合并就很容易失败,到时候你就得打开这些文件,对着丑陋的代码一行一行看了。
长痛不如短痛,NexT主题在下一个版本中进行了巨大的重构,这些代码被通过Hexo提供的一个叫做theme_inject
的功能干掉了,所以之前我更新主题的时候发现,呀!代码怎么全变了呢?这要我如何修改,所以之前的很长一段时间我的主题都停留在7.1.2版本,主要原因就是重构太大没办法把我自己修改的代码合并进去了。
所以趁最近一段时间,我大概重新了解了一下NexT进行的重构内容,仿照它实现其他第三方主题插件的思路,以插件的形式把disqus-php-api集成到了NexT主题中。
现在,你不需要对NexT主题代码进行任何修改了。你只需要使用以下命令安装npm插件即可。1
$ npm install hexo-disqus-php-api
然后只需要在Hexo的配置文件_config.yml
中添加相应的设置项1
2
3
4
5
6
7
8disqusapi:
enable: true
api: # Your server endpoint
forum: # Your disqus shortname
mode: 2
timeout: 2000
emojipreview: true
relatedtype: Related
这些配置项的含义可以在disqus-php-api中找到。好了!一切完成!
由于从此不再需要修改任何主题代码了,而主题的配置文件也可以通过source/_data/next.yml
重写,所以主题就已经完全和网站的任何配置解耦合了,那么为啥不直接把主题文件夹从仓库中删除,然后让Travis自动下载呢?
达成这一目的很简单,只需要在travis.yml
中额外添加两行命令:
1 | ... |
这两行代码表示创建对应的文件夹,并从NexT项目中拉取最新的稳定版代码。
最后你只需要给Travis设置定时运行任务,比如每周,这样Travis每周就会帮你部署一次网站,你的网站也可以在每周都保持最新版本了。
最后再引用一句来自群友的评论
I like NexT, beacuse there is a large num of developers had a good maintenance of this theme. In the meanwhile, I dislike NexT, because there was huge differences between two edtions.
作为NexT的活跃贡献者之一,我真诚希望NexT能继续积极开发维护,带来更多更有趣的功能及特色。
]]>学习一下SOTA语言模型
这篇文章可以称得上是2018年NLP方面一个里程碑式的论文了。当时,BERT模型在GLUE评测榜上横扫其他所有模型,在11个NLP任务上达到最高。尽管这篇论文的阅读笔记在各种博客、论坛等地方都能看到,但我觉得仍然有必要仔细的阅读一遍原文。一来可以加深对论文的理解,二来通过阅读笔记的形式可以更好地记忆这篇文章的细节,不容易忘记。BERT这篇文章通俗易懂,整体结构完整,条理非常清晰,适合所有学习NLP的人阅读。但阅读前需要对Transformer有所了解。
如上图所示,BERT模型几乎完全基于Transformer结构的堆叠,更确切地说,BERT模型是基于Transformer结构的Encoder部分。这也是该模型与GPT模型的最大不同,后者使用的是Transformer结构的Decoder部分,这意味着句子中的每个单词在计算self-attention时只能看到它左边的部分。
简单地回顾一下Transformer结构,它由6层Encoder和6层Decoder组成,Encoder用于将输入转变成某种中间表示形式,而Decoder则从中间表示形式输出具体的序列结果。两者的区别还包括,Encoder的self-attention是双向的,每个单词都会和句子中的所有其他单词做点积。而Decoder的self-attention为了防止提前看到后面的单词,在计算self-attention时会先乘以一个上三角的mask矩阵。
BERT与Transformer的另外一个主要区别是,BERT中的Positional Embedding是由模型自己学习的,而Transformer中则是硬编码的。但BERT这篇论文中没有介绍Positional Embedding具体是如何学习的。
一种显而易见的办法是,单独添加一个Embedding层,然后学习一个$\text{seq_length} \times \text{word_dim}$的矩阵,这样就可以构建一个Lookup表来处理任意位置的Positional Embedding。
最后,BERT的输入由三部分求和得到:单词的Embedding、Positional Embedding以及Segment Embedding,Segment Embedding用于区分输入的两句句子。输入之所以是两句句子主要是训练BERT过程中的Next Sentence Prediction (NSP) 任务,这在后面一节会继续介绍。
搞清楚BERT模型的结构之后,下面来看训练BERT的具体方法。BERT通过两个子任务进行训练。1. Masked LM (MLM) 2. Next Sentence Prediction (NSP)
这两个任务是同时进行的,如上图所示。模型的一系列输出中,左侧的NSP
标签是NSP任务的输出结果,而后面的所有输出都是MLM任务的输出结果。
该任务相比传统的Language Model任务,多了一个Mask的操作。具体来说,由于BERT使用了双向的Transformer Encoder结构,每个单词都能看到句子中的所有其他单词,因此如果直接使用Language Model任务来训练BERT,那么由于模型已经“看到”了整个句子,因此很容易就能预测出所有的单词。为了解决整个问题,BERT提出了MLM任务来对模型进行训练,就是将句子输入到模型中时,随机的遮掉一些单词,BERT使用[MASK]
这个token来代替句子中的原有单词。同时,如果一直使用[MASK]
来代替句子中的原有单词,那么在后续对BERT作fine-tuning时由于输入数据不一定含有[MASK]
,会导致输入空间不一致的问题。BERT并不总是使用[MASK]
替换单词。事实上,对于一句句子,BERT首先选择$15\%$的单词。然后对于选中的哪些单词,以$80\%$的概率替换为[MASK]
,$10\%$的概率随机替换为另一个单词,$10\%$的概率保持不变。
一个可能存在的问题是,这一做法是否会影响BERT对Language Model的学习效果?作者在附录中提到,由于随机替换为另一个单词只占全部数据的$15\% \times 10\% =1.5\%$,因此不会影响Language Model的学习效果。
LM任务只能学习句子本身的特征,对于如问答、自然语言推断等NLP任务,需要学习句子之间的关系,BERT提出使用NSP任务来学习句子之间的关系。每一个训练样本中包含两个句子A和B,NSP要求判断B是不是A的后面一句句子。训练样本中$50\%$的数据满足B是A的后续句子,剩下的$50\%$则是随机选择的句子作为B。后续的实验也表明,NSP任务的训练使得BERT在问答、句子推断等任务上取得了很好的效果。
BERT同时使用MLM和NSP这两个任务进行训练。比如以下是两个样本例子:
1.1
2Input: [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP]
Output: <IsNext> the man went to the store [SEP] he bought a gallon of milk [SEP]
2.1
2Input: [CLS] the man went to [MASK] store [SEP] penguin [MASK] are flight ##less birds [SEP]
Output: <NotNext> the man went to the store [SEP] penguin ##s are flight ##less birds [SEP]
NSP和MLM是两个不同的分类任务,直接在transformer结构后面接一层全连接,然后再套一个softmax就可以用交叉熵来计算Loss了。训练的总Loss就是这两部分Loss的和。
训练好了BERT,怎么把BERT运用到具体的NLP任务中呢?作者建议使用两种办法,feature-base和fine-tuning。feature-base就是固定BERT的参数,直接使用BERT的输出来作为提取的特征,接到其他的模型中。而fine-tuning就是直接在BERT后面接一层全连接+softmax,然后直接在BERT上面跑end-to-end地跑下游任务。作者在论文中说,基本上只需要3-4个epoch就能跑到相当不错的效果了,因此fine-tuning需要的计算开销非常小。并且,由于BERT使用句子对进行预训练,因此对于单个句子的NLP任务,比如句子分类、标注任务,只需要让A等于那个句子,B置空即可。而对于多句句子的任务,可以适当地分配A和B。比如在阅读理解中,可以把A作为阅读材料,B作为问题;对于句子关系判断任务,可以直接将两句句子放到A和B中等。
实验部分这里就不细讲了,简单地说,就是BERT在GLUE评测的12个NLP任务中的11个横扫其他全部模型,总分跃居第一,而且很多任务的准确率相比SOTA甚至有$10\%-20\%$的提升,关于具体的实验结果可以去看原论文。
BERT的提出象征着NLP的一个飞跃式的发展,NLP方面的深度学习方法从此从预训练词向量发展到预训练模型。虽然作者谦虚的表示,BERT的提升最本质的原因只是把GPT的单向Transformer模型改进到双向的Transformer,但是从大量的实验上仍然可以表明,BERT的贡献无疑是十分巨大的。另一方面,BERT的成功还表明了海量无标签文本语料的重要性,催生了后续一大批以更大量文本作为预训练数据的模型的诞生。
]]>使用DCNN对语言进行建模
使用CNN进行语言建模已经取得了较广泛的应用。本文作者提出了一个动态卷积网络DCNN,这是一个针对卷积神经网络的扩展,不需要依赖语法树,并且作者提出了许多比较新颖的概念,比如宽卷积、动态k-max pooling,这些特性使得DCNN可以捕获长短依赖,并且丰富了DCNN提取的特征。
句子经过embedding后,首先会通过宽卷积。通常情况下,卷积核只会在句子本身的窗口上移动,因此卷积操作会忽略边缘的数据,这种操作也被称为窄卷积。对于句子来说,如果句子的长度是$l$,卷积核的宽度是$s(s>1)$,那么卷积后的宽度变为$l-s+1$。对于宽卷积来说,卷积核会超过句子本身的窗口,多余的位置会通过padding补齐,因此宽卷积操作后,宽度变为$l+s-1$,对于$d$维的embedding操作,宽卷积后的矩阵大小为$(l+s-1) \times d$。
通常情况下,卷积操作后会使用max-pooling来过滤有意义的值。在本文中,作者使用k-max pooling来代替max-pooling。这两者的区别是,max-pooling只取最大的一个数值,而k-max pooling则取最大的$k$个数值,相比max-pooling,k-max pooling能捕获更多的特征。并且对于得到的这$k$个数值,它们的顺序和在原矩阵上的顺序是一致的。这样做的好处是,句子原先的先后顺序不会被破坏掉,因此句子原先存在的某些依赖可以仍然保持。
作者所谓的动态k-max,其实是指每一层的$k$是不固定的,实际上,在固定输入长度后,每一层的$k$仍然是提前算好的,并不会在训练时发生改变。
其中$L$表示宽卷积的总层数,$l$表示当前宽卷积的层数,$k_{top}$表示最后一层的k值。
使用多个feature map,并且k-max pooling后使用一层非线性激活函数,基本上和CNN类似,不细讲了。唯一需要注意一点的是,作者说
After (dynamic) k-max pooling is applied to the result of a convolution, a bias and a non-linear function g are applied component-wise to the pooled matrix.
这句句子的意思到底是每一层的max-pooling后面都接了全连接,还是只有最后一层接了全连接呢?按常理来说,只在最后一层全连接更加常见。但我看到很多其他关于这篇的论文笔记上说每一层后面都接了?单从作者给的模型图上面来说,似乎只有最后一层接了全连接,但也有可能是作者没有画全吧。
最后作者还是用了一个称之为Folding的操作,它用在最后一层卷积的k-max pooling后面,这个操作具体来说就是将$d$维的矩阵的所有相邻维度两两相加,变成$d/2$维的矩阵。因此该操作不需要引入额外的参数,并且这个操作发生在全连接层之前,因此不会产生交叉依赖的问题。
总的来说,这篇文章提出了一种新颖的动态CNN网络。相比普通的CNN,它不仅通过使用k-max pooling提取更多的特征,并且使用宽卷积避免了无法很好地提取句子边缘特征的问题。此外,这篇文章中提出的folding,dynamic k-max pooling操作对现在的网络构建仍然具有一定的参考意义。
]]>今天的论文来自于较老的几篇论文,使用CNN进行文本分类。
CNN最早被成功运用在图像处理中,因为图像的位置不变性、大小不变性使得CNN处理图像再适合不过。而将CNN运用于文本分类流行于2014-2015年左右,大概处于在NLP被RNN统治的前几年,因此虽然这些论文年代已经相对比较久远,但仍然值得一读,因为通过对这些论文的阅读,还能大致了解为什么CNN在NLP领域也能取得成功,CNN在NLP领域存在什么问题,以及在NLP领域CNN的使用是如何慢慢过渡到RNN的使用的。
这是发表在EMNLP 2014上的一篇short paper,这篇文章使用多个filter组成的CNN构建了一个分类模型。
模型的结构直接用论文中的一张图就可以比较清楚的说明。
输入的文本经过word2vec的embedding生成长度为$k$的向量后,成为一个$l \times k$的矩阵$\mathbf{x}_{1;n}=\mathbf{x}_1 \oplus \mathbf{x}_2 \oplus … \oplus \mathbf{x}_l$。然后使用宽度为$k$,长度为$3,4,5$的卷积核(filter)对这个矩阵进行扫描。
因为卷积核的宽度和embedding的维度相同,因此相当于卷积核在单词的方向上滑动(sliding window)。对于每个单独的卷积核$h \times k$,将生成对应 $l-h+1$ 个feature。
然后做一个max pooling
这个$\hat c$就是当前这个卷积核的特征了。这样做的一个好处是,可以使用不同长度的卷积核,因为每个卷积核最后都会生成一个$1 \times 1$的特征,因此可以把这些特征拼起来,再接一个softmax就可以用做分类了。
作者在实际训练的时候使用在全连接层使用了dropout,另外还加了l2正则。实际训练的时候使用长度分别为3, 4, 5的卷积核,每个长度100个。使用的数据集基本上都是情感分类相关的,有:MR(影评,2分类)、SST-1(情感,5分类)、SST-2(情感,2分类)、Subj(主观性,2分类)、TREC(问题分类,6分类)、CR(商品评价,2分类)、MPQA(观点评价,2分类)。
此外,作者尝试使用了不同的预训练模型组合,包括:不使用预训练模型,随机初始化embedding、使用静态的预训练模型、训练时微调的预训练模型、以及同时结合静态和微调的预训练模型(使用两个通道,一个通道静态,一个通道微调,卷积的时候直接相加)。最终的结论是,微调的比不微调的要好一点,但是两者结合的和微调的差不多。
我认为这篇文章最有价值的贡献就在于如何把CNN运用到NLP中,作者使用了sliding window的方法,保持宽度不变的同时使用了不同长度的卷积核,通过一个max pooling可以把这些卷积核的特征拼在一起,十分巧妙。max pooling看起来是最常用的压缩特征的方法,也有人用average pooling的,但是看起来还是max pooling效果最好?我一个不太理解的地方在于,为啥max pooling是最有效的呢,如果正数和负数是对称的,那我如果我用min pooling,每次取最小的那个有没有效果呢?
这篇文章发表在NIPS 2015上。这篇文章和上面Kim这篇非常类似,最大的区别在于Kim使用了word2vec作为预训练模型,而这篇直接使用one-hot把文本作为字符编码,直接将字符编码的矩阵作为输入,不使用任何预训练模型。另外一点不同的是,这篇文章使用了多层的卷积层,而Kim这篇是单层的。
模型结构非常类似。作者使用维度为70的one-hot向量编码句子中的每一个字符,然后把它们全部拼起来。作者选取前1014个字符,截断多余的。拼成一个$1014 \times 70$的矩阵。接下来对这个矩阵进行卷积操作,方法应该也是类似的,卷积核在矩阵上进行一维方向上的滑动。在前2层,使用高度为7的卷积核,每个卷积层后面接一个高度为3的max-pooling。后面4层分别使用高度为3的卷积核,并且最后一层再接一个max-pooling。因此对于输入长度为1014,前6层后的长度变为$(((1014-7+1)/3-7+1)/3-2-2-2-2)/3=34$,矩阵大小变为$34 \times 70$,然后拍平成一维的向量,就可以接后面的全连接层了。
作者对训练数据做了增强,具体就是使用一些同义词替换原来的句子,丰富训练数据。训练数据也使用了大规模的文本语料,包括AG新闻分类、雅虎新闻分类等等。最后实验发现,在数据规模没那么大的时候(~50万),n-gram+TFIDF的表现还是最后,但在数据规模更大的情况下(~100万-300万),作者提出的模型效果更好。
这篇文章不借助任何先验知识(预训练模型),但能比使用word2vec的CNN模型表现更好(相差不大),一方面是由于这篇文章的使用多层的CNN结构,学习特征的能力更强。另一方面,使用字符级别的特征可以不去在意文本本身的语义信息,从而更好地处理通用的文本序列,甚至可以是用户自己定义的某种语言,相比借助预训练模型的网络具有更好地泛化能力。
这篇文章来自于AAAI 2015。作者认为,现有的CNN模型受限于固定长度的窗口大小,无法很好的学习上下文,这会导致诸如一词多义,长依赖语义等问题没有办法被现有的模型学习到,为了解决这个问题,作者自己提出来了一种称之为RCNN的结构,虽然这篇也含有CNN,但实际上并非广义上的CNN,而是作者自己提出来的一种模型结构。
本文的模型结构如下图所示。
作者使用$\mathbf{c}_l(w_i)$和$\mathbf{c}_r(w_i)$来分别表示当前单词的上文和下文,$\mathbf{e}(w_i)$则表示单词本身的embedding。这样$\mathbf{c}_l(w_i)$和$\mathbf{c}_r(w_i)$的就可以用如下公式计算。
这样$\mathbf{c}_l(w_i)$就包含了所有$w_i$左边(上文)单词的语义信息,$\mathbf{c}_r(w_i)$就包含了所有$w_i$右边(下文)单词的语义信息,这三个向量每个向量的维度都是50,接下来只要把$\mathbf{c}_l(w_i)$, $\mathbf{e}(w_i)$, $\mathbf{c}_r(w_i)$三个向量拼起来,就可以作为每个单词的语义表示了。
后面的套路就和大部分文章差不多了,每个$\mathbf{x}_i$先接一个线性层,然后把$\mathbf{x}_i$拼起来后用一次max-pooling,最后再接一层全连接+softmax就结束了。
论文的其他部分没什么特别的创新之处,在此就不赘述了。
这篇文章提出了一种新颖的RCNN结构,相比CNN,RCNN可以捕获更多的上下文语义。而相比RecursiveNN,它可以通过max-pooling筛选更有价值的信息,并且构造模型的复杂度更低。这篇文章算是同时结合了RNN和CNN的优点,但我认为主要还是依靠RNN。这篇文章也算是比较早的使用RNN进行文本分类的论文之一了,之后几年就开始有大量的运用RNN/LSTM到NLP的论文涌现。
总的来说,CNN在NLP领域确实取得了一定程度的成功,这一方面归功于深度学习中强大的计算学习能力,另一方面归功于CNN本身,将图像处理中正方形的卷积核稍作改动,使用滑动窗口的方法将卷积核沿着句子中的单词方向上滑动,这一做法本身非常符合直觉,因此确实取得了一定的效果。然而,我们也能看到CNN在NLP领域中存在的问题,比如正是由于滑动窗口的方法固定了窗口大小,使得模型效果很容易受到其影响,另外这一方法使得模型无法很好地学习单词上下文的信息,这些问题反过来也限制了模型的性能。
使用PyTorch的一些笔记,以防写完就忘,看完API又想起来,长此以往。
LSTM中的hidden state
其实就是指每一个LSTM cell的输出,而cell state
则是每次传递到下一层的「长时记忆」,我总觉得这个名字起的特别别扭,所以总不能很好的理解。下面这张图能更好的说明这些变量的意义。
再来简单的回顾一下LSTM的几个公式
其中$h_t$和$c_t$就是所谓的hidden state
和cell state
了。可以看到LSTM中所谓的output gate
,即$o_t$其实是中间状态,它和cell state
经过$\tanh$相乘,得到了hidden state
,也就是输出值。
PyTorch中LSTM的输出结果是一个二元组套二元组(output, (h_n, c_n))
。第一个output
是每一个timestamp的输出,也就是每一个cell的hidden state
。第二个输出是一个二元组,分别表示最后一个timestamp的hidden state
和cell state
。因此,如果把h_n
和c_n
记录下来,就可以保留整个LSTM的状态了。
PyTorch中可以通过bidirectional=True
来方便的将LSTM设置为双向,此时output
会自动把每一个timestamp的正向和反向LSTM拼在一起。而h_n
和c_n
的第一维长度会变为2(单向是长度为1)。而且此时有
即正向output
的最后一个timestemp(对应LSTM的最后一个cell)的输出和正向的hidden state
相同,反向output
的最后一个timestamp(对应LSTM的第一个cell)的输出和反向的hidden state
相同。
此外,在PyTorch中,LSTM输出的形状和别的框架不太一样,它是序列长度优先的,(seq_len, batch_size, hz),如果觉得不习惯,可以通过batch_first=True
来设定为batch_size
优先。
如何求出1-n每个数的全部因子?
在继续往下阅读之前,请先想一想,如果给定一个$n,n\leq100000$,让你求出1~n的全部因子,你需要如何实现?要编写多少行代码?复杂度如何?
可能会这么考虑,如果对于每个数$x$,枚举1~n逐一判断每个数是否是$x$的因子,复杂度为$\mathcal{O}(n^2)$,显然是不可接受的,即使枚举到$\sqrt n$,复杂度也有$\mathcal{O}(n \sqrt n)$,依然太大,有没有更好的办法?
在我一开始解决这个问题的时候,我先求出了1-n的全部素数,这在$\mathcal{O}(n)$时间内可以解决,然后对1-n中的每个数因式分解,注意到每个数$x$可以写成
的形式,所以$x$共有$c=(q_1+1)*(q_2+1)*…*(q_k+1)$个因数,穷举这些因数,所以每个数可以在$\mathcal{O}(\textrm{c})$的时间内解决,总时间复杂度为$\mathcal{O}(nc)$,由于$100000$以内的数含有最多的因子数也就100多个,因此时间复杂度上可以接受。
听起来很合理是吗?然鹅,我写到一半就写不下去了。因为……实!现!起!来!太!麻!烦!了!
维护好素数表之后,你要对每个数因式分解,然后遍历统计出$q_1,q_2,…,q_k$的值,其中有些值还是0,为了去掉这些0加快穷举速度,你需要用一个map<int,int>
记录$p_i,q_i$,然后怎么穷举因子呢?还要再写一个dfs遍历map……
太麻烦了吧!有没有什么好办法呢?就是那种……很简单的……只需要几行代码就能全部搞定的那种
其实只需要三行就能搞定啦1
2
3
4
5for (int i = 2; i <= n; i++) {
for (int j = i; j <= n; j += i) {
fact[j].push_back(i);
}
}
这个方法其实和使用筛法求素数是一模一样的,就是1-n的每个因子$x$一定是$kn$的一个因子,所以遍历所有因子,计算这些因子的倍数并加上这些因子就可以了。可以算出总操作次数为$n(1+\frac{1}{2}+\frac{1}{3}+…+\frac{1}{n})$,由欧拉公式大概可以估计出,总复杂度为$\mathcal{O}(nlogn)$
好了,至于本文的标题Codeforces - 1139 D. Steps to One其实并不是求个因子就完事啦,这题还要用容斥原理+dp求解,但这些都不是重点了,所以我们就一笔带过吧。设$dp[x]$表示当前gcd为x时的期望长度,则dp方程如下
其中$f(x,i)$是指满足$gcd(x,k)=i,1\leq k \leq m$的$k$的个数,$f(x,i)$可以通过容斥原理在线性时间内求出,最后的答案为
虽然看起来不容易,我也以为有点难,但是实现起来不是很复杂,很快就可以写完了(并不)
这题还可以用莫比乌斯反演来求$f(x,i)$,啊,还是等有时间再学吧。
]]>关于动态规划的一些新感悟。
自从这学期学校开设了算法课之后,受到算法课老师的鼓励我突然又有了写算法题的动力。特别是老师第一节课重新回顾的动态规划算法,使我对动态规划有了一些新的理解。
其实之前我一直对动态规划有一种说不出来的敬而远之地感受。我最早接触动态规划算法可以追溯到高中的时候了,那个时候去少年宫学各种算法(顺便和小伙伴们愉快的玩耍),记得学到最短路径的dijkstra算法,当时学了之后不由感叹,哇!还能想出这样的算法,也太强了吧!这怎么想得出来嘛!所以从那个时候起我每次知道某道题要用动态规划去求解时总会不由自主地头大。因此其实很长一段时间以来,我的动态规划的水平仅仅停留在最粗浅的三角形路径最大问题上(就是那个很naive的给一个三角形让你求一条路径使得从左上角到右上角权值和最大)。
当然也有很多小伙伴和学长学姐告诉过我,动态规划的方程式要满足三个特性,什么最优子结构,无后效性,子问题重叠,我都能熟练背诵了。但是知道这三个特性又有什么用呢,大多数情况下,我要么不知道如何表示状态,要么不知道如何写出方程式。当然写不出来方程式,我就会去看题解,看它的方程式,然后看了方程式就会实现了,但是看完写完了AC了,之后如果过一段时间再去会过来看这道题,我八成又忘记当初的那个方程式了。
所以我冥冥之中总觉得,我不能说不会动态规划算法,我也能大致理解动态规划算法的原理,但我可能一直缺少某种方法或者工具,导致在实际做题时始终无法得到正确的求解状态和方程式。
不过在听了上一周的算法课之后,我总算顿悟了一些东西!虽然,我不能保证今后碰到的所有动态规划题目我肯定全部能做出来,但我至少在动态规划问题上面增长了不少的信心。长久以来,我一直对动态规划的方程式有很多疑问。为什么方程式是这么写的?为什么有些方程式要从左边和右边分别计算,有些方程式只需要计算一边?为什么写方程式的时候只要考虑当前的状态就行了?为什么考虑了当前的状态整个问题就能解决了?怎么样才能写出合理的方程式呢?
老师在课上介绍动态规划算法的时候非常简单,几乎是举了一个例子之后就跳过了,不过我从这个例子中学到了动态规划算法的本质,那就是:填表。
可能有些人看到这里要失望了,我卖了这么大的一个关子,结果居然就是两个字填表。但事实是,在上这节算法课之前我完全不了解动态规划居然和填表有密切的关系。在填表的过程中,你可以逐渐了解到表中的每个数字是如何计算出来的,而列出动态规划方程的本质其实就是弄清楚表中每个格子的数字是如何被计算出来的,存在怎样的依赖关系。你也许一开始无法直接写出方程式,但通过在表格中的填数,你可以慢慢冷静下来,一步一步的去思考表格中每个数字的形成过程,这实质上也正是动态规划方程的状态转移过程以及它们的递归关系。想清楚怎么填表,也就想清楚了怎么写方程式。
你也许还会有疑问,很多动态规划的问题不仅仅是二维的,有时候甚至是三维的,四维的,这个时候怎么填表呢?我相信,在熟练掌握了二维的动态规划后,你其实不必真的写一张二维的表格。你只要在心中默默地有一张表格,想清楚状态之间的递推和依赖关系,就能很自然的写出方程式了。
最近完成的几道dp题目:
- 乌龟棋
- $pos \leftarrow i+j \times 2+k \times 3+l \times 4$
- $dp[i+1][j][k][l]=dp[i][j][k][l]+s[pos+1]$
- $dp[i][j+1][k][l]=dp[i][j][k][l]+s[pos+2]…$ 以此类推
- Codeforces 1114D. Flood Fill
- $dp[l-1][r][0]=\min \{ dp[l][r][0] + diff(a[l - 1], a[l]), dp[l][r][1] + diff(a[l - 1], a[r]) \} $
- $dp[l][r+1][1]=\min \{ dp[l][r][1] + diff(a[r + 1], a[r]), dp[l][r][1] + diff(a[r + 1], a[l]) \} $
- Codeforces 1132F. Clear the String
- $dp[l][r] = \max \{1 + dp[l + 1][r], \min \limits_{l < k \le r, s_k = s_r} dp[l + 1][k - 1] + dp[k][r] \}$
最后,祝每个人在做动态规划题时能做到:眼前无表,心中有表。
]]>使用对象存储部署静态网站,并通过亚马逊CDN(CloudFront)大大加快网站的访问速度
我原来的Hexo博客部署在GitHub Pages上,因为GitHub Pages在国外,所以为了加快访问速度,我做了很多优化的工作。然而,连接的响应延迟实在是不能忍,初次打开网站的时间有时候可能要半分钟之久,另外GitHub Pages无法被百度访问到,因此百度也不会收录GitHub Pages部署的网站,所以我最近在不断寻找其他的代替方案。
首先,我寻找了一些GitHub Pages的代替方案。国内也有一些网站提供了类似GitHub Pages的服务,比如Coding Pages和Gitee Pages等。这两个Pages服务同样也不需要备案,所以我都试用了一下。发现速度还是一如既往地很慢,Coding Pages服务器部署在美国等地,而Gitee Pages只有一个单独的阿里云香港结点,速度甚至还不如GitHub Pages。国外也有一些类似的静态网站托管平台,比如netlify和heroku等。这两个体验还不错,都可以直接连GitHub的一个仓库,并在Push的时候自动更新,然而netlify的访问速度和GitHub Pages差不多,heroku的访问速度较快,但是Free dyno计划不支持自定义域名的SSL加密,解决速度慢的一个有效办法是使用CDN加速,但是国内的CDN加速都需要备案。
服务 | 访问速度 | 支持自定义域名 | 支持SSL | 构建方式 | 其他限制 |
---|---|---|---|---|---|
Coding Pages | 较慢 | 是 | 是 | Git Push | 无 |
Gitee Pages | 慢 | 付费购买Pro版 | 付费 | Git Push | 无 |
GitHub Pages | 较慢 | 是 | 是 | Git Push | 百度不收录 |
netlify | 一般 | 是 | 是 | 自动同步GitHub仓库 | 无 |
heroku | 较快 | 是 | 付费支持 | 自动同步GitHub仓库 | 定时休眠 |
除了这些平台之外,还有很多国内网的对象存储服务也提供了静态网站的托管,而且还支持自定义域名的绑定,但在国内要使用自定义域名绑定同样需要给域名备案,否则只能用提供的域名访问。确实对象存储服务应当是目前最快的托管静态网站的平台了,但是如何解决自定义域名的问题呢?我的第一个想法是,使用国外的对象存储服务,于是我很自然地想到了亚马逊的S3存储。
大概看了一下文档,发现S3同样也是支持静态网站的托管的,我把文件传上去试了一下,发现确实可以托管静态网站,但是自定义域名不支持HTTPS,这就很头疼了。我继续查阅相关的文档,发现官方给出的方法是使用CloudFront作S3的分发,而CloudFront可以通过ACM生成证书,来支持HTTPS的自定义域名。我研究了半天,发现亚马逊所谓的CloudFront其实就是CDN服务,只是起了一个名字而已。
看到这里,我心里有了大概的一个解决方案,就是使用亚马逊云的S3做静态网站托管,配合CloudFront做分发。因此我初步尝试了一下,不得不说,S3提供的配置选项非常非常详细,相比国内的OSS提供商就提供公有读/私有写这种权限,S3大概列出了20多种权限,并且需要编写权限策略脚本来控制权限。详细归详细,带来的问题就是花了我好长时间大概搞懂了整个配置过程。配置完S3后再配置CloudFront,又是各种繁琐的配置过程。配置完又是各种错误,各种Google之后才好不容易解决了。在看到StackOverflow别人的解答后我突然发现,CloudFront其实并不一定需要和亚马逊云的其他服务配合使用,也可以直接提供源站让CloudFront给我加速呀。这样一方面我不用再配置繁琐的S3了,另一方面我也不用为S3付费了,使用国内的OSS还能方便不少。
考虑到腾讯云的对象存储COS每个月提供50GB的免费流量包,我最后的部署方案就是,将静态网站托管在COS上,然后使用静态网站托管功能生成静态网站,COS会提供一个静态网站的访问节点,然后我再使用CloudFront将该访问结点作为源站,然后使用我自己的域名作为CloudFront的对外访问节点。由于COS提供的是腾讯云的地址,没有使用自定义域名,所以直接可以访问,而CloudFront服务在国外,因此不需要给域名备案,这样整个问题就解决啦!
下面,我将大致的介绍整个部署的步骤。你需要准备的东西有:
首先,登录到腾讯云对象存储COS,点击左侧的存储桶列表,创建一个新的存储桶。由于浏览用户并不会直接接触存储桶,访问存储桶的主要是亚马逊云的CloudFront服务,在选择存储桶地域时,推荐选择美国的节点以加快CloudFront访问存储桶的速度。请注意要将访问权限设置为“公有读私有写”以对外提供访问。
接下来,上传Hexo的public文件夹中的所有内容。你可以直接使用Hexo的COS的部署工具hexo-deployer-cos来进行部署。
最后,存储桶的基础配置中开启静态网站按钮,记录下COS提供的访问节点。你也可以直接打开该访问节点来测试静态网站是否部署成功。
到这里,存储桶的配置部分就完成了。
在开启CloudFront分配之前,我们还需要在AWS Certificate Manager(ACM)中添加一张自定义域名的证书以支持HTTPS访问。访问AWS Certificate Manager页面,选择请求证书来生成证书,你可以导入已有的证书。然后输入你的自定义域名,AWS可以直接分发泛域名证书,你还可以在这里输入多个域名。
接下来,你可以选择使用DNS验证或者使用电子邮件验证的方式验证域名所有者。DNS验证要求你在域名记录中添加一条CNAME记录,使用电子邮件验证会向你的域名的WHOIS信息发送邮件,或者向该域名的所属邮箱发送邮件,两种方式皆可,根据需要选择即可。然后根据要求完成验证。
ACM支持自动续期证书,因此如果你保留DNS记录,ACM会在证书快要过期时自动续期,不需要你做另外的操作了。至此,ACM证书添加完毕。
现在打开亚马逊云的CloudFront,创建一个Web内容分发。在源域名中填写刚刚COS提供的静态网站访问域名,在源协议策略设置为HTTPS。在默认缓存行为设置中开启自动压缩对象,其余设置根据实际需要修改,也可以为默认。
在分配设置中的备用域名填写你的自定义域名,并在SSL证书中选择“自定义SSL证书”,然后选择刚刚在ACM中添加的证书。
最后点击创建分配,CloudFront会自动开始部署。大约等待20分钟左右后就可以完成了!部署完成后,将你的域名指向CloudFront提供的域名,CDN就全部配置完成了。
以上就是COS+CloudFront的部署方法,在完成CloudFront部署之后,你可能还需要配置文件的缓存时间以进一步提高性能。在价格方面,AWS用信用卡注册后会提供一个一年的免费套餐,里面包含了50GB的流量以及每月2000000个HTTP/HTTPS请求,之后的流量单价也不高,应该是完全可以接受的。COS每个月提供50GB的免费流量套餐,对于Hexo这种小流量的博客网站来说应该也完全够用了。经过测试,初次打开网站的速度可以控制在5S之内,相比远比动不动30S已经有了巨大的提高。
此外,我最近还转移了DNS提供商,发现原来使用的DNSPod已经不能同时添加CNAME和MX记录了,原来是可以的,我还在奇怪为什么突然就不行了,后来查了一下发现原来CNAME和MX本来就不能共存的,DNSPod可以共存是因为之前不规范。搜索了一圈,发现国内的CloudXNS可以通过LINK记录设置隐式CNAME,国外的Cloudflare可以通过CNAME Flattening(也叫ANAME)实现共存。我两个都试了一下,最终选择了国内的CloudXNC,感觉国内的解析应该会更快一点吧,不过我哪天又换掉了也说不定呢 ╮(╯▽╰)╭
你还可以点击下方的链接阅读到更多的Hexo性能优化文章
如果在部署过程中碰到任何问题,欢迎在下方留言,欢迎互相交流,讨论。
]]>用尽各种手段,进一步将网站的传输开销缩短70%以上!
去年的一篇文章提到,把图片以及绝大部分的第三方JS和CSS文件转移到CDN加速服务上,源站的总传输大小从500多KB缩短到了110KB,大约节省了80%的传输开销。今天,我又进一步优化了整个网站,最终测试下,首页仅有33.3KB的数据来自于源站,相比之前的约110KB又进一步节省了70%的大小。
下面这篇文章提供了一个更快访问Hexo的解决方案
Chrome开发者工具中提供的Network选项卡可以很方便的检测整个网站的文件传输情况。通过输入domain:kaitohh.com
筛选仅来自于源站的文件,可以看到所有来自于源站的文件及大小。
可以发现绝大部分文件都是一些网站本身的CSS和JS,最大的文件是当前网站的HTML代码,这些文件由于经常会发生变动,因此不能直接存在对象存储中。接下来,逐个预览这些文件,发现这些文件居然都没有最小化!
那么我们的优化思路就很简单了,只要在部署Hexo之前,把网站的所有HTML、JS、CSS都最小化一遍,就能很大程度的减少这些文件的大小了。
幸运的是,Hexo恰好有这样的第三方插件hexo-all-minifier,安装完这个插件之后,每次在Hexo部署的时候就会自动最小化静态文件了。测试发现,经过最小化后的主页HTML大小从73.2KB缩小到了12.1KB,效果非常显著。
在我之前看来,一个页面100多KB的传输并不是不能接受,所以之前也没有做过多的尝试。直到今天我才发现单单最小化文件居然可以减少如此之多的文件大小。我觉得,优化网站的核心问题并不是问题本身,而在于分析问题。通过观察Network的文件组成,能够发现隐含在其中的最小化问题,这才是解决问题的关键所在。
]]>总结一下<algorithm>头文件中的常用函数。
每隔一段时间,我总能在<algorithm>中发现一些神奇的函数,这些函数我之前基本上没有听说过,所以最近我阅读了一下algorithm头文件的相关文档,把那些很实用,但是之前没怎么听说过的函数简单的罗列一下。我主要阅读的文档来自DevDocs上的algorithm头文件介绍,此外Visual C++也有类似的文档供参考。
1 | void sort(RandomIt first, RandomIt last); // 排序 |
一个多月没写博客了,赶紧填个坑。
离上一篇博客居然已经过去一个多月啦,看来研究生还是要比本科忙多了,每天都有事情要做。最近呢,就是参加了昨天的Google Kickstart Round G 2018,本来顺利做完了2.5题,做完的时候有60名左右,结果有个大数据犯了一个低级错误,结果一个大数据挂了,最后只有100多名!啊!好气啊!看了一下题解,发现方法和我自己实现的方法差挺多的,所以想简单讲一下我的算法。
这道题,就是给你一个序列$A_n,n\leq7000$,让你计算满足$A_i,A_j,A_k$三个数中任意两个数乘积等于第三个数的这样的$(i,j,k) , i \lt j \lt k$三元组的个数。
题解中给出的方法是使用HashSet
存入每个数,同样也可以使用二分。注意到序列的顺序无关,因此我们可以先将数列排序,然后枚举i和j,注意到在绝大多数情况下,$A_i*A_j \geq A_i,A_j$。因此我们要找的乘积一定在$A_i$和$A_j$的右边,而由于整个序列是有序的,因此我们可以通过二分来快速找到相应的值,伪代码如下所示。
1 | for i = 1 to n |
由于我们需要求出序列中$A_i * A_j$的个数,因此在C++
中,可以分别调用lower_bound
和upper_bound
来求出上界和下界,然后相减即可求出个数。
前面提到在大多数情况下$A_i*A_j \geq A_i,A_j$是成立的,那么是否有不成立的情况呢?有的,在$A_i=0$或者$A_j=0$的时候等式就不成立了。这个时候三元组对应的序列就变成了$(0, 0, A_k)$。因此在排好序的序列中,我们需要特判$A_i$和$A_j$都等于0的情况,可以计算得出此时三元组的个数恰好为序列中所有在$A_j$右边的元素的个数。也就是$n - j$。整个算法的复杂度为$\mathcal{O}(n^2\log n)$
这道题,是给你$N$个$[L_i, R_i]$连续区间组成的分数,然后有$Q$个查询,问这些区间中所有数字组成的序列的第$K_i$大的分数是多少,其中$N$和$Q$的范围都在$5*10^5$级别。
题解的方法是离散化+线段树区间维护,似乎有一些麻烦。实际上可以直接将区间拆成$2N$个点,然后从大到小排序。接下来就是处理区间端点的问题了,再遍历断点的过程中,我们需要一个变量$cur$用于记录当前分数有多少个人同分,$sum$记录目前总共排好了多少个人,这样在遍历的过程中不断的将$K_i$和$sum$进行比较就可以了。另外需要注意的是,在遍历过程中如果端点的发生了跳跃,我们需要直接算出$K_i$对应的分数,方法就是计算$sum$与$K_i$的差值,然后整除$cur$即可。整个算法中,排序复杂度是$\mathcal{O}(N\log N+Q\log Q)$,遍历的复杂度是$\mathcal{O}(N+Q)$
]]>使用CDN和对象存储OSS来优化博客的访问速度
由于我的整个博客部署在GitHub Pages上,从国内直接打开的访问速度是很慢的。过慢的访问速度会使得用户在访问网站的时候失去耐心,导致有些用户可能会直接关闭你的网站,这就会使得网站的用户留存率降低。说句题外话,这就是为什么很多网站在设计进度条的时候并没有反映网站的真实访问速度,而是先给你一个很快的加载动画,让会让你产生好像网站就要加载好的错觉,在一定程度上能增加用户的耐心。这种手段除了应用在微信的网页进度条上,还包括比如一些清理软件设计的清理系统进度条上,还有之前Windows 7在文件浏览器中的文件扫描进度条上等等——好像说的有点远了。总之在这篇文章中,我会以我的Hexo博客为例,介绍一些优化网站访问速度的方法,这也是我在优化这个博客时使用的方法。
在开始之前,我还是想啰嗦两句。优化网站的访问速度,其本质就是加快网站资源的下载速度。一般而言,一个网页包含的资源大概有HTML源代码、CSS文件、JS文件、XHR请求、以及媒体资源如图片、视频、字体等。那么应该加快网站的哪些资源呢?首先,HTML代码是用户打开网站时读取的第一个文件,而XHR请求通常会取决于用户每次的访问而有所不同,因此这两类文件无法被直接优化。而剩下的资源,比如CSS文件、JS文件,他们以链接的形式通过<link>
或者<script>
标记嵌入在HTML中。另外,媒体资源也会通过相应的标记比如<img>
、<video>
嵌入其中。我们可以把这些以链接形式嵌入在HTML代码中的文件称之为静态资源文件,这一类文件就是我们着重需要优化的地方了。
还要补充一点的是,在国内加快GitHub Pages访问速度最有效的方法当然是不使用GitHub Pages,而是使用一台国内的服务器来部署,毕竟连接国外的服务器在一开始就会带来至少200ms到300ms的延迟。但即使你使用国内服务器进行部署,通过使用一些优化访问速度的技巧仍然会带来一定的帮助,比如可以减少服务器消耗的资源,节省流量费用等等。
CDN是加快网站访问速度的非常常用的一个办法,你可以在这一篇中通过一个故事大致了解CDN的原理及作用。目前国内有很多运营商都提供了CDN的加速服务,最常见的如阿里云和腾讯云等都有CDN服务,其中价格也相对便宜,比如腾讯云半年100GB的流量包只要20元,阿里云一年100GB的流量包只要24元,对于一个普通的博客网站来说,除非是遭遇大规模的DDOS攻击,否则完全够用了。配置CDN服务器需要你将网站域名的CNAME指向CDN提供商的服务器,同时给源站重新设定一个域名,并在CDN配置界面中指向它,总的来说配置相对简单,可以参考CDN提供商的相关文档,再次不再赘述了。
需要注意的一点是,阿里云和腾讯云的CDN大都要求你的域名通过备案,所以部署在GitHub Pages上的网站是没办法使用它们的CDN服务的。但是,网站的一部分公共资源仍然是可以使用CDN的,比如BootCDN提供了各大静态的CSS、JS、字体等各类静态资源提供了免费的CDN加速服务。因此你可以将你网站所使用的各类公共JS插件以及CSS等使用BootCDN提供的CDN链接替换。对于Hexo来说,一般情况下你可能需要修改源Html代码中的<script>
标记链接才可以。然而Hexo的Next主题提供的配置文件已经预留好了这些CDN配置项,因此你只需要在对应的配置选项中填入BootCDN提供的CDN链接即可。你可以在BootCDN中找到大量的Hexo中需要使用的JS和CSS脚本、如jQuery
、Fancybox
、Velocity
、PACE
等等。
如果你打开开发者工具的Network
选项卡,刷新一次网页,然后按照Size的大小将所有的数据包排序,你会发现数据包最大的无非就是两类文件了,一类是JS脚本文件,另一类就是媒体文件,比如图片。JS脚本的优化我们已经在上一节的CDN优化中完成了,那么如何加快图片的传输速度呢,方法无非是两种,减小文件体积,或是加快文件下载速度。对于图片来说,压缩图片大小是一个比较可行的方法。
第一,压缩图片的尺寸。在插入一张图片之前,我们最好先确认插入在网页后的图片尺寸是否合图片的原尺寸一致,在网页中图片实际大小很小的情况下,使用一张分辨率很高的图片是没有意义的,相反还会增大图片的大小,得不偿失。因此在这种情况下,我们需要先缩小图片的尺寸,这对减小图片的文件大小是很有帮助的。
第二,我们可以使用压缩图片的工具,来做到在图片质量几乎不损失的情况下大幅度的减小图片的文件大小。TinyPNG是一个免费的图片压缩网站,可以将你的图片在几乎不损失画质的情况下减少大约70%-80%的文件大小,所以我们可以把网站上的所有图片都用这个网站来优化一次,你还可以自动化这个操作,比如ImgBot提供了自动优化每次提交时的图片文件大小。对于Hexo博客来说,可能也会有相关的插件可以使用,当然自己写一个自动化TinyPNG的API操作应该也不是什么难事。
最后简单的介绍对象存储的优化方法。对象存储是近几年流行的一种存储方式,简单地说,就是每一份数据文件作为一个单独的对象来进行存储,不需要通过数据库来进行结构上的关联了。更加通俗地说,对象存储就有点类似于以前的图片外链。对象存储由一个个的存储桶构成,在你上传到存储桶之后,就可以直接通过链接来访问上传的文件了。因此通过对象存储,我们可以把网站的所有静态文件都通过对象存储来保存起来,然后通过链接来引用,对于网站中的图片来说,对象存储就是非常适合存储的对象。我们可以直接把图片全部上传到存储桶中,然后把网站中的图片链接指向存储桶中的地址就可以了。
这里还有一些细节需要注意,首先,我们可能需要给存储桶配置允许的Referer
,防止其他的网站直接使用你的存储桶的链接来引用图片,消耗你的流量,此外可能还需要配置CORS
来允许跨域。另外,对象存储一般按照流量收费,价格一般相对便宜。腾讯云提供了每个月50GB的免费额度,对于小型的博客网站来说一般也够用了。
以上就是关于网站速度优化的一些方法。下图是一个优化后的Network
示例。
你可以明显的看到,在全部大于10KB的数据包中,只有needmoreshare.css
、main.css
和网站的源Html
代码是通过网站请求的,其他的数据包都来自于CDN、对象存储,或是外部的JS链接。
你还可以了解更多的Hexo优化技巧
希望以上的网站优化技巧能给大家带来一些帮助,如果你还知道其他任何实用的优化技巧,或是对这篇文章有什么意见或建议,欢迎在下面评论留言告诉我。
]]>本文将通过一个故事带你了解CDN
B报纸是最近新发行的一种特殊的报纸、B报纸会在每周定期发布,并且不对外出售。在B报纸刚刚发行的时候由于读者太少,所以只有在W报社才能看到。此外,报社每次只会印刷少量的B报纸供阅览。小U平时很喜欢看B报纸,因此他会经常专门跑去报社看报纸。
在读者比较少的时候,这种办法相对奏效,但是当观看B报纸的人越来越多的时候,问题就出现了。第一,小U离报社很远,每次要看B报纸的时候,小U都要花好长时间的路程才能到报社。第二,由于看B报纸的人越来越多,有时印刷好的所有B报纸都已经被其他的用户拿去看了,小U这个时候必须要排队,并且等到有人看完之后才能看B报纸。
W报社也注意到了这个问题,它最开始采取的办法是每次增大印刷量,这样就能同时有更多的人来看B报纸了。果然增大印刷量之后排队的人变少了,但是好景不长,随着看B报纸的人变得更多,大家又要到报社前排队了。此外,包括小U在内的一些人每次还是得在路上花好长的时间到报社。另外一个问题是,每天下班的时间是排队的高峰,大量的读者在报社前排队,而在其他时间看B报纸的人就不是很多,这个时候多印刷的B报纸又有点浪费了。
那么如何才能解决这个问题呢?W报社想了一个两全其美的办法。办法很简单,它开始和各地的书报亭展开合作,现在书报亭可以去报社订阅一定量的B报纸,并且书报亭也可以直接浏览B报纸了。
现在小U想要看B报纸,他不需要每次都跑到遥远的报社去看了,他可以直接去楼下小区旁的书报亭看,大大节省了时间,同时也提升了他的积极性。
如果有的时候,恰巧大量的读者来到同一家书报亭看B报纸,或者有的时候书报亭还没有来得及获得最新一期的B报纸怎么办呢?作为书报亭,它也有两种选择,第一,它可以直接去报社拿最新一期的B报纸。第二,它也可以看看附近其他的书报亭有没有最新一期的B报纸。无论哪种方法,书报亭花费的时间也不会比读者直接去报社看报纸慢,同时,下一次还有读者看B报纸的时候也能直接把上一次拿到的最新一期的报纸直接给读者看了。
B报纸凭借优质的内容和服务,很快受到了广泛的欢迎。
我把CDN和书报亭做了类比,可能不是完全准确,但希望通过上述的故事让大家对CDN能有一个直观的认识。
在没有CDN的时候,网站是如何响应用户的请求的呢?简单地说,每次用户访问网站时(浏览报纸),都需要连接到网站对应的服务器(W报社),服务器收到请求后,回复相应的网站内容(当然这其中的步骤没有这么简单,还包括了DNS查询、路由、监听等一系列的工作)。你可以发现,每次用户访问网站时,都必须向网站背后对应的服务器请求需要的资源,因此服务器需要直接面对每个用户的请求,在请求量变多的时候就有可能产生性能问题。
那么如果有了CDN,情况会如何呢?CDN的本质,是将用户的请求中转到某一个中间的服务器上(书报亭)。当用户请求网站时,由这些中间服务器代替网站源服务器响应用户请求,如果用户请求的一些资源不包含在中间服务器上,那么此时再由中间服务器向源服务器请求,从而大大减少了源服务器的资源消耗。另外,CDN的负载均衡还能将用户的请求分发到最佳的中间服务器上,从而大大的加快了用户的访问速度。
希望以上的文字能让你对CDN有一个初步的了解。我之前一直很好奇访问域名不变的情况下CDN是如何生效的,最近几天恰好看了腾讯云的一些CDN文档才大概了解了一些原理,其实是修改你的域名CNAME记录为CDN提供商的域名,同时在CDN的配置页面设定源站的地址,这样当用户访问你的域名地址的时候就能请求CDN服务器了。
如果有任何疑问或者建议,欢迎在评论区里留言。
]]>一个开源的评论系统,可惜已经不太维护了,于是我进行了一些简单的修改,并做好了Docker镜像。
我这两天还在寻找合适的评论系统,主要是因为当前可用的评论系统都不能满足基本的要求。我的基本要求其实也就两点,第一,可以通过邮箱直接评论,不需要注册。第二,在有人回复的时候可以发送邮件进行通知。同时,评论者被回复时也可以收到相应的通知。上一篇中提到的Valine勉强支持,可惜Valine在安全性上存在很大的问题。Livere总的来说还不错,但缺乏邮件提醒功能。最终,我还是使用了一个开源评论系统isso,并进行了一部分的调整,部署在了我自己的服务器上。我进行了一些修改,并已经做好了Docker镜像,可供直接拉取使用。
2019.2.1更新: 本站现在使用Disqus作为评论系统。
docker-compose
文件: docker-compose.ymlisso是一个用Python写的开源评论系统,目前在GitHub上已经超过3k个stars,然而这个项目在前几年相对活跃,最近几年作者已经很久没有维护了。主要是其他的一些参与者在帮忙维护,因此isso的官方网站中所使用的版本已经是很老的版本了。而GitHub上最新的版本相比原作者大概在2016年提交的最后一个版本多了许多新的特性,比如支持Gravatar头像,支持评论回复时邮件提醒等。目前的维护者表示,由于只有原作者有权限在pip上提交新版本,更新官方网站等,因此如果你想要安装最新版本,你需要直接从GitHub上的master
分支中拉取最新的代码。如果你使用pip install
命令来安装的话,你只能得到2016年的旧版本。
我最初使用其GitHub仓库中提供的Dockerfile来自己构建镜像,然而产生了一个关于defaults.ini
读取错误的bug,所以我提交了一个issues #476。解决方法是把其提供的默认的isso.conf
中的内容覆盖defaults.ini
即可,后来发现这个问题产生的原因是defaults.ini
实际上是一个symlink文件,而Windows不支持symlink文件,因此isso直接读取了defaults.ini
作为配置文件,从而产生了错误。另一个问题是,isso提供的邮件回复模板非常简陋,所以我使用html将其进行了修改,我参考了Vline-Admin中提供的邮件模板并进行了一部分调整。邮件预览如下所示
我还在配置文件isso.conf
中添加了一个配置项[smtp]name
,该配置项可以用来指定你的博客名字,这在发送邮件提醒的时候会被用到。
修改好邮件模板后,我重新构建了Docker镜像,并发布到了kaitohh/isso,如果需要使用可以直接pull下来,挂载好配置文件和数据库后即可直接使用。需要注意的是,官网的配置文档已经过时了,你可以直接访问GitHub仓库上的对应文件来查看最新的文档。
我编写了docker-compose.yml
文件用来将isso部署到服务器。我通过挂载/config
和/db
来导入配置文件和数据库。我还使用了nginx-proxy
构建外部代理,它通过服务发现自动代理web
服务器,并且启用了SSL支持,通过挂载/etc/nginx/certs
来导入证书。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17version: '2'
services:
web:
image: kaitohh/isso:latest
volumes:
- ./config:/config
- ./db:/db
environment:
- VIRTUAL_HOST=domain.com
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- "./cert:/etc/nginx/certs"
文件目录结构如下所示1
2
3
4
5
6
7
8
9
10deploy/
├── cert
│ ├── domain.com.crt
│ └── domain.com.key
├── config
│ └── isso.cfg
├── db
│ └── comments.db
├── docker-compose.yml
└── Dockerfile
最后通过docker-compose up
即可启动服务器。
isso自带的样式非常难看,如果要使用自定义的样式,可以将data-isso-css
设置为false
,然后添加自己的CSS样式即可。
isso由于目前缺乏专业的维护,一些缺陷正暴露出来。比如实现发送邮件的函数实现较差,导致我修改邮件模板的时候是直接硬编码的,这就导致可读性很差,而且非常难以维护。此外,isso最近添加的一些功能缺乏可配置性,实现方法存在缺陷,导致部分不必要的功能无法关闭,额外消耗了服务器资源。另外,isso数据库只支持本地的SQLite3
数据库,导致效率较低,更关键的是数据库无法和服务器分离,这就导致项目本身无法在自动化平台上构建,比如Heroku,后者支持自动构建镜像,并自动挂载数据库,但由于isso的SQLite
只支持本地,导致Heroku无法挂载数据库,数据库和镜像绑定,这在每次重启实例时会丢失数据。
我还查阅了isso仓库中的以前issues和PR记录,发现作者原本是已经计划采用SQLAlchemy
来连接数据库了#61 #108,但可能由于各种各样的原因,导致这个feature至今仍然没有开发完,另外由于原作者已经很久没有维护,并且代码仓库面临越来越冗余的问题,这个feature可能在未来的很长一段时间内也仍然不会支持。
一个成功的开源项目需要良好的社区,持续集成,以及严格规范的PR提交制度。isso作为一个将近5年的项目,在一开始受到了热烈的欢迎,但目前正面临不断被淘汰的困境。近年来,又有一些比较优秀的评论系统诞生,比如Mozilla的开源项目Talk被国外越来越多的新闻媒体采用,Talk项目集成了Docker容器,而且支持Heroku的一键部署,目前正受到社区越来越多的欢迎。另外,commento目前正处于封闭测试阶段,未来会提供免费的评论系统服务,也支持用户自己部署。尽管如此,至少在目前看来,isso仍然是优秀的开源评论系统,作为个人博客的评论系统应该也足够使用了。
总之,在找到下一个合适的评论系统之前,我仍然会继续使用isso评论系统,同时也欢迎访问我的网站来查看isso评论系统的效果。
]]>前几天还在说,使用Hexo搭建博客比较顺利,没什么大坑,后来就接二连三的遇到各种问题。
其实这些问题也不全是Hexo的原因,这些问题在搭建一个静态博客的时候都有可能遇到,不过还是能拿来讲讲,以供参考。Hexo的配置确实非常容易,这一点不可否认。只需要安装好Node.js
以及其他必要的依赖,按照官网提供的文档一步一步做就行了。我把博客部署在GitHub Pages
上,这样就省去了自己搭建服务器的成本,为了加快访问GitHub的速度,我配置了CDN和对象存储的优化,这在后续的博客中也会慢慢介绍。
评论系统是我碰到的最大的一个问题,我在上一篇文章中提到我使用了Disqus来做评论系统,但我后来才发现原来Disqus在国内并不能访问。这就很尴尬了,网上也有一些通过代理来访问Disqus的解决方案,比如disqus-proxy提供了一个通过判断是否能连接Disqus来显示原页面或者是简易评论框的方法,但是简易评论框过于简单,而且只能够匿名评论,样式也不够美观,当然我也懒得改CSS。此外disqus-php-api直接仿照原生的Disqus做了一个评论框,所有操作通过调用API实现,这种方法相对完善,但API支持的操作终究有限,评论框的功能相比原生还是少了一些,比如点赞,第三方登录之类的功能都无法完成,所以之后我就放弃了Disqus,寻求其他的评论系统。
畅言是目前国内为数不多的评论系统提供商了,几年前的一些比较著名的评论系统,比如多说、网易云跟帖、友言之类的在近两年都关闭了,看来国内的评论系统很不好过呀。然而畅言需要网站备案才可用,备案又是一件特别麻烦的事情,所以只能排除这个方案了。
Disqus是国外最流行的评论系统了,但可惜在国内无法访问。另外Disqus支持的第三方登录都是一些国外的SNS比如Facebook, Twitter之类的,国内的SNS均不支持。另外,支持匿名评论也是Disqus的一个优点。
除了依赖第三方提供的评论系统之外,我还看到了很多很有创意的评论系统解决方案。
gitment的想法很有创意,它通过GitHub提供的API把每篇文章的评论对应到某一个repo的一个issue上,这样当用户评论的时候会自动在对应的issue中插入一条对应的记录。由于GitHub的issue系统本身支持Markdown,而且还支持emoji的投票,因此评论系统还原生实现了富文本和点赞/反对这一类的功能。唯一的缺点就是用户必须拥有GitHub账户并登录后才能评论,但考虑到很多技术博客的受众对象,这条限制在很多情况下是可以接受的。
Valine实现了一个纯前端的评论系统,后端基于Leancloud,Leancloud提供BaaS后端服务,直接提供RESTful的数据存储API,因此可以直接在前端进行数据的CURD操作。稍微了解了一下,BaaS似乎是近年才流行起来的一种开发模式,看了一下这篇文章,感觉挺有意思,有空可以介绍一下。
上面的这些评论系统就简单的介绍了一下。这些方案都有自己的优点,但我最后使用了LiveRe第三方评论系统,来自韩国公司。配置比较简单,功能也没有Disqus那么丰富,但它有两个最大的优点。第一,它支持国内的各大SNS网站登录。第二,它可以在国内流畅访问。
所以我最后就采用LiveRe了,在网站上注册后并新建一个,然后Install一个City版评论系统就行了,记录下提供的livere_uid
。受到Hexo-Next主题的加持,只要把livere_uid
在配置页面填好就完成了。这期间我还发现了一个关于链接标识名的bug,简单地说,就是相同的页面的两个链接,比如这两个链接1
2www.example.com/my-article/
www.example.com/my-article/index.html
在正常情况下,它们应当指向同一个页面,但由于LiveRe直接读取页面URL作为其评论的标识符,这两个链接被当作不同的页面,所以会导致显示了不同的评论。参考了LiveRe的文档后,发现LiveRe提供了window.livereOptions
中的refer
来手动指定标识符,因此只要在初始化评论框的时候设定好参数即可。1
2
3
4
5...
window.livereOptions = {
refer: '{{ page.path }}'
};
...
顺便写了个PR #395交了上去,开发者很快就通过了,看起来会在下一个版本(6.5.0)中修复这个问题。
分享功能我在之前也已经集成过很多次了,原来用的比较多的是addthis,这在theme主题中也集成了,但集成的不是很好,没办法将分享按钮以inline
的形式插入在文章中,只能放置在边框中。在PC上的效果还好,但在移动端会严重遮挡浏览区域。好在theme主题中也集成了needmoreshare2的插件支持,所以直接用就行了。当中一些样式有点奇怪,所以顺便改了一下#11.
阅读量统计是一个很脆弱的实现,因为这个功能只是简单的在每次打开的时候+1,也不会过滤重复什么的。这里也有几种方法,比如Google的Firebase提供了云端的数据库,然而在国内依然无法访问。所以最后还是采用了LeanCloud的云端数据库方案,不够安全,但只是作为一个简单的统计阅读量的功能来讲,勉强也可以接受了。
其他还有一些比较简单的配置,比如添加Google Analytics用来统计,以及增加一些js插件扩展,包括fancybox、pace等等,这些插件大都是给网站加一些特技,变得更加酷炫而已,方法也都比较简单,这里就不再赘述了。
以上是我在配置Hexo时的一些经验分享,如果有任何问题欢迎在下方留言。此外,我还对博客做了一些访问速度的优化,这会在之后的文章中另作介绍。
]]>心血来潮使用Hexo在GitHub上部署了一个Blog,总的来说还是挺容易的,没什么大坑。
记得搭博客最早在高三的时候就试过了,那个时候是在新浪云(SAE)上面搭了一个WordPress,其实现在看来感觉WordPress过于臃肿,不适合做随便写写的那种博客。但那个时候WordPress的宣传语好像是,不用敲一行代码就能搭一个个人博客。Emmmm,要知道那个时候我才刚开始写C和C++,成天面对黑底白字的控制台的时候突然告诉我,现在可以一句代码都不写,直接搭一个个人的博客了,还是很有吸引力的。
但是搭WordPress途中好像遇到了特别多的坑,一开始配置SAE里的数据库连接啦,后来配置邮件服务啦(SAE当时关闭了PHP的邮件接口,所以要通过插件才能实现),然后好像还凑合着写了几段PHP的脚本用来做博客的每日备份和访问量统计,结果会发到我的邮箱中。写的文章也没什么大的营养,大部分都是WordPress的一些配置技巧啊啥的,现在这些文章可能在我某台电脑的数据库文件里,之前本来打算在每次建立新的博客的时候合并过去,但一直懒得做,加之文章也没什么价值,所以就一直封存起来了。
记得文章下面的大部分的评论都是广告,有些我怀疑是爬虫自动评论的。由于SAE按流量收费,所以我设定了每日的使用量限额,所以有时候爬虫一多网站就挂了。后来大概开了一年半就把SAE关掉了,你可以看到邮件最后发送的时间大概是15年的11月份。后来好像百度云(BAE)也可以用了,我就把博客放到了BAE上面,不过WordPress的配置实在太麻烦,换一个托管云就要重新配置一次,所以也就不了了之了。
托WordPress的福,后来我每次想到自己搭一个博客的时候总会想到当初配置WordPress的不愉快经历,结果每次都只能束之高阁。中间我又把博客迁移到了CSDN,后来又迁移到博客园,但大概的剧情基本上都是,开博客的前一段时间比较勤奋,有时会点东西,但一段时间过去就懒得动了。我有很多次想要写博客的冲动,比如一年前在 hacker.org 上解决了 Crossflip 和 One of us 这两个问题,当时很想把算法和思路介绍一下,但是拖了一天又一天,后来也没有去写。今天我又看了一下这两个问题,当时的算法已经忘了差不多了,希望哪天有空的时候可以把这两个坑填完。
之前一直看到很多人在GitHub上搭建静态网站,用 Disqus 做评论系统,所以我今天就自己试了一下。感觉优势很多,一来静态网站托管在GitHub上,不用自己开服务器了,二来也不用担心网站被DDOS啥的,安全了许多。Hexo直接用Markdown来写文章,终于可以摆脱各种反人类的富文本编辑器了。缺点嘛,可能就是国内访问会比较慢,不过感觉总体也能接受吧。
差不多就写这些了,这篇题目叫做“第一篇”,其实我每次开博客都会写“第一篇”,然后就搁置好长一段时间不管了,所以,暂且就希望这不会成为最后一篇吧。另外,原来在博客园的文章这两天看看能不能把它迁移过来,要把原来的文章全部改写成markdown的格式,真是件麻烦的事情。
]]>