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 网页 爬虫
7
26
2013
5

flock——Linux 下的文件锁

当多个进程可能会对同样的数据执行操作时,这些进程需要保证其它进程没有也在操作,以免损坏数据。

通常,这样的进程会使用一个「锁文件」,也就是建立一个文件来告诉别的进程自己在运行,如果检测到那个文件存在则认为有操作同样数据的进程在工作。这样的问题是,进程不小心意外死亡了,没有清理掉那个锁文件,那么只能由用户手动来清理了。像 pacman 或者 apt-get 一些数据库服务经常在意外关闭时留下锁文件需要用户清理。我以前写了个 pidfile,它会将自己的 pid 写到文件里去,所以,如果启动时文件存在,但是对应的进程不存在,那么它也可以知道没有其它进程要访问它要访问的数据(这里只讨论如何避免数据的并发讨论,不考虑进程意外退出时的数据完整性)。但是,Linux 的 pid 是会复用的。而且,检查 pidfile 也有点麻烦不是么?(还有竞态呢)

某天,我发现了 flock 这个系统调用。flock 是对于整个文件的建议性锁。也就是说,如果一个进程在一个文件(inode)上放了锁,那么其它进程是可以知道的。(建议性锁不强求进程遵守。)最棒的一点是,它的第一个参数是文件描述符,在此文件描述符关闭时,锁会自动释放。而当进程终止时,所有的文件描述符均会被关闭。于是,很多时候就不用考虑解锁的事情啦。

flock 有个对应的 shell 命令也叫 flock,很好用的。使用最广泛的 cronie 这个定时任务服务很笨的,不像小巧的 dcron 那样同一任务不会同时跑多个。于是乎,服务器上经常看到一堆未退出的 cron 任务进程。把所有这样的任务包一层 flock 就不会导致 cronie 启动 N 个进程做同一件事啦:

flock -n /tmp/.my.lock -c 'command to run'

即使是 dcron,有时会有两个操作同一数据的任务,也需要使用 flock 来调度。不过这次不用-n参数让文件被锁住时失败退出了。我们要等拥有锁的进程完事再执行。如下,两个任务(有所修改),一个是从远程同步数据到本地的,另一个是备份同步过来的数据的。同时执行的话,就会备份到不完整的数据了。

*/7 *    * * * ID=syncdata       LANG=zh_CN.UTF-8 flock /tmp/.backingup -c my_backup_script
@daily         ID=backupdata     LANG=zh_CN.UTF-8 [ -d ~/data ] && cd ~/data && nice -n19 ionice -c3 flock /tmp/.backingup -c "tar cJf backup_$(date +"%Y%m%d").tar.xz data_dir --exclude='*~'"

flock 命令除了接收文件名参数外,还可以接收文件描述符参数。这种方法在 shell 脚本里特别有用。比如如下代码:

lockit () {
  exec 7<>.lock
  flock -n 7 || {
    echo "Waiting for lock to release..."
    flock 7
  }
}

exec行打开.lock文件为 7 号文件描述符,然后拿 flock 尝试锁它。如果失败了,就输出一条消息,并且等待锁被释放。那个 7 号文件描述符就让它一直开着了,反正脚本执行完毕内核会释放,也不用去调用trap内建命令了。

上边有一点很有意思的是,flock 是一个子进程,但是因为文件描述符在 fork 和 execve 中会共享,而 flock 锁在 fork 和 execve 时也不会改变,所以子进程在那个文件描述符上加锁后,即使它退出了,因为那个文件描述符父进程还有一份,所以不会被关闭,锁也就得以保留。(所以,如果余下的脚本里要是有进程带着那个文件描述符 fork 到后台锁就不会在脚本执行完后自动解除啦……)

PS: 经我测试,其它一些类 Unix 系统上或者没有 flock 这个系统调用,只有 fcntl 那个,或者行为和 Linux 的不一样。

Category: Linux | Tags: linux shell
7
22
2013
2

使用 PostgreSQL 游标分页

