3
18
2013
16

使用 cx_freeze 打包 Python 程序

首先,当然是给一个目标系统安装 cx_freeze。虽然 cx_freeze 是跨平台的,但没发现它支持在一个平台上打包出另一个平台的二进制文件,而且那样还得准备那个平台上的库文件。我的目标平台是 Windows XP,所以还要准备一个 Dependency Walker

其次,使用cxfreeze-quickstart向导生成配置文件setup.py。当然,如果已经有setup.py文件的话直接修改就是了。下边是一个示例:

import sys

from cx_Freeze import setup, Executable

# Dependencies are automatically detected, but it might need
# fine tuning.
buildOptions = dict(
  packages = [], excludes = [],
  include_files = ['images', 'data.sqlite'],
)

name = 'example'

if sys.platform == 'win32':
  name = name + '.exe'

base = None
if sys.platform == "win32":
    base = "Win32GUI"

executables = [
  Executable('main.py', base = base, targetName = name,
             compress = True,
            )
]

setup(name='Example',
      version = '1.0',
      description = 'An example program',
      options = dict(build_exe = buildOptions),
      executables = executables)

当然,这里有不少我改过的地方。在buildOptions变量中我加了data.sqlite文件和images目录到include_files中去。它们会被放到生成的二进制文件相同的目录。

cx_freeze 在打包 Windows 可执行文件时并不会像 gcc 那样自动添加.exe后缀,所以我要手动加上。

Executable的调用中,要写成base='Win32GUI'这样子。cxfreeze-quickstart目前直接写在第二个参数的位置上的方法是不对的。base的默认值是Console,在 Windows 下运行时是会出现黑色的cmd.exe窗口的。参见StackOverflow: Hide console window with wxPython and cxFreeze

这样还没有完成。打包后测试发现PyQt4.QtNetwork的库文件没有打包进去,可能是因为它是从共享库中引用的,cx_freeze 没有检测到这个依赖。在程序中 import 一下就可以了。另外一个问题是,在没有安装相关库的干净的目标系统上执行时还遇到以下错误信息:

DLL load failed: 找不到指定的模块
DLL load failed: The specified module could not be found.

其上还有一个 Traceback。这是因为有些(据说主要是 Microsoft Visual C++ Redistributable 的) DLL (非 Python 模块)没有被打包进去。从 Traceback 中找到引发这个错误的 DLL(或者 pyd)文件名,将其在打包系统中使用前边提到的 Dependency Walker 打开,在左边的树形库列表中找到目标系统上可能没有的库文件,将其复制到 cx_freeze 生成二进制文件的目录中即可。比如我这里需要手动添加msvcr100.dllmsvcp100.dll

最后,打包过的程序执行时__main__模块是没有__file__属性的,所以无法通过这个变量来切换到程序所在的目录,进而读取自己的数据文件。但是,打包过的程序有sys.frozen属性,程序自身的路径存放在sys.executable中,所以程序中需要作下判断:

import os
import sys

if hasattr(sys, 'frozen'):
  me = sys.executable
else:
  me = __file__
mydir = os.path.dirname(me)

参见StackOverflow: How do I get the path of the current executed file in python?

最终打出来的可执行文件和库文件比较大,PyQt 程序总共有 40M 之多。使用 7z 压缩之后能减小到 10M 多。

Category: python | Tags: python windows
12
1
2012
6

继续修改邮件主题

上回说到,中文邮件列表主题标签中的序号让我这位 mutt 用户很是眼烦,于是拿 Python 写了个脚本处理掉了序号。然而,主题中让人眼烦的岂止是标题?看看下面这些:

回复: Re: [shlug] 求助:c程序的效率比java还慢
[CPyUG] Re: [CPyUG:183226] [OT] 自动 tag 加上序号了?

既然已经知道如何处理了,加上有风间星魂的基于正则的极简语法解析器,不妨再处理下。达到的效果如下:

  • 去掉所有的回复字样,在最开头加上「Re: 」
  • 去掉所有标签内的数字
  • 去掉重复的标签

代码在这里。修改后的解析器在这里

Category: python | Tags: mail mutt python
11
24
2012
0

使用 procmail 修改邮件主题

