Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

PySpaアドベントカレンダー 22日目

はじめに

このエントリはPySpaアドベントカレンダーの22日目のエントリです。

PHPとかJavaがクソとか言うエントリではありませんのでご注意願います。

PySpaについて

まあ言うことはありません。ロビーでイリーガルな話やヒドいい話などをする合宿です。

コードとか二の次であってみんなの闇を共有する合宿です。

多分逮捕者がでてもおかしくないです。

最近作ってるもの

せっかくなので最近作ってるもの話をしておきます。

dismage

dismage - MySQL Protocol Server

要は MySQL Protocol を話す Server です。WSGI ライクなインターフェイスを持っています。

isucon 用に作りはじめたのがきっかけです。内部は libdrizzle で作られています。

全て非同期のAPIを使って実装されていますが、毎度のことながら greenlet でその辺を隠蔽してあるので使う人何も意識することはありません。

ほぼ C で書いてあるのでパフォーマンスはそれなりに出ます。

パフォーマンスをあげるためキャッシュなどをする場合、Web アプリケーション側に手が入ってしまったりするのですが(Middleware使えばいいのですが) それらをDB側の方へ追いやることができます。

サンプルコード

from dismage import *
from dismage import patch
patch.patch_all()

def app(cmd, cmd_data, start_result):

    print("cmd:%s data:%s start_response:%s" % (cmd, cmd_data, start_result))
    if cmd == COMMAND_INIT_DB or "COMMIT" in cmd_data:
        return
    columns = [(COLUMN_TYPE_VARCHAR, 10, 'name'), (COLUMN_TYPE_INT24, 8, 'age')]
    start_result(columns=columns)
    return [('john', 30), ('smith', 28)]

listen(port=3307)
run(app)
  • cmd には COMMAND の種別が入ってくる
  • cmd_data には SQL 本文が入ってくる
  • start_result には SELECT 文で返す際の行の定義を設定する

上記の例だと以下のSQLを実行して結果を返しているようなイメージになります。

SELECT name, age FROM x;

SELECT 場合のみ start_result に返す行の定義を設定しますが、その他の場合には設定しなくてよいです。

返り値もNoneで返せばOKです。

非同期対応

socket にパッチがあたってるので pure python な 通信は非同期通信が行われます。

例えば内部キャッシュに無いデータを本来の MySQL へ問い合わせる(中継するイメージ)場合 pymysql などを使えば非同期で MySQL へ問い合わされます。

応用

まだ開発中ですが応用の用途としては以下のようなものがあります。

  • テスト用の MySQL Sever として使う
  • 複数の DB に同時に書き込む
  • SQL を解析して自前で sharding する
  • SQL を解析して一部の SQL を Hive に流す
  • SQL を解析して一部の SQL を Impala に流す
  • INSERT 文を解析して KVS にデータを入れる
  • INSERT 文を解析して fluentd にデータを入れる

ドライバーによっては SQL かどうか判断していないので好きなデータを送信して処理することもできると思います。

(URL を送ってアクセスした結果を返すなどなど)

また WSGI Middleware のようなものも書けるので Middlwareで SQL の方言を直したり、フィルターをかけたりといったこともできます。

お手伝いしてくれる人など募集してます。

(特にlibdrizzleの認証とこに詳しい人)

素数を求める

特に難しいことはない。
とりあえずメモリが許すまで。
100万個ぐらいは数えれる。


fn prime(n: uint) {
    let mut prime: ~[uint] = do vec::build_sized(n) |push| {
        let mut i: uint = 0;
        while i < n {
            push(0);
            i += 1;
        }};
    let mut ptr: uint = 0;
    let mut j: uint = 5;

    prime[ptr] = 2;
    ptr += 1;
    prime[ptr] = 3;
    ptr += 1;
    
    loop {
        let mut i: uint = 1;
        let mut flg: bool = false;
        
        while prime[i] * prime[i] <= j  {

            if (j % prime[i] == 0) {
                flg = true;
                break;
            }
            i += 1;
        }
        if !flg {
            prime[ptr] = j;
            ptr += 1;
        }
        if prime[n - 1] > 0 {
            break;
        }
        j += 2;
    }

    for prime.each |e| {
        io::println(fmt!("%u", *e));
    }
}

