9
21
2011
2

使用Xtest模拟鼠标点击

最近做一个小工具,需要模拟鼠标点击事件。当然,我可不想去调用 xdotool 或者 xmacro,效率什么不说,光是添加这么个罕见的依赖就不喜欢。顺便也好练习下 C 编程。

Xtest 的函数名长参数列表也长,不过用起来很简单。我所需要调用的函数就两个:

  • XTestFakeMotionEvent:把鼠标光标移动到指定坐标;
  • XTestFakeButtonEvent:模拟鼠标键

Xtest 的函数手册都在一个 manpage 里。看一下就知道用法了。

XTestFakeMotionEvent有五个参数,第一个是Display指针,然后依次是屏幕号、坐标和延时。屏幕号写-1就是默认了。延时我用0就好了。XTestFakeButtonEvent有四个参数,第一个依旧是Display指针,然后是按键号、是不是按下(还是放开按键)、延时。左键是1其它依次递加。不知道为什么这些函数要有个延时的参数。

#include<X11/Xlib.h>
#include<X11/extensions/XTest.h>

/* ... */

int clickAt(int x, int y){
  Display *dpy = XOpenDisplay(NULL);
  if(dpy == NULL){
    return 0;
  }

  XEvent event;

  /* get info about current pointer position */
  XQueryPointer(dpy, RootWindow(dpy, DefaultScreen(dpy)),
      &event.xbutton.root, &event.xbutton.window,
      &event.xbutton.x_root, &event.xbutton.y_root,
      &event.xbutton.x, &event.xbutton.y,
      &event.xbutton.state);

  XTestFakeMotionEvent(dpy, -1, x, y, 0);
  XTestFakeButtonEvent(dpy, 1, 1, 0);
  XTestFakeButtonEvent(dpy, 1, 0, 0);
  /* place the mouse where it was */
  XTestFakeMotionEvent(dpy, -1, event.xbutton.x, event.xbutton.y, 0);
  XCloseDisplay(dpy);
  return 1;
}

这个函数实现了点击指定的屏幕坐标,完事之后再把鼠标光标移回去。最开始是没有移回去的,然后测试的时候我经常找不到鼠标光标了。。。。

C 语言用起来挺不爽的,所以后来做了个 Python 模块。不过功能很不完整,以后有需要时再慢慢加啦。要是谁有兴趣也可以自己加了后给我发 pull request 就更好了。代码地址:https://github.com/lilydjwg/winterpy/blob/master/pyso/X.c,编译命令:

gcc -O2 -shared -lX11 -lXtst `pkg-config --cflags --libs python3` X.c -o X.so

编译后import X,然后help(X)就知道用法了。

Category: Linux | Tags: python C代码 X Window
7
31
2011
5

在 ncurses 中使用 readline

有一天,我发现了一个很好用的 Python shell——bpython。它使用了 ncurses 来做界面,使用了 pygments 来高亮代码,怎么看都比 ipython 漂亮,更不用说 Python 自己的了。不过既然它使用了 ncurses,麻烦也就来了——ncurses 不支持 readline!虽然有些模拟,但终究是不好用,M-f M-b不起使用,M-数字也不能用。于是我再次去 google 同时使用 ncurses 和 readline 这两个库的办法。

功夫不负有心人,这次终于 google 到了点有用的东西

The basic idea is to use call rl_callback_read_char() when input from the user is available (determined with select, or similar), then print 'rl_line_buffer' as you would any other string in ncurses, and optionally set A_REVERSE on the position indicated by rl_point. (or just reposition the cursor I guess, either works...)

不过可惜的是,这封邮件给出的代码在我这里并没有跑起来。其实跑起来了也用处不大,因为我需要的那部分代码独立性太差了,还是得重写。

花了一个下午,我终于弄出了一个雏形。

首先,这个rl_callback_read_char()是这么用的(文档):

#include<stdio.h>
#include<readline/readline.h>

