10
24
2012
33

欧拉项目第14题,Haskell 和 Erlang 等语言版

看到别人的 Racket 版,心里痒痒,写了个 Haskell 版。只用了尾递归,没用什么高级特性。题目地址。

calc3xp1 :: Int -> Int
calc3xp1 n | even n = n `div` 2
           | otherwise = 3 * n + 1

countlen :: Int -> Int
countlen = countlen_tail 0

countlen_tail :: Int -> Int -> Int
countlen_tail c n | n == 1 = c
                  | otherwise = countlen_tail (c+1) $ calc3xp1 n

findmax :: Int -> Int
findmax = findmax_tail 1 1 1

findmax_tail max maxn n final | n >= final = maxn
                              | otherwise = if new_len > max
                                               then findmax_tail new_len n n' final
                                               else findmax_tail max maxn n' final
                                             where new_len = countlen n
                                                   n' = n + 1

main = print $ findmax 1000000
>>> time ./3xp1
./3xp1  14.92s user 0.02s system 99% cpu 14.955 total

Erlang 版本直接照抄 Haskell 版:

-module(e3xp1).
-export([main/1]).

calc3xp1(N) when N rem 2 == 0 ->
  N div 2;
calc3xp1(N) ->
  3 * N + 1.

countlen(N) -> countlen_tail(0, N).

countlen_tail(C, 1) ->
  C;
countlen_tail(C, N) ->
  countlen_tail(C+1, calc3xp1(N)).

findmax(N) ->
  findmax_tail(1, 1, 1, N).

findmax_tail(_, Maxn, N, Final) when N >= Final ->
  Maxn;
findmax_tail(Max, Maxn, N, Final) ->
  Newlen = countlen(N),
  if Newlen > Max -> findmax_tail(Newlen, N, N+1, Final);
    true -> findmax_tail(Max, Maxn, N+1, Final)
  end.

main(_) ->
  io:format("~B~n", [findmax(1000000)]).

它在六分钟后还没能输出结果……

>>> time escript e3xp1.erl
^C
escript e3xp1.erl  374.55s user 0.94s system 99% cpu 6:15.76 total

Racket 版在同一机器上用时:

>>> time racket 3xp1.racket
racket 3xp1.racket  3.22s user 0.22s system 99% cpu 3.448 total

这是为什么呢?

PS: C 更快,不到半秒就搞定了……

更新:又试了试 Lua 版本的,和 Haskell 版速度相当。但是——LuaJIT 竟然只 2.4s 出结果,因为换了机器,所以实际上它应该比 Racket 版快一倍以上。

再次更新:Erlang 版编译后还是挺快的:

>>> erlc e3xp1.erl
>>> time erl -noshell -s e3xp1 main -s init stop
erl -noshell -s e3xp1 main -s init stop  5.59s user 0.01s system 84% cpu 6.608 total

不过,它什么事都不干启动后立即停止也花了一秒多,比 Java 更厉害:

>>> time erl -noshell -s init stop
erl -noshell -s init stop  0.06s user 0.01s system 6% cpu 1.077 total

2014年12月24日更新:Rust 版也非常快,而且写起来舒服:

fn calc3xpi(n: uint) -> uint {
    if n % 2 == 0 {
        n / 2
    } else {
        3 * n + 1
    }
}

fn countlen(&n: &uint) -> uint {
    let mut c = 0;
    let mut x = n;
    while x != 1 {
        c += 1;
        x = calc3xpi(x);
    }
    c
}

fn findmax(n: uint) -> uint {
    range(1, n).max_by(countlen).unwrap()
}

fn main(){
    let mut stdout = std::io::stdout();
    stdout.write_uint(findmax(1000000));
    stdout.write_char('\n');
}
10
12
2012
18

wget 下载云诺网盘文件

今天下载别人在云诺网盘上分享的文件,无奈网络太差,火狐才下了不到百分之一就认为下载完成了。每当这个时候,我便会祭出「中国特色网络」下的下载利器——wget。每当浏览器下不好文件时,wget 总会不屈不挠地一次次坚持,直到文件真正下载完成。