使用 PostgreSQL 的命名游标对查询结果进行分页,相对于 OFFSET+LIMIT 查询,相当于保留了每个查询结果,避免了在翻页时的重复计算。1, 2, 3

这是我自己的测试结果。数据量不大,这个测试用的结果集才 20 条结果,所以效果不太明显。(好吧,其实我这边目前的数据量也没必要用现在这个复杂的方案。只是尝试新东西而已啦 ^_^)

In [m]: %%timeit
   ....: for i in range(10):
   ....:   c.execute(sql_c, (sql_m, i*2, 2))
   ....:   list(c)
   ....: 
100 loops, best of 3: 9.83 ms per loop

In [n]: %%timeit
   ....: for i in range(10):
   ....:   c.execute(sql_m.replace('%', '%%') + ' offset %s limit %s', (i*2, 2))
   ....:   list(c)
   ....: 
10 loops, best of 3: 19.8 ms per loop

我使用了一个 PostgreSQL 函数来创建或者复用 cursor。此函数输入参数有:查询语句、位置偏移、获取的数量。这个函数会检查是否已经存在对应的 cursor,如果没有就把查询语句的 md5 值加前缀「p」作为名字。查询语句当然是程序拼接的,不会有人工输入的那种意义相同但是某些写法不一样造成的不同。

PostgreSQL cursor 有两个很重要的特性。其一,它的内容不会随着数据的更新而更新。所以,在相关数据更新之后,已经创建的 cursor 的数据就陈旧了。我创建了一个创建触发器的函数以便清理陈这些旧的 cursor。另外,cursor 是会占用内存或者磁盘空间的,因此要清理掉长期不使用的 cursor。为此,我维护了一张记录 cursor 最后使用时间的表,以及一个清理函数。

PostgreSQL cursor 特性之二:即使指定了WITH HOLD,cursor 的生存期也只在当前会话(连接),并且只在当前会话中看得到。所以,清理函数cleanupCursors还需要将没有记录的 cursor 清除。

CREATE OR REPLACE FUNCTION createCursorTable(name text) RETURNS void AS $$
BEGIN
  EXECUTE format('CREATE TABLE IF NOT EXISTS %I (
    name text UNIQUE,
    last_used TIMESTAMP WITH TIME ZONE default current_timestamp
  )', name);
  EXECUTE format('CREATE INDEX ON %I (last_used)', name);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION createTriggerFor(tname text, cname text) RETURNS void AS $$
BEGIN
  EXECUTE format($f$
    CREATE TRIGGER %I
    AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON %I
    FOR EACH STATEMENT
    EXECUTE PROCEDURE cleanupTriggerFunc (%L)
  $f$, 'cleanupCursorForTable_' || tname || '_' || cname,
  tname, cname);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION dropTriggerFor(tname text, cname text) RETURNS void AS $$
BEGIN
  EXECUTE format($f$ DROP TRIGGER %I on %I $f$,
    'cleanupCursorForTable_' || tname || '_' || cname, tname);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION cleanupTriggerFunc() RETURNS TRIGGER AS $$
DECLARE
  cname text := TG_ARGV[0];
BEGIN
  EXECUTE format('SELECT cleanupCursors(%L, 0)', cname);
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION fetchFromCursor(tname text, query text, off integer, size integer)
RETURNS SETOF record AS $$
DECLARE
  cname text := 'p' || md5(query);
  need_update boolean := false;
BEGIN
  PERFORM name FROM pg_cursors WHERE name = cname;
  IF NOT FOUND THEN
    EXECUTE format('DECLARE %I SCROLL CURSOR WITH HOLD FOR ', cname) || query;
    RAISE NOTICE 'new cursor % created', cname;
    BEGIN
      EXECUTE format('INSERT INTO %I (name) VALUES (%L)', tname, cname);
    EXCEPTION
      WHEN unique_violation THEN
        need_update := true;
    END;
  ELSE
    need_update := true;
  END IF;

  IF need_update THEN
    EXECUTE format('UPDATE %I SET last_used = current_timestamp WHERE name = %L',
      tname, cname);
  END IF;

  EXECUTE format('MOVE ABSOLUTE ' || off || ' FROM %I', cname);
  RETURN QUERY EXECUTE format('FETCH ' || size || ' FROM %I', cname);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION cleanupCursors(tname text, timeout real) RETURNS integer AS $$
