2014/12/08

Riot API: 画像のバッチダウンロード

この記事はRiot APIのAdvent Calendar 2014の8日目の記事です。

前回はプロフィールアイコンのurlをCDNから取得するところまでデモしましたが、最新の画像をバッチで一気にダウンロード、使いやすくなるようにファイルをローカルで保存するのをやってみます。


1)チャンピオンのリストを取得

バージョン取得などは前回とほぼ同じので、説明を飛ばします:

#!/usr/bin/env python

import os
import requests
import shutil

DD_URL = 'http://ddragon.leagueoflegends.com'

res = requests.get(DD_URL + '/realms/na.json')
res.raise_for_status()

version = res.json()['n']['champion']

champion_info_url= DD_URL + '/cdn/' + version + '/data/en_US/champion.json'
res = requests.get(champion_info_url)
res.raise_for_status()

data = res.json()['data']
 

ブラウザでチャンピオンjsonデーターを確認するとわかり安いと思いますが、全チャンピオンの名前や基本パラメーターが入っているhash tableを'data'に取得できました。

2)保存先のディレクトリーを用意する

今後ダウンロードをする画像を保存先ディレクトリがあるかどうか確認して、なければつくりましょう。


output_dir = os.path.dirname(os.path.realpath(__file__)) + '/champion'

try:
    os.stat(output_dir)
except:
    os.mkdir(output_dir)
 

3)キャラクターをループで回して、画像を保存しておく

DataDragonではチャンピオンの名前をファイル名として使われています:
http://ddragon.leagueoflegends.com/cdn/4.20.1/img/champion/Aatrox.png

チャンピオンの名前わかれば取得しやすいでしょうけど、たとえば、Riot APIで最近のゲーム履歴(/game/by-summoner/SUMMONER_ID/recent)取得した場合、利用したチャンピオンのkey(ID)しか返ってこないので、名前だけだとかなり不便ですね。
そのため、画像をダウンロードしたら、ファイル名をチャンピオンの名前じゃなくて、チャンピオンのkeyをつけましょう。


champion_img_base = DD_URL + "/cdn/" + version + "/img/champion/"

for name in data:
    img_url = champion_img_base + name + ".png"
    filename = data[name]['key'] + ".png"

    res = requests.get(img_url, stream=True)
    if res.status_code == 200:
        with open(output_dir + '/' + filename, 'wb') as f:
            res.raw.decode_content = True
            shutil.copyfileobj(res.raw, f)
 

以上!
ダウンロードできたかどうかを確認すると:


ls champion/
1.png 106.png 114.png 122.png 14.png 161.png 21.png 25.png 28.png 34.png 40.png 45.png 55.png 61.png 7.png 79.png 85.png 96.png
10.png 107.png 115.png 126.png 143.png 17.png 22.png 254.png 29.png 35.png 41.png 48.png 56.png 62.png 72.png 8.png 86.png 98.png
101.png 11.png 117.png 127.png 15.png 18.png 222.png 26.png 3.png 36.png 412.png 5.png 57.png 63.png 74.png 80.png 89.png 99.png
102.png 110.png 119.png 13.png 150.png 19.png 23.png 266.png 30.png 37.png 42.png 50.png 58.png 64.png 75.png 81.png 9.png
103.png 111.png 12.png 131.png 154.png 2.png 236.png 267.png 31.png 38.png 429.png 51.png 59.png 67.png 76.png 82.png 90.png
104.png 112.png 120.png 133.png 157.png 20.png 238.png 268.png 32.png 39.png 43.png 53.png 6.png 68.png 77.png 83.png 91.png
105.png 113.png 121.png 134.png 16.png 201.png 24.png 27.png 33.png 4.png 44.png 54.png 60.png 69.png 78.png 84.png 92.png
  

全部揃ってありますね!これで新しいチャンピオンが追加された場合でもスクリプトを動かすだけですぐに画像をダウンロードできそうですね。

2014/12/06

Riot API: プロフィールアイコンの取得

この記事はRiot APIのAdvent Calendarの6日目の投稿です。

