ITエンジニアのフォーチュンクッキー

AKB48には特別思い入れはないが、「恋するフォーチュンクッキー」には2つの意味でニヤリとした。一つは「巧い」ということ。わかりやすさと懐かしい感じ、お見事。もうひとつは BSD Unix 文化に一定以上染まったエンジニアであれば、誰しもが感じたであろう感覚。「恋する~」前は「フォーチュンクッキー」という英語圏文化は老若男女が知っている言葉ではなかったと思うが、BSD Unix の /usr/games/fortune を通して知っていたのだ、我々は。

などという、時期外れの前置きはともかくとして。

ブログパーツであるとかプログラミングの「ネタ」を何か作ろうとするときには、割と「ランダムに何かする」とかそういうことを考える機会が多いのであって、そうするとどういうわけか必ずといってもいいほど /usr/games/fortune のことを思い出す。

1 me@host: ~$ fortune
2 A man is never more truthful than when he acknowledges himself a liar.
3         - Mark Twain

どちらかといえば「お堅い」linux でこれがデフォルトで入っていることはまずないし、今更こんなテキストベースのものをフューチャーしても仕方がないし、ということなのかもしれないけれど、昔ながらの UNIX ユーザとしては、こういった、他愛もないけれど伝統的なプログラムは、入っていて欲しい、などと思う。今時はディスク容量だけはありあまるほどあるわけで、かさばるものでもないのであるし。fortune の話題は「fortuneで爽やかな1日を始めよう」など割と見つかるので探してみればいい。

Windows 生活主体で何が嬉しくてテキストベースの fortune が嬉しいか、という話は脇におくとして、Python をインストールしてあれば、Python で書き直したものを書いた人がおられるので、気分を味わうことは出来る。fortune — Display random quotes がそれ。ただこれ、そのまま使うのではなく、持ってきて書き換えて遊ぶことを考えたほうがいい。たとえば

1 from grizzled.cmdline import CommandLineParser

と grizzled という OSS に依存しているが、Python 2.7 であれば argparse で書ける内容であえて grizzled 使う理由はないし、コマンドラインプログラムとして機能させることだけのパッケージになっているが、せっかく Python にしたのなら、モジュールにすればいいのに、とも思う。fortunes ファイルを pickle で永続化する仕組みになっているのだけれど、pickle のプロトコルバージョンの扱いもたぶん書き直したほうがいい。

シンプルに直すとfortune — Display random quotesはこれだけ:

 1 # -*- coding: utf-8 -*-
 2 import random
 3 import os
 4 import sys
 5 import cPickle as pickle
 6 
 7 __all__ = ['main', 'get_random_fortune', 'make_fortune_data_file']
 8 
 9 _PICKLE_PROTOCOL = 0
10 
11 def random_int(start, end):
12     try:
13         # Use SystemRandom, if it's available, since it's likely to have
14         # more entropy.
15         r = random.SystemRandom()
16     except:
17         r = random
18 
19     return r.randint(start, end)
20 
21 def get_random_fortune(fortune_file=os.path.join(os.path.abspath(os.path.dirname(__file__)), "fortunes")):
22     """
23     Get a random fortune from the specified file. Barfs if the corresponding
24     ``.dat`` file isn't present.
25 
26     :Parameters:
27         fortune_file : str
28             path to file containing fortune cookies
29 
30     :rtype:  str
31     :return: the random fortune
32     """
33     fortune_index_file = fortune_file + '.dat'
34     if not os.path.exists(fortune_index_file):
35         raise ValueError, 'Can\'t find file "%s"' % fortune_index_file
36 
37     with open(fortune_index_file) as fortuneIndex:
38         data = pickle.load(fortuneIndex)
39     randomRecord = random_int(0, len(data) - 1)
40     (start, length) = data[randomRecord]
41 
42     with open(fortune_file, 'rU') as f:
43         f.seek(start)
44         fortuneCookie = f.read(length)
45 
46     return fortuneCookie
47 
48 def _read_fortunes(fortune_file):
49     """ Yield fortunes as lists of lines """
50     result = []
51     start = None
52     pos = 0
53     for line in fortune_file:
54         if line == "%\n":
55             if pos == 0: # "%" at top of file. Skip it.
56                 continue
57             yield (start, pos - start, result)
58             result = []
59             start = None
60         else:
61             if start == None:
62                 start = pos
63             result.append(line)
64         pos += len(line)
65 
66     if result:
67         yield (start, pos - start, result)
68 
69 def make_fortune_data_file(fortune_file, quiet=False):
70     """
71     Create or update the data file for a fortune cookie file.
72 
73     :Parameters:
74         fortune_file : str
75             path to file containing fortune cookies
76         quiet : bool
77             If ``True``, don't display progress messages
78     """
79     fortune_index_file = fortune_file + '.dat'
80     if not quiet:
81         print 'Updating "%s" from "%s"...' % (fortune_index_file, fortune_file)
82 
83     data = []
84     shortest = sys.maxint
85     longest = 0
86     for start, length, fortune in _read_fortunes(open(fortune_file, 'rU')):
87         data += [(start, length)]
88         shortest = min(shortest, length)
89         longest = max(longest, length)
90 
91     with open(fortune_index_file, 'wb') as fo:
92         pickle.dump(data, fo, _PICKLE_PROTOCOL)
93 
94     if not quiet:
95         print 'Processed %d fortunes.\nLongest: %d\nShortest %d' %\
96               (len(data), longest, shortest)

mainとしての機能は省略した。pickleファイル更新の make_fortune_data_file と、それにより作成した「格言とかジョーク」をランダムに取り出す get_random_fortune。エントリポイントはこの2つのみ。

fortunesファイルは、同じ作者の「個人ファイル?」が https://github.com/bmc/fortunes/に置いてある。

10年くらい前であれば「これで cgi を作ろう」なんてのがそれなりのネタになったかもしれないが、かなり浦島太郎ネタになるので、飛びつかないように。「fortuneのようなもの」は探せばいくらでも見つかるはず。Wordpress プラグインにもあったと思う。「飛びつかないように」といってるワタシがなぜこんなネタを書いているかというと。Python + json + cgi + jquery という組み合わせで遊んでみようと思い立って、そのお題として一番手軽そうだったから。Python + json + cgi + jquery ネタは私が WEB 系を得意にしていないのとサーバの関係でうまくいっていないが、何か出来たらコッソリお知らせしようと思う。期待せずに待っていて欲しい。(誰がだ。)