不过,这次对付云诺这个互联网友好发展的阻碍者时出现了问题。wget 总是下载到那个网页,即使指定 UserAgent 或者 Cookie 也没用。后来经过我不懈地尝试,发现指定Referer即可,例如:

wget --header 'Referer: http://s.yunio.com/xMliei' http://s.yunio.com/public/download/token/xMliei

另外,要取得正确的文件名,需要使用--content-disposition选项。不过,可能还需要使用ascii2uni -a J来处理百分号转义。下边是我的~/.wgetrc

# 不要乱转义中文
--restrict-file-names=nocontrol
# 使用重定向后的文件名
--trust-server-names=on
--content-disposition=on
Category: 网络 | Tags: wget 网络
10
7
2012
11

Fcitx Lua 插件:国际音标输入

GTK 右键的输入法菜单中有一项「IPA」,用于输入国际音标的。不过为了输入几个国际音标去够鼠标点菜单太麻烦了。既然是输入,交给我最爱的 fcitx 输入法去处理就好了嘛。

GTK 的国际音标输入很简单,每一两个字符对应一个音标字符。不过,因为通常是连续输入好几个国际音标,因此简单地使用 fcitx 的「快速输入」模块的话,每输入一个得打一次前缀,太痛苦了。于是我用 fcitx 的 Lua 模块来做。

要注意的是,fcitx 的 Lua 支持默认没有开启,编译时需要在 cmake 参数中加上-DENABLE_LUA=On。Arch 用户可以从 archlinuxcn 源安装 fcitx-lilydjwg-git。其它发行版可能有单独的fcitx-lua包,也可能需要自行编译。

安装方法很简单,把ipa.lua放到~/.config/fcitx/lua目录下即可。然后按Ctrl-5(默认)重新加载 fcitx 配置即可。

使用方法是,使用预定义的快速输入快捷键(默认是;)进入「快速输入」模式后,输入命令前缀yb,然后按 GTK 那个 IPA 输入法的方式输入即可,按空格提交输入。不知道对应关系的可查看脚本源码。

[ˌðæts ˈɔːl ˈθænks].

Category: Linux | Tags: fcitx Lua 中文支持
10
7
2012
0

拼音声调数字转字符

Suǒwèide 「pīnyīn shēngdiào shùzì zhuǎn zìfú」 jiùshì bǎ 「pīnyīn+shùzìdiàbiǎo de shēngdiào」 zhuǎnchén Unicode zìfú biǎoshì.

所谓的「拼音声调数字转字符」就是把「拼音+数字表示的声调」转成 Unicode 字符表示,为了是做成 fcitx 的拼音输入插件,以方便输入上段的内容。

算法是参考别人的,把所有带声调的音节后缀穷举出来再转换,简单暴力好用。我改写的 Python 3 版在 github/winterpy 上,支持大写和「ü」。

为了在 fcitx 中使用,我又改写了一 Lua 版本,代码如下:

#!/usr/bin/env lua

-- http://www.robertyu.com/wikiperdido/Pinyin%20Parser%20for%20MoinMoin

-- definitions
-- For the pinyin tone rules (which vowel?), see
-- http://www.pinyin.info/rules/where.html

local strsub = string.gsub
local _strupper = string.upper

-- map (final) constanant+tone to tone+constanant
mapConstTone2ToneConst = {
  n1 = '1n',
  n2 = '2n',
  n3 = '3n',
  n4 = '4n',
  ng1 = '1ng',
  ng2 = '2ng',
  ng3 = '3ng',
  ng4 = '4ng',
  r1 = '1r',
  r2 = '2r',
  r3 = '3r',
  r4 = '4r',
}

-- map vowel+vowel+tone to vowel+tone+vowel
mapVowelVowelTone2VowelToneVowel = {
  ai1 = 'a1i',
  ai2 = 'a2i',
  ai3 = 'a3i',
  ai4 = 'a4i',
  ao1 = 'a1o',
  ao2 = 'a2o',
  ao3 = 'a3o',
  ao4 = 'a4o',
  ei1 = 'e1i',
  ei2 = 'e2i',
  ei3 = 'e3i',
  ei4 = 'e4i',
  ou1 = 'o1u',
  ou2 = 'o2u',
  ou3 = 'o3u',
  ou4 = 'o4u',
}

