9
29
2014
4

使用 Python 自制 expect 功能

Tcl 的 expect 工具是一个十分有用的自动化工具,经常被用来喂 ssh 密码什么的。不过配置 ssh Control Master英文介绍, 中文翻译之后,我发现第一次连接之后退出,expect 脚本不肯退出了,而且Ctrl-C什么的都不管用,除非我杀掉实际连接到远程主机的 ssh 控制进程。很显然,这是因为 ssh fork 出来的 ssh 进程依旧保持着伪终端的打开状态,expect 依旧在等待来自其上的输出。

研究之后,我发现虽然我可以给我的 expect 脚本加上命令行参数的处理之类的功能,但是interact之后必须等待伪终端关闭没有办法绕过。罢了,本来对 Tcl 就不熟,拿 Python 重写一个吧。

Python 也有个类似的库,叫 pexpect,不过上次我看它的时候还只支持 Python 2,而且使用起来似乎有点问题。所以干脆自制一个满足自己需求的好了。

程序不长,一百多行,但也比预期的长了不少。主要都是些终端和文件描述符的处理。哦还有信号。

我以是否存在子进程来作为判断是否结束interact的标志,而不管是不是还有进程在用这个伪终端。在收到SIGCHLD信号时使用waitid系统调用可以清理任意一已终止的子进程,使用WNOHANG标志在有子进程但是没有已退出者时不要等待、直接返回。于是,在有子进程退出时,反复调用waitid直到它报错就说明已经没有子进程存在啦。

另一个需要处理的信号是SIGWINCH,就是终端大小改变时以此终端为控制终端的前台进程会收到的那个信号。当脚本所在的终端大小改变时,需要重新设置脚本创建的伪终端的大小。不知道为什么,pexpect 竟然没有自己处理这个。

代码照旧在 GitHub 上。以下是使用示例:

#!/usr/bin/env python3

import sys

import expect

def main(host):
  p = expect.Expect()
  p.spawn(['ssh', host])
  p.expect_line('# ')
  p.send('. ./tide\n')
  p.send('TERM=screen-256color exec zsh -l\n')
  p.interact()

if __name__ == '__main__':
  host = 'phone'
  if len(sys.argv) == 2:
    host += sys.argv[1]
  main(host)

这是之前有问题的 expect 脚本:

#!/usr/bin/expect -f

set host [if {$argc eq 0} {lindex phone} {lindex phone$argv}]
spawn -noecho ssh $host
expect "*# "
send ". ./tide\n"
send "TERM=screen-256color exec zsh -l\n"
interact

这是我的 ssh control master 相关配置:

ControlPath ~/.ssh/master-%r@%h:%p
ControlMaster auto
ControlPersist yes
Category: Linux | Tags: linux python 终端 expect tcl
8
5
2014
9

CoolShell 解密游戏解答

这里有人给出了自己的解法,但是我不喜,所以有了本文。注意,本文中的 shell 代码均为 zsh。如果你在用 Windows,那建议还是不要玩了,那个对付这种事情太难用了。

