1
9
2012
5

一个 Python 调试函数

Python 有个code模块,可以在程序中开个 REPL 交互命令行,就像 Python 解释器的交互执行一样,调试时非常方便。为了偷懒,我又把它包装了下,写下了repl函数(on github):

def repl(local, histfile=None, banner=None):
  import readline
  import rlcompleter
  readline.parse_and_bind('tab: complete')
  if histfile is not None and os.path.exists(histfile):
    # avoid duplicate reading
    readline.clear_history()
    readline.set_history_length(10000)
    readline.read_history_file(histfile)
  import code
  readline.set_completer(rlcompleter.Completer(local).complete)
  code.interact(local=local, banner=banner)
  if histfile is not None:
    readline.write_history_file(histfile)

之所以要现在把这个函数拿出来,是因为我终于解决了一件让我郁闷很久的问题——补全。历史记录是早就弄好了的,可是补全却经常不给力,补不出东西来,只有少数时候比较正常。这个和 Python 解释器自己的 REPL 不一样。最近在开发 XMPP 群,经常要用到,于是终于去读了rlcompleter.py的代码。还好不长,很快就搞定了:默认使用的是__main__.__dict__这个里边的对象进行补全,而不是globals()。给readline重新设置下补全函数就好了:

readline.set_completer(rlcompleter.Completer(local).complete)
Category: python | Tags: python
11
28
2011
7

弄了个支持 readline 的 MongoDB shell

不知道怎么搞的,新版 MongoDB 自带的 mongo shell 现在不支持 readline,而且使用一个极简到工作不正常的 linenoise。编译时加上 readline 也没用。虽然内建了简单的历史记录和补全,可是历史记录不能搜索,补全只能像 Vim 命令行默认的那样一个一个切换不能像 bash/zsh 那样全部列出来。这些还不是最令人郁闷的。最让我受不了的问题和十年前的 DOS 版 WPS 里遇到的差不多——当年的 WPS 里删汉字一次只删一半,现在的 mongo shell 里删汉字一次只删三分之一!而且光标定位是错的,按字节算的!

于是乎拿 Python 写了一个 shell。不愧是 Python,内置的东西真不错,不到100行就写好了。不过用到了自己另外的库函数,另外用到了自己的 colorless 程序来高亮显示查询结果。如果不想要 pygments 这个依赖的话,可以用 less 程序代替。以下贴个无高亮版的,高亮版的见 github

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

import sys
import os
from pprint import pprint
import subprocess

from pymongo import Connection
import pymongo.cursor

# 这个模块位于 github 上的 winterpy 仓库的 pylib/cli.py
from cli import repl

import locale
locale.setlocale(locale.LC_ALL, '')
del locale

host = 'localhost'
port = 27017
db = 'test'

def displayfunc(value):
  if isinstance(value, pymongo.cursor.Cursor):
    p = subprocess.Popen(['less', '-RFX'], stdin=subprocess.PIPE,
                        universal_newlines=True)
    pprint(list(value), stream=p.stdin)
    p.stdin.close()
    p.wait()
  else:
    pprint(value)

def main():
  global db
  conn = Connection(host=host, port=port)
  db = conn[db]

  v = globals().copy()
  v.update(locals())
  del v['repl'], v['argv'], v['main'], v['v'], v['host'], v['port']
  del v['displayfunc'], v['subprocess']
  del v['__name__'], v['__cached__'], v['__doc__'], v['__file__'], v['__package__']
  sys.displayhook = displayfunc

  repl(
    v, os.path.expanduser('~/.mongo_history'),
    banner = 'Python MongoDB console',
  )

if __name__ == '__main__':
  argv = sys.argv
  if len(argv) == 2:
    if '/' in argv[1]:
      host, db = argv[1].split('/', 1)
    if ':' in host:
      host, port = host.split(':', 1)
  elif len(argv) == 1:
    pass
  else:
    sys.exit('argument error')

  main()