-- map vowel-number combination to unicode
mapVowelTone2Unicode = {
  a1 = 'ā',
  a2 = 'á',
  a3 = 'ǎ',
  a4 = 'à',
  e1 = 'ē',
  e2 = 'é',
  e3 = 'ě',
  e4 = 'è',
  i1 = 'ī',
  i2 = 'í',
  i3 = 'ǐ',
  i4 = 'ì',
  o1 = 'ō',
  o2 = 'ó',
  o3 = 'ǒ',
  o4 = 'ò',
  u1 = 'ū',
  u2 = 'ú',
  u3 = 'ǔ',
  u4 = 'ù',
  v1 = 'ǜ',
  v2 = 'ǘ',
  v3 = 'ǚ',
  v4 = 'ǜ',
}

function strupper(c)
  local specials = {
    ['ā'] = 'Ā',
    ['á'] = 'Á',
    ['ǎ'] = 'Ǎ',
    ['à'] = 'À',
    ['ē'] = 'Ē',
    ['é'] = 'É',
    ['ě'] = 'Ě',
    ['è'] = 'È',
    ['ī'] = 'Ī',
    ['í'] = 'Í',
    ['ǐ'] = 'Ǐ',
    ['ì'] = 'Ì',
    ['ō'] = 'Ō',
    ['ó'] = 'Ó',
    ['ǒ'] = 'Ǒ',
    ['ò'] = 'Ò',
    ['ū'] = 'Ū',
    ['ú'] = 'Ú',
    ['ǔ'] = 'Ǔ',
    ['ù'] = 'Ù',
    ['ǜ'] = 'Ǜ',
    ['ǘ'] = 'Ǘ',
    ['ǚ'] = 'Ǚ',
    ['ǜ'] = 'Ǜ',
  }
  if specials[c] then
    return specials[c]
  else
    return _strupper(c)
  end
end

function ConvertPinyinToneNumbers(lineIn)
  local lineOut = lineIn

  -- first transform
  for x, y in pairs(mapConstTone2ToneConst) do
    lineOut = strsub(strsub(lineOut, x, y), strupper(x), strupper(y))
  end

  -- second transform
  for x, y in pairs(mapVowelVowelTone2VowelToneVowel) do
    lineOut = strsub(strsub(lineOut, x, y), strupper(x), strupper(y))
  end

  -- third transform
  for x, y in pairs(mapVowelTone2Unicode) do
    lineOut = strsub(strsub(lineOut, x, y), strupper(x), strupper(y))
  end

  return strsub(strsub(lineOut, 'v', 'ü'), 'V', 'Ü')
end

local function main()
  local lineOut
  for lineIn in io.stdin:lines() do
    lineOut = ConvertPinyinToneNumbers(lineIn)
    print(lineOut)
  end
end

main()

很可惜的是,fcitx 的 Lua 模块目前不支持屏蔽数字键选字,所以暂无法在 fcitx 中使用。

Category: 编程 | Tags: Lua Python 中文支持 拼音
9
30
2012
3

使用 PyQt 滚动播放卫星云图

自从和 GNOME 开发者接触过之后,我决定放弃断断续续学了一段时间的 GTK 而转向 Qt 了。看了两三天的 PyQt4 tutorial,恰好遇到一需要界面的脚本,本来我会搞成 Web 的,但既然学了 Qt 嘛,当然得练练。

起因是这样子的。一天和群里的人聊到天气之类的,然后有人扔了张卫星云图出来。多年未见的云图啊,再次见到感觉好亲切,虽然我的地理和气象知识已经忘记了好多了。然后得到了这个网页。本来呢,他们想搞成在以前的天气预报里那样逐幅图像显示的滚动播放效果的,但政府网站嘛,出点问题很正常,比如我第一次播放的时候图片基本上动不了(可能是因为没有预加载以及网络不畅造成的),第二次的时候图像时不时闪一下。而且这样可控性太差,想看的时段根本看不清就跳走了,不想看的时段却一直慢慢地跳。所以我拿 Python 写了个通过 slider 滑块来控制显示的图像简易脚本。写完后才发现使用 sxiv 然后按住N/P键是一样的效果……

