8
28
2012
10

UDP打洞实验

两台没有外网 IP、在 NAT 后边的主机如何直连?UDP打洞通常可行,但是需要第三方服务器。方法如下:

在服务器 S 上监听一个 UDP 端口,在收到 UDP 数据包后把源地址发回去。代码如下(github):

import sys
import time
import socket

def main(port):
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  s.bind(('', port))
  try:
    while True:
      data, addr = s.recvfrom(4096)
      back = 'Your address is %r\n' % (addr,)
      s.sendto(back.encode(), addr)
      print(time.strftime('%Y-%m-%d %H:%M:%S'), addr, 'just sent us a message:', data.decode('utf-8', 'replace'), end='')
  except KeyboardInterrupt:
    print()

if __name__ == '__main__':
  try:
    main(int(sys.argv[1]))
  except (ValueError, IndexError):
    sys.exit('which port to listen?')

主机 A 发送数据包:

$ socat readline udp:xmpp.vim-cn.com:2727,sourceport=4567
my addr?
Your address is ('a.b.c.d', 40060)

输入任意消息并回车,一个 UDP 就从本地的 4567 发送出去了。从上述示例我们可以看到,NAT 设备转发时是从 40060 端口发送出去的。为了让服务器返回的数据能够到达内网主机,在一段时间内,NAT 设备会记住外网来自 40060 端口的 UDP 数据包要发送给主机 a.b.c.d 的 4567 端口。完全圆锥型NAT不会在意外部数据包是从什么地方发回来的。受限圆锥型NAT会忽略掉其它主机的数据包,上例中只认可来自 xmpp.vim-cn.com 的数据包。端口受限圆锥型NAT更进一步地要求源端口(上例中是 2727)必须跟之前发出的数据包的目的端口一致。当然,「之前发出的数据包」不必是最后一个。所以,除了最后一种——对称NAT——之外,其它类型的NAT都是有可能成功穿透的。参见维基百科条目网络地址转换STUN

后来通过 pystun 程序,我得知我所处的 NAT 是完全圆锥型的。

在知道 A 的发送地址后,主机 B 就可以向这个地址发送数据了。接下来的操作使用 socat 命令就是:

# host A
$ socat readline udp-listen:4567
# host B
$ socat readline udp:A:4567

然后 B 先发送数据让 A 知道 B 的地址(socat 会 connect 到这个地址),双方就可以相互通信了。当然,因为是 UDP 协议,所以通信是不可靠的,丢包啊乱序啊都有可能。

2013年10月13日更新:想要连接到 NAT 后边的 mosh 请看这里

Category: 网络 | Tags: python 网络 socat UDP
6
20
2012
4

使用 pygit2 创建提交

pygit2 是 libgit2 的 Python 绑定,而 libgit2 是一个可动态链接的 git 库,除去头文件和 pkgconfig 信息就一个 .so 文件。它是我在 The Architecture of Open Source Applications(AOSA)第二巻讲 git 的部分中看到的。git 本身遵循了传统的 Unix 哲学,提供了一系列的命令来管理源码库。这对于 shell 脚本是非常不错,可是对于嵌入到其它应用(如 IDE、Web 服务)中却不太好用。于是,我们有了 libgit2。

很遗憾的是,我并没有找到 API 文档,只有一些示例性的用法介绍,更别提教程之类。即使在 pygit2 中,使用help命令能够得到的信息也很有限。所以,我只能在 Python 这样动态语言的交互式会话时独自探索。

下面是我搜索出来的使用 pygit2 进行提交的过程:

导入需要用到的模块:

import pygit2
import time

我的 git 仓库,还有 index:

repo = pygit2.Repository('/home/lilydjwg/.vim/.git')
ind = repo.index

先看看未提交到 index 的修改(相当于git diff

print(ind.diff())

唔,我看到就一个plugin/colorizer.vim文件修改了。把它加到 index 中(相当于git add)。如果是git rm的话就用del ind[filename]了。操作之后要调用write()方法写入更改。

ind.add('plugin/colorizer.vim')
ind.write()

写入 tree 对象,其返回值是二进制编码的 hash 值(使用binascii.b2a_hex可编码成 git 命令中使用的字符串)

oid = ind.write_tree()

作者和提交者的信息,其中最后一个参数(offset)是以分钟计的时区偏移(当然是相对于 UTC)。邮件地址很显然被打码了 :-)