fn main() {
    let args = os::args();
    let n = int::from_str(args[1]).get() as uint;
    prime(n);
}

KVMで無線LANブリッジ接続(DHCP)

最近ではVMが立ち上げれるぐらいのノートパソコンも安価に手に入るようになりました。
私のマシンもi7搭載のノートでKVMも入れてるのですが、ブリッジ接続の例がeth0のばっかで無線LANでブリッジをやる方法がよくわかりませんでした。
(NATで使ってた)
で最近マジメに設定してみようと思い調べたのでメモっておきたいと思います。
ちなみに環境はUbuntuです。RedHat系だと簡単にできるのかも知れません。

ちなみに適当にeth0と同じように書いてもwlan0ははじかれたり、それっぽい設定をしてもDHCPでうまくつながらなかったりします。
この方法だとゲスト、ホストともにDHCPでつながります。
あっちこっち持ち運んで作業するノートなどではDHCPの方が楽だと思います。

#!/bin/sh

/sbin/brctl addbr br0
/usr/sbin/tunctl -t tap0
/sbin/brctl addif br0 tap0
/sbin/ip addr add 192.168.2.200 dev br0
/sbin/ip link set br0 up

/bin/echo 1 > /proc/sys/net/ipv4/conf/wlan0/proxy_arp
/bin/echo 1 > /proc/sys/net/ipv4/conf/br0/proxy_arp
/bin/echo 1 > /proc/sys/net/ipv4/conf/tap0/proxy_arp

/usr/sbin/parprouted wlan0 br0
/usr/sbin/bcrelay -d -i br0 -o wlan0

ip addr addのとこはDHCPレンジ外を指定します。
このシェルをrootで実行後、virt-managerでゲストのネットワークデバイスで br0を指定すればつながります。
rc.localなどに書いてもいいかもしれませんね。

protocolを使って既存の関数の振る舞いを変える

こんにちわ、高校生です。
今回はprotocolを使った話です。

通常の場合

user=> (bit-and "生" "死")
IllegalArgumentException bit operation not supported for: class java.lang.String  clojure.lang.Numbers.bitOpsCast (Numbers.java:994)

bit-andはNumbersしか受け付けない関数なので当たり前のごとくうまくいきません。
異なる型でもいい感じに処理をして欲しい場合にはprotocolで既存関数も拡張ができます。

(defprotocol bit-protocol
  (bit-and [x y]))

(extend-protocol bit-protocol
  java.lang.String
  (bit-and [x y]
    (let [a (Character/codePointAt x 0)
          b (Character/codePointAt y 0)
          c (bit-and a b)]
      (apply str (Character/toChars c))))

  java.lang.Object
  (bit-and [x y]
    (clojure.core/bit-and x y)))

(bit-and "生" "死")

上記のように型ごとに処理が書けます。

Warning: protocol #'user/bit-protocol is overwriting function bit-and
WARNING: bit-and already refers to: #'clojure.core/bit-and in namespace: user, being replaced by: #'user/bit-and

Warningが出るものの既存の関数をoverwriteすることができます。
もちとんns上も置き換わるのでそちらの警告も出ますが。

Parsecを使ってjinja2風テンプレートをパースする。

http://github.com/mopemope/clarsec2を使って以前のjinja2風のテンプレを解析する。
まずはおおまかなブロックを検出するだけだけど。

input

