2013-12-08

これ、どこから来てんのよ?

また1年更新してないとか


アドベントカレンダーにならないと、ここを更新しないというのが定着しちゃってますが、今年も無事(?)更新です。

ご存知の方は多いのですが、ワシは X-Listing という会社にいて、この会社は広告配信とかやっています。ネット広告関連の技術には、いろいろと面白いネタもあるんですが、あまり詳しく書いちゃうと、ボスからお暇を頂戴するハメになるので、今回は配信システムそのものではなくて、その周辺にあるちょっとした小話です。

奴らはどこから来るのか


ネット広告の敵。それはスパマーです。spamといえば、メールに関するものが代表的なので、よく知られているんですけど、実は「検索スパム」というのもあるんですね。

検索スパムっていうのは、まぁ、アレです。ようするに「汚いSEO」の一環で行われる行為で、特定の検索サイトとかで、同系統のキーワードをbotを使って検索しまくるということです。これをすることで、
  • 狙ったワードが検索ワードの入力中のサジェストに出やすくなる
  • 検索急上昇ワード欄に表示される
  •  トピックワードとして検索欄の近くにリンク付きでワードが表示される 
なんて効果を狙ってるわけです。この辺の処理は自動化してるサイトが多くて、なんか怪しい結果になっているんじゃないかを調べるチェックは定期的に入るんですが、週末はこのチェックがおろそかになったりします。その隙を突いてくるとか、マジで止めてほしいんですが、お手軽で効果がある根強い手法でもあるわけです。

狙ったワードをなるべく人目に触れさせるという、上のような検索スパムならまだ可愛いもんなんですが、最悪なのは、リスティング広告スパムというやつで、ライバルの広告主を蹴落とすという、もう、最低な奴らがいるのです。
  1. リスティング広告は入札制なので、高いクリック単価を設定している広告主が優先して上位表示されるようになっている
  2.  同じ検索ワードでバトルしていると、だんだん入札価格が釣り上がってくる
  3. 体力が無い広告主は競争から脱落する。
  4. 「検索結果1ページ目に表示されないなら、それはネット上に存在しないに等しい」という怪しげな格言の存在
  5. なんとかしてライバルを蹴落とせないか?
  6.  普通の広告主は、1日上限予算というものを設定している。つまりある程度のクリックがあると自動的に出稿が止まる
  7. うまく偽装して、botにクリックを発生させ、予算上限到達させる。広告システムはこういう怪しげなクリックを発見するための能力を持っているのだけれども、イタチごっこなわけです
  8. 広告主か、配信システムを提供している会社の中の人が、こういうスパムクリックを発見すると、無効扱いにして、予算消化もなかったことになるので、配信は再開される。
  9. が、配信停止から再開までにはタイムラグがある。この間はライバル不在
  10. (゚д゚)ウマー
もうね。マジで止めてほしいです。こういうの。で、スパムクリックの検出方法とか、細かい話は企業秘密なので、ちょっと書けないんですけど、どっからspam来てるのよというのには、ちょっと興味ありませんか?ありますよね。

IPアドレスから面白そうなデータを作れないかとやってみる


UserAgentなんて偽装されまくっているので、ほとんど当てにならないんですが、IPアドレスはバッチリ収集できます。なにせウチのサーバと直接通信してますしね。IPアドレスから素敵情報を引っ張り出せると言えば、whois とか traceroute 結果とか色々りますが、なにをおいても GeoIP でしょう。今回は、GeoIP City Lite という、ありがたくも無料で提供されている、IPアドレスと地理情報のマップデータを使います。CSVのZIPファイルをDLします。

ZIPファイルを展開すると GeoLiteCity-Blocks.csv と、 GeoLiteCity-Location.csv というファイルが出てきます。BlocksはIPアドレスを地域番号のマップで、Locationの方が地域番号と緯度経度などの地理情報のマップです。

これを読み込んで、ちゃっちゃと処理するプログラムをでっち上げます。