Category: python | Tags: python MongoDB linenoise readline
9
24
2011
8

通过PyGObject调用GDK截图

Linux 下截个图挺麻烦的。最开始我想学 scrot 使用 Xlib,结果因为看不懂而放弃,转而使用GDK。搜到了TualatriX的这篇《几十行代码构造一个截屏软件》。虽然才不到50行的 C 代码,但我还是觉得有点长。

本来准备像上次的《使用Xtest模拟鼠标点击》一样写成 Python 模块的,后来从 Vayn 那里看到原来可以通过 PyGObject 来调用 GTK 及 GDK 等等(hello world 程序)。于是我也用这种方式完成了截图的代码,才十几行,原理和TualatriX的完全一样。

import mimetypes
from gi.repository import Gdk

def screenshot(filename, rect=None, filetype=None):
  screen = Gdk.Screen.get_default()
  if rect is None:
    rect = (0, 0, screen.width(), screen.height())
  if filetype is None:
    t = mimetypes.guess_type(filename)[0]
    if t is None:
      raise ValueError('cannot guess filetype for filename: %s' % filename)
    filetype = t.split('/')[1]

  rootwin = screen.get_root_window()
  pixbuf = Gdk.pixbuf_get_from_window(rootwin, *rect)
  pixbuf.savev(filename, filetype, (), ())

不过没有找到PyGObject的文档。官方说可以自己从 gir 文件生成,但是那个脚本在最新版的代码中才有,而那个代码也要求Glib非常新,我的 Arch 上都没有那么新,于是作罢。所以用法除了自己按 GDK 的文档猜就是 Google 了。那个savev的参数我都找到mono的文档去了。。。

PyGObject 默认是使用 GTK 3。也可以指定使用 GTK 2:

import gi
gi.require_version("Gdk", "2.0")
gi.require_version("Gtk", "2.0")
from gi.repository import Gdk, Gtk

/usr/lib/girepository-1.0/下还有一些typelib文件,说明这些库都有 GObject Introspection 支持,可以用包括 Python 3 在内的任何其支持的语言访问。不过我调用 xlib 时出错了:

>>> from gi.repository import xlib
>>> d = xlib.open_display()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.2/site-packages/gi/types.py", line 44, in function
    return info.invoke(*args)