int main(void){
  int cont = 1;

  void callback(char *text){
    if(text == NULL){
      rl_callback_handler_remove();
      putchar('\n');
      cont = 0;
    }else{
      printf("%s.\n", text);
    }
  }

  rl_callback_handler_install(">> ", callback);
  while(cont){
    rl_callback_read_char();
  }
  return 0;
}
      

首先安装个回调函数,它将在 readline 读取到一行内容时调用。当标准输入可用的时候,调用rl_callback_read_char()来读取字符。另外注意,这里我用了 gcc 的嵌套函数支持,免得弄出不少全局变量。

知道怎么用rl_callback_read_char()之后,就可以按那封邮件所说的,把它和 ncurses 联合起来了。代码修改自NCURSES Programming HOWTO。思路很简单,readline 负责读取并处理用户输入,显示是自己处理的。不过作为中文用户,纠结了半天的中文问题。最开始是有了 ncurses 之后,中文显示异常。这个是通过setlocale解决的。然后又是光标放的位置不对。于是又用上了我同样不熟悉的 wchar,自己计算光标应该放在哪里。

#define _XOPEN_SOURCE 700       /* for wcswidth and 700 is for mbsnrtowcs */
#include<wchar.h>
#include<ncurses.h>		/* ncurses.h includes stdio.h */
#include<stdlib.h>
#include<string.h>
#include<readline/readline.h>
#include<locale.h>
 
int mygetstr(char *str, int y, int x){
  WINDOW *win;
  int size, col;
  int ok = 0;
  int width;
  wchar_t wstr[80];
  char *p;
 
  getmaxyx(stdscr, size, col);
 
  void getaline(char *s){
    str = s;
    rl_callback_handler_remove();
    ok = 1;
  }
 
  rl_callback_handler_install("", getaline);
  win = newwin(1, col-x, y, x);
  while(1){
    rl_callback_read_char();
    if(ok)
      break;
    werase(win);
    strncpy(str, rl_line_buffer, 80);
    p = str;
    /* how many column chars before cursor occupies? */
    size = mbsnrtowcs(wstr, (const char**)&p, rl_point, 80, NULL);
    width = wcswidth(wstr, size);
    mvwprintw(win, 0, 0, "%s", str);
    /* put the cursor at right column */
    wmove(win, 0, width);
    wrefresh(win);
  }
  delwin(win);
  return 0;
}
 
int main(){
  char mesg[] = "Enter a string: ";
  char str[80];
  int row, col;
 
  setlocale(LC_ALL, "");        /* make ncurses handle Chinese correctly */
  initscr();
  getmaxyx(stdscr, row, col);
  mvprintw(row / 2, (col - strlen(mesg)) / 2, "%s", mesg);
  refresh();
  mygetstr(str, row / 2, (col + strlen(mesg)) / 2);
  mvprintw(LINES - 2, 0, "You Entered: %s", str);
  getch();
  endwin();
 
  return 0;
}
      

注意:此代码只是演示用,缓冲区溢出什么的我都没处理。

终于搞定了 C 下结合两者的使用,接下来 Python 版思路是有了,但因为其标准库 readline 中没有提供rl_callback_read_char()函数,所以只能用 ctypes 了。下面是在 Python 里使用rl_callback_read_char()的示例,ncurses 部分我暂时不想折腾了。

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

import sys
import readline
import ctypes
import ctypes.util

rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)

def callback(s):
  if s is None:
    rllib.rl_callback_handler_remove()
    sys.stdout.write('\n')
    sys.exit()
  elif not s:
    pass
  else:
    print('%s.' % s.decode())
    # 这样也可以
    # print(readline.get_line_buffer())

cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)

rllib.rl_callback_handler_install.restype = None
rllib.rl_callback_handler_install(ctypes.c_char_p(b">> "), cbfunc(callback))

while True:
  rllib.rl_callback_read_char()
      

2011年8月4日更新:今天终于完成了个 quick and dirty 的 Python 版:

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

import sys
import readline
import ctypes
import ctypes.util
import curses
import struct
from 字符集 import width
 
rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)
 
