基于 LR(1) 和 LALR 的 Parser Generator

最近处理文本比较多,先前想增强下正则,看来不够用了,有同学推荐了我 Pyl 和 Lark,看了两眼,初看还行,但细看有一些不太喜欢的地方,于是刚好春节几天有空,从头写了一个 LR(1) / LALR 的 Generator,只有一个 LIBLR.py 的单文件,没有其它依赖:

用法很简单,给定文法,返回 Parser:

import LIBLR

# 注意这里是 r 字符串,方便后面写正则
# 所有词法规则用 @ 开头,从上到下依次匹配
grammar = r'''
start: WORD ',' WORD '!';

@ignore [ \r\n\t]*
@match WORD \w+
'''

parser = LIBLR.create_parser(grammar)
print(parser('Hello, World !'))

输出:

Node(Symbol('start'), ['Hello', ',', 'World', '!'])

默认没有加 Semantic Action 的话,会返回一颗带注释的语法分析树(annotated parse-tree)。

支持语义动作(Semantic Action),可以在生成式中用 {name} 定义,对应 name 的方法会在回调中被调用:

import LIBLR

# 注意这里是 r 字符串,方便后面写正则
grammar = r'''
# 事先声明终结符
%token number

E: E '+' T          {add}
 | E '-' T          {sub}
 | T                {get1}
 ;

T: T '*' F          {mul}
 | T '/' F          {div}
 | F                {get1}
 ;

F: number           {getint}
 | '(' E ')'        {get2}
 ;

# 忽略空白
@ignore [ \r\n\t]*
# 词法规则
@match number \d+
'''

# 定义语义动作:各个动作由类成员实现,每个方法的
# 第一个参数 rule 是对应的生成式
# 第二个参数 args 是各个部分的值,类似 yacc/bison 中的 $0-$N 
# args[1] 是生成式右边第一个符号的值,以此类推
# args[0] 是继承属性
class SemanticAction:
    def add (self, rule, args):
        return args[1] + args[3]
    def sub (self, rule, args):
        return args[1] - args[3]
    def mul (self, rule, args):
        return args[1] * args[3]
    def div (self, rule, args):
        return args[1] / args[3]
    def get1 (self, rule, args):
        return args[1]
    def get2 (self, rule, args):
        return args[2]
    def getint (self, rule, args):
        return int(args[1])

parser = LIBLR.create_parser(grammar, SemanticAction())
print(parser('1+2*3'))

输出:

(点击 more 查看更多)

Continue reading

Loading

Posted in 编译原理 | Tagged | 3 Comments

Python 中使用组合方式构建复杂正则

正则写复杂了很麻烦,难写难调试,只需要两个函数,就能用简单正则组合构建复杂正则:

比如输入一个字符串规则,可以使用 {name} 引用前面定义的规则:

# rules definition
rules = r'''
    protocol = http|https
    login_name = [^:@\r\n\t ]+
    login_pass = [^@\r\n\t ]+
    login = {login_name}(:{login_pass})?
    host = [^:/@\r\n\t ]+
    port = \d+
    optional_port = (?:[:]{port})?
    path = /[^\r\n\t ]*
    url = {protocol}://({login}[@])?{host}{optional_port}{path}?
'''

然后调用 regex_build 函数,将上面的规则转换成一个字典并输出:

# expand patterns in a dictionary
m = regex_build(rules, capture = True)

# list generated patterns
for k, v in m.items(): 
    print(k, '=', v)

结果:

protocol = (?P<protocol>http|https)
login_name = (?P<login_name>[^:@\r\n\t ]+)
login_pass = (?P<login_pass>[^@\r\n\t ]+)
login = (?P<login>(?P<login_name>[^:@\r\n\t ]+)(:(?P<login_pass>[^@\r\n\t ]+))?)
host = (?P<host>[^:/@\r\n\t ]+)
port = (?P<port>\d+)
optional_port = (?P<optional_port>(?:[:](?P<port>\d+))?)
path = (?P<path>/[^\r\n\t ]*)
url = (?P<url>(?P<protocol>http|https)://((?P<login>(?P<login_name>[^:@\r\n\t ]+)(:(?P<login_pass>[^@\r\n\t ]+))?)[@])?(?P<host>[^:/@\r\n\t ]+)(?P<optional_port>(?:[:](?P<port>\d+))?)(?P<path>/[^\r\n\t ]*)?)

用手写直接写是很难写出这么复杂的正则的,写出来也很难调试,而组合方式构建正则的话,可以将小的简单正则提前测试好,要用的时候再组装起来,就不容易出错,上面就是组装替换后的结果。

下面用里面的 url 这个规则来匹配一下:

(点击 more 展开)

Continue reading

Loading

Posted in 编译原理 | Tagged | 1 Comment

性能测试:asyncio vs gevent vs native epoll

测试一下 python 的 asyncio 和 gevent 的性能,再和同等 C 程序对比一下,先安装依赖:

pip3 install hiredis gevent

如果是 Linux 的话,可以选择安装 uvloop 的包,可以测试加速 asyncio 的效果。

测试程序:echo_bench_gevent.py

import sys
import gevent
import gevent.monkey
import hiredis

from gevent.server import StreamServer
gevent.monkey.patch_all()

d = {}

def process(req):
    # only support get/set
    cmd = req[0].lower()
    if cmd == b'set':
        d[req[1]] = req[2]
        return b"+OK\r\n"
    elif cmd == b'get':
        v = d.get(req[1])
        if v is None:
            return b'$-1\r\n'
        else:
            return b'$1\r\n1\r\n'
    else:
        print(cmd)
        raise NotImplementedError()
    return b''

def handle(sock, addr):
    reader = hiredis.Reader()
    while True:
        buf = sock.recv(4096)
        if not buf:
            return
        reader.feed(buf)
        while True:
            req = reader.gets()
            if not req:
                break
            sock.sendall(process(req))
    return 0

print('serving on 0.0.0.0:5000')
server = StreamServer(('0.0.0.0', 5000), handle)
server.serve_forever()

测试程序:echo_bench_asyncio.py

(点击 Read more 展开)

Continue reading

Loading

Posted in 编程技术 | Tagged | 1 Comment

别被忽悠了 Lua 数组真的也可以从 0 开始索引?

先前我说 Lua 数组从 1 开始不太爽,很多人来纠正我说也可以从 0 开始,比如:

local m = { [0] = 100, 101, 102, 103 }

然后访问时 m[0] 也可以正常访问到第 0 个元素,所以 “Lua 给你充分自由度,让你可以从任意下标索引数组”,貌似好像说的很有道理,但是不是这样呢?

我们先用 # 符号打印下上面数组的长度:

print('size', #m)

输出是:3 ,而不是实际元素个数 4,因为 # 就是从 1 开始数起的,所以如果你代码里用了 m[0] ,你也需要额外方式计算长度,同时保证用到这个数组的其他代码也遵从这样计算。

还有一个问题,使用 ipairs 遍历的时候,m[0] 不会被遍历进去:

for i, j in ipairs(m) do
    print(i, '->', j)
end

输出是:

1       ->      101
2       ->      102
3       ->      103

看到没,你的 m[0] 没了,即便你写了个 m[0] = 100 ,再 ipairs 那里也不认,Lua 没把他算在整数索引范围。那么如果你创建一个数组从 0 开始索引的话,你就要通知所有用你数组的人,既不能用 # 也不能用 ipairs 来遍历,这种沟通成本和后续无穷的麻烦,你愿意接受吗?

那么你说,我们不用 ipairs ,改用 pairs 来遍历行不行?行,你可以这么写:

for i, j in pairs(m) do
    print(i, '->', j)
end

但数组从 0 开始的话,0 元素没有保存在 array part 里,会导致遍历顺序不一样(因为优先遍历 array part),上面代码的输出是:

1       ->      101
2       ->      102
3       ->      103
0       ->      100

看到没,先遍历的 1-3(他们在 array part 里),最后再遍历 hash part 里 0。你喜欢这样的无序遍历的数组么?还是继续坚持 for i = 0, N-1 do 来自己遍历,并通知你的同事这样才能保持顺序。

最后一个问题是,一个 table 中 1-n 的连续整数索引都会被保存到 array part 里,而其他会被保存到 hash part 里,不管是检索还是遍历,都会优先到 array part 里用 O(1) 的方式检索,不行再到 hash part 用非 O(1) 的方式同其他 key 一起检索,那么你 m[0] 是游离在 array part 外的键,不但遍历顺序靠后,没和其他元素放一起,每次检索还有额外代价。

因此 Lua 支持数组从 0 开始索引么?只能说允许你这么用,但是语言层面并不提供足够的支持。

那么又会有人混淆视听的说:“从 1 开始也挺好的啊,我有着并没用什么问题”,但他们是不是忘记了 Lua 是嵌入式语言,要依靠宿主 C 语言提供运行环境,数组从 1 开始的话,和 C 语言宿主存在一个换算的关系,两边都写得话,一会从 0 一会从 1 ,引入了额外的负担,不留神就 BUG 了。

扩展阅读:还有觉得从 1 开始更合理的点这里

为什么 C 语言数组是从 0 开始计数的?

Loading

Posted in 编程技术 | Tagged , | 1 Comment

为什么 C 语言数组是从 0 开始计数的?

C 语言等大多数编程语言的数组从 0 开始而不从 1 开始,有两个原因:

第一:地址计算更方便

C 语言从 0 开始的话,array[i] 的地址就正好是:

(array + i) 

如果是从 1 开始的话,就是

(array + i - 1) 

多一次计算,性能受影响,再扩展到二维数组的话 array[i][j] 从 0 开始的地址是:

(array + i * N + j) 

多整洁,而从 1 开始要变成

(array + (i - 1) * N + (j - 1)) 

更繁琐。并且用 1 开始的话,同一个地址用 “指针+偏移”寻址和用 “数组+下标” 寻址还不能统一,经常要换算,何必呢?

第二:计算机硬件系统就是从 0 开始寻址的

物理内存地址寻址,端口寻址都是从 0 开始的,比如 32 位电脑的内存,地址范围就是:

[0, 2 ^ 32 - 1]

刚好用一个 32 位整数就能表达,而如果内存从 1 开始寻址,那么 32 位电脑的地址范围就会变成:

[1, 2 ^ 32]

那么最高地址 2 ^ 32 就需要一个 33 位的整数才能表达了,纯粹浪费资源。

其他的端口地址,DMA 通道等也都遵从这个从 0 开始的原则,那么用 3 比特表示 DMA 通道的话,更好可以表达 8 个通道 (0 – 7),而从 1 开始的话,同样 3 比特就只能表达 7 个通道了(1 – 7),一样是在浪费资源。

所以贴近系统的语言自然选择遵从硬件设定,除了第一条说的寻址计算更简单外,也能和计算机系统保持一致性,同时还能统一指针寻址和数组寻址的用户体验。

Dijkstra 解释过编程语言这么做的原因只是遵从硬件设计:

The decision taken by the language specification & compiler-designers is based on the decision made by computer system-designers to start count at 0.

所以 C 语言数组从零开始,目的在于:1)性能更好;2)统一数组和指针寻址;3)遵从硬件寻址法。