glib.GError: Could not locate XOpenDisplay: `XOpenDisplay': python3: undefined symbol: XOpenDisplay

GObject Introspection 这个东西挺好的,除了文档。文档啊,就算不能支持 Python 的 docstring,至少也弄个 HTML 版出来啊,现在只有堆 XML 文件叫我怎么情何以堪啊,现在用 PyGObject 写代码就像在猜谜。。。

最后,代码的 github 链接

Category: python | Tags: gtk linux python
4
19
2011
3

Vim的Python3有内存泄漏?继续修正!

给Vim的Python3支持打了个补丁,发到邮件列表上只有Bram表示希望有人来测试就没有下方了。于是,这么久了,这个补丁的内存泄漏问题一直未被发现,直到看到蓝色基因的这篇文章。花了一个下午,发现我原来的补丁不仅没有修正本来就有的内存泄漏,反而雪上加霜,浪费了更多的内存。现在终于弄好了,放在我的陈列室里了,同时还莫名其妙地修正了另一个小问题

既然是内存泄漏,我首先想到的是valgrind这个工具。于是跑了一下:

valgrind --leak-check=full --show-reachable=yes vim

在开启的 Vim 中我 source 了蓝色基因的测试脚本:

lcd %:h
tabedit tmpbuffer
setlocal buftype=nofile
 
python3 << EOF
for i in range(3):
    flines= ['x'*200] * 50000
    vim.command("%s+\\_.*++g")
    for fl in flines:
        vim.current.buffer.append(fl)
    del flines[:]
EOF

整个过程CPU占到100%,而且运行速度极慢,内存消耗也非常多。最后Vim终于按我的指令退出时,valgrind刷屏了大约十几秒钟!而其间我看到除了Python的字样外,还有不少rb的字样。难道Ruby支持也有类似的问题?不过我不管它。重新编译了个只有--enable-python3interp选项的 Vim,这回跑起来快了一些,也没有那么多不相干的内存泄漏了。我也学聪明了点,把信息重定向到文件:

valgrind  --leak-check=full --show-reachable=yes src/vim 2> log

这样可以方便地在log中找“if_py”字符串了。可惜我弄的时候没想到自己会来写博客,所以log文件并没有保存。。。

首先我找到了DoPy3Command这个函数,valgrind说它里面分配的内存没有被释放。这里边的PyUnicode_AsEncodedString这块是我加的:

    /* PyRun_SimpleString expects a UTF-8 string. Wrong encoding may cause
     * SyntaxError (unicode error). */
    cmdstr = PyUnicode_Decode(cmd, strlen(cmd), p_enc, NULL);
    PyRun_SimpleString(PyBytes_AsString(PyUnicode_AsEncodedString(cmdstr, "utf-8", NULL)));

然后我能怎么办呢?当然是查Python的文档了。于是注意到文档上说PyUnicode_AsEncodedString返回的是新的引用。又去看官方教程上的示例,才知道如果一个API返回了新的引用,那么用完后应当手动Py_XDECREF!就像是strdup函数,它内部帮你malloc了,你自己用完后要记着free掉。(Py_XDECREFPy_DECREF的差别是,前者可以传NULL。)

于是就改吧,所有通过PyUnicode_AsEncodedString得到的对象都要Py_XDECREF下。为此,不仅需要临时变量来存储这个对象,更让我郁闷的是,在两个Python版本共有的函数StringToLine中有这样一段代码:

    str = PyString_AsString(bytes);
    len = PyString_Size(bytes);

这里的两个函数/宏我之前是这样定义的:

#define _PyUnicode_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL)
#define PyString_AsString(obj) PyBytes_AsString(_PyUnicode_AsBytes(obj))
#define PyString_Size(obj) PyBytes_GET_SIZE(_PyUnicode_AsBytes(obj))

这下我没辙了,只好又改了if_py_both.hif_python.c文件,加了两个宏:PyString_AsBytesPyString_FreeBytes。它们在 Python2 的代码中什么也不做,但是在 Python3 的代码中用来保存和释放中间对象:

#define PyString_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL);
#define PyString_FreeBytes(obj) Py_XDECREF(bytes)
#define PyString_AsString(obj) PyBytes_AsString(obj)
#define PyString_Size(obj) PyBytes_GET_SIZE(bytes)

有人说,if it ain't broken, don't fix it。可是,虽然问题只出在 Python3 部分,我还是得改 Python2 部分,感觉很不爽。

这样改完,再次反复运行测试代码,结果不遂人愿,依旧泄漏了不少内存。于是继续valgrind,又找到这里:

    static void
BufferDestructor(PyObject *self)
{
    BufferObject *this = (BufferObject *)(self);

    if (this->buf && this->buf != INVALID_BUFFER_VALUE)
	this->buf->b_python3_ref = NULL;
}

然后再次查教程中的示例:

static void
Noddy_dealloc(Noddy* self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

再看看 Python2 部分的代码,在相应的函数里有Py_DECREF,于是把这示例的最后一行给BufferDestructor以及WindowDestructorRangeDestructor加上。再测试,内存不再消耗100多M了,反复source也不会继续增加,于是作出结论:Vim 的 Python3 支持部分没有已知的 bug 了!

做完这一切,我只想说:Vim 这 Python3 支持也太 broken 了吧,中文经常乱码就算了,vim.error不能用我也忍了,竟然还内存泄漏!难道写这个代码的人也是初学Python C API啊?

不过抱怨归抱怨,还是很感谢原作者的,不然我连修正都不可能。不过,patch 弄好也提交了,却一直没人理我,原作者难道是一时兴起才写的、然后就消失了?

最后,补丁现在放到陈列室了。

Category: python | Tags: vim python C代码
3
4
2011
4

Python3.2mu 与 Vim

曾经,我辛苦两星期自以为终于弄好了 Vim 的 +python3 特性,却未曾想到,编译新发布的安装 Python3.2 后 Vim 的 Python3 支持再次悲剧……

事情是这样的。在vim-cn群有人编译 Python3.2 出错问我。我于是把之前为尝新鲜而 make 的 Python3.2 又 make install 了。然后 ./configure 时就出问题了。具体错误不记得了,反正是找不到什么文件。后来我找出了我以前写的一个从 C 调用 Python 代码的小程序,编译通过,链接时找不到某些符号。折腾了好久,才知道是 Python3.2 的安装出错了,./configure 时要加 --enable-shared 参数。当然,我还比较习惯加上 --with-wide-unicode 参数。

于是我的 C 小程序编译运行成功。但 Vim 的依旧悲剧。看了 src/configure.in,注意到它并没有使用 pkg-config,而是按以前 Python 的头文件和库文件的规律硬编码进去的。这时我才发现 Python 的相关文件/目录都多了个 mu 后缀:

>>> pkg-config --cflags --libs python-3.2
-I/usr/local/include/python3.2mu  -L/usr/local/lib -lpython3.2mu
>>> ls -li /usr/local/bin/python*
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3.2
164216 lrwxrwxrwx 1 root root    18 2011-03-01 23:18 /usr/local/bin/python3.2-config -> python3.2mu-config
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3.2mu
164107 -rwxr-xr-x 1 root root  1827 2011-03-01 23:18 /usr/local/bin/python3.2mu-config
164252 lrwxrwxrwx 1 root root    16 2011-03-01 23:18 /usr/local/bin/python3-config -> python3.2-config

这个 mu 后缀是什么意思呢?搜了半天,终于找到了:m 是普通版,u 是宽字符版(--with-wide-unicode),还有个 d 表示使用了 --with-pydebug 参数编译的。加了这些后缀,于是 Vim 配置脚本的硬编码就失败了。(它为什么要硬编码呢……T.T)对于 mu 版,修改方法是这样的:

# For Python3.2
if which python3 >/dev/null 2>&1 && [ $(python3 -c 'import sys; print(sys.version_info.minor)') -ge 2 ]; then
  sed -i -e 's|-lpython${vi_cv_var_python3_version}[dmu]*|-lpython${vi_cv_var_python3_version}mu|' \
         -e 's|python${vi_cv_var_python3_version}/config[^"]*|python${vi_cv_var_python3_version}/config-3.2mu|' \
         -e 's|include/python${vi_cv_var_python3_version}[dmu]*|include/python${vi_cv_var_python3_version}mu|' \
    src/configure.in
  # Fixed: no longer needed.
  # sed -i -e 's|PyEval_InitThreads();|/* PyEval_InitThreads(); */|' \
  #   src/if_python3.c
  autoconf=1
fi

[ $autoconf -eq 1 ] && (cd src && autoconf)

后面那个对 src/if_python3.c 的修改我也不知道是为什么,反正不这样的话调用 Python 时就 SIGABRT 出错退出,而这样改了之后好像也没什么负面影响。至于找出这个语句的办法嘛,当然是不知比 jdb 好用多少倍的 gdb 啰。


2011年4月19日更新修正了 Python3 接口的内存泄漏问题,发现已不再需要删掉那句代码了(删掉后反而出错)。

Category: python | Tags: python vim 编译
10
29
2010
7

金山快盘之跨平台同步脚本

金山快盘是和Dropbox差不多的同步服务,区别只是快盘(目前)只支持Windows,容量只有2G多(我的Dropbox都4G多了~),安全性不好(网页版的使用不加密的HTTP协议)、同步文件夹被设为系统属性等等。

由于某些原因,同学和我通过金山快盘分享文件。于是开始研究之。客户端上传下载时使用XML格式,但数据都加密过了,无解。于是转攻网页版。基本上都是很简单的json,上传使用了个Flash,用Wireshark抓包一看还是HTTP POST。

于是ksync诞生了,仅支持从服务器到本机的单向共享文件同步。如有其它需求可自行扩展。本程序仅需要Python3支持,目前仅在Ubuntu Linux上测试通过。

下载链接。使用前记得编辑ksync文件,把自己的用户名、cookie文件存放路径、同步目标路径替换掉。

Category: python | Tags: linux python json
10
21
2010
6

Python HTTP 请求时对重定向中的 cookie 的处理

首先说明一下,我使用的是 Python3 的 urllib,但 Python2.x 同理(使用 urllib2)。

想用脚本去登录一个网站。和很多网站一样,该网站使用 cookie 来保存会话信息。这个我以前是自己提取 response 中的 Set-Cookie 头来处理的。这次本想如法炮制,却发现没保存需要的 cookie,所以登录失败。

很郁闷地想了半天,最后出去 wireshark 抓包,终于发现原来重要的 cookie 在登录后的应答中,但这个应答是个 302 重定向,所以 urllib 默认的 opener (urllib.request.urlopen)直接就跟从这个重定向了,没有对 cookie 进行任何处理。

我首先想到的是,不要跟从重定向。我看到有个 HTTPRedirectHandler,但文档里没写它怎么用。郁闷……自己找到 request.py 文件看源代码,折腾了好久无果,遂想到 Google (早该想到了。。。)于是找到了 StackOverflow 上。有两个解决办法:要么不跟从重定向,要么弄个 HTTPCookieProcessor 保存 cookie 信息。看我自己的需求,当然选后者了。而且,那个回答问题的人也没有给出如何不让它跟从重定向(所给代码只是在重定向前对 cookie 进行处理而已)。

于是,我再一次地打开了 http.cookiejar 的文档,尝试弄明白这东西到底怎么用。当初折腾 cookie 的时候,没弄明白这个,所以才自己处理的。

看 request.py 里的代码,这个 CookieJar 用起来相当不错:

class HTTPCookieProcessor(BaseHandler):
    def __init__(self, cookiejar=None):
        import http.cookiejar
        if cookiejar is None:
            cookiejar = http.cookiejar.CookieJar()
        self.cookiejar = cookiejar

    def http_request(self, request):
        self.cookiejar.add_cookie_header(request)
        return request

    def http_response(self, request, response):
        self.cookiejar.extract_cookies(response, request)
        return response

    https_request = http_request
    https_response = http_response

不过我需要将 cookie 信息保存到文件。从文档上看到有个 FileCookieJar。我尝试了下,出错了,没有 _really_load 方法,我晕。。。之后才注意到其源代码开头有个ASCII图:

						CookieJar____
                        /     \      \
            FileCookieJar      \      \
             /    |   \         \      \
 MozillaCookieJar | LWPCookieJar \      \
                  |               |      \
                  |   ---MSIEBase |       \
                  |  /      |     |        \
                  | /   MSIEDBCookieJar BSDDBCookieJar
                  |/
               MSIECookieJar

原来具体实现还在子类啊。好吧,我就用 MozillaCookieJar 好了。

用法很简单,初始化时把文件名传给它,载入用 load(),保存用 save()。不过要注意的是,文件不存在时不能载入,touch 个空文件出来也不行的。

另外,那个 StackOverflow 的页面还提到了 mechanize 这个模块,有时间去尝试下 :-)

最后,如果我不要它重定向该怎么做呢?难道非要我去用更底层的 http.client?

Category: python | Tags: python 网络

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com