author = pygit2.Signature('依云', 'a@b.c', int(time.time()), 480)

创建提交。其中HEAD是个「符号引用」(symbolic reference),而repo.head就是当前最后一个提交了,oid属性还是二进制编码的 hash 啦。这里,提交者和作者是同一人,因此我都使用刚刚创建的author对象了。这步就是git commit命令了。

repo.create_commit('HEAD', author, author, 'colorizer: solved name color conflict', oid, [repo.head.oid])

在命令行下看看结果是否正确:

>>> git cat-file -p HEAD
tree 20e8937d41b6df16da2c8c5661f9c4a8dd31b5a1
parent ab9c662ce0d1cb2deac7a9ae388ecb40d8ec5e15
author 依云 <a@b.c> 1340188028 +0800
committer 依云 <a@b.c> 1340188028 +0800

colorizer: solved name color conflict
Category: python | Tags: python Git
6
11
2012
11

rpysh——Windows Python 命令行也要 readline!

rpysh 是为习惯 Linux 的 Pythoners 在不得不处理 Windows 上的事务时写的远程 shell。

源起

前些天,我尝试了使用 Python 控制 Word。但我对 Windows 下的交互式 Python shell 很不满意。

首先,我尝试的是 cmd.exe 那个黑窗口。太难用了!只有最基本的行编辑、在不知不觉中历史记录被窜改、复制粘贴极其麻烦。补全当然也是没有的。

于是,尝试 IDLE。这家伙我选了「IDLE Classic Unix」,但是能工作的键并不多。比如我刚刚尝试的Ctrl-u就不管用。而Ctrl-p竟然是把光标向上移动,回车才会把那行的内容取到输入命令的那行。这样一来,想再次执行最后一条语句,需要视上条命令输出的行数按几下Ctrl-p。另外,鼠标在窗口内点击后光标会被移开。这样,我使用鼠标从其它窗口切回来时,还得再手动定位光标,极其不爽。至于补全么,太智能了,所以在我输入时不时会出现这种情况:

乱七八糟的补全

还有一个问题:我查资料、做笔记、写代码都在 Linux 上,虽然Ctrl-CCtrl-V在物理机和虚拟机间能够无缝操作,但比起选中+中键粘贴的 X 主选区还是麻烦多了!

没办法,我只好重拾很久以前的想法——写个程序,在 Linux 上操作,在 Windows 上执行!

——等等!这和 ssh 差不多吗?或者 telnet?

——不不,Cygwin 的 ssh 跑不了 Windows 控制台程序,而且,不还是没 readline 支持么?

实现

毫无疑问是网络通信了。距离上一次不成功的尝试已经过去很久了,我不仅更加了解了code模块的能力,也知道 Python 命令行补全是怎么回事了。也就是说,Windows 版的 Python 是有补全的接口的,只是没有 readline 的等价物来调用。跑在 Windows 上的服务端要完成以下操作:

  1. 重写相关方法,把用户数据由标准输入改到从客户端读取
  2. 标准输出重定向到网络 socket
  3. 收到客户端的补全请求后,使用rlcompleter模块获取补全结果,再回送给客户端

对于第一点,实际上取代code.InteractiveConsole实例的raw_input方法就行。它和内建的input()函数具有相同的输入和输出形式,也就是会接收命令提示符。将这个直接发给客户端好了。

第二点很简单,直接socket.makefile然后把sys.stdout指过去。

第三点,为了简单起见,我另开了个线程和 socket,专门用于补全。需要传递的参数和返回值全部 pickle 了扔给对方就是了。

写完这些我才发现,其实我的raw_input方法和补全函数具有相似的执行逻辑:发送参数到网络,再从网络获取执行结果——也就是远程过程调用呵。

使用方法

rpyshd.py可选一个参数作为端口号,为方便起见,提供默认值8980。也是为了方便双击执行起见,我添加了.py后缀。