# -*- coding: utf-8 -*-
import sys
import os
import os.path
import bisect
import collections
import csv
import traceback
import pprint


class LocationData(object):
    def lookup_by_ip_address(self, ip_address):
        raise NotImplementedError()


class GeoIPLocationData(LocationData):
    def __init__(self, block_data, location_data):
        self.locations = dict()
        self.blocks = list()

        for row in csv.reader(open(location_data, "r"), dialect="excel"):
            entry = dict()

            try:
                for (idx, (name, data_type)) in enumerate((
                    ("country", str),
                    ("region", str),
                    ("city", str),
                    ("postal_code",  str),
                    ("latitude", float),
                    ("longitude", float),
                    ("metro_code", str),
                    ("area_code", str)), 1):
                    
                    entry[name] = data_type(row[idx])

                self.locations[int(row[0])] = entry

            except Exception, exc:
                continue

        for row in csv.reader(open(block_data, "r"), dialect="excel"):
            try:
                self.blocks.append(tuple([int(x) for x in row]))
            except ValueError, exc:
                pass



    def lookup_with_ip_address(self, ip_address):
        return self.locations[
            self.blocks[
                bisect.bisect_right(
                    self.blocks, (
                        reduce(lambda x, y: int(x) * 256 + int(y), ip_address.split('.')),
                        4294967295, 0)) - 1][2]]



def main():
    location = GeoIPLocationData(
        "./geoip_data/GeoLiteCity-Blocks.csv",
        "./geoip_data/GeoLiteCity-Location.csv")

    counter = collections.Counter()

    for line in sys.stdin:
        (ip_address, count) = line.strip().split("\t")
        result = location.lookup_with_ip_address(ip_address.split("/")[0])
        counter[(result["country"], result["city"], result["latitude"], result["longitude"])] += 1

    for ((country, city, latitude, longitude), count) in counter.iteritems():
        print("\t".join(map(lambda x: str(x), (country, city, latitude, longitude, count))))
        

if __name__ == "__main__":
    main()

なんか、微妙に無駄なことやっている部分もあるけれど、使っているツールからの抜粋なので、気にしないでくださいね。プログラムは IPアドレスと、出現回数の TSV ファイルを標準入力から突っ込むと、標準出力にロケージョン情報を吐き出すという感じです。

これだけだと、実際にどのへんからのアクセスが多いのかわからんので、Google Earthとかで読めるKMLに変換してみます。

# -*- coding: utf-8 -*-
import sys

print('''

''')

for line in sys.stdin:
    line = unicode(line, "utf8", "ignore").encode("utf8")
    (country, city, latitude, longitude, count) = line.strip().split("\t")
    
    if city == "":
        city = "Unknown"

    if int(count) >= 10:
        print(
            '''%s-%s-%s%s,%s''' % (country, city, count, longitude, latitude))

print('''

''')

まぁ見ればわかるかと。こうして作ったKMLをGoogle Earthに読み込ませると………


あら素敵。やっぱりPythonはこの手のツール作るの簡単で( ・∀・)イイ!!ですねというお話でした。

2012-12-30

たまにはベンチとかして遊んでみる

年末でおうちにいるので、ちょっと遊んでみることにしましょう。
satosystemsさんによる、こんなブログエントリ「フィボナッチで各種言語をベンチマーク」をみつけました。ほんとに沢山の言語で同じアルゴリズムのプログラムを記述して実行に必要な時間を計測してます。で、結果は記事を見ていただくとして、

Python(CPython) 53.651
Lisp (GNU CLisp) 211.802

とか書いてあるわけです。んー、よりによってCLispですか。この処理系はちょっと遅いしなぁ。他の言語はNative Code Compilerとか使ってるし、ワシが日頃使っているCommon Lisp処理系であるところのSBCLで計測してみました。コードは全く同じで以下の通り。
(defun fib (n)
  (if (< n 2) n
      (+ (fib (- n 2)) (fib (- n 1)))))