代码如下,需要 PyQt4 或者 PySide。对于后者,Arch 用户可以从 Arch Linux CN 源安装而不必通过 AUR 自行编译(C++ 编译很费时的)。

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

# TODO: 并发下载
# TODO: 下载进度显示
# TODO: 允许加载已经下载但网页上没有的云图
# TODO: 网络作为可选

import os
import sys
import re
import gzip
import urllib.request

pic_dir = '.'

try:
  from PySide import QtGui, QtCore
except ImportError:
  from PyQt4 import QtGui, QtCore

def getPage():
  request = urllib.request.Request('http://www.weather.com.cn/static/product_video_v1.php?class=JC_YT_DL_WXZXCSYT')
  request.add_header('Accept-Encoding', 'gzip')
  res = urllib.request.urlopen(request)
  return gzip.decompress(res.read()).decode('utf-8')

def getPics(page):
  urlre = re.compile(r'\bhttp://i.weather.com.cn/i/product/pic/m/sevp_nsmc_wxcl_asc_e99_achn_lno_py_\d{17}.jpg\b')
  return sorted({x.replace('/m/', '/l/') for x in urlre.findall(page)})

def download(pics):
  ret = []
  for p in pics:
    file = os.path.split(p)[1]
    file = os.path.join(pic_dir, file)
    ret.append(file)
    if os.path.exists(file):
      continue
    data = urllib.request.urlopen(p).read()
    open(file, 'wb').write(data)
  return ret

class YuntuShow(QtGui.QWidget):
  def __init__(self, pics):
    super().__init__()
    self.pics = pics
    self.initUI()

  def initUI(self):
    pic = QtGui.QPixmap(self.pics[-1])
    self.pic = piclabel = QtGui.QLabel(self)
    piclabel.setPixmap(pic)

    slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
    slider.setTickPosition(QtGui.QSlider.TicksBelow)
    m = len(self.pics) - 1
    slider.setMaximum(m)
    slider.setSliderPosition(m)
    slider.valueChanged[int].connect(self.changePic)

    vbox = QtGui.QVBoxLayout()
    vbox.addWidget(piclabel)
    vbox.addWidget(slider)
    self.setLayout(vbox)

    self.resize(960 + 10, 720 + 50)
    self.setWindowTitle('YuntuShow')
    self.show()

  def keyPressEvent(self, e):
    if e.key() == QtCore.Qt.Key_Q:
      self.close()

  def changePic(self, value):
    pic = QtGui.QPixmap(self.pics[value])
    self.pic.setPixmap(pic)

def main():
  urls = getPics(getPage())
  pics = download(urls)
  app = QtGui.QApplication(sys.argv)
  yt = YuntuShow(pics)
  sys.exit(app.exec_())

if __name__ == '__main__':
  main()

短吧?而且我也只看了三天的教程用的时候查了下文档而已哦~比起 GTK 来快速多了。不过这其中有个很重要的原因是我已经通过 GTK 了解了基本的 GUI 编程了。

因为最开始会从网络下载数据和图像,所以要等一会儿窗口才会出现。

Category: python | Tags: python Qt pyqt pyside
9
18
2012
12

截短 UTF-8 字符串

用于显示时,经常会遇到显示的文本太长需要截短的情况。如果是如 ASCII 这样的定长编码,截短到指定长度自然不成问题。可如果源字符串是 UTF-8 编码的呢?ANSI C 里只管字节不管编码,所以如果想只用 ANSI C 提供的功能的话,就只能自己写了。因为需求仅仅是截短字符串而已,也不要求多么精确,所以没有去做编解码,只是丢弃按字节截短后的字符串最后的无效编码而已。而且目标语种是 Lua,也不方便搞位操作。

维基百科可知,UTF-8 多字节字符第一字节的最高两位为11,而其它字节的最高两位均为10。所以就把后面那些10xxxxxx连同最开始的11xxxxxx去掉好了。这样会多截掉一个多字节字符,但无所谓了。