DECLARE
  c record;
  i integer := 0;
BEGIN
  FOR c IN
    EXECUTE format($f$
      SELECT name FROM %I
      WHERE extract('epoch' from current_timestamp - last_used) > %L
    $f$, tname, timeout)
  LOOP
    PERFORM name FROM pg_cursors WHERE name = c.name;
    IF FOUND THEN
      RAISE NOTICE 'closing cursor %', c.name;
      EXECUTE format('CLOSE %I', c.name);
    END IF;
    RAISE NOTICE 'clean up record for cursor %', c.name;
    EXECUTE format($f$ DELETE FROM %I WHERE name = %L$f$, tname, c.name);
    i := i + 1;
  END LOOP;

  FOR c IN
    EXECUTE format($f$
      SELECT name FROM pg_cursors WHERE name NOT IN (
        SELECT name FROM %I
      ) AND length(name) = 33 AND substring(name for 1) = 'p'
    $f$, tname)
  LOOP
    RAISE NOTICE 'closing cursor % not present in table %', c.name, tname;
    EXECUTE format('CLOSE %I', c.name);
    i := i + 1;
  END LOOP;

  RETURN i;
END;
$$ LANGUAGE plpgsql;

使用时需要经常去调用下cleanupCursors函数。

PostgreSQL 函数还有这么一个特性,当函数返回setof record时,PostgreSQL 不知道怎么解读那些 record。所以用fetchFromCursor函数时得明确指定获取结果的行类型:

select * from fetchFromCursor('cursors', $$select name from users where name like 'a%' order by last_login_time$$, 0, 10) as f(name text);

有点麻烦。

Category: 数据存储 | Tags: PostgreSQL
7
10
2013
4

grub2 引导 openSUSE 安装镜像

想安装 openSUSE 12.2,但是目标机器没有光驱,亦没有可用的能够容纳下 DVD 镜像的 U 盘。尝试 dd 镜像到 U 盘,报告找不到光驱还是什么的,启动失败,自动重启。 官方 Wiki 上 http://en.opensuse.org/Installation_without_CD 这个页面已经被删除。其它页面只有如何将 ISO 镜像弄到 U 盘上的说明,没有说明如何正确启动之。grub2 带内核参数install=hd:$isofile失败。这个据说只对 DVD 镜像有效。

最终,像很早之前那样阅读init脚本后,终于得出正确的启动方法:

menuentry "openSUSE 12.2 KDE LiveCD x86_64" {
    set isofile="/images/openSUSE-12.2-KDE-LiveCD-x86_64.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/boot/x86_64/loader/linux isofrom=/dev/disk/by-label/4lin:$isofile
    echo "Loading initrd..."
    initrd (loop)/boot/x86_64/loader/initrd
}

其中,isofrom指定 ISO 文件所在的设备和路径,以冒号分隔。如果没有写对的话,将得到Failed to find MBR identifier !错误。

2013年12月22日更新:对于 openSUSE 13.1,其引导命令应该这么写:

menuentry "openSUSE 13.1 KDE Live x86_64 (zh_CN)" {
	set isofile="/images/openSUSE-13.1-KDE-Live-x86_64.iso"
	echo "Setup loop device..."
	loopback loop $isofile
	echo "Loading kernel..."
	linux (loop)/boot/x86_64/loader/linux isofrom_device=/dev/disk/by-label/4lin isofrom_system=$isofile LANG=zh_CN.UTF-8
	echo "Loading initrd..."
	initrd (loop)/boot/x86_64/loader/initrd
}
Category: Linux | Tags: linux grub grub2
7
9
2013
4

把标准输出伪装成终端