除此之外还有一些理论上的原因。

第三:数学上的原因

除去数组索引外,Dijkstra 主张一切计数应该从 0 开始,并且写了一篇文章解释:

(点击 more/continue 继续)

Continue reading

Loading

Posted in 编程技术 | Tagged | 1 Comment

《原神》也在使用 KCP 加速游戏消息

最近看到米哈游《原神》的客户端安装文件里附带了 KCP 的 LICENSE:

于是找米哈游的同学求证了一下,果然他们在游戏里使用 KCP 来保证游戏消息可以以较低的延迟进行传输,这里还有一篇文章分析了原神使用 KCP 的具体细节:

文章见:https://forum.ragezone.com/f861/genshin-impact-private-server-1191004/index7.html

KCP 是我之前开源的一套低延迟可靠传输协议,能够有比 TCP/QUIC 更快的端到端传输效果,适合游戏、音视频以及各类延迟敏感的应用。

欢迎大家尝试:

目前使用 KCP 的商用项目包括不限于:

  • 原神:米哈游的《原神》使用 KCP 降低游戏消息的传输耗时,提升操作的体验。
  • SpatialOS: 大型多人分布式游戏服务端引擎,BigWorld 的后继者,使用 KCP 加速数据传输。
  • 西山居:使用 KCP 进行游戏数据加速。
  • CC:网易 CC 使用 kcp 加速视频推流,有效提高流畅性
  • BOBO:网易 BOBO 使用 kcp 加速主播推流
  • UU:网易 UU 加速器使用 KCP/KCPTUN 经行远程传输加速。
  • 阿里云:阿里云的视频传输加速服务 GRTN 使用 KCP 进行音视频数据传输优化。
  • 云帆加速:使用 KCP 加速文件传输和视频推流,优化了台湾主播推流的流畅度。
  • 明日帝国:Game K17 的 《明日帝国》,使用 KCP 加速游戏消息,让全球玩家流畅联网
  • 仙灵大作战:4399 的 MOBA游戏,使用 KCP 优化游戏同步