Riot APIで取得できるJSONデータをみるとなんかワクワクして、LOLKingなどMobafireみたいなかっこいいサイト作りたくなりますよね?
でももちろん、ウェブサイトを作った場合に、最新のアイコンなどの画像も必要になりますので、今回はPythonをつかって自分のプロフィールアイコンのurlを取得するところまでデモしたいと思います。

Riot Gamesのアセット・リポジトリはData Dragonというサービスに通じてアクセスできます。
Riot APIと別のチームが管理しているらしいので、アップデート直後にData Dragonへの反映が遅れたり、 データーが一致しない可能性もありますが、それでも最新の画像の取得には一番便利なツールになります。

ではプロフィールアイコンの取得をやりましょう

1) バージョン取得

画像データーはバッチでアップロードされていて、一番最初にダウンロードしたい画像のバージョンを取得する必要があります。 自分のアカウントはNorth Americaサーバーにありますので、naのjsonフィードを使います。

# datadragon.py
import requests    # pip install requests

DD_URL = 'http://ddragon.leagueoflegends.com'

version_path = '/realms/na.json'
res = requests.get(DD_URL + version_path)
res.raise_for_status()      # catch non 2xx status

print(res.text)
  

プリントの結果はこんな感じです:


{"n":{"item":"4.20.1","rune":"4.17.1","mastery":"4.17.1","summoner":"4.20.1","champion":"4.20.1",            "profileicon":"4.20.1","language":"4.20.1"},"v":"4.20.1","l":"en_US","cdn":"http:\/\/ddragon.leagueoflegends.  com\/cdn","dd":"4.17.1","lg":"0.152.55","css":"0.152.55","profileiconmax":28,"store":null}
  

今回はプロフィールアイコンのバージョンが取得したいと思いますので、こういうふうにprofileiconのバージョン番号だけ取得しましょう:


version = res.json()['n']['profileicon']
  

2) 自分のプロフィールアイコンIDを取得

次に自分のプロフィールアイコンIDを取得します。今回はデータードラゴンじゃなくて、通常のRiotAPIを使いましょう。


API_URL = 'https://na.api.pvp.net/api/lol/na'
summoner_by_name_path = "/summoner/by-name/laouji";
profile_url = API_URL + "/v1.4" + summoner_by_name_path + "?api_key=" + API_KEY

res = requests.get(profile_url)
res.raise_for_status()
 

res.textの中身を確認すると:


"laouji":{"id":46048341,"name":"laouji","profileIconId":607,"summonerLevel":23,"revisionDate":1411808085000}}
 

3)画像のURLを組み合わせる

APIコールの結果に必要なprofileIconIdありましたので、画像のurlを組み合わせるのが簡単です:


def icon_url ( version, icon_id ):
    url = DD_URL + '/cdn/' + version + '/img/profileicon/' + str(icon_id) + '.png'
    return url

icon_id = res.json()['laouji']['profileIconId']
icon_url = icon_url(version, icon_id)
 

自分の場合はこれでした:

2014/08/24

OpenRestyとLapisでSupervisorctlを実行できるシンプルなウェブアプリ作成

周りのマークアップエンジニアがgit使いこなしていて、平気でブランチを切り替えたりして開発サーバー上で作業して貰っています。ただし、ブランチ切り変えになると、たまにアプリリスタートも必要で、マークアップじゃ一人で作業したいブランチを開発サーバーに反映できない場合があります。

Plack::Loader::Shotgunを使ってアプリさえ実行すれば、こういった問題をよりやすく解決できる気もしますが、一応、Nginx OpenRestyを使ってみたかったので、試しにウェブインタフェースを使ってプロセスをリスタートできるアプリを作っちゃいました。

*OpenRestyについてはOpenRestyの公式ページを参考にしてください

**フレームワークはLapisというOpenResty上で動くLuaのウェブフレームワークです。おしゃんてぃでおすすめです。

最近process管理のため主にSupervisordを使っています。周りの人がそれをrootを使って実行しガチなんですが、やっぱり開発環境といってもウェブのユーザがrootのプロセスが実行できたら怖いですし、nginx自体も普段nobodyユーザによって実行されるので、まずSupervisordをnobodyユーザとして実行しました。 そこでnobodyがアクセスできるディレクトリを作って、以下のlapis app.luaを作成しました:
--app.lua
local lapis = require("lapis")
local app_helpers = require("lapis.application")
local validate = require("lapis.validate")
local cjson = require("cjson")