function truncateUTF8String(s, n)
  local r = string.sub(s, 1, n)
  local last = string.byte(r, n)
  if not last then return r end
  while last >= 128 and last <= 192 do
    n = n - 1
    r = string.sub(r, 1, n)
    last = string.byte(r, n)
  end
  if last >= 128 then
    r = string.sub(r, 1, n-1)
  end
  return r
end

2012年9月27日更新:感谢Fermat618提供的思路,更新了更简洁、准确的代码如下:

function truncateUTF8String(s, n)
  local dropping = string.byte(s, n+1)
  if not dropping then return s end
  if dropping >= 128 and dropping < 192 then
    return truncateUTF8String(s, n-1)
  end
  return string.sub(s, 1, n)
end
Category: 编程 | Tags: 乱码 Lua 中文支持
8
28
2012
10

UDP打洞实验

两台没有外网 IP、在 NAT 后边的主机如何直连?UDP打洞通常可行,但是需要第三方服务器。方法如下:

在服务器 S 上监听一个 UDP 端口,在收到 UDP 数据包后把源地址发回去。代码如下(github):

import sys
import time
import socket

def main(port):
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  s.bind(('', port))
  try:
    while True:
      data, addr = s.recvfrom(4096)
      back = 'Your address is %r\n' % (addr,)
      s.sendto(back.encode(), addr)
      print(time.strftime('%Y-%m-%d %H:%M:%S'), addr, 'just sent us a message:', data.decode('utf-8', 'replace'), end='')
  except KeyboardInterrupt:
    print()

if __name__ == '__main__':
  try:
    main(int(sys.argv[1]))
  except (ValueError, IndexError):
    sys.exit('which port to listen?')

主机 A 发送数据包:

$ socat readline udp:xmpp.vim-cn.com:2727,sourceport=4567
my addr?
Your address is ('a.b.c.d', 40060)

输入任意消息并回车,一个 UDP 就从本地的 4567 发送出去了。从上述示例我们可以看到,NAT 设备转发时是从 40060 端口发送出去的。为了让服务器返回的数据能够到达内网主机,在一段时间内,NAT 设备会记住外网来自 40060 端口的 UDP 数据包要发送给主机 a.b.c.d 的 4567 端口。完全圆锥型NAT不会在意外部数据包是从什么地方发回来的。受限圆锥型NAT会忽略掉其它主机的数据包,上例中只认可来自 xmpp.vim-cn.com 的数据包。端口受限圆锥型NAT更进一步地要求源端口(上例中是 2727)必须跟之前发出的数据包的目的端口一致。当然,「之前发出的数据包」不必是最后一个。所以,除了最后一种——对称NAT——之外,其它类型的NAT都是有可能成功穿透的。参见维基百科条目网络地址转换STUN

后来通过 pystun 程序,我得知我所处的 NAT 是完全圆锥型的。

在知道 A 的发送地址后,主机 B 就可以向这个地址发送数据了。接下来的操作使用 socat 命令就是:

# host A
$ socat readline udp-listen:4567
# host B
$ socat readline udp:A:4567

然后 B 先发送数据让 A 知道 B 的地址(socat 会 connect 到这个地址),双方就可以相互通信了。当然,因为是 UDP 协议,所以通信是不可靠的,丢包啊乱序啊都有可能。

2013年10月13日更新:想要连接到 NAT 后边的 mosh 请看这里

Category: 网络 | Tags: python 网络 socat UDP
8
20
2012
7

GM 脚本:修正 github

从某时起,Github 和 Linux 一样,开始有着越来越多的 bug 和让人不舒服的地方。本文所附的 GreaseMonkey 脚本修正以下问题:

  • 项目首页默认下载文件格式是 zip 而不是 gzip
  • 新建项目后,从已有项目创建的提示命令使用 HTTPS 而不再是 SSH 协议。这直接导致 git 向用户询问用户名和密码,而不使用用户已经上传并确认的密钥。

Google Code 后来也加入了 git 支持,但是我极少使用。为什么呢?因为我讨厌输入用户名和密码!虽然 Github 没有像 Google Code 那样给你生成个随机密码,但这种麻烦且不安全的方式能避免我就决不容忍。你的密码会比密钥还长吗?你使用密钥时需要输入或者显示密钥的内容吗??