def getstr(win, y, x):
  _, col = win.getmaxyx()
  inputbox = curses.newwin(1, col-x, y, x)
  ret = ''
  ok = False
  def callback(s):
    nonlocal ok, ret
    if s is None:
      rllib.rl_callback_handler_remove()
      raise EOFError
    elif not s:
      ok = True
    else:
      ret = s.decode()
      ok = True
  
  cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)
  
  rllib.rl_callback_handler_install.restype = None
  rllib.rl_callback_handler_install(ctypes.c_char_p(b""), cbfunc(callback))
  
  while True:
    rllib.rl_callback_read_char()
    if ok:
      break
    inputbox.erase()
    # 这样获取的值不对。。。
    # bbuf = ctypes.string_at(rllib.rl_line_buffer)
    buf = readline.get_line_buffer()
    bbuf = buf.encode()
    inputbox.addstr(0, 0, buf)
    rl_point = struct.unpack('I', ctypes.string_at(rllib.rl_point, 4))[0]
    w = width(bbuf[:rl_point].decode())
    inputbox.move(0, w)
    inputbox.refresh()

  del inputbox

  return ret

msg = '输入字符串:'
win = curses.initscr()
curses.noecho()
row, col = win.getmaxyx()
win.addstr(row // 2, (col - width(msg)) // 2, msg)
win.refresh()
s = getstr(win, row // 2, (col + width(msg)) // 2)
win.addstr(row - 2, 0, '你输入了: ' + s)
win.getch()
curses.endwin()
      
Category: Linux | Tags: C代码 ncurses python readline
6
15
2011
33

让 Vim 在终端下和 GVIM 一样漂亮:gui2term.py 更新至 3.0 版

gui2term.py是我用Python写的一个脚本,用于给仅支持 GVIM 的配色方案添加256色终端支持。介绍文章以及截图可以看Dante的《强烈推荐-将gui配色转化为终端配色的vim插件-gui2term-py》以及muzuiget的《Vim终端配色转换工具》。

缘起

GVIM 比起终端版的 Vim 来,支持丰富的色彩,而且使用 RGB 表示,更易于编辑,所以有很多配色方案都是只适用于 GVIM 的。但很多人不甘心忍受终端下丑陋的 Vim,于是,如muzuiget的博文所述,有好几个插件都尝试让终端版 Vim 能够使用为 GVIM 写的插件。但是,它们无一例外都是在 Vim 运行时动态转换配色的,估计有点影响启动速度。更重要的是,它们都是在 RGB 空间寻找最接近的终端色彩。这样造成的结果是,经常找到的颜色不是视觉上最接近的。甚至,在转换 spring 配色的时候,原本浅绿色的背景变成了灰色!

于是,我决定写一个脚本,永久性地为配色添加终端支持;同时,使用更好的算法来寻找最接近的颜色。所以,gui2term.py 是独立于 Vim 的 Python 脚本。所以,在最初的版本中我使用了 HSL 空间。没办法,不懂相关的色彩知识,只好凭感觉选择算法了。

更新

前天,我收到了一封来自 Eric Pruitt 的邮件。他建议我使用 colormath 这个库来寻找最接近的颜色。我看了下,发现 colormath 依赖于 numpy。我可不喜欢这么大的依赖。于是,我从 colormath 以及别的地方找出了需要用到的三个算法:RGB 空间到 XYZ 空间的转换、XYZ 空间到 CIELab 空间的转换,以及一个名为 delta_e_cie2000 的 delta 算法。

新的算法比原先的要好一些。我测试了三个配色(lilypink、spring、以及另外一个自己修改的)。其中,有两个的转换结果与 GUI 版更为接近(但是其中一个我更喜欢之前的算法转的),另外一个与原来的算法结果相同。

除此之外,我也更改了寻找 rgb.txt 的逻辑,因为不少人用的时候程序报告找不到 rgb.txt 文件。现在,gui2term.py 会首先尝试使用 locate 程序寻找 rgb.txt。

Category: Vim | Tags: python vim 配色
4
22
2011
5

初次使用 git 的“核弹级选项”:filter-branch

当初看 Pro Git 时就被作者这个“核弹级选项”的称呼吓到了,因此一直没敢好奇地去尝试。核弹啊,用对了威力无穷,用错了破坏力无穷!

但是,今天,我不得不用了,因为我想把我的 Python 脚本放到 github 上去公开。由于之前没想过要公开,所以不能肯定是不是把诸如密码之类的敏感数据直接写代码里了。于是我就用 git 的 pickaxe 选项-S来找找看。这个我也是今天才学到的哦,来源是stackoverflow上的若干问题,具体记不清了。

这个选项结合git log,其功能是把提交中包含某个文本的提交找出来。比如git log --stat -S密码就把所有提交中包含“密码”二字的提交找出来了,同时--stat告诉 git 我希望看到文件列表,以确定敏感数据在哪个文件里。另外说下zsh,我设置了hist_ignore_space选项,所以当查找密码时我在命令行最开始输入个空格,这样 zsh 就不会把这条命令写到命令历史里。

找了下,还真发现了两个包含密码的文件(其中之一 base64 过,另一个直接明文。。。)。我决定把它们转移到其它目录下,所以要删除这些文件。同时我意外地发现,有些提交中我引用了之前的提交,比如“(after xxx) fix xxx”这种。重写 git 历史后这些提交的 sha1 值会改变,所以这些提交信息也要重写。最开始我想把提交 sha1 修改为重写后的值,后来发现有点麻烦,得通过提交 sha1 查询一个备份中的提交信息(如果不事先保存相关信息的话),然后再通过提交信息查询重写后的提交的 sha1 值,还不知道 filter-branch 时能不能查询历史,于是作罢。还好这个仓库中只有一个这样的提交,所以我直接修改成~n表示引用第 n 次前的提交算了。最后,整个命令出来了:

git filter-branch --tree-filter 'rm -rf files_to_remove' --msg-filter '
sed s/d101601/~8/
' --prune-empty -f HEAD --all

命令挺复杂的,所以我是调用 Vim 写好的,然后在一个 clone 出来的仓库里先试运行。先解释下各个参数:

  • --tree-filter表示修改文件列表。
  • --msg-filter表示修改提交信息,原提交信息从标准输入读入,新提交信息输出到标准输出。
  • --prune-empty表示如果修改后的提交为空则扔掉不要。在一次试运行中我发现虽然文件被删除了,但是还剩下个空的提交,就查了下 man 文档,找到了这个选项。
  • -f是忽略备份。不加这个选项第二次运行这个命令时会出错,意思是 git 上次做了备份,现在再要运行的话得处理掉上次的备份。
  • --all是针对所有的分支。

试运行了几次,看到 150 多次提交逐一被重写,然后检查下,发现要删除的文件确实被删除了。于是高兴地到 github 建立新仓库,把脚本上传了。仓库名叫winterpy,因为网友 Vayn 建议我用 summerpy,而我更喜欢冬季 :-P

折腾完毕,我更加喜欢 git 了 :-)

Category: 版本控制 | Tags: python Git
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 编译
2
22
2011
3

Vim7.3 的 Python3 支持修正补丁

Vim 7.3 增加了对 Python3 的支持,但其有不少 bug,从不能正确地向缓冲区中添加中文文本,到 buffer 对象不支持 slice 操作,vim.error 不是 BaseException 的子类而是一个 str,以及各种中文乱码/UnicodeDecodeError,让我这个 Python3 的坚定支持者非常郁闷,于是趁假期把 Vim 好好修理了一番。

此次修正历时两周,涉及 src/if_{py_both.h,python.c,python3.c},共 3 files changed, 308 insertions(+), 265 deletions(-),修正的具体项目为

  • 向缓冲区添加文本时正确处理编码
  • buffer 对象支持 slice 赋值
  • vim.error 不再是字符串
  • py3file 让 Python 检测文件编码
  • 向 Python 传递缓冲区字符串时使用正确的编码(这解决了 gundo 在非 UTF-8 编码时的解码出错)
  • py3 命令输入使用 'encoding' 解码后再以 UTF-8 编码(这解决了在 'encoding' 非 UTF-8 时含中文的 py3 命令的 SyntaxError)
  • 向标准输出写文本时使用正确的编码(这样 print() 之类不会输出乱码)

以上数据要感谢git工具。

目前我在 Ubuntu Linux 10.10 32bit 和 Windows XP SP3 (使用 MinGW 编译)上测试没有问题,有兴趣的请帮忙再测试下。下载地址

另外,附上 gundo 的 Python 2 & 3 兼容版以及使用 Python3 支持的 Python 补全插件 python3complete.vim(放到 ~/.vim/autoload 下)。


PS:Python3.2 发布了,增加了一些很好的新特性,比如argparse模块,str.format_map方法等等。


2011年3月3日更新:给 Vim 编译 Python3.2 支持也有些艰难,参见这篇文章

2011年4月19日更新:今天解决了内存泄漏的问题,补丁已更新。另外,本补丁将在陈列室维护。

2011年6月19日更新:此补丁已被官方采纳。

Category: Vim | Tags: C代码 python vim
12
15
2010
4

从UDP到解决DNS污染的脚本

最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:

q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01'
s.sendto(q, ('8.8.8.8', 53))

\x07twitter\x03com\x00就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:

>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'46.82.174.68'
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'128.121.146.100'

再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01),是收不到回答的。