rpyshc相当于telnet命令了,直接接主机地址和端口号两个参数即可。

缺陷

  • 从标准输入读数据时在服务端
  • 偶尔提示符出现不及时
  • 虽然我实现了Ctrl-C,但是实际上没什么用,因为收到消息时之前的操作肯定已经执行完了
Category: python | Tags: linux python readline windows
6
6
2012
12

编程获取本机IPv4及IPv6地址

首先,我要通过编程直接获取,而不是去读诸如ifconfig等命令的输出。

其实是只想获取IPv6地址的,不过我猜想它们差不多,也确实看到不少相关搜索结果,于是顺带着看了。

首先,使用gethostbyname查自己通常是不行的,因为可能得到127.0.0.1,而且我猜,这样不能处理拥有多个IPv4地址的情况。另外一种方式是连上某个主机,然后调用getsockname。这样需要能够直接连上那个主机,好处是如果有多个网络接口,这样可以知道到底走的是哪个接口,调试网络时不错。我最满意的方案在这里,使用ioctl来获取。这个方法可以获取指定网络接口的IPv4地址。至于有哪些网络接口嘛,直接读/proc/net/dev吧。

import fcntl
import socket
import struct
ifname = b'eth0'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8915 是 SIOCGIFADDR
ip = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24])
print(ip)

然而,这样只能获取IPv4地址。创建个AF_INET6的 socket 传过去会报错「Inappropriate ioctl for device」。那怎么办呢?Google 没找到,我去搜了下内核源码。inet_ioctl里有对SIOCGIFADDR的处理。但是,inet6_ioctl里却没有了。

于是,我只好去下载ifconfig所属的 net-tools 的源码,找到相关代码:

#if HAVE_AFINET6
    /* FIXME: should be integrated into interface.c.   */

    if ((f = fopen(_PATH_PROCNET_IFINET6, "r")) != NULL) {
    while (fscanf(f, "%4s%4s%4s%4s%4s%4s%4s%4s %08x %02x %02x %02x %20s\n",
              addr6p[0], addr6p[1], addr6p[2], addr6p[3],
              addr6p[4], addr6p[5], addr6p[6], addr6p[7],
          &if_idx, &plen, &scope, &dad_status, devname) != EOF) {
        if (!strcmp(devname, ptr->name)) {
        sprintf(addr6, "%s:%s:%s:%s:%s:%s:%s:%s",
            addr6p[0], addr6p[1], addr6p[2], addr6p[3],
            addr6p[4], addr6p[5], addr6p[6], addr6p[7]);

这里就是ifconfig输出IPv6部分的代码了。可以看到它打开了一个奇怪的文件。跟过去,发现是

#define _PATH_PROCNET_IFINET6       "/proc/net/if_inet6"

囧,这个文件我早就发现过了的。看来和IPv4的情况不同,IPv6地址只能通过/proc里的文件获取了。而且输出成人可读格式不容易(ifconfig是自己实现的)。

PS: 我还发现了件好玩的事,在 Linux 源码的include/linux/sockios.h中,SIOCGIFINDEX中的字母 C 写漏了。通过git blame我发现,这个拼写错误在至少七年前 Linux 内核代码迁移到 git 前就修正了。Linus Torvalds 说之前的代码导入到 git 后有 3.2GB。我不得不承认这是个无比正确的决定,因为现在的.git已经有600多兆了,git 不支持断点续传,clone 下来已经很不容易了。

另外,我还联想到了 Unix 系统调用中的creat,以及 HTTP 协议中的referer :D

#define SIOCGIFINDEX    0x8933      /* name -> if_index mapping */
#define SIOGIFINDEX SIOCGIFINDEX    /* misprint compatibility :-)   */
Category: Linux | Tags: C代码 linux python 网络
5
14
2012
8

xmpptalk 聊天机器人及 Gtalk 群推荐

xmpptalk是一个搭建 XMPP 群(通常称 Gtalk 群)的软件。它使用 Python 编写,但与之前的 gaetalk 不同,它不受限于平台,而是可在任何 Linux 系统上均可运行,比如各种 VPS。其它类 Unix 平台尚未测试,但是即使有问题,也应该能够很快解决。

如何搭建

搭建 XMPP 群首先需要有一台运行类 Unix 系统的服务器。其次需要一个 XMPP 帐号。请不要使用@gmail.com的帐号,因为频繁地发送带链接的消息,或者过快地发送消息,会被 Gtalk 阻止。这里有一个免费 XMPP 服务器列表。XMPP 服务器也可自行搭建,推荐使用prosody。我曾经遇到 ejabberd 在发送长消息时网络阻塞严重,甚至导致机器人与服务器的连接断掉。

然后是 xmpptalk 的使用。目前版本的 xmpptalk 还处理 Alpha 阶段,搭建有些复杂,有不少依赖,请参阅这里的简要说明。主要的依赖有:

  • Python 3.2+
  • MongoDB
  • pyxmpp2
  • mongokit

其中 mongokit 是修改过的,请从安装脚本中寻找地址并下载。

因为依赖复杂,所以我在项目的scripts目录下提供了一个自动化脚本quickinstall.sh以便安装各种依赖,请直接运行(而不要用sh来运行)。不过如果源里有的软件,还是推荐从源里安装(比如那个脚本需要安装 git 和 hg)。在同一目录下还有 MongoDB 的示例配置文件以及建立相关目录和用户的脚本。

配置群,请修改config.py文件。此文件是 Python 语法,请按注释进行配置。

依赖都满足并且配置完成后请运行以下命令对数据库进行初始化:

python3 dbman.py

如果没有出错说明一切正常,可以运行./main.py开群了。如果出错了请修正后重新运行。不过要是数据库已经建立的话,请先使用 Mongo Shell 删除数据库。

如果希望群在后台运行(而不是像我把它放在 tmux 会话中),可以使用如下命令:

./main.py --fork

注意:由于未知原因,群对成员的在线与否可能会出错,建议每隔一段时间重启一次(管理员使用-restart命令即可)。

2012年8月24日更新:StarBrilliant 写了一篇更为详细的安装与配置记录

2013年3月17日更新:感谢苏学姐,她写了一篇关于在 OpenShift 搭建群的详细教程

群的简单使用

加入群只需要添加群帐号为好友即可。成功的话会收到一条欢迎消息,告知用户的默认昵称。这个昵称从用户的设置信息(vCard)中读取,如果失败,会生成一个在本群内唯一的 id,其前半部分是用户 JID 的用户名,后半部分是加盐 hash 后的域名,以防止用户 JID 泄漏。不过鉴于大部分用户都是@gmail.com,所以有心人还是可以猜,不过没有办法确定猜得是否正确(除非能够访问群数据库或者配置信息)。

欢迎信息会告诉新加入的用户使用-nick命令可以修改自己在群里的昵称。像很多其它的 XMPP 群一样,本群软件提供了不少用户命令。使用-help可以获得一个简要帮助。为了方便用户,实际上求助的命令是模糊匹配的,在所有我去过的群中的求助命令都会有效。群命令全部为小写,大写无效。并且,命令前不可有空格,否则作为普通消息发给群成员。

昵称有一些限制,主要是不允许各种标点和特殊符号,以及长度有限。昵称的最大「宽度」(一个汉字相当于两个英文字母)和昵称中允许出现的非字母字符是可以配置的。

本群软件支持用户间的私信,使用-pm 对方昵称 消息内容发送。因为昵称里可以有空格,所以这里的昵称允许使用\来转义,也可以用双引号引起来。私信设计为向特定用户发送不想让其它人看到的消息,比如自己的 JID 或者邮件地址。它不宜用作私下交流;此情况请加对方单聊。私信的发送是不可靠的。群总是会把私信发出去,但是不确定对方是否成功收到(比如某些服务器会拒收离线消息,又或者暂时无法连接对方服务器)。所以私信的发送并不会像其它命令一样会有回应。

如果暂时不想接收群消息,可以让群在一定时间内不向自己发送消息。命令为-stop 时间。时间可指定单位m(分钟)、h(小时)、d(天)。不指定则为秒。如-stop 2h就会在接下来的两小时收不到群消息。私信不会被阻止。在停止接收群消息的时候可以使用命令,包括使用-stop命令来修改要暂停的时长。如果在停止接收群消息的时候发送消息,或者发送ping消息,停止状态将取消。

ping消息是一个特殊的消息。向群发送只包含ping的消息,可以用于以下情况:

  • 看看自己是不是掉线了,以及群是不是出故障了
  • 取消停止接收群消息
  • 查看当前的日期时间星期几(时区在群里配置里,可用-about命令查看)

群推荐

注意:为了避免有人不停改昵称给正常聊天造成困扰,以下介绍的群,技术群的昵称每十天才允许改一次,而水群是三天。不过如果手误改错了可联系管理员修正。

技术群 JID:test@vim-cn.com

这是一个关于 Vim、Linux、Python 等的讨论群。无关话题最好不要讨论,可能会被禁言的哦。另外有个 GTK 专用群:mop@vim-cn.com。

水群 JID:water@vim-cn.com

此群不限话题,但不建议大量讨论政治、军事、游戏、IT技术等等具有非常明确的话题归类的内容。

最后,贴个自己搭建的网页版客户端,方便没有客户端或者客户端不给力的人使用:https://chat.vim-cn.com/,可以登录任何互联网上的 XMPP 服务器。证书是自签名的,只作加密用。

Category: python | Tags: python XMPP
5
12
2012
0

用 Python 控制 MS Word 之手册在哪里

通过 pywin32 模块使用Python來控制MS Word的文章不少,可是看过后我发现他们有一个共同的问题:参考手册在哪里?

因为是通过 COM 接口进行通信的,不能使用help()或者dir()命令来获取 API 帮助,所以迫切地需要详细的文档,才能知道那些对象拥有的属性和方法。我在网上尝试找了 VBA 的参考手册,这个微软倒是有,但没找到离线版。虽说有也多半是 Windows 专用程序来阅读,颇为不便,但至少不受制于网络嘛。最后找来找去,原来 Word 2010 自带了!一如既往地隐蔽哦。点右上角的帮助图标出现「Word 帮助」窗口后,点「搜索」按钮的下拉箭头,选择「开发人员参考」,里边那个「Word 2010 开发人员参考」就是了。

「开发人员参考」菜单项

别问不同的帮助主题和「搜索」有什么关系,也别问「Visual Basic 语言参考」和「Microsoft 窗体参考」为什么会在这里。微软一向是忽视逻辑的。

另外,工具栏最右边那两个按钮也很有用。

4
3
2012
6

Tornado 与文件上传

Tornado 自身是不支持大文件上传的。对于接收到的文件,它会把文件内容保存在内存里,而不是像 PHP 那样保存在临时文件里。这样对于大文件,问题很明显了——内容不够。所以,Tornado 上传文件的大小限制在 100M 以下了。Tornado 官方建议使用 nginx 的上传模块来处理文件上传。但是,我这个服务连 nginx 都没用的,不想为了这个还专门跑个 nginx。

于是,我尝试性地写了这么几百行代码。POST 上传的数据是multipart/form-data格式的,没有找到对应的 RFC,就对照着 HttpFox 显示的实际上传数据和 tornado 已有的代码进行修改。我理解的multipart/form-data格式是这样子的:

首先,在请求头里指定Content-Type: multipart/form-data; boundary=---------------------------12724806401896502337880080173,其中 boundary 的值是浏览器生成的,它用来分隔上传的不同文件。请求体一开始便是添加了--前缀的这个 boundary。刚开始我没太注意前边的横线多了两个,造成接收到的数据不对。在之后是\r\n,然后是和请求头格式一致的信息,如:

Content-Disposition: form-data; name="file"; filename="name.txt"
Content-Type: application/octet-stream

Content-Disposition中指明了文件对应表单的域名以及上传的文件名。文件名的编码看来没有定论,我的火狐用的是 UTF-8 编码。这些信息之后又是\r\n\r\n,然后是文件内容。还好这文件内容没有经过任何编码,直接保存即可。完了之后,如果还有下一个域的数据,那么在一个\r\n后就是类似的格式,否则在\r\n后是带--前缀和--后缀的 boundary。Tornado 的代码暗示数据结尾的\r\n是可选的。

整个格式是这样子的:

-----------------------------12724806401896502337880080173
Content-Disposition: form-data; name="file"; filename="name.txt""
Content-Type: application/octet-stream

This is file content.

-----------------------------12724806401896502337880080173
Content-Disposition: form-data; name="file"; filename="c"
Content-Type: text/plain

Another file content.
-----------------------------12724806401896502337880080173--

所以,要把数据保存到临时文件里去,不需要担心怎么进行流式解码了,只要确定了文件数据的起始和结束就好。为了做到这个,我只好每次都将读到的数据的最后一段长度为带前缀的 boundary 的长度加一的部分保存下来与下次读到的数据合并再处理,以此保存每段数据都是检查过 boundary 的。再加上一是为了防止\r\n被打断,下次找到 boundary 后取它前边的数据时出错。这个 edge case 还是今天写这文章时才想到,又花了不少时间测试。

最后记下 md5sum 的用法。计算 md5 时,把输出重定向到文件,校验时直接md5sum -c md5文件就可以了,不需要人工对比。

又,netcat 很好用。Arch 下使用 OpenBSD 版 netcat 发送 HTTP 请求的命令是:

nc.openbsd -q0 localhost 4322 < post

Ubuntu 现在默认的 netcat 就是 OpenBSD 版,所以直接用nc命令就可以了。

Category: python | Tags: http python tornado
3
18
2012
4

使用 gnokii 读取 3G 网卡的短信

使用 gnokii 读取 3G 网卡短信的方法ArchWiki上有写。安装 gnokii 后复制配置文件并将自己添加到uucp用户组中:

cp /etc/gnokiirc ~/.config/gnokii/config
sudo gpasswd -a `whoami` uucp

然后修改下配置文件,主要是port = /dev/ttyUSB0model = AT这两处。用户组的修改要下次登录时才生效,或者使用newgrp命令来登录到uucp组。据说此命令在某些 shell 里是内建命令,不过在 bash 和 zsh 里没有,只能调用外部命令,所以会开启一个新的 shell。

newgrp uucp

现在就可以读短信了。接收新信息并存储,可使用命令

gnokii --smsreader

完事之后按Ctrl-C中断。要把短信读出来,gnokii 可以把短信存储为 mbox 格式。这是一种邮件格式,使用 mutt 即可阅读。-f后边的sms即是要保存的文件名。注意,gnokii 仍会将消息输出到终端。ArchWiki 上说的是使用 xgnokii GUI 程序来读取,但是我没有找到这个程序(只有 manpage)。

gnokii --getsms SM 0 end -f sms

对于中文短信,这样会导致乱码。所以我写了个 Python 脚本来处理。三件事:一是将内容编码标识为 UTF-8,二是把按字节截断的邮件主题(短信正文的前若干个字节)最后几个无效的编码替换掉,三是将邮件主题按标准进行编码。这些事 Python 处理起来挺容易的 ;-)

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os
import sys
import email.header

for i in os.fdopen(sys.stdin.fileno(), encoding='utf-8', errors='replace'):
  if i.startswith('Subject: '):
    s = i[9:-1]
    print('Content-Type: text/plain; charset=utf-8')
    print('Subject:', email.header.Header(s, 'utf-8').encode())
  else:
    sys.stdout.write(i)
Category: Linux | Tags: LInux Python 网络 mutt
3
2
2012
6

为 Chito 修改 Markdown

我使用 Markdown 写博客已经有段时间了,但是一直以来有个小小的问题:对于代码块,markdown 生成的是一个<pre>标签里套一个<code>标签。缩进四个空格还好,用 Vim 的列编辑就行了(>操作不行,因为空行不会被缩进),可是删除这些<code>标签并加上相应的语言标识很烦。于是有了以下 Python 代码,使用的是 Python 版的 markdown,支持使用~~~~作为代码分隔符,如:

~~~~python|这是 Python 代码
print('Hello Python!')
~~~~

将会被翻译为

<pre class="brush: python;" title="这是 Python 代码">print('Hello Python!')
</pre>

程序如下:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import sys
from itertools import takewhile

import markdown
from lxml.html import fromstring, tostring

def parseAttr(s):
  a = s.split('|')
  if len(a) > 3:
    raise ValueError('Too many attributes')
  a = list(map(str.strip, a))
  if len(a) == 3:
    a[2] = bool(a[2])
  elif len(a) == 2:
    a.append(False)
  elif len(a) == 1:
    a.extend(['', False])
  else:
    a = ['plain', '', False]
  if not a[0]:
    a[0] = 'plain'
  return a

def analyseAttrs(text):
  '''Attributes are defined like this:

  ~~~~lang|title|collapse

  In place of ``collapse``, anything not empty is considered true.
  '''
  incode = False
  lines = []
  attrs = []
  istilda = lambda ch: ch == '~'
  for l in text.split('\n'):
    if l.startswith('~~~~'):
      if not incode:
        incode = len(tuple(takewhile(istilda, l)))
        attr = parseAttr(l.lstrip('~'))
        attrs.append(attr)
        l = tildas = '~' * incode
      else:
        if l.find(tildas) == 0:
          incode = False
    lines.append(l)
  return '\n'.join(lines), attrs

def applyAttrs(html, attrs):
  doc = fromstring(html)
  for i, code in enumerate(doc.xpath('//pre/code')):
    pre = code.getparent()
    text = pre[0].text
    del pre[:]
    pre.text = text
    attr = attrs[i]
    c = 'brush: %s;' % attr[0]
    if attr[2]:
      c += ' collapse: true;'
    pre.set('class', c)
    if attr[1]:
      pre.set('title', attr[1])
  return tostring(doc, encoding=str)[5:-6] + '\n'

def main():
  text = sys.stdin.read()
  text, attrs = analyseAttrs(text)
  out = markdown.markdown(text, ['fenced_code'])
  out = applyAttrs(out, attrs)
  sys.stdout.write(out)

if __name__ == '__main__':
  main()
Category: python | Tags: chito markdown python
3
2
2012
6

在 fcitx 中切换国标与传统引号

国家标准使用这些引号:‘’“”,而我发现传统中文的引号更漂亮:「」『』。我切换到传统引号已经有一段时间了,但最近发现有时还是需要使用国标引号,而 fcitx 的现任开发者认为不需要加入该切换功能。好在 fcitx 的配置文件都是文本,又有 fcitx-remote 工具,所以自己很容易地手动实现了两个版本的——Haskell 版本纯粹是练习用,因为没有扩展路径中的~的现成函数,所以只好自己找了个实现,代码有些长。

import Control.Applicative ((<$>))
import System.Cmd (rawSystem)
import System.Directory (getHomeDirectory)
import System.Posix.User
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

main = do
  file <- getFile
  TIO.readFile file >>= (TIO.writeFile file) . (T.map (trChar "“”‘’『』「」" "『』「」“”‘’"))
  reloadFcitx

getFile :: IO FilePath
getFile = expandUser "~/.config/fcitx/data/punc.mb.zh_CN"

reloadFcitx :: IO ()
reloadFcitx = rawSystem "fcitx-remote" ["fcitx-remote", "-r"] >> return ()

expandUser :: FilePath -> IO FilePath
expandUser "~"         = getHomeDirectory
expandUser ('~':'/':p) = fmap (++ "/" ++ p) getHomeDirectory
expandUser ('~':up)    = let (u, p) = break (== '/') up
                             in fmap (++ tail p) (homeDirectory <$> getUserEntryForName u)
expandUser p           = return p

trChar :: [Char] -> [Char] -> Char -> Char
trChar from to ch = case i of
                         Just i -> to !! i
                         _      -> ch
                         where i = elemIndex ch from

Python 版本就很简洁了:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os

m = str.maketrans('“”‘’『』「」', '『』「」“”‘’')
file = os.path.expanduser("~/.config/fcitx/data/punc.mb.zh_CN")

c = open(file).read().translate(m)
open(file, 'w').write(c)
os.execvp('fcitx-remote', ['fcitx-remote', '-r'])
Category: Linux | Tags: fcitx Haskell python

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com