可惜知道这个题晚了两天,没能进前X名 :-(

0. Fuck your brain

机器上没有 brainfuck 编译器,于是 Google「brainfuck online」,得到这个。贴进去运行即可。

1. Multiply

一个值是 42。另一个要观察数列。直接把数列贴到数列百科全书即可。然后乘起来。

2. Keyboard

这个也很容易,不是把 Dvorak 键盘当成 Qwerty,那就是把 Qwerty 当成 Dvorak 了。对照着 Dvorak 的键位输入下边那串字符串,得到一 C 源码。编译、运行之即可。

3. QR Code

扫码,得到:

[abcdefghijklmnopqrstuvwxyz] <=> [pvwdgazxubqfsnrhocitlkeymj]

是个字符映射关系。Python 有现成的函数来处理这个。也是有正反两种可能,都试试就可以了。

>>> T = str.maketrans('pvwdgazxubqfsnrhocitlkeymj', 'abcdefghijklmnopqrstuvwxyz')
>>> s = 'Wxgcg txgcg ui p ixgff, txgcg ui p epm. I gyhgwt mrl lig txg ixgff wrsspnd tr irfkg txui hcrvfgs, nre, hfgpig tcm liunz txg crt13 ra "ixgff" t
r gntgc ngyt fgkgf.'
>>> s.translate(T)
'Where there is a shell, there is a way. I expect you use the shell command to solve this problem, now, please try using the rot13 of "shell" to enter next level.'

按照提示执行命令:

rot13 <<< shell

Arch 上,rot13 命令位于 bsd-games 包。或者在 Vim 里把光标移动到「shell」单词上按g?aw也能得到结果。

题目开始有趣起来了~

4. cat

这个题目更有趣了。源码里一堆乱七八糟的数据。先把它们弄到一个单独的 Vim 缓冲区,然后找到所有的五字符回文字符串:

%!grep -oP '(.)(.)(.)\2\1'

不是所有回文都被接受。仔细观察示例可以发现,中间一定是个小写字母,左边一定是一字母一数字。但是过滤后还是有太多结果。限制左边的字母为大写字母之后可以得结果。删掉不符合条件的,然后把中间的字符连起来即可。

v/\v^([A-Z][0-9]|[0-9][A-Z])\l/d

5. variables

初看,提示莫名其妙。后来注意到图片链接到了有意思的地方。访问得到另一个数字「32722」。显然是要用这个数字放在 URL 上继续访问了。直接拿 shell 访问:

$ n=1024
$ while true; do n=$(curl -sS http://fun.coolshell.cn/n/$n); echo $n; done

访问上百次之后出现一句话,给出了下一关的地址。

6. tree

这关要求从一棵二叉树的中序和后序遍历中还原其最深的路径。不知道怎么做,直接 Google「reconstruct a binary tree from in-order and post-order」,看来有不少人都在做类似的东西啊。我看的是 LeetCode 上的这篇文章。有代码,但我懒得写程序把树画出来或者是找最深的了。反正这树也不大,懂得了方法,直接在 dia 里手工构建出来了。当然,我只构建了最深的那部分。SVG 导出图片

然后就是拿密码解那个字符串了。要注意的是,不要自己去解 base64,不然 openssl 报错的……

7. N Queens

八皇后问题的变种。我直接使用了 Rosetta Code 上的代码。当然要小改一下,直接输出结果而不是打印出图案:

main = mapM_ print $ queens 9

然后找到符合那个 SHA1 值的解就可以了:

$ ./queens | tr -d ',[]' | while read code; do [[ $(sha1sum <<<zWp8LGn01wxJ7$code | awk '{print $1}') == e48d316ed573d3273931e19f9ac9f9e6039a4242 ]] && echo $code; done

8. Excel Column

26 进制转十进制:

>>> def debase26(x):
...   return sum(26 ** i * (ord(d) - ord('A') + 1) for i, d in enumerate(x[::-1]))
...
>>> debase26('COOLSHELL') // base26('SHELL')
85165

结果得到的页面说要转回 26 进制。好吧:(可惜没能在一行内搞定)

>>> def base64(x):
...   L = []
...   while True:
...     x, d = divmod(x, 26)
...     if d == 0: break
...     L.append(d)
...   return ''.join(chr(x + ord('A') - 1) for x in L[::-1])
...
>>> base64(85165)
'DUYO'

9. Fraternal Organisation

这个我没能解出来 QAQ 这两个图片看起来有些莫名其妙。我没注意到图片的名字和鼠标放上去的小提示。最后是看前边那个链接里的答案才知道原来还有个「猪圈密码」-_-|||

PS: 最近博客访问和评论速度都挺慢的,请见谅。

Category: Linux | Tags: linux python shell fun
7
18
2014
11

搜索,快人一步

缘起

在一群文件里搜索特定的文本,第一个想到的工具是经典的 grep。自从知道 ag——The silver searcher 之后,我就只在命令行管道里用 grep 啦。

ag 的优势:

  • 命令短
  • 和 ack 以及 git grep 一样,默认会忽略掉你通常不想看的文本(二进制文件、被版本控制系统忽略掉的文件)
  • C 编写的,比 ack 更快!

作者挺在乎程序的执行效率。这也很重要,因为文件多啊,几十上百兆的源码找起来可费时了。

不过在我这里,ag 取代掉的既不是 grep 也不是 ack。因为我之前用的是 cgvg,和 ack 一样也是 Perl 写的,但不一样的是,它包含两个命令:一个(cg)用来搜索,另一个(vg)用来在编辑器里打开!

不知道为什么其它工具的作者都没有想到这一点。匹配的地方找到了,大部分情况都需要用编辑器打开看看,也许再改改吧?cgvg 免去了复制路径到编辑器里并跳转到特定位置这一烦琐的步骤。

所以换用 ag 之后,我自然也希望能够很便利地使用编辑器查看匹配的地方了。为此,我做了三个工具。好吧,其实有一个是在 cgvg 时代就已经有了的。

搜索

实践一下。假设我要寻找 Vim 源码中涉及到p_enc变量的地方。使用 agg 命令来搜索并使用翻页器 less 显示结果(截图时没有显示出 less 的存在):

agg result

agg 脚本更改了 ag 的显示样式,每一项开头都有一个序号,就和 cg 命令一样。同样地,agg 也把这个结果保存在主目录下的一个文件,以供 agv 命令使用。

注意,agg 并不能用来按文件名搜索。这种情况还是用 ag -g pattern

使用编辑器打开

agv 命令不给定参数,会显示上一次搜索的结果。如果给出一个编号,就会将结果在AGV_EDITOR环境变量指定的编辑器里打开,并跳转到对应的地方。和 vg 不同的是,agg/agv 为每一个终端维护了一个结果列表,这样就可以在不同的终端里搜索不同的内容而不会相互干扰了。

比如我们要去第 14 号结果所在的地方,只要执行agv 14,然后就到了:

agv result

我这里是在已有的 gVim 里打开的哦。因为我设置的AGV_EDITOR的值是vv $file:$line:$col。这是一句包含占位符的 shell 命令。$file$line$col分别是结果所在的文件、行号、列号。而vv,则是我写的另一个工具,用来在已经打开的 gVim 里打开文件,并且跳转到特定的地方

vv 需要 Python easygui 库,以及 gVim(或者 Vim)的 +clientserver 支持。vv 不仅支持上述格式的参数,作为一名 Pythonista 所编写的工具,它也支持从 Python 报错时打印的 Traceback 的行中提取文件名和行号。比如:

vv 'File "/usr/lib/python3.4/sre_parse.py", line 358, in _parse_sub'

这里,不一定要复制一整行。包含必要的信息(文件名和行号)就可以了。

当然,手动复制粘贴比较累。所以配合我的 zsh 全局别名:

alias -g XS='"$(xsel)"'

我只需要选中那一行,然后执行

vv XS

就可以了。zsh 会帮我把XS展开成我刚刚选择的文本。

ag.vim

ag.vim 是一个 ack.vim 的修改版,用于在 Vim 中调用 ag,就像 ack.vim 在 Vim 中调用 ack、grep.vim 在 Vim 中调用 grep 一样。

因为经常搜索在当前光标下的内容,我写了这么一条自定义命令:

command Agg exe 'Ag -Q ' . expand('<cword>')

仓库地址

https://github.com/lilydjwg/search-and-view

Category: Linux | Tags: python grep AG
6
17
2014
3

Google IP 可用性检测脚本

需要 Python 3.4+,一个参数用来选择测试搜索服务还是 GAE 服务。测试 GAE 服务的话需要先修改开头的两个变量。从标准输入读取 IP 地址或者 IP 段(形如 192.168.0.0/16)列表,每行一个。可用 IP 输出到标准输出。实时测试结果输出到标准错误。50 线程并发。

#!/usr/bin/env python3

import sys
from ipaddress import IPv4Network
import http.client as client
from concurrent.futures import ThreadPoolExecutor
import argparse
import ssl
import socket

# 先按自己的情况修改以下几行
APP_ID = 'your_id_here'
APP_PATH = '/fetch.py'

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('/etc/ssl/certs/ca-certificates.crt')

class HTTPSConnection(client.HTTPSConnection):
  def __init__(self, *args, hostname=None, **kwargs):
    self._hostname = hostname
    super().__init__(*args, **kwargs)

  def connect(self):
    super(client.HTTPSConnection, self).connect()

    if self._tunnel_host:
      server_hostname = self._tunnel_host
    else:
      server_hostname = self._hostname or self.host
      sni_hostname = server_hostname if ssl.HAS_SNI else None

    self.sock = self._context.wrap_socket(self.sock,
                                          server_hostname=sni_hostname)
    if not self._context.check_hostname and self._check_hostname:
      try:
        ssl.match_hostname(self.sock.getpeercert(), server_hostname)
      except Exception:
        self.sock.shutdown(socket.SHUT_RDWR)
        self.sock.close()
        raise

def check_ip_p(ip, func):
  if func(ip):
    print(ip, flush=True)

def check_for_gae(ip):
  return _check(APP_ID + '.appspot.com', APP_PATH, ip)

def check_for_search(ip):
  return _check('www.google.com', '/', ip)

def _check(host, path, ip):
  for chance in range(1,-1,-1):
    try:
      conn = HTTPSConnection(
        ip, timeout = 5,
        context = context,
        hostname = host,
      )
      conn.request('GET', path, headers = {
        'Host': host,
      })
      response = conn.getresponse()
      if response.status < 400:
        print('GOOD:', ip, file=sys.stderr)
      else:
        raise Exception('HTTP Error %s %s' % (
          response.status, response.reason))
      return True
    except KeyboardInterrupt:
      raise
    except Exception as e:
      if isinstance(e, ssl.CertificateError):
        print('WARN: %s is not Google\'s!' % ip, file=sys.stderr)
        chance = 0
      if chance == 0:
        print('BAD :', ip, e, file=sys.stderr)
        return False
      else:
        print('RE  :', ip, e, file=sys.stderr)

def main():
  parser = argparse.ArgumentParser(description='Check Google IPs')
  parser.add_argument('service', choices=['search', 'gae'],
                      help='service to check')
  args = parser.parse_args()
  func = globals()['check_for_' + args.service]

  count = 0
  with ThreadPoolExecutor(max_workers=50) as executor:
    for l in sys.stdin:
      l = l.strip()
      if '/' in l:
        for ip in IPv4Network(l).hosts():
          executor.submit(check_ip_p, str(ip), func)
          count += 1
      else:
        executor.submit(check_ip_p, l, func)
        count += 1
  print('%d IP checked.' % count)

if __name__ == '__main__':
  main()

脚本下载地址


2014年9月3日重要更新:由于失误,之前的脚本没有检查 SSL/TLS 证书,所以将错误的 IP 认为是可用的。现已更新。

Category: python | Tags: python google 网络 中国特色
6
1
2014
11

给 Python 加上分数和十进制数字面量表示

听说 Ruby 支持分数字面量呢——

irb(main):001:0> 1/3r
=> (1/3)
irb(main):002:0> 1/3r+1/2r
=> (5/6)

可是在 Python 里只能这样子:

>>> from fractions import Fraction as R
>>> R(1, 3)
Fraction(1, 3)
>>> R(1, 3) + R(1, 2)
Fraction(5, 6)

学习《用 `accio` 代替 `import`:深入理解自举和 Python 语法》改改 Python 解释器,让它也支持分数字面量,应该会很有趣呢 =w=

去翻了一下 Grammar/Grammar 文件,有些失望。那里只有语法定义,没有词法定义呢。以为词法定义在另一个地方,所以找了找,无果。只有 C 代码。想到复数的 j 字尾,直接拿 ag 搜带引号的 "j"'j',最终确认它在 Parser/tokenizer.c 里。也就是说,Python 的词法分析是自己实现的。

在那个四千多行的tok_get函数里,有一部分代码在尝试解析一个数,也就是语法里的 NUMBER。照着解析复数的办法,把 d 后缀和 r 后缀也加进去:

diff -r bf65e7db066d Parser/tokenizer.c
--- a/Parser/tokenizer.c    Mon Apr 14 22:27:27 2014 -0400
+++ b/Parser/tokenizer.c    Fri May 30 20:12:07 2014 +0800
@@ -1528,6 +1528,10 @@
                 goto fraction;
             if (c == 'j' || c == 'J')
                 goto imaginary;
+            if (c == 'd' || c == 'D')
+                goto decimal;
+            if (c == 'r' || c == 'R')
+                goto rational;
             if (c == 'x' || c == 'X') {

                 /* Hex */
@@ -1621,6 +1625,12 @@
                     /* Imaginary part */
         imaginary:
                     c = tok_nextc(tok);
+                else if (c == 'd' || c == 'D')
+        decimal:
+                    c = tok_nextc(tok);
+                else if (c == 'r' || c == 'R')
+        rational:
+                    c = tok_nextc(tok);
             }
         }
         tok_backup(tok, c);

d 后缀是我给十进制数——就是会计里会用到的精确的十进制小数——准备的。

然后可以编译出来试试。这个 configure 命令是从 Arch 官方编译脚本里找的。

./configure --enable-shared --with-threads --with-computed-gotos --enable-ipv6 --with-valgrind --with-system-expat --with-dbmliborder=gdbm:ndbm --with-system-ffi --with-system-libmpdec --without-ensurepip
make

因为我不执行安装步骤,而又用了共享库,所以要这样子执行:

LD_LIBRARY_PATH=. ./python

试试看:

>>> 4d
ValueError: could not convert string to float: 4d

有效果,不报语法错了呢。

现在报ValueError,因为我还没告诉 Python 如何解析我新加的字面量表示呢。解析代码位于Python/ast.cparsenumber函数。最终的补丁如下:

diff -r bf65e7db066d Python/ast.c
--- a/Python/ast.c  Mon Apr 14 22:27:27 2014 -0400
+++ b/Python/ast.c  Fri May 30 20:12:07 2014 +0800
@@ -3650,12 +3650,29 @@
     long x;
     double dx;
     Py_complex compl;
-    int imflag;
+    char typeflag;
+    PyObject *mod, *type, *ret;

     assert(s != NULL);
     errno = 0;
     end = s + strlen(s) - 1;
-    imflag = *end == 'j' || *end == 'J';
+    switch(*end){
+        case 'j':
+        case 'J':
+            typeflag = 'j';
+            break;
+        case 'd':
+        case 'D':
+            typeflag = 'd';
+            break;
+        case 'r':
+        case 'R':
+            typeflag = 'r';
+            break;
+        default:
+            typeflag = 'i';
+    }
+
     if (s[0] == '0') {
         x = (long) PyOS_strtoul(s, (char **)&end, 0);
         if (x < 0 && errno == 0) {
@@ -3670,13 +3687,43 @@
         return PyLong_FromLong(x);
     }
     /* XXX Huge floats may silently fail */
-    if (imflag) {
+    if (typeflag == 'j') {
         compl.real = 0.;
         compl.imag = PyOS_string_to_double(s, (char **)&end, NULL);
         if (compl.imag == -1.0 && PyErr_Occurred())
             return NULL;
         return PyComplex_FromCComplex(compl);
     }
+    else if (typeflag == 'd') {
+      mod = PyImport_ImportModule("decimal");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Decimal");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
+    else if (typeflag == 'r') {
+      mod = PyImport_ImportModule("fractions");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Fraction");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
     else
     {
         dx = PyOS_string_to_double(s, NULL, NULL);

因为只是玩玩,所以不太认真,没仔细做错误处理;因为decimalfractions模块是从外部文件导入的,所以可能被覆盖掉,从而导致报错,并且这错误是无法通过异常处理捕获的。

不出问题的话,再次make之后,就可以开始玩了。不过在此之前,再多做几个补丁,让 Python 把分数和十进制数显示得简洁好看一点:

diff -r bf65e7db066d Lib/decimal.py
--- a/Lib/decimal.py    Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/decimal.py    Fri May 30 20:12:07 2014 +0800
@@ -1015,7 +1015,7 @@
     def __repr__(self):
         """Represents the number as an instance of Decimal."""
         # Invariant:  eval(repr(d)) == d
-        return "Decimal('%s')" % str(self)
+        return str(self) + 'd'

     def __str__(self, eng=False, context=None):
         """Return string representation of the number in scientific notation.
diff -r bf65e7db066d Lib/fractions.py
--- a/Lib/fractions.py  Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/fractions.py  Fri May 30 20:12:07 2014 +0800
@@ -280,7 +280,7 @@

     def __repr__(self):
         """repr(self)"""
-        return ('Fraction(%s, %s)' % (self._numerator, self._denominator))
+        return str(self) + 'r'

     def __str__(self):
         """str(self)"""
diff -r bf65e7db066d Modules/_decimal/_decimal.c
--- a/Modules/_decimal/_decimal.c   Mon Apr 14 22:27:27 2014 -0400
+++ b/Modules/_decimal/_decimal.c   Fri May 30 20:12:07 2014 +0800
@@ -3092,18 +3092,10 @@
 static PyObject *
 dec_repr(PyObject *dec)
 {
-    PyObject *res, *context;
-    char *cp;
-
-    CURRENT_CONTEXT(context);
-    cp = mpd_to_sci(MPD(dec), CtxCaps(context));
-    if (cp == NULL) {
-        PyErr_NoMemory();
-        return NULL;
-    }
-
-    res = PyUnicode_FromFormat("Decimal('%s')", cp);
-    mpd_free(cp);
+    PyObject *res, *str;
+    str = dec_str(dec);
+    res = PyUnicode_FromFormat("%Ud", str);
+    Py_DECREF(str);
     return res;
 }

下面是最终成果啦:

>>> 0.1 + 0.2 == 0.3
False
>>> 0.1d + 0.2d == 0.3d
True
>>> 1/3r + 1/2r
5/6r
>>> 0.4/1.2r
0.33333333333333337
>>> 0.4r/1.2r
1/3r

可以看到,与复数类似,分数字面量其实包含了一次除法。所以如果分子写浮点数的话,最终结果是会被转成浮点数的呢。这个和 Ruby 的行为是一样的 =w=

Category: python | Tags: Python C代码
5
27
2014
13

纯真 IP 数据库 QQWry 解析库 Python 3 版

这东西挺好用的,可惜我只寻到一多年以前的 Python 2 版本的,作者是 AutumnCat,不认识。但注释里提到的修改者 bones7456 是鼎鼎大名的骨头兄,现其博客已经长草……

一直以来,我都是通过子进程调用来使用的,因为我写的代码是 Python 3 版,比如这个寻找文本里的 IP 地址并标记的 ipmarkup 脚本。配合 Python 3.2 加入的 functools.lru_cache,效率还不错的样子。但近期有大量 IP 需要查询,才感到每个 IP 都开个子进程的方式实在太慢。遂将其修改为 Python 3 版,并加入了些 Python 后来才流行的 idiom。

脚本还是扔到 winterpy 仓库里了。GPLv2 授权的。

2014年8月2日更新:增加了在线更新的功能,从此不需要 Wine 就能更新数据库啦 :-) 更新方法来自微菜。更新命令如下:

python3 -m QQWry update
Category: python | Tags: python IP地址
3
13
2014
4

Python 3 的 super() 和 __class__

子类里访问父类的同名属性,而又不想直接引用父类的名字,因为说不定什么时候会去修改它,所以数据还是只保留一份的好。其实呢,还有更好的理由不去直接引用父类的名字,参见 Python’s super() considered super! | Deep Thoughts by Raymond Hettinger

这时候就该 super() 登场啦——

class A:
  def m(self):
    print('A')

class B(A):
  def m(self):
    print('B')
    super().m()

B().m()

当然 Python 2 里 super() 是一定要参数的,所以得这么写:

class B(A):
  def m(self):
    print('B')
    super(B, self).m()

需要提到自己的名字。这个名字也是动态查找的,在这种情况下替换第三方库中的类会出问题。

super() 很好地解决了访问父类中的方法的问题。那么,如果要访问父类的父类(准确地说,是方法解析顺序(MRO)中位于第三的类)的属性呢?

比如,B 类是继承 A 的,它重写了 A 的 m 方法。现在我们需要一个 C 类,它需要 B 类的一些方法,但是不要 B 的 m 方法,而改用 A 的。怎么间接地引用到 A 的 m 方法呢?使用self.__class__肯定是不行的,因为 C 还可能被进一步继承。

从文档中我注意到,super 的实现是通过插入一个名为 __class__ 的名字来实现的(super 会从调用栈里去查找这个 __class__ 名字)。所以,就像文档里暗示的,其实可以直接在定义方法时访问 __class__ 名字,它总是该方法被定义的类。继续我们的单字母类:

class C(B):
  def m(self):
    print('C')
    # see the difference!
    print(__class__.__mro__)
    print(self.__class__.__mro__)
    __class__.__mro__[2].m(self)

class D(C):
  def m(self):
    print('D')
    super().m()

o = D()
o.m()

会得到:

D
C
(<class 't.C'>, <class 't.B'>, <class 't.A'>, <class 'object'>)
(<class 't.D'>, <class 't.C'>, <class 't.B'>, <class 't.A'>, <class 'object'>)
A

不过,PyPy 并不支持这个 __class__ 名字。

Category: python | Tags: Python
3
6
2014
5

使用 PyQt 转换网页到 PDF

代码很简单,功能也很简单 =w=

#!/usr/bin/env python3

import sys

try:
  from PyQt4 import QtWebKit
  from PyQt4.QtCore import QUrl
  from PyQt4.QtGui import QApplication, QPrinter
except ImportError:
  from PySide import QtWebKit
  from PySide.QtCore import QUrl
  from PySide.QtGui import QApplication, QPrinter

app = QApplication(sys.argv)

def done(status):
  p = QPrinter()
  p.setOutputFormat(QPrinter.PdfFormat)
  p.setOutputFileName('a.pdf')
  view.print(p)
  app.exit()

view = QtWebKit.QWebView()
view.load(QUrl('http://lilydjwg.is-programmer.com/'))
view.loadFinished[bool].connect(done)
# PySide does not have QApplication.exec
app.exec_()

注意:虽然没有图形界面,但是还是需要 X 连接……

Category: python | Tags: Python PyQt Qt
3
3
2014
17

《冰雪奇缘》之缘——A站弹幕 JSON 转 crt

都是《冰雪奇缘》惹的祸,不仅画面美仑美奂,音乐激荡人心,而且还玩多语言版本的主题曲

you-get 竟助纣为虐,把歌词给下回来了!只叹 soimort 没有好人做到底,留一个语义不详的 JSON 让我情何以堪。

好在 muzuiget 才识不凡,有码略释其义,遂如我光影、声音、文字三位一体之愿!

编成代码数十行,只为女王歌一曲:

#!/usr/bin/env python3

import json
import sys

def cmtjson_reader(f):
  data = json.load(f)
  for o in data:
    c = o['c'].split(',')
    m = o['m']
    # see
    # https://github.com/muzuiget/niconvert/blob/master/niconvert/libsite/acfun.py
    start = float(c[0])
    yield start, m

def format_time(t):
  s, ms = divmod(t, 1)
  s = int(s)
  ms = int(ms * 1000)
  m, s = divmod(s, 60)
  h, m = divmod(m, 60)
  return '%d:%02d:%02d,%03d' % (h, m, s, ms)

def crt_writer(f):
  i = 0

  fmt = '%d\n%s --> %s\n%s\n\n'
  try:
    while True:
      t, m = yield
      ts = format_time(t)
      if i != 0:
        f.write(fmt % (i, format_time(old_t), ts, old_m))
      i += 1
      old_t = t
      old_m = m
  except GeneratorExit:
    f.write(fmt % (
      i+1, format_time(t), format_time(t+2), m))

def main():
  w = crt_writer(sys.stdout)
  w.send(None)
  old_d0 = 0
  for d in cmtjson_reader(sys.stdin):
    if d[0] < old_d0:
      break
    w.send(d)
    old_d0 = d[0]

if __name__ == '__main__':
  main()

mplayer 君虽识得 UTF-8 之码,却不通 fontconfig 之术,部分文字沦为了下划线。又及。

Category: python | Tags: python
2
23
2014
11

让我们收养孤儿进程吧

稍微了解一点类 UNIX 系统的进程管理的都知道,当一个进程的父进程死亡之后,它就变成了孤儿进程,会由进程号 1 的 init 进程收养,并且在它死亡时由 init 来收尸。但是,自从使用 systemd 来管理用户级服务进程之后,我发现 systemd --user 管理的进程总是在它之下,即使进程已经 fork 了好几次。systemd 是怎么做到的呢?

对一个软件的实现有不懂的想了解当然是读它的源码了。这种东西可没有另外的文档,因为源码本身即文档。当然之前我也 Google 过,没有得到结果。在又一个全新的源码树里寻寻觅觅一两天之后,终于找到了这个:

        if (arg_running_as == SYSTEMD_USER) {
                /* Become reaper of our children */
                if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
                        log_warning("Failed to make us a subreaper: %m");
                        if (errno == EINVAL)
                                log_info("Perhaps the kernel version is too old (< 3.4?)");
                }
        }

原来是通过prctl系统调用实现的。于是去翻 prctl 的 man 手册,得知PR_SET_CHILD_SUBREAPER是 Linux 3.4 加入的新特性。把它设置为非零值,当前进程就会变成 subreaper,会像 1 号进程那样收养孤儿进程了。

当然用 C 写不好玩,于是先用 python-cffi 玩了会儿,最后还是写了个 Python 模块,也是抓住机会练习一下 C 啦。有个 python-prctl 模块,但是它没有包含这个调用。

#include<sys/prctl.h>
#include<Python.h>

static PyObject* subreap(PyObject *self, PyObject *args){
  PyObject* pyreaping;
  int reaping;
  int result;

  if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &pyreaping))
    return NULL;
  reaping = pyreaping == Py_True;

  Py_BEGIN_ALLOW_THREADS
  result = prctl(PR_SET_CHILD_SUBREAPER, reaping);
  Py_END_ALLOW_THREADS

  if(result != 0){
    return PyErr_SetFromErrno(PyExc_OSError);
  }else{
    Py_RETURN_NONE;
  }
}

static PyMethodDef mysysutil_methods[] = {
  {"subreap", subreap, METH_VARARGS},
  {NULL, NULL}    /* Sentinel */
};

static PyModuleDef mysysutil = {
  PyModuleDef_HEAD_INIT,
  "mysysutil",
  "My system utils",
  -1,
  mysysutil_methods,
  NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_mysysutil(void){
  PyObject* m;

  m = PyModule_Create(&mysysutil);
  if(m == NULL)
    return NULL;
  return m;
}

编译之后,

>>> import mysysutil
>>> mysysutil.subreap(True)

然后开子进程,不管它 fork 多少次,都依然会在这个 Python 进程之下啦。

但是,这样子不太好玩呢。如果我登陆之后所有启动的子进程都在一个进程之下不是更有意思么?于是我打上了 Awesome 的主意,因为它支持运行任意的 Lua 代码嘛。于是我又给这个 prctl 调用弄了个 Lua 绑定。最终的版本如下:

#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>

#include<sys/prctl.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

static int l_setsubreap(lua_State * L){
  int reap;
  if(lua_isboolean(L, 1)){
    reap = lua_toboolean(L, 1);
  }else{
    return luaL_argerror(L, 1, "not a boolean");
  }
  if(prctl(PR_SET_CHILD_SUBREAPER, reap) != 0){
    return luaL_error(L, "prctl failed: %s", strerror(errno));
  }
  return 0;
}

static int l_ignore_SIGCHLD(lua_State * L){
  signal(SIGCHLD, SIG_IGN);
  return 0;
}

static int l_reap(lua_State * L){
  int pid, st;
  pid = waitpid(-1, &st, WNOHANG);
  lua_pushinteger(L, st);
  lua_pushinteger(L, pid);
  return 2;
}

static const struct luaL_Reg l_lib[] = {
  {"setsubreap", l_setsubreap},
  {"reap", l_reap},
  {"ignore_SIGCHLD", l_ignore_SIGCHLD},
  {NULL, NULL}
};

int luaopen_clua(lua_State * L){
  lua_newtable(L);
  luaL_setfuncs(L, l_lib, 0);
  return 1;
}

除了调用 prctl 外,还增加了显式忽略 SIGCHLD 信号,以及非阻塞地调用 waitpid 收割单个僵尸进程的函数,因为 Awesome 本身没处理子进程退出,我一不小心弄出了好几个僵尸进程……对了,那个 waitpid 要注意给弄成非阻塞的,不然一不小心就会出问题

用的时候就是这样子,可以写到rc.lua里,也可以在 awesome-client 里调用:

package.cpath = package.cpath .. ';/home/lilydjwg/scripts/lua/cmod/?.so'
clua = require('clua')
clua.setsubreap(true)
clua.ignore_SIGCHLD()

最终,我的进程树成了这样子:

htop-awesome-tree

可以看到,由 Awesome 启动的进程已经全部待在 Awesome 进程树之下了。systemd --user 是由 PAM 启动的,所以不在 Awesome 树下。但是,那些 dbus 的东西和 gconfd-2、at-spi 之类的是怎么回事呀……

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