KCP 成功的运行在多个用户规模上亿的项目上,为他们提供了更加灵敏和丝滑网络体验。

Loading

Posted in 游戏开发, 网络编程 | Tagged | Leave a comment

笔记软件为何需要本地存储?

不要忘记历史:

  • Evernote:导出备份的 .enex 文件, 再导入时提示有几篇日志图片 太多,没有会员无 法导入。
  • 印象笔记:用户因为从 Evernote 导入到印象笔记时触发了一个 BUG,五年笔 记丢失。新版本禁止导出公开格式的 .enex 文件,只能导出自己加密的 .note 格式,别的软件无法识别,只能映像笔记自己导出导入。
  • Notion:因为服务器在境外,偶尔会有无法访问的情况。未来有被墙的风险。
  • Wolai:CEO 公开声称用户上传非法信息要报警。CEO 公开声称自己审查用户笔记。公开挂程面试序员的隐私信息。
  • 百度:百度盘扫描用户上传文件并做精准广告推送(上传证件图片的人被推荐电子证件钱包) 百度盘替换用户视频,换成净网行动的宣传视频。
  • 某在线文档:用户用在线编辑的文稿,因为保存到在线云盘,数日后触发关键字被删除。
  • 语雀:本来免费的,近期突然宣布新的收费策略,规定免费用户总文档数量不能超过 100 篇(包括小记、文档、数据表、表格、画板等),见这里:如何看待语雀付费策略?遭到大量投诉后又改为:免费用户每月 100篇,还是无法分享。

当年 github 就是天天被码云投诉,然后被墙掉了(不一定全是因为它,但它投诉了不少);现在码云又在投诉仅有的 gitlab ,oschina 上天天看得到 gitlab 的黑文章,比如:

扒一扒极狐 GitLab 的底裤 – OSCHINA – 中文开源技术交流社区

OSCHINA 和码云是一家,天天发这些,也不标注下 “利益相关”,兴许各位的 notion 最近经常不容易访问到,也是被国内的竞争对手天天举报吧?按某些公司的尿性,面试程序员的隐私可以挂,用户的笔记随便审核举报,投诉下它 notion 简直小儿科,也许哪天真的就完全用不了。

因此,你的笔记如果打算保留十年以上,请选择支持本地存储+公开格式(最好文本)的软件,前者在于自己掌握数据,后者在于自己保留可以随时离开的权力。

Loading

Posted in 随笔 | 7 Comments

什么是 Zettelkasten 卡片盒笔记法?

我也用了 zk 一段时间了,觉得挺科学,应该是我近半年学习到最有价值的东西吧,分享一些我的理解和整理的笔记吧,以下观点来自对他人观点的整理和总结:

  • 不用纠结 Zettelkasten 这个单词是啥,不是高深的玄学,就是德语的 “卡片盒”。
  • 你无需像卢曼那样靠 zk 在 30 年里出版 60 本书,200 篇论文,但是 zk 是你的 “第二大脑”,通过修炼你的第二大脑,可以助你形成更深刻的洞见,并更高效率的进行输出。
  • 想了解 zk 的背景介绍,可以看这篇文章:卢曼:与卡片盒交流

以下内容可能初读有点烧脑,每句话都是一个观点,都能展开成一段文字,但我挤干了所有水分,留下这么几百字,真的理解了,能持续受益,当然,在真正有价值的东西上花点时间很合算。

整个 zk 系统包含三个核心概念:

(点击 Read more 展开)

Continue reading

Loading

Posted in 大浪淘沙 | Tagged | Leave a comment