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はこの手のツール作るの簡単で( ・∀・)イイ!!ですねというお話でした。