8
5
2013
10

rst_tables 改进版

rst_tables 是一个用来创建和格式化 rst(reStructuredText)格式文档中的表格用的。此文档里的表格得画成表格的样子,囧死了……比如(网页上显示的可能没对齐,在 Vim 里应该很齐的www):

+----------+----------+-----------------------------------------+
| 格式名称 | 使用频率 | 使用场景                                |
+==========+==========+=========================================+
| markdown | 非常高   | 简单的文字,如博客、简单文档            |
+----------+----------+-----------------------------------------+
| rst      | 较低     | 较复杂的文档,如包含表格或者描述性列表。|
|          |          | 以及 Python 库的文档。                  |
+----------+----------+-----------------------------------------+

所以,作为编辑器之神的 Vim,当然会有更方便的创建这种非人道的表格的办法啦。(其实我是看到 Vimwiki 的表格挺不错的 n(≧▽≦)n

略作搜索,找到了 rst_tables。它是这样子写的(墙外视频演示):

格式名称  使用频率  使用场景
markdown  非常高  简单的文字,如博客、简单文档
rst  较低  较复杂的文档,如包含表格或者描述性列表。以及 Python 库的文档。

每行的单元格间空两格,然后光标放在光标上,按\\c(其实是<leader><leader>ccreate),就创建好啦。如果后期又修改了,按\\fformat)就可以重新格式化啦。

rst 的表格里可以写多行文字,就如前边所示那样。修改表格第一行那些减号的数量后再按\\f,可以调整栏宽。

好啦,rst_tables 本身的介绍至此结束。下面讲讲我作出的改进:

  1. 去除对 vim_bridges Python 库的依赖。根本没大量使用的东西,也没省下几行代码,何必用呢。
  2. 正确对齐和排版中文。官方版考虑了中文字符的宽度,但是用 Python 的 textwrap 来排版,造成各种混乱。我给改成用 Vim 原生排版功能排了。
  3. 使用 Python 3 接口,免得非 UTF-8 'encoding' 时出问题。同时使用了 Vim 7.4 新添加的 Python 接口。
  4. 如果没有 Python 支持,不要载入。
  5. 键映射局部于缓冲区。
  6. 放到 plugin 目录下,因为那些 Python 函数定义不需要载入多次。

安装很简单,把这个文件(使用「Raw」链接来下载)扔到 ~/.vim/plugin 下即可。

Category: Vim | Tags: vim python 中文支持
7
30
2013
20

对比不同字体中的同一字符

有人在 openSUSE 中文论坛询问他的输入法打出的「妩媚」的「妩」字为什么显示成「女」+「元」。怀疑是字体的问题,于是空闲时用好友写的 python-fontconfig 配合 Pillow (PIL 的一个 fork)写了个脚本,使用系统上所有包含这个「妩」字的字体来显示这个字,看看到底是哪些字体有问题。

(更新后的)脚本如下:

Google Chrome / Chromium 用户请注意:如果复制得到的代码中含有不间断空格(0xa0),请手动替换下。

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

from PIL import Image, ImageDraw, ImageFont
import fontconfig

ch = '妩'
def get_fonts():
  ret = []
  for f in fontconfig.query():
    f = fontconfig.FcFont(f)
    if f.has_char(ch):
      ret.append((f.file, f.bestname))
  return ret

w, h = 800, 20000
image = Image.new('RGB', (w, h), 'white')
draw = ImageDraw.Draw(image)
pos = 0
w = 0
strs = ch
for fontfile, fontname in get_fonts():
  font = ImageFont.truetype(fontfile, 24)
  s = '%s: %s' % (fontname, strs)
  font_width, font_height = font.getsize(s)
  w = max((font_width, w))
  draw.text((10, pos), s, font=font, fill='black')
  pos += font_height
  h = pos

image = image.crop((0, 0, w+10, h))
image.save('fonts.png')

寻找字体,然后渲染到当前目录下的fonts.png文件中。寻找字体的过程挺花时间的,要耐心等待。最后结果如下:

我这里,文泉驿微米黑、方正魏碑、某个 Droid Sans Fallback 字体中「妩」字的字形不对。(我这里有三个字体文件都叫「Droid Sans Fallback」……)>