Google Groups 上的邮件列表可以根据管理员的设置自动添加在邮件主题前添加指定的字符串。对于 mutt,固定的字符串没什么,但当这个添加的字符串不断地变化呢?比如这个字符串设置成[vim-cn:%d]%d会被邮件的序号所取代,于是每封邮件都有了不同主题。(对于回复邮件,按 RFC 5322 3.6.5 节的意思,应当将开头的一个Re:不予考虑。)

这样的邮件会话,mutt 一看,哎呀,会话中出现了新的主题,得显示一下啦。于是,本来通常情况下一个会话只会显示一次的邮件主题,在添加了邮件序号后变成了N个。对比下图中黄线上方和下方的区别。

mutt subject display

本来呢,vim-cn 列表是下边那个样子的。最近觉得 vim-cn 的邮件多了起来,看起来眼烦,正好有管理权限,就去把%d删掉了。可谁知我才收到几封不带序号的 vim-cn 邮件呢,python-cn 却开始加序号了,不知道管理员是不是想看看第20万封邮件是谁发的。唉,既然改变不了那个列表的设置,那么就改变本地的邮件处理好了。

在开始使用 mutt 等工具的时候,我一直是把 procmail 当成邮件分发和过滤工具的。如今需要它来修改邮件了,语法还不会呢。另外它的示例中大多是 formail 工具,但我实在不想再学一种语法晦涩的工具了,于是自己搞。本来觉得挺简单的,一句 sed 就能搞定的东西,等真正查看邮件源码时才发现,远没有想像的那么简单!

Subject: =?GB2312?B?W0NQeVVHOjE4MzI0OV0gcHl0aG9uIL+qt6LN+NKz087Pt7rNyta7+rbL?=
        =?GB2312?B?zfjC59POz7e+rdHpx/PW+g==?=

第一,它编码过了;第二,它分成了多行。哦还有第三,邮件正文即使出现也不要处理,不然我这文章发过去不是变了样么?

后两点还好,awk 可以搞定,可是这编码不是那么容易呢,于是用上了 Python。既然 Python 3.3 已经发布,所以试了试新的yield from语法。反正我不认为我会需要在只有 Python 3.2 或者更早的系统上使用这个脚本。

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

import sys
import re
from email import header

subject_seq = re.compile(r'''((?:..[::]\s?)?  # Re、回复等
                             \[[^:]+)
                             :\d+              # 要删除的序号''', re.X)

def stripSeq(input):
  subject = None
  while True:
    l = next(input)
    if l.startswith('Subject: '):
      # Subject appears
      subject = l
      continue
    elif subject and l[0] in ' \t':
      # Subject continues
      subject += l
    elif subject:
      # Subject ends
      s = subject[9:]
      h = header.decode_header(s)
      assert len(h) == 1, 'unexpected subject line: ' + s
      s, enc = h[0]
      if isinstance(s, bytes):
        s = s.decode(enc)
      m = subject_seq.match(s)
      if not m:
        yield subject
      else:
        s = m.group(1) + s[m.end():]
        yield 'Subject: ' + header.Header(s, 'utf-8').encode() + '\n'
      subject = None
      yield l
    elif l.strip() == '':
      # mail body
      yield from input
    else:
      yield l

if __name__ == '__main__':
  sys.stdout.writelines(stripSeq(iter(sys.stdin)))

Github 上的地址

procmail 的规则如下,参考了 Stackoverflow 的这个回答

:0 fw
| ~/scripts/python/pyexe/procmail.py
Category: python | Tags: mail mutt python procmail
10
26
2012
4

在 Python 中流式解压 gzip 数据

在处理 HTTP 响应时,我需要来一段数据就处理一段。为了节约网络资源,我开启了 gzip 传输。然后问题来了:有什么办法把 gzip 过的数据一段段传进去,它能一段段地解压并吐出数据呢?gzip 模块虽然支持fileobj参数,但是它读不到数据时认为数据流已经结束,然后进行 CRC 校验……这里有个人也这样尝试过。解决办法也有了

d = zlib.decompressobj(16+zlib.MAX_WBITS)

使用一个神奇的数字构造一个decompress对象,然后不断地调用它的decompress方法就可以得到一段段的解压数据了 :-)