<html>
  <body>
    {% foo %}
      {{ variable }}
    {% endfoo %}
    <div>
      {#_
      Comment
      Comment
      #}
    </div>
  </body>
</html>

コード

(use '[clarsec])
(use '[clarsec.monad])
(require '[clojure.string :as string])
(import '(java.util.regex Pattern))

(def sp-chars #"([\\\\*+\\[\\](){}\\$.?\\^|])")

(defn- esc-regex [in]
  (string/replace in sp-chars "\\\\$1"))

(defn- build-reg [args]
  (string/join "|" (map esc-regex args)))

(defn- create-regex [tags]
  (Pattern/compile (str "^(.*?)(" (build-reg tags) ")") Pattern/DOTALL))

(defmacro make-data [k]
  `(fn [x#] {~k x#}))

(defn text-data-p [regex conv-fn]
  (make-parser
    (fn p-text-data [^String strn]
      (let [found (re-find (re-matcher (create-regex regex) strn))
            [pair text token] (or found [nil nil nil])]
        (if (and token (not= token pair))
          (consumed (conv-fn text) (.substring strn (- (.length pair) (.length token))))
          (failed))))))

(defmacro surround2 [st ed]
  `(fn [p#] (between (symb ~st) (symb ~ed) p#)))

(defn combine [p last]
   (let-bind [r p
              l last]
            (result (concat [r] l))))

;; -------------------------------------

(def tag-block ((surround2 "{%" "%}")
                  (<$> (make-data :tag-block) identifier)))

(def variant-block ((surround2 "{{" "}}")
                  (<$> (make-data :variant-block) identifier)))

(def comment-block ((surround2 "{#" "#}")
                  (<$> (make-data :comment-block) (text-data-p ["#}"] #(str %)))))


(def text-data (text-data-p ["{{" "{%" "{#"] (make-data :text-data)))

(def all
  (<$> (make-data :text-data)
     (make-parser
       (fn p-any-token [strn]
         (consumed strn "")))))

(def blocks (many1 (<|> text-data tag-block variant-block comment-block)))

(def content (slurp "/tmp/index.html"))
(parse$ (combine blocks all) content)

最後の部分はまあ適当なんですけど。
上半分はutilとかキモいパーサーとかです。
正規表現とミックスしたものが使えるので便利ですね。

肝心はココです。
なんとなくどんなものの組み合わせ構成されてるかわかりますね。

(def tag-block ((surround2 "{%" "%}")
                  (<$> (make-data :tag-block) identifier)))

(def variant-block ((surround2 "{{" "}}")
                  (<$> (make-data :variant-block) identifier)))

(def comment-block ((surround2 "{#" "#}")
                  (<$> (make-data :comment-block) (text-data-p ["#}"] #(str %)))))


(def text-data (text-data-p ["{{" "{%" "{#"] (make-data :text-data)))

(def all
  (<$> (make-data :text-data)
     (make-parser
       (fn p-any-token [strn]
         (consumed strn "")))))

(def blocks (many1 (<|> text-data tag-block variant-block comment-block)))

(def content (slurp "/tmp/index.html"))
(parse$ (combine blocks all) content)

結果:

{:type :consumed, :value (({:text-data "\n \n "} {:tag-block "foo"} {:variant-block "variable"} {:tag-block "endfoo"} {:text-data "

\n "} {:comment-block "_\n Comment\n Comment\n "}) [:text-data "
\n \n\n\n"]), :rest ""}

一応パースできてるっぽい。
正規表現エスケープまみれになったコードよりわかりやすく(?)スッキリ(?)書けます。
速度はもちのろん出ません!!!
(<|>でtry-errorを繰り返しまくるので)

Parsecを使ってパースする

こんにちわ、高校生です。
にわかLisperです。みなさん、本当にClojure書いてますか?
正規表現とかしんどいし関数型言語ならParsec使えよ!という話があると思うので適当に書いてみました。

まずはcsvですよね。

(use '[clarsec])

(def input
       "Year,Make,Model,Length
       1997,Ford,Model-350,234
       2000,Mercury,\"Model 800\",238")

(def cell (lexeme (<|> string-literal
             (stringify (many (none-of ",\n"))))))

(def line (sep-by-1 cell comma))

(def csv (sep-by line eol))

(:value (parse$ csv input))

まあこんな感じです。分かる人にはまあなんとなくわかると思います。
結果:

 (("Year" "Make" "Model" "Length") ("1997" "Ford" "Model-350" "234") ("2000" "Mercury" "Model 800" "238"))

とまあパースできてますね。

次に簡易でxmlをパースしてみます。

(use '[clarsec])
(use '[clarsec.monad])


(def input
     "    <library>
              <book>
                  <title>Joy of Clojure</title>
                  <author>Fogus</author>
              </book>
              <book>
                  <title>Structured and Interpretation of Computer Programs</title>
                  <author>MIT</author>
              </book>
          </library>")

(defn arrows [p] (between (symb "<") (symb ">") p))
(def open-tag (arrows identifier))
(defn close-tag [expect-name] (arrows (symb (str "/" expect-name))))

(defn element [p]
  (let-bind [tag-name open-tag
             contents p
             (close-tag tag-name)]
            {(keyword tag-name) contents}))

(def xml
     (let [list$ #(flatten (list %&))]
       (element
        (<|> (<$> #(apply merge-with list$ %) (many1 (lazy xml)))
             (stringify (many (<|> letter space)))))))

(:value (parse$ xml input))

betweenとか便利ですよね。
えーlet-bindとかありますけど。。。モナド周りとかまあなんとなくといった感じです。
要素は辞書形式で表しています。
結果をflattenで綺麗にしています。
xmlの定義で自身を参照していますが、lazyにしておかないとStackが溢れます。
(要はdelayで必要になったときに計算を始めます)

結果はこんな感じ。

{:library {:book ({:author "Fogus", :title "Joy of Clojure"} {:author "MIT", :title "Structured and Interpretation of Computer Programs"})}}

エラーになる場合

;; 数値を許可していないので失敗する
(parse$ xml "<test>ABC123</test>")

結果

{:error-line <test>ABC1, :line-number 1, :type :failed}

とある程度どこで失敗したかわかります。

clarsecあれこれ

元々、clarsecはvimclojureの人が公開していたのですが闇に消えています。

他の開発者の人がそれらをgithubとかにあげていたのですが、エラー処理がなかったりエラー処理を実装しようとしていて放置されていたりと散々な感じでした。
仕方ないのである程度みんなのコードを寄せ集めたりして色々やって少し動くとこまでできました。
リポジトリは以下です。複雑なものはまだ試してないので自己責任で。
いろいろ手直ししてくれる人も募集しています。
https://github.com/mopemope/clarsec2

jinja2風なテンプレートをパースする

こんにちは、高校生です。
Parserはいつも人の使ったり、ragel先輩に吐かせたり楽してたのですがシンプルなものなら書けるかなと思い書いてみました。

(import '(java.util.regex Pattern))
(require '[clojure.string :as string])

(def sp-chars #"([\\\\*+\\[\\](){}\\$.?\\^|])")

(def ^:dynamic *bt* "{%")
(def ^:dynamic *bv* "{{")
(def ^:dynamic *bc* "{#")

(def ^:dynamic *et* "%}")
(def ^:dynamic *ev* "}}")
(def ^:dynamic *ec* "#}")

(defrecord Text [data])
(defrecord Tag [data])
(defrecord Variable [data])
(defrecord Comment [data])

(defn- esc [hin]
  (string/replace in sp-chars "\\\\$1"))

(defn- build-reg [args]
  (string/join "|" (map esc args)))


(defn- create-regex [tags]
  (Pattern/compile (str "^(.*?)(" (build-reg tags) ")") Pattern/DOTALL))

(def bregex (create-regex [*bt* *bv* *bc*]))
(def eregex (create-regex [*et* *ev* *ec*]))
(def reg {true bregex false eregex})

(def content (slurp "/tmp/index.html"))

(defn- get-node [token text]
  (condp = token
    *bt* (Tag. (string/trim text))
    *bv* (Variable. (string/trim text))
    *bc* (Comment. text)
    (Text. text)))

(defn- parse-tmpl [input]
  (loop [input input mode true next-token nil ast []]
    (let [found (re-find (re-matcher (reg mode) input))
          [pair text token] (or found [input input nil])
          ast (if (empty? text) ast (conj ast (get-node next-token text)))]
      ; (println "pair" pair)
      ; (println "text" text )
      ; (println "token" token)
      (if token
         (let [rest (.substring input (.length pair))
               mode (not mode)]
           (recur rest mode token ast))
        ast))))

(parse-tmpl content)

思ったより普通ですね。

テンプレート

<html>
  <body>
    {% foo %}
      {{ variable }}
    {% endfoo %}
    <div>
      {#_
      Comment
      Comment
      #}
    </div>
  </body>
</html>

結果

[#user.Text{:data "\n \n "} #user.Tag{:data "foo"} #user.Text{:data "\n "} #user.Variable{:data "variable"} #user.Text{:data "\n "} #user.Tag{:data "endfoo"} #user.Text{:data "\n

\n "} #user.Comment{:data " \n Comment \n Comment\n "} #user.Text{:data "\n
\n \n\n"}]

とまあシンプルなASTができたっぽい。
Nodeをdefrecordで作ってるので組み立てる際にprotocolを定義してなめればそれなりのテンプレートエンジンもすぐ作れそうですね。