7
26
2013
6

飞速中文网小说下载脚本

  • JavaScript 加密什么的最讨厌了 :-(
    • eval 一个不依赖外部变量的函数立即调用很天真,看我 nodejs 来干掉你!
    • HTTP 请求的验证首先尝试 Referer,「小甜饼」没有想像中的那么重要。
    • curl 和各命令行工具处理起文本很顺手呢
    • 但是 Python 也没多几行呢
  • Requests 效率比 lxml 自己那个好太多
  • progressbar 太先进了,我还是自个儿写吧……
  • argparse 写 Python 命令行程序必备啊~
  • string.Template也很好用哦
  • 以下是主代码啦,除了标准库以及 lxml 和 requests,没有的模块都在无所不能的 winterpy 仓库里。其实主代码也在的。
#!/usr/bin/env python3
# vim:fileencoding=utf-8

import sys
from functools import partial
from string import Template
import argparse
import base64
from urllib.parse import unquote

from lxml.html import fromstring
import requests

from htmlutils import extractText
from termutils import foreach

session = requests.Session()

def main(index, filename='$name-$author.txt', start=0):
  r = session.get(index)
  r.encoding = 'gb18030'
  doc = fromstring(r.text, base_url=index)
  doc.make_links_absolute()
  name = doc.xpath('//div[@class="info"]/p[1]/a/text()')[0]
  author = doc.xpath('//div[@class="info"]/p[1]/span/text()')[0].split()[-1]

  nametmpl = Template(filename)
  fname = nametmpl.substitute(name=name, author=author)
  with open(fname, 'w') as f:
    sys.stderr.write('下载到文件 %s。\n' % fname)
    links = doc.xpath('//div[@class="chapterlist"]/ul/li/a')
    try:
      foreach(links, partial(gather_content, f.write), start=start)
    except KeyboardInterrupt:
      sys.stderr.write('\n')
      sys.exit(130)

  sys.stderr.write('\n')
  return True

def gather_content(write, i, l):
  # curl -XPOST -F bookid=2747 -F chapterid=2098547 'http://www.feisuzw.com/skin/hongxiu/include/fe1sushow.php'
  #      --referer http://www.feisuzw.com/Html/2747/2098547.html
  # tail +4
  # base64 -d
  # sed 's/&#&/u/g'
  # ascii2uni -qaF
  # ascii2uni -qaJ
  # <p> paragraphs
  url = l.get('href')
  _, _, _, _, bookid, chapterid = url.split('/')
  chapterid = chapterid.split('.', 1)[0]
  r = session.post('http://www.feisuzw.com/skin/hongxiu/include/fe1sushow.php', data={
    'bookid': bookid, 'chapterid': chapterid,
  }, headers={'Referer': url})

  text = r.content[3:] # strip BOM
  text = base64.decodebytes(text).replace(b'&#&', br'\u')
  text = text.decode('unicode_escape')
  text = unquote(text)
  text = text.replace('<p>', '').replace('</p>', '\n\n')

  title = l.text
  write(title)
  write('\n\n')
  write(text)
  write('\n')
  return title

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='下载飞速中文网小说')
  parser.add_argument('url',
                      help='小说首页链接')
  parser.add_argument('name', default='$name-$author.txt', nargs='?',
                      help='保存文件名模板(支持 $name 和 $author')
  parser.add_argument('-s', '--start', default=1, type=int, metavar='N',
                      help='下载起始页位置(以 1 开始)')
  args = parser.parse_args()
  main(args.url, args.name, args.start-1)
Category: python | Tags: python 网页 爬虫
5
25
2013
8

给 Python 的正则匹配限制执行时间

看到这个标题,你也许会想,这个需要限制么?不是很快就出来结果了么?

感谢 Just Great Software,虽然我没买它的产品,但是其说明书(可免费下载)中的正则教程详细地论述了这点。所以我在自己的 xmpptalk 机器人中一直不敢接受用户输入的正则表达式。引述其中的一句话:「People with little regex experience have surprising skill at coming up with exponentially complex regular expressions.」(不太懂正则的人经常能令人惊奇地写出指数级复杂度的正则。)