然后,我发现我真的想太多了——我用的可流式解析 HTTP 响应的库 http-parser 原生支持解压的,而且同时支持 gzip 和 deflate 方法!这个库能很好地适配到异步 I/O 框架中,可是文档太少了,这个解压的支持 docstring 里都没写,看了源码才知道 :-(

Category: python | Tags: Python HTTP
9
30
2012
3

使用 PyQt 滚动播放卫星云图

自从和 GNOME 开发者接触过之后,我决定放弃断断续续学了一段时间的 GTK 而转向 Qt 了。看了两三天的 PyQt4 tutorial,恰好遇到一需要界面的脚本,本来我会搞成 Web 的,但既然学了 Qt 嘛,当然得练练。

起因是这样子的。一天和群里的人聊到天气之类的,然后有人扔了张卫星云图出来。多年未见的云图啊,再次见到感觉好亲切,虽然我的地理和气象知识已经忘记了好多了。然后得到了这个网页。本来呢,他们想搞成在以前的天气预报里那样逐幅图像显示的滚动播放效果的,但政府网站嘛,出点问题很正常,比如我第一次播放的时候图片基本上动不了(可能是因为没有预加载以及网络不畅造成的),第二次的时候图像时不时闪一下。而且这样可控性太差,想看的时段根本看不清就跳走了,不想看的时段却一直慢慢地跳。所以我拿 Python 写了个通过 slider 滑块来控制显示的图像简易脚本。写完后才发现使用 sxiv 然后按住N/P键是一样的效果……

代码如下,需要 PyQt4 或者 PySide。对于后者,Arch 用户可以从 Arch Linux CN 源安装而不必通过 AUR 自行编译(C++ 编译很费时的)。

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

# TODO: 并发下载
# TODO: 下载进度显示
# TODO: 允许加载已经下载但网页上没有的云图
# TODO: 网络作为可选

import os
import sys
import re
import gzip
import urllib.request

pic_dir = '.'

try:
  from PySide import QtGui, QtCore
except ImportError:
  from PyQt4 import QtGui, QtCore

def getPage():
  request = urllib.request.Request('http://www.weather.com.cn/static/product_video_v1.php?class=JC_YT_DL_WXZXCSYT')
  request.add_header('Accept-Encoding', 'gzip')
  res = urllib.request.urlopen(request)
  return gzip.decompress(res.read()).decode('utf-8')

def getPics(page):
  urlre = re.compile(r'\bhttp://i.weather.com.cn/i/product/pic/m/sevp_nsmc_wxcl_asc_e99_achn_lno_py_\d{17}.jpg\b')
  return sorted({x.replace('/m/', '/l/') for x in urlre.findall(page)})

def download(pics):
  ret = []
  for p in pics:
    file = os.path.split(p)[1]
    file = os.path.join(pic_dir, file)
    ret.append(file)
    if os.path.exists(file):
      continue
    data = urllib.request.urlopen(p).read()
    open(file, 'wb').write(data)
  return ret

class YuntuShow(QtGui.QWidget):
  def __init__(self, pics):
    super().__init__()
    self.pics = pics
    self.initUI()

  def initUI(self):
    pic = QtGui.QPixmap(self.pics[-1])
    self.pic = piclabel = QtGui.QLabel(self)
    piclabel.setPixmap(pic)

    slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
    slider.setTickPosition(QtGui.QSlider.TicksBelow)
    m = len(self.pics) - 1
    slider.setMaximum(m)
    slider.setSliderPosition(m)
    slider.valueChanged[int].connect(self.changePic)

    vbox = QtGui.QVBoxLayout()
    vbox.addWidget(piclabel)
    vbox.addWidget(slider)
    self.setLayout(vbox)

    self.resize(960 + 10, 720 + 50)
    self.setWindowTitle('YuntuShow')
    self.show()

  def keyPressEvent(self, e):
    if e.key() == QtCore.Qt.Key_Q:
      self.close()

  def changePic(self, value):
    pic = QtGui.QPixmap(self.pics[value])
    self.pic.setPixmap(pic)

def main():
  urls = getPics(getPage())
  pics = download(urls)
  app = QtGui.QApplication(sys.argv)
  yt = YuntuShow(pics)
  sys.exit(app.exec_())

if __name__ == '__main__':
  main()

短吧?而且我也只看了三天的教程用的时候查了下文档而已哦~比起 GTK 来快速多了。不过这其中有个很重要的原因是我已经通过 GTK 了解了基本的 GUI 编程了。

因为最开始会从网络下载数据和图像,所以要等一会儿窗口才会出现。

Category: python | Tags: python Qt pyqt pyside
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
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
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
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

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com