fcitx-diagnose 是 fcitx 输入法的非常优秀的诊断脚本。当输出到终端时,fcitx-diagnose 会给输出加上易于区分不同类型的消息的彩色高亮。可是,当用户把输出重定向到文件以便让其他人帮助查看时,这些高亮就没了。fcitx-diagnose 的输出很长,但如果通过管道给 less 查看的看,这些彩色也会消失。

要是 fcitx-diagnose 支持--color=always这样的选项就好了。可是 yyc 说他懒得写。getopt我只在 C 里用过,好麻烦的,所以我也懒得写。于是,我还是用我的 ptyless 好了。后来又想到,用于改变 I/O 缓冲方式的 unbuffer 和 stdbuf 应该也可以。测试结果表明,只有 unbuffer 可行,因为它是和 ptyless 一样使用伪终端的。stdbuf 则是使用 LD_PRELOAD 载入一个动态链接库的方式来设置缓冲区。

不过,既然 stdbuf 用 LD_PRELOAD 来设置缓冲区,我何不来用相同的办法改变isatty()函数的返回值呢?同时,我也学学 stdbuf,试了下__attribute__ ((constructor))指令。

#include<stdarg.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<dlfcn.h>

static int (*orig_isatty)(int) = 0;

int isatty(int fd){
  if(fd == 1){
    return 1;
  }
  return orig_isatty(fd);
}

void die(char *fmt, ...) {
  va_list args;
  va_start(args, fmt);
  vfprintf(stderr, fmt, args);
  va_end(args);
  fprintf(stderr, "\n");
  fflush(stderr);
  exit(-1);
}

__attribute__ ((constructor)) static void setup(void) {
  void *libhdl;
  char *dlerr;

  if (!(libhdl=dlopen("libc.so.6", RTLD_LAZY)))
    die("Failed to patch library calls: %s", dlerror());

  orig_isatty = dlsym(libhdl, "isatty");
  if ((dlerr=dlerror()) != NULL)
    die("Failed to patch isatty() library call: %s", dlerr);
}

然后,像 stdbuf、proxychains 那样做了个包装,不用自己手动设置 LD_PRELOAD 环境变量了。这也是我第一次使用 CMake,比 GNU 的 autotools 那套简单多了 :-)

使用方法很简单:

  1. 克隆或者下载源码
  2. 编译之
    $ mkdir -p build && cd build
    $ cmake .. # 或者安装到 /usr 下: cmake .. -DCMAKE_INSTALL_PREFIX=/usr
    $ make
    
  3. 安装之
    $ sudo make install
    $ sudo ldconfig
    
  4. 可以使用了:
    $ stdoutisatty fcitx-diagnose | less
    
Category: Linux | Tags: C代码 linux 终端 shell
7
3
2013
7

手动保存/读取 zsh 历史记录

关于历史记录,zsh 有很多选项。我的配置是:

HISTFILE=~/.histfile
HISTSIZE=10000
SAVEHIST=10000

# 不保留重复的历史记录项
setopt hist_ignore_all_dups
# 在命令前添加空格,不将此命令添加到记录文件中
setopt hist_ignore_space
# zsh 4.3.6 doesn't have this option
setopt hist_fcntl_lock 2>/dev/null
setopt hist_reduce_blanks

最多保留一万行不重复的历史记录。对其的读取和保存没做额外的配置,因此 zsh 会在启动时读取一次,在退出时保存一次。这样,如果同时开了多个 zsh,它们不会共享启动后的历史记录项,因为还没有写到文件中去。

其实是有选项来方便在多个 zsh 中及时共享历史记录的:

setopt SHARE_HISTORY

但是这样的话,每次显示提示符时 zsh 均会读取一次历史记录,而每当新的历史记录产生时 zsh 都会写入一次。磁盘 I/O 太频繁了,我不喜欢。我只需要在我想的时候,能够手动保存和读取历史记录就可以了。读过长长的文档,发现fc可以做到这点:

# 读取历史记录
fc -IR
# 保存历史记录
fc -IA