但很不幸,我从这里抄到的匹配网址的正则就有这种问题。在将其的修改版给我的 XMPP 机器人 Lisa 使用后,Lisa 两次被含有括号的链接搞到没响应……

所以,如果要使用用户输入的正则,我必须限制其匹配时间。方法也很简单——使用信号就可以了。当 Python 在匹配正则时如果收到信号,会转而调用信号处理器,然后再接着匹配。如果信号处理器抛出了异常,那么此异常会传播到调用正则匹配的地方,从而中断匹配操作。

示例如下:

#!/usr/bin/env python3

import re
# import regex as re
import signal

def timed_out(b, c):
  print('alarmed')
  raise RuntimeError()

signal.signal(signal.SIGALRM, timed_out)
signal.setitimer(signal.ITIMER_REAL, 0.1, 0)
s = '<aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>'
r = re.compile(r'''(?:<(?:[^<>]+)*>)+b''')
try:
  r.findall(s)
except RuntimeError:
  print('time exceeded')

被注释掉的那句是调用mrab-regex-hg这个正则引擎的;它不会回溯时出这种问题。

优化下代码,写成方便使用(使用了TimeoutError,所以适用于 Python 3.3+):

import contextlib
import signal

@contextlib.contextmanager
def execution_timeout(timeout):
  def timed_out(signum, sigframe):
    raise TimeoutError

  old_hdl = signal.signal(signal.SIGALRM, timed_out)
  old_itimer = signal.setitimer(signal.ITIMER_REAL, timeout, 0)
  yield
  signal.setitimer(signal.ITIMER_REAL, *old_itimer)
  signal.signal(signal.SIGALRM, old_hdl)
Category: python | Tags: linux python 正则表达式
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
11
10
2012
8

如何更安全地覆写数据文件

经常地,程序在开始执行某项任务需要从文件读取数据。在任务完成后数据得到更新,新的数据会覆写到之前读取的文件中。怎么将数据写回到文件呢?一个直觉的方案是:

with open(datafile, 'w') as f:
  f.write(data)

在通常情况下,它能够正确地完成写回数据的任务。如果出于某种原因文件打开失败,通常也不会有人忘记处理。但是,当写入操作失败了呢?

时不时地编译程序看到 gcc 大把地警告:

警告:忽略声明有 warn_unused_result 属性的‘write’的返回值 [-Wunused-result]

在 Python 中,写文件时如果失败会抛出异常,上层的异常处理机制似乎能够作出相应的应对。但是,真的尽力了吗?

我也曾以为这样不会出问题。直到有一天,本地信箱里出现了这样的错误信息:

OSError: [Errno 28] No space left on device

可能是由于内核的某个 bug,我本来就所剩无几的 /home 分区没空闲空间了。一个 cronjob 在写回数据时发生异常。于是,新的数据没能写入文件。那旧数据呢?因为是以「写」方式打开文件,所以它也没了……

在那次事件之后,那段写回数据的代码变成了这个样子:

with open(datafile + '.tmp', 'w') as f:
  f.write(t)
# if the above write failed (because disk is full), the old data should be kept
os.rename(datafile + '.tmp', datafile)

注意:测试表明不使用with或者显式地关闭文件的做法是有问题的,即使在 CPython 中。

try:
  open('/dev/full', 'w').write('abc')
except:
  print('fine.')

在 Python 2.7 中会打印错误信息,Python 3.3.0 中无任何信息。都没有预料中的异常被捕获。

>>> python t.py
>>> python2 t.py
close failed in file object destructor:
IOError: [Errno 28] No space left on device

今天之所以写这个,是因为 Arch Linux CN 的群服务器遇到磁盘配额用尽的问题。XMPP 服务器 Prosody 在写入联系人信息时只写了一小部分,大部分数据丢失。这里有 bug 报告

2013年7月21日更新:Sublime Text 2 作为商业软件,竟然不仅不采用「新建+重命名」的方式写入文件,而且连写入是否成功都不检查。难怪 Linux 版中文输入法的问题迟迟不修复,原来连造成用户数据丢失的问题都无所谓

Category: 编程 | Tags: python prosody
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

| Theme: Aeros 2.0 by TheBuckmaker.com