local capture_errors = app_helpers.capture_errors

local app = lapis.Application()
app:enable("etlua")
app.layout = false

validate.validate_functions.alphanumeric = function(input)
     return string.match(input, "^[%w_%-]+$"), "must be alphanumeric"
end

-- たたいたコマンドのSTDOUTパージング
function app:parse_status(line)
    local parts = {}
    for word in line:gmatch("%S+") do table.insert(parts, word) end

    local status = {
        ["name"] = parts[1],
        ["status"] = parts[2],
    }
    if status["status"] == 'RUNNING' then
        status["pid"] = string.gsub(parts[4], ",", "")
        status["uptime"] = parts[#parts]
    elseif status["status"] == 'STOPPED' then
        status["uptime"] = string.format("%s %s %s", parts[3], parts[4], parts[5])
    end

    return status
end

-- トップページにstatusの結果をテーブルで表示したいので、結果をselfにいれるとテンプレートで使えるようになる
app:get("/", function(self)
    local handle = io.popen("/usr/bin/supervisorctl status" .. " 2>&1")

    self.supervisor_status = {}
    for line in handle:lines() do
        table.insert(
            self.supervisor_status,
            self.app:parse_status(line)
        )
    end

    handle:close()

    return { render = 'index' }
end)

-- 最低限のvalidationとリスタートをかける処理
app:post("/:app_name/restart/", capture_errors(function(self)
    validate.assert_valid(self.params, {
        { "app_name", exists = true, alphanumeric = true }
    })

    local app_name = self.params.app_name
    local handle = io.popen("/usr/bin/supervisorctl restart " .. app_name .. " 2>&1")

    self.message = {}
    for line in handle:lines() do
        table.insert(self.message, line)
    end

    return cjson.encode(self.message)
end))

return app
.

出来上がったものはこんな感じ:

2014/07/27

Amazon SNSを使ってSESメールのホワイトリスト管理

ご無沙汰です。

メールマガジンって色々大変ですよね?私がその大変さを実感したのが最近ばかりのことですが、今日はAmazonのSimple Notification Serviceを使ってホワイトリストの管理を簡単にできる方法を紹介させてもらいたいと思います。

SESを使ってメールを送信した時に、ユーザのメールアドレスが存在しなかったため送信完了できなかった場合(Bounce)や、ユーザが「迷惑メール」のボタンを押した(Complaint)場合、Amazonからエラーの詳細が書いてあるメールを送ってもらうのがデフォルトの設定かと思いますが、それ以外にもSNSの通信を送ってもらうこともできます。

そしてSNSにはHTTPSのインタフェースもあるので、簡単なAPIを立ち上げて、BounceとComplaintを自動的にブラックリスト化をすることが意外と簡単です。

SNS Topicの作成の仕方や送信先(endpoint)の認証の仕方が丁寧にドキュメントに書いてありますが、BounceとComplaintの場合どんなメッセージが書いてくるかというと、こんな感じです:

POST / HTTP/1.1
x-amz-sns-message-type: Notification
x-amz-sns-message-id: 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324
x-amz-sns-topic-arn: arn:aws:sns:us-east-1:123456789012:MyTopic
x-amz-sns-subscription-arn: arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96
Content-Length: 773
Content-Type: text/plain; charset=UTF-8
Host: example.com
Connection: Keep-Alive
User-Agent: Amazon Simple Notification Service Agent

{
  "Type" : "Notification",
  "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
  "TopicArn" : "arn:aws:sns:us-east-1:123456789012:MyTopic",
  "Message" : "{\\"notificationType\\":\\"Bounce\\",\\"bounce\\":{\\"bounceSubType\\":\\"General\\",\\"bounceType\\":\\"Permanent\\",\\"reportingMTA\\":\\"dsn; a8-41.smtp-out.amazonses.com\\",   \\"bouncedRecipients\\":[{\\"status\\":\\"5.1.1\\",\\"action\\":\\"failed\\",\\"diagnosticCode\\":\\"smtp; 554 5.1.1 <recipient@example.com>: Recipient address rejected: User unknown\\",      \\"emailAddress\\":\\"recipient@example.com\\"}],\\"timestamp\\":\\"2014-07-27T09:39:22.070Z\\",\\"feedbackId\\":\\"00000147773054e9-96ae8a22-c833-4967-874c-d56accc9fd2d-000000\\"},\\"mail\\":{\\"timestamp\\":\\"2014-07-27T09:39:18.000Z\\",\\"source\\":\\"noreply@example.come\\",\\"messageId\\":\\"0000014777304606-b5a16bd1-5a20-40ef-993b-0395f14de101-000000\\",\\"destination\\":         [\\"recipient@example.com\\"]}}",
  "Timestamp" : "2012-05-02T00:54:06.655Z",
  "SignatureVersion" : "1",
  "Signature" : "EXAMPLEw6JRNwm1LFQL4ICB0bnXrdB8ClRMTQFGBqwLpGbM78tJ4etTwC5zU7O3tS6tGpey3ejedNdOJ+1fkIp9F2/LmNVKb5aFlYq+9rk9ZiPph5YlLmWsDcyC5T+Sy9/umic5S0UQc2PEtgdpVBahwNOdMW4JPwk0kAJJztnc=",
  "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
  "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96"
  }

リクエストbodyのJSONなのですが、JSONなのにContent-Typeが「text/plain」のでご注意ください。そして、今回の話題のブラックリストに入れたいメールアドレスがMessageの項目に入っています。エスケープされているJSONなので、これもデコード必要ですね。

私はPlack::Requestからrawのリクエストbodyを取得して、デコードしてみました。


if ($request->headers->header('x-amz-sns-message-type') eq "Notification") {
        my $raw_body = $request->raw_body;
        $raw_body =~ s/\\\\/\\/g;

        my $decoder = JSON::XS->new->utf8;
        my $json = $decoder->decode($raw_body) or die "could not decode json";
        my $message = $decoder->decode($json->{Message}) or die "could not decode json";

        my @addresses;
        if ($message->{notificationType} eq "Bounce") {
            my $bounced_recipients = $message->{bounce}->{bouncedRecipients};
            @addresses = map { $_->{emailAddress} } @$bounced_recipients;

        } elsif ($message->{notificationType} eq "Complaint") {
            my $complaining_recipients = $message->{complaint}->{complainedRecipients};
            @addresses = map { $_->{emailAddress} } @$complaining_recipients;

        }

        for my $address (@addresses) {
            #対象メールアドレスをブラックリストに追加
        }
}


意外と簡単でした!これでばっちり!

2013/07/11

VimからCorona SDKのシミュレーターを起動する方法

最近アプリ作りたくて、フレームワーク色々いじってきたところです。JavaやC++とかを使うとちゃんとしたIDEがないと困るかと思いますが、Eclipseって使うとなんかだるいですよね。。。 まぁ、とにかく気軽にスクリプト言語でかけるフレームワークを探しているところCorona SDKを見つけました。Lua全くわかんないというのがありましたが、どっちかというとC++を習うほど大変じゃないですし、なんとvimでの開発環境を構築するのが簡単すぎて、ほぼ一目惚れでした。

SDKをインストールができたら、以下をvimrcに張っておくぐらいで十分:


map  :!/Applications/CoronaSDK/Corona\ Terminal -project %:p -skin iPhone<CR>


*Mac上のCorona Terminalのデファルトパスの場合です

main.luaがvimで開いた時、を押すことによってコロナのiPhoneシミュレーターを起動できるので、とても便利です。

2013/05/29

[技術メモ] CentOS 5.8でOpenSSLを更新

先月、hachioji.pmでinstagramのAPIをたたいて画像を表示する機能を作ったのですが、ローカルで問題なく動かすことができたのに、レンタルサーバーにアップしたら、IO::Socket::SSLで

Client side SNI not supported for this openssl

ってエラで動かなくて、びっくりしました。(汗)
具体的になにをインストールしたらいいかわからなくて、そこでちょっと挫折して、直すのに大分さぼっちゃいましたが、次回のため、一応どう対応したかをメモっときましょう。

僕が使ってるCentOS 5.8にデファルトで入ってるOpenSSLのバージョンが0.9.8ですが、IO::Socket::SSLのドキュメントによるとOpenSSL 1.0以上が必要そうです:

Support for SNI on the client side was added somewhere in the OpenSSL 0.9.8 series, but only with 1.0 a bug was fixed when the server could not decide about its hostname. Therefore client side SNI is only supported with OpenSSL 1.0 or higher in IO::Socket::SSL.

OpenSSLを更新するにはAxivoのRepository Packageをいれるとyum installでできます。

rpm -ivh --nosignature http://rpm.axivo.com/redhat/axivo-release-5-1.noarch.rpm


それができたら、インストールするのにopenssl.x86_64が必要なので、それが入ってること確認できたら、以下のコマンドで更新:

yum remove openssl.i686
yum --enablerepo=axivo update openssl


詳しくはこちら

2013/04/21

英語圏の知らない英語、第1回:justの使い方

このブログなんですが、日本語で書き続けるべきか、母語の英語で書けばいいのか、色々検討していたのですが、やっぱり日本語で書きたいと決めました。確かに英語で書けば自分の言いたいことをもっとはっきり言えるはずですが、日本語で書くと「わかりづらい」とか「面白くない」とかを理由にして英語に切り替えれば、諦めたということになると思ったからです。
最近、Google I/Oのカンファレンスのビデオを見てて「The Myth of the Genius Programmer」というtalkを見ました。その中で「人は周りに評価されることで上手になるから、どんなに下手でも早めに自分のコードをgithubなど人が見えるところに投稿するといい。」というメッセージが載っています。
このブログってまさにそのためのもので、初心者の自分のコードを人に見せることで恥ずかしくても、とにかくプライドを捨てて投稿しています。
そして人間の言語も似てるものです。話せば話す程上手くなるので、間違いを気にせず、だんだんコミュニケーションをとればいいですよね。
でもその中で、もう一つの問題が出てしまいます。誰もこのブログ読んでませんw そのためにもうちょっとブログの範囲を広げられればいいと思って、英語や言語取得などについても少し書いてみようと思います。東京に住んでいる外国人として、和製英語で誤解が発生しているのを何回も経験したことあるので、和製英語と英語圏の使ってる英語の使い分けについてちょっと説明させていただきたいと思います。

さて、今日のword of the dayとして「just」を話題にしたいと思います。
昨日街歩いていたら、床屋の看板に「just cut 10 min」を見かけました。この文章は日本人にどうとられるか確実にわかりませんが、日本語ではジャストっていうは「丁度いい」という意味で使われているので、これはおそらく「10分でお似合いの髪型にしてあげます」的なことを伝えたかったでしょうと思います。ただ、英語の上記の文章ですと英語圏の人なら「カットのみ 10分」に取られます。なぜならば、英語では「just」というのは「ただの〜・〜だけ・〜に過ぎない」という意味になります。一般的にネガティブな意味になっているので、そんな看板をみて「その床屋さんでシャンプーはしてくれないのかな」とか思う人も出てしまうでしょう。
英語で上記の文章を書き直せば、「the perfect cut in under ten minutes」が正解ですかね。
確かに英語で「just right」「just perfect」という言い方もあります。こちらはネガティブではなくて、日本語のジャストに近い意味を持っています。おそらく日本語のジャストの由来がこのjust right(丁度いい)ですが、「いいことだらけで悪いことがまざっていない」という意味でjust(だけ)が使えるわけだと思います。
おまけによく見られる和製英語を英語圏に通じそうな英語に翻訳してみました:

和製英語 英語
ジャストカット perfect haircut
ジャストフィット close fitting / snug
ジャストタイミング perfect timing
9時ジャスト exactly 9 o'clock

※実は形容動詞のjustが「だけ」意外の意味がもう一つあります。それは「ぎりぎり」って意味です。例えば、「just in time」(ぎりぎり間に合った)、「just married」(結婚したばかり)とか様々な表現に出たりします。