SBCL 1.0.55のコンパイラによるNative Codeの出力は以下のとおり。
; disassembly for FIB
; 0784808D:       488B55F0         MOV RDX, [RBP-16]
;      091:       BF04000000       MOV EDI, 4
;      096:       488D0C2530040020 LEA RCX, [#x20000430]      ; GENERIC-<
;      09E:       FFD1             CALL RCX
;      0A0:       0F8C94000000     JL L1
;      0A6:       488B55F0         MOV RDX, [RBP-16]
;      0AA:       BF04000000       MOV EDI, 4
;      0AF:       4C8D1C255B020020 LEA R11, [#x2000025B]      ; GENERIC--
;      0B7:       41FFD3           CALL R11
;      0BA:       480F42E3         CMOVB RSP, RBX
;      0BE:       488D5C24F0       LEA RBX, [RSP-16]
;      0C3:       4883EC18         SUB RSP, 24
;      0C7:       488B0562FFFFFF   MOV RAX, [RIP-158]
;      0CE:       B902000000       MOV ECX, 2
;      0D3:       48892B           MOV [RBX], RBP
;      0D6:       488BEB           MOV RBP, RBX
;      0D9:       FF5009           CALL QWORD PTR [RAX+9]
;      0DC:       480F42E3         CMOVB RSP, RBX
;      0E0:       488955F8         MOV [RBP-8], RDX
;      0E4:       488B55F0         MOV RDX, [RBP-16]
;      0E8:       BF02000000       MOV EDI, 2
;      0ED:       4C8D1C255B020020 LEA R11, [#x2000025B]      ; GENERIC--
;      0F5:       41FFD3           CALL R11
;      0F8:       480F42E3         CMOVB RSP, RBX
;      0FC:       488D5C24F0       LEA RBX, [RSP-16]
;      101:       4883EC18         SUB RSP, 24
;      105:       488B0524FFFFFF   MOV RAX, [RIP-220]
;      10C:       B902000000       MOV ECX, 2
;      111:       48892B           MOV [RBX], RBP
;      114:       488BEB           MOV RBP, RBX
;      117:       FF5009           CALL QWORD PTR [RAX+9]
;      11A:       480F42E3         CMOVB RSP, RBX
;      11E:       488BFA           MOV RDI, RDX
;      121:       488B55F8         MOV RDX, [RBP-8]
;      125:       4C8D1C25E0010020 LEA R11, [#x200001E0]      ; GENERIC-+
;      12D:       41FFD3           CALL R11
;      130:       480F42E3         CMOVB RSP, RBX
;      134: L0:   488BE5           MOV RSP, RBP
;      137:       F8               CLC
;      138:       5D               POP RBP
;      139:       C3               RET
;      13A: L1:   488B55F0         MOV RDX, [RBP-16]
;      13E:       EBF4             JMP L0
;      140:       CC0A             BREAK 10                   ; error trap
;      142:       02               BYTE #X02
;      143:       18               BYTE #X18
;      144:       54               BYTE #X54                  ; RCX
型宣言とかしてないので、GENERIC-+とかCallしていてイマイチな感じだけれども、実行してみます。マシンはCore2Quad Q9550 2.8GHz, Memory 4GiB、Gentoo GNU/Linux Kernel 3.6.6です。
CL-USER> (time (print (fib 38)))

39088169 
Evaluation took:
  1.630 seconds of real time
  1.704106 seconds of total run time (1.704106 user, 0.000000 system)
  104.54% CPU
  4,617,572,036 processor cycles
  0 bytes consed
  
39088169
元記事のsatosystemsさんの実行環境は
すべての言語と処理系の実行環境は Ubuntu 10.04 が搭載された Let's Note Y4(Pentium M 778MHz、メモリ 1GB)です。
とのことなので、直接値の比較はできません。自分のマシン上でのCPythonの結果は22.84秒でしたので、satosystemsさんのマシンより (53.65 / 22.84 =) 2.35倍速いと仮定すると、SBCLでの推定実行時間は (1.63 * 2.35 =) 3.83秒ということになります。
これがどのへんの位置になるかというと、

Scala1.90
Fortran (gfortran)1.92
Go (run)1.96
Haskell (ghci)1.99
Scheme (ikarus)2.07
JavaScript (node.js)2.62
Boo (compile)2.67
Java (gcj)2.88
Boo (interpret)3.56
Common Lisp (SBCL)3.83
Scheme (Gambit: gcs -O2)4.52
Scheme (Gambit: gcs)7.13
Forth (gforth-fast)7.39
OCaml (ocaml)7.53
OCaml (ocamlc)7.53
Forth (gforth)8.39
Python (CPython)58.65
Lisp (GNU CLisp)211.80
Ruby (CRuby)213.18

と、まぁ、妥当な線に落ち着いたかと。ちなみに、目一杯(declare (optimize (speed 3) (safety 0)) (type fixnum n)) とかしてみたり、色々と弄り回しても、生成コードも実行時間もほとんど変化しませんでした。元記事の結果を見れば分かるのですが、同じ言語でも処理系によって全然結果が違います。つまりは、「言語というより処理系のベンチマーク」ってことなわけですね。

注:コンパイル時に評価しちゃうとか色々と反則技はありますが、こういうのは普通にやってこそ意味があるっぽいので、モヒカンな方々は野暮なツッコミしないでね!

2012-12-11

PySpaとPyFesに参加するワケ

はい。こんにちは。「ワシがいる会社はCommon Lispな会社じゃねーよ。Primary LanguageはPythonだよ!」と誤解を払拭して回る最近ですが、皆様いかがお過ごしでしょうか。さて、PySpaアドベントカレンダーになんか書こうと思って、このブログページの編集を開始してみたら、前回投稿が2年前というアレゲな状態で、もう、なんというか。

で、PySpaって、参加経験がある人にとっては「ああ、楽しいよね。アレ」という存在であり、また、参加したことの無い人にとっては「アレって、いったい何やってるんだ?」という、ものすごく「よくわからない」イベントなわけです。

当時はPython hack-a-thon、今はPython Developers Festaと呼ばれるPythonの名を冠している割には全然Pythonじゃないイベントに顔を出し始めて、そこでPySpaというなにやら怪しげ/楽しそうなイベントがあるという情報をキャッチしたのが3年くらい前。そして過去3回ほどPySpaに参加してるわけですが、妻子持ちのイイ年ぶっこいたオッサンが、空気も読まずに連続参加するには、それなりの理由があるわけです。



ワシがいま居る会社は、ネット広告の配信をやっていて、エンジニアは広告配信とその関連システムをひたすら作るのがミッションなわけです。自社サービスのためのシステム開発なので、リリースはあっても納品とかありません。基本的にエンドレスでひたすらマイルストーンを目指して、それを通過していくわけです。広告技術の進歩は早くて、それをキャッチアップすべく、どんどん前に進んでいかざるを得ないので、ネット広告というドメインでの技術力はどんどんUPしていきます。とにかく目の前の課題に集中しなくちゃやっていけないので、それ以外の部分がどうにも疎かになりがちです。ようするに「今、コレがアツい!」の波に乗り遅れるどころか、気づかずにスルーとか、平気で起こっちゃう。

この文章を読んでいる多くの方が、まだお若いのだと思うのですが、アラフォーともなってくると、それなりの功夫(kung-fu)も積んできていて、よくあるパターンな部分は、その経験で処理できちゃうという場面が多くなってきます。つまり新しい事を積極的に学ばなくても、日常には不便しなくなってくる。さらに20代の頃のあの2徹、3徹をものともしない体力、新しい概念とかを苦もなく吸収できていた脳力もだんだんと………。夢も希望も無い話なんですが、自分が実際にその年に到達してみる
とよく分かるものです。

で、こういう状況を打破する、最も簡単かつ、効果的な方法。「外に出る」です。社内で蛸壺に篭っていても、行き着く先はともすれば尸解仙。とにかく雑多な話題が多く飛び出す場に自分を置くことが大切になってきます。そして面白そうなトピックを集めて、実際に自分であれこれ弄り回すこと。

結婚して、子供が生まれて、社内でもそれなりのポジションになっていってと、まぁ昭和的なストーリーを地で行っちゃうと、エンジニアにとって最も重要な「自分のための時間投資」がどんどんできなくなってきます。休日に技術書を読んで写経してみる。こんな当たり前にできていたことが、子供の「お父さん遊んで〜」に阻まれるのです。まぁピカピカ&カチンコチン泥団子を作ったりして一緒に楽しく遊んじゃうわけですが。ならば、その少ない自分のために使える時間の密度を上げる必要があります。

世の中には多くの「勉強会」が開催されています。専門的なものや、明日の業務に即使える的なものもあるでしょう。そのような中、できる限り参加しようと決めているのは、Python Developers Festa と PySpaという2つです。この選択には、これらのイベントの「雑食性」という大きな理由があります。

Python Developers Festaに参加したことがある方なら、分かると思うのですが、このイベントには普通の「勉強会」にありがちな「今回はみんなでこれをやりましょう」的な縛りがものすごく希薄で、それはもう、参加者それぞれが思い思いに過ごす場として機能しています。ルネッサンスから近代にかけての宮廷のサロン状態とでもいいましょうか。とにかくカバーできる話題の広さといったら、素晴らしいの一言。

このネットの時代に、せっかく沢山の人が集まるメリットは何かということを考える時、この雑多感が非常に重要なものに思えるのです。1つ2つの小さなテーマにまとまるのなら、オンラインの方がよっぽど効率がいいでしょう。そしてPySpaはPyFesの濃縮度を更にUPした環境といえば、間違いは無いはずです。

実はPySpa参加者は妻帯者が多いのです。「熱海」の「温泉旅館」で「3日間」という、これはもう「テッキーな野郎共とモヒモヒしに行くだけだよ〜」という言葉が完全に意味をなさず「ぜってー浮気だろ」なシチュエーションにも関わらず、困難を極めるであろうYAuthを取得して皆が芳泉閣にやってくるのです。これだけで、このPySpaというイベントの持つ引力がどれほどのものかを十分にあらわしているはずです。

そうそう。PySpaとかPyFesとか、なんか凄そうな人たちばっかりで、(俺|私)ごときが参加しても大丈夫なのだろうかと、尻込みしているそこの貴方へ。

自分が大したことでは無いと思っていることが、他の人にとっては魔法であることは、よくあることである 
「お客さん」にならずに「参加者」になろう。たまにマサカリが飛んでくることがあるかもしれないけれど、それは貴方の人格をdisっているわけじゃない。色々なバックグラウンドを持つ人と色々な話をしよう。自分の持っているものを吐き出した人の所にこそ、色々なものが集まってきて、それはきっと貴方の大切な財産になるだろうから。

2010-12-19

Webアプリの開発(デバッグ?)中に使えるちょっと便利な道具の話

こんにちわ。Pythonアドベントカレンダー に参加して、 この記事を書くまでblogをやったことのない ransui です。Pythonはお仕事でバリバリ使ってますが、広告配信システムやら、ログ分析やらで、最近のWebフレームワークってのはあまり使ってないんです。というか、未だにZope2とかTurboGears 1.xが現役。

で、当初の目論見としては、Pythonの標準ライブラリだけでTiny Web Frameworkみたいな感じでいこうかな。なんて思ってましたが、思い切り清水川君にやられちゃったので、おじさんがもう10年以上使っている、Webアプリ開発時に使えるちょっとしたツールの話をしようかと思います。

ちょっとしたプログラムを書いて、その挙動がおかしいとか、なんかエラーが出るとか、テストを通過しないというときはデバッグ作業をするわけですが、そのときに「ここぞ」という場所でprint()で変数の内容を見てみるなんてことを良くやります。本格的にデバッガを使ったりするまでもない時にはお手軽に使える手法で「printデバッグ」なんて言う事もあります。というか、みんな良くやるよね?

たいていのWebフレームワークには【デバッグモード】とかがあって、プログラムの実行中になにかエラーとかが起こると、すごく美しいスタックトレース画面がブラウザに表示されたりして、嬉しいんですけど、そのエラーが起こった細かい経緯とか原因まで推測するのはなかなか難しいものです。

でprintデバックしてやろうとか思うのですが、Webフレームワークによってsys.stdoutの行き先がまちまちだったりして、いつでも出来るとはかぎらないし、だからといってログに出してtail -fってのもタイムラグあったり、loggingの設定がやたら複雑だったりするとゲンナリなわけです。

必要は発明の母。ここで一発、超簡単・単純なprintデバッグ用の道具を作ります。まずはソースコードを見てくださいな


# -*- coding: utf-8 -*-
import sys
import socket
import optparse

class UDPMonitor(object):
    def __init__(self, host="localhost", port="8181", buffer_size=4096):
        self.host = host
        self.port = port
        self.buffer_size = buffer_size


    def monitor(self):
        print("This is UDPMonitor, waiting message on %s:%s" % (
            self.host, self.port))
              
        udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp_sock.bind(("", self.port))

        while True:
            (data, (address, port)) = udp_sock.recvfrom(self.buffer_size)
            print(repr(data)[1:-1])


    def send_message(self, msg, target_host=None, target_port=None):
        udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp_socket.bind(("", 0))
        udp_socket.sendto(msg, (
            target_host if target_host else self.target_host,
            target_port if target_port else self.target_port))
        
    
def main():
    parser = optparse.OptionParser()
    parser.add_option("-a", "--address",
                      action="store", dest="address", type="string",
                      default="localhost", help="host name or address")
    parser.add_option("-p", "--port",
                      action="store", dest="port", type="int",
                      default=8181, help="port number")
    parser.add_option("-s", "--server",  dest="server_mode",
                      action="store_true", help="run as monitor")

    (options, args) = parser.parse_args()

    if options.server_mode:
        UDPMonitor(host=options.address, port=options.port).monitor()

    else:
        UDPMonitor().send_message(" ".join(args), options.address, options.port)
        

if __name__ == "__main__":
    main()

はい。大体なにをやっているかはわかると思います。

ようするにprintでstdoutに出力する代わりにネット越しにメッセージ流すということです。このプログラムの味噌はUDPを使っていることで、デバッグするコード側では、いちいちコネクションを張らずにメッセージをお手軽に投げつけることができます。あ、簡単のためにバッファ長は固定にしちゃってますので、投げつけるメッセージのサイズには注意が必要です。

上のプログラムは基本部分だけで、ホントに指定された文字列を投げつけるだけしかできませんが、色々と応用もできます。たとえば

  • 時刻情報を自動的に付加するようにしてみる
  • カレントスレッドの情報を自動的に付加するようにしてみる
  • メッセージ送信部分をデコレータ関数に仕込んで
    • 呼び出されたメソッド名を送信する
    • メソッドが呼び出されたときの引数の内容を送信する
    • メソッドの戻り値の内容を送信する
  • ORM使っているときに、最終的に実行されるSQL文を確認する
  • ある場所から、他の場所までの処理時間をリアルタイムに観測する

などなど。

TCP前提のアプリケーションレベルプロトコル全盛の現在でもUDPにはコネクションレスという強力な武器があり、使い捨て的な道具を作るときには結構役にたつものです。

所詮デバッグ時に一時的に使う道具なので、あまり豪華にする必要もないですし、使い回しとかはあまり考えずに、上のような基本部分を手持ちのカードとしておいて、状況にあわせてちょっと拡張して使うってのが、お手軽で良いのではと思います。

スイスアーミーナイフの性能を余すところ無く使いこなすものスマートでカッコいいですが、そのへんの木片と金属片でささっと必要な機能をもったナイフを作って仕事を片付けるってのもイイとおもうんですが、どうでしょう?

さて、次の @zenich さんが見事にWebフレームワーク話に軌道修正してくれることを期待しつつ、バトンを渡すことにします。