有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:

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

from socketserver import *
from socket import *
import sys, os

'''
来源 http://code.google.com/p/pydnsproxy/
'''

DEF_LOCAL_HOST = '127.0.0.1'
DEF_REMOTE_SERVER = '8.8.8.8'
DEF_PORT = 5350
DEF_CONF_FILE = 'dnsserver.conf'
DEF_TIMEOUT = 0.4

gl_remote_server = None

class LocalDNSHandler(BaseRequestHandler):
  def setup(self):
    global gl_remote_server
    if not gl_remote_server:
      remote_server = DEF_REMOTE_SERVER
    else:
      remote_server = gl_remote_server
    self.dnsserver = (remote_server, 53)

  def handle(self):
    data, socket = self.request
    rspdata = self._getResponse(data)
    socket.sendto(rspdata, self.client_address)

  def _getResponse(self, data):
    "Send client's DNS request (data) to remote DNS server, and return its response."
    sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server
    sock.sendto(data, self.dnsserver)
    sock.settimeout(5)
    while True:
      try:
        rspdata = sock.recv(4096)
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Try again")
    # "delicious food" for GFW:
    while True:
      sock.settimeout(DEF_TIMEOUT)
      try:
        rspdata = sock.recv(4096)
        print("GFWed?")
      except timeout:
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Trying again")
    return rspdata

class LocalDNSServer(ThreadingUDPServer):
  pass

def main():
  global gl_remote_server
  try:
    if hasattr(sys, 'frozen'):
      dir = os.path.dirname(sys.executable)
    else:
      dir = os.path.dirname(__file__)
    confFile = os.path.join(dir, DEF_CONF_FILE)
    f = open(confFile, 'r')
    dns = f.read().split('=')
    f.close()
    if len(dns) == 2:
      if dns[0].strip().lower() == 'dns':
        gl_remote_server = dns[1].strip()
      else:
        pass
  except:
    pass
  dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler)
  dnsserver.serve_forever()

if __name__ == '__main__':
  main()

注意到和原程序不同的是,我捕获了错误号为11的socket.error异常。这个是EAGAIN,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:

EAGAIN or EWOULDBLOCK
       The socket is marked  nonblocking  and  the  receive  operation
       would  block, or a receive timeout had been set and the timeout
       expired before data was received.  POSIX.1-2001  allows  either
       error  to be returned for this case, and does not require these
       constants to have the same value,  so  a  portable  application
       should check for both possibilities.

不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain这样的语句了。

2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文

Category: 网络 | Tags: DNS python 网络 UDP
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 网络

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