// ==UserScript==
// @name           github fixes
// @namespace      http://lilydjwg.is-programmer.com/ 
// @description    下载默认 gzip 格式,新建项目时使用 ssh 协议
// @include        https://github.com/*
// @version	   1.4
// @grant          none
// ==/UserScript==
 
var dl = document.querySelector('[icon_class=octicon-cloud-download]');
if(!dl){
  dl = document.querySelector('a[title^=Download]');
}
if(dl){
  dl.title = dl.title.replace('zip', 'gzip');
  dl.href = dl.href.replace(/archive\/[^\/]+.zip/, 'tarball/master');
  var infotext = dl.childNodes[dl.childNodes.length-1];
  infotext.textContent = infotext.textContent.replace('ZIP', 'GZIP');
}

var repourl = document.querySelectorAll('.js-live-clone-url');
var re = /https:\/\/github\.com\/([^\/]+)\/(.*)/;
var span, m;
var i, len;
for(i=0, len=repourl.length; i<len; i++){
  span = repourl[i];
  m = re.exec(span.textContent);
  if(m){
    span.textContent = 'git@github.com:'+m[1]+'/'+m[2];
  }
}

点此安装

2012年9月9日更新:跟随 github 的更新,修正修改默认下载的格式失败的问题。

2012年11月27日更新:跟随 github 更新,使用带版本号的下载链接地址。

2013年5月17日更新:跟随 github 更新,更新 CSS 选择器。

2013年6月18日更新:支持 新版界面

Category: 版本控制 | Tags: github GreaseMonkey
8
2
2012
14

GM 脚本:桌面浏览器登录招商银行手机版,及 mitmproxy 的初次使用

招商银行网银需要控件,只支持 Windows 和 Mac。但是手机版不需要安装任何软件可直接登录。通过桌面浏览器访问https://mobile.cmbchina.com/MobileHtml/Login/LoginA.aspx可以看到登录界面,但登录时被拒绝,弹出警告「为了您的资产安全,请用手机访问手机银行!」。更改 UserAgent 失败。通过 Firebug 发现其 POST 数据中包含从 JavaScript 取到的navigator.UserAgentscreen.widthscreen.heightnavigator.platform的值,以 XML 发送给服务器。于是尝试修改之。

这次使用 privoxy 不行了,因为是 HTTPS 加密连接,privoxy 看不到内容。于是用上了前不久才发现的工具mitmproxy,一个支持 SSL 的中间人代理,并支持交互、命令行和脚本化查看、编辑功能。在看了下请求数据后,按i输入要中断的请求的模式-u LoginA,在请求 URL 包含LoginA字样时中断以进行人工编辑。使用j键移动到停下来的橙色请求上:

中断浏览器请求

按回车显示详细信息,按e进行编辑,f选择编辑表单域,编辑完成后退回到请求列表界面,按a继续,再按a接受响应信息。

编辑POST表单

经过多次 Google 和编辑尝试,招行终于不再要求我使用手机访问了。不过很显示,我不能每次登录都使用 mitmproxy 手工编辑对不?于是写了个 GreaseMonkey 脚本。

此脚本用到了unsafeWindow,也就是页面本身中的那个window对象,而不是被GreaseMonkey wrap 过的。这样才能修改页面中定义的函数。注意据说这样做有安全风险。详见 GreaseMonkey Wiki

点击此处安装此脚本早已失效

Category: 火狐 | Tags: GreaseMonkey 火狐 mitmproxy
7
29
2012
1

Revert new site identity feature and show favicon in urlbar for Firefox 14 & 15

(中文文章请见此处。)

If a user can be cheated by a site icon, the user can still be cheated even  when you remove that icon.

For the patch for Firefox 14, click here; for the omni.ja file, check this. This includes the long-lost feature that double-clicking on the space in a tab group will open a new tab.

For Firefox 15 patch.The omni.ja file is at the same location.

For Firefox 16 patch.The omni.ja file is at the same location.

Category: 火狐 | Tags: 火狐

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com