-I表示「incremental」,只有新的项目被处理。-R是读取,而-A是写入。千万不要用-IW,这样会丢失原有的历史记录。

Category: shell | Tags: zsh
6
27
2013
10

使用 SQLite3 的第三方扩展来修改火狐历史记录中的 URL

在火狐 17 以前,我可以这样子访问我本地的 Python 文档的:

jar:file:///home/lilydjwg/docs/Python/python.zip!/index.html

访问的实际上是一个 zip 文件中的内容。网页这种纯文本的东西压缩率高,35M 的 Python 3.3 文档,压缩后只有 7.1M。一来节省磁盘空间(我的 /home 分区己用 98% 了 TwT),二来读取快。

可是,自从火狐 17 开始,虽然 jar: 协议依旧支持,但是似乎其中的部分或者全部 JavaScript 脚本不会被执行。最明显的是,Python 这种用 Sphinx 制作的文档的搜索功能没了!

在拒绝升级火狐很长一段时间之后,Arch 把火狐 16 要用的库文件升级了……于是只好换回未压缩的一大堆文件。可问题是,我以前在火狐地址栏输入re就有 Python 正则表达式模块的文档的补全、输入py m就有 Python 标准库模块列表的补全,地址转换后,这些历史记录里的地址就全失效啦。

现在想想,其实我可以使用 Redirector 插件搞定的。但当时没想到,也是想更根本地解决问题,便直接修改火狐的地址记录数据库了。

此数据库是 SQLite3 数据库,位于火狐配置目录下的places.sqlite3文件中。moz_places表中记录了历史记录和书签中的项目的 URL 地址,只修改它就可以了。但问题是,这不像我当初 MediaWiki URL 路径中去掉index.php那样,用replace函数就可以搞定:

UPDATE OR REPLACE moz_places SET url = REPLACE(url, '/index.php', '') WHERE url LIKE 'http://localhost/wiki/index.php/%';

我需要正则表达式

于是找到了这个 glib_replace 模块,支持使用 glib 的正则表达式来进行替换。下回来编译成 .so 文件后这样子用:

SELECT load_extension('./glib_replace.so');
UPDATE OR REPLACE moz_places SET url = regex_replace('^jar:((?:.(?![^/]+\.zip!))+)(/[^/]+)\.zip!(.*)$', url, '\1\2\3') WHERE url LIKE 'jar:file:///home/lilydjwg/docs/Python/python%'; 

跑完就好啦!

PS: 如果你的 URL 中有 % 字符,记得在 like 操作符参数中转义成 %% 哦~

参考链接

Category: 火狐 | Tags: 正则表达式 火狐 sqlite3
6
2
2013
9

转换文本照片为 DjVu 格式

前几天听群里的网友聊起 DjVu 文档格式对扫描文本的压缩率很不错,又忆起自己手头有本书的照片版,不光近百兆占地方,而且一堆 JPEG 图片也不方便阅读,于是想着把它转成 DjVu 格式试试。

Google 了一下,发现有 DjVuLibre 这么套工具。用法没能指望上 Google,还是老老实实地看 man 文档的。

首先,把一张张的 JPEG 图片转成一个个单页的 DjVu 文档。命令名字很奇怪,叫c44(我还一不小心打成了c99囧)。转换比较费 CPU,所以用parallel来利用多核:

parallel c44 ::: *.JPG

然后当前目录下就出现了一堆与 JPEG 图片同名的.djvu文件。

接下来,把这些文档合并起来。命令叫djvmm想来指的是多页(multi-page)。

djvm -c doc.djvu *.djvu

这样就好啦。页面顺序是按照在命令行上给出的顺序。这里是按文件名排序的。看了看生成的doc.djvu,只有 15M 耶。我对比了下 DjVu 文档和原图片的质量,在放大的时候还是能看到差了一些的,不过文本清晰得足够阅读就好啦。

Category: Linux | Tags: DjVu
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 正则表达式

| Theme: Aeros 2.0 by TheBuckmaker.com