API」タグアーカイブ

3年目プログラマが参考にしたQiita記事まとめ①

特にカテゴリもレベルも偏らずにざっくばらんに、参考になったQiita記事を一言添えて列挙するシリーズ1本目

翻訳: WebAPI 設計のベストプラクティス

WebAPI、というかRestFulAPIに関するベストプラクティスをまとめた記事を日本語訳した記事。

RestFulの基本的な説明から、具体的な設計方法までわかりやすく解説されている。

個人的には「ページング情報はレスポンスヘッダに入れよう」の項が一番参考になり、その後実務でもSPAでのページング処理を実装する際に大いに役立った。

「WebAPI 設計のベストプラクティス」に対する所感

題の通り、一つ目の記事に関する捕捉記事。両記事に目を通すとよく分かるが、WebAPIの設計に絶対の正解は無い。どのような設計が最もRestFul的で、ユーザビリティに富んでいるかは、やはりその時々で違ってくるので、色々なパターンを抑えて最良の選択をできるようにしたい。

うまくメソッド名を付けるための参考情報

基本的にプログラミングは英語で行う必要があるので、英語が苦手な人にとっては特に苦労する命名作業。頻出単語と、その単語を含んだメソッドがどんな動きをするのかのルールを抑えておくだけで、コードを書くときにも読むときにも役立つ。
最近でも、命名に迷った時は軽くこの記事に目を通すようにし、この記事を見ても適切な名前が浮かばない場合はそもそも設計に誤りがあると考えるようにしている。(適切な名前を付けられないロジックは、複数の責務が混在してる可能性が高いため)

ベアプログラミングが無理ならサイレントベアプログラミングを検討しよう

(サイレント)ペアプログラミングではなく、(サイレント)ベアプログラミング。

サイレントベアプログラミングは記事内での造語だが、ベアプログラミングは昔からある手法で、「解らないことをクマのぬいぐるみに相談すると、自然と自己解決できる」というもの。もちろんクマのぬいぐるみが解決してくれるわけではなく、質問するためにまず問題点を整理するプロセスが重要ということ。記事内でのベアプログラミングは、ぬいぐるみ相手に話しかける必要があるので職場などで行うには難易度が少々高い。そのための代替案がベアプログラミングだ。

一通り記事を読んで思ったが、これプログラマなら誰しも多かれ少なかれ脳内ではやっているのでは‥‥?

ターミナル 作業効率化 tips集

ターミナルでキー入力する際のTips集。常日頃からCUI上で開発を行っていると、コマンド入力を頻繁に、それこそ息をするように行う必要があるので、こういった小さなTipsの積み重ねが生産性に大きく影響する。

ちなみに私はMacの標準ターミナルでなく、iTerm2を使っているが、基本的に記事内のTipsの多くはどのツールでも共通だったりするので問題は無い。

新人プログラマに知ってもらいたいメソッドを読みやすく維持するいくつかの原則

この手の記事はだいたいリーダブルコードを読むだけで済む話だが、一冊丸々読まずともこの記事の内容を押さえているだけでかなり変わると思う。

新人プログラマ向けと言ってるが、正直プログラミングを始めたばかりの人にこれを求めるのは少々酷かもしれない。とは言っても早い段階からこういった考えを持たせたほうが、余計なクセが付かずに済むので、遅すぎず早すぎないタイミングで学べると良さそう。

何かのときにすっと出したい、プログラミングに関する法則・原則一覧

DRYとかKISSとか驚き最小の原則とかそういったものの紹介記事。こういうのを頭に入れておいて、かつ言葉もセットで覚えておくと、コードレビューがスマートに行えそう。

個人的にはボーイスカウトの規則が好きで、日々実践をしているのだが、テストが整備されていないコードに対して行うのは勇気がいるので単体テストぐらいは整備されてて欲しい。

あきらめるにはまだ早い!ソースコードの品質向上に効果的なアプローチ

組織内のコード品質を高めるための手段まとめ。あまり即時性のあるものではなく、こういった仕組みを組織内に導入すれば、長い目で見て確実にコード品質が高まり、生産性の向上に繋がるといった感じ。
なかなか簡単に実行できることじゃないものが多いが、できそうなことから少しずつ手を出せるようにしたい。

コードレビューの極意。それは「自分のことは棚に上げる」こと!!

題の通り。これはコードレビューに関わらず、何かに対してダメ出しをする時の原則だと思う。
自分もできてないけど、そうした方が良いと思うから他の人にそれを要求するようにすれば、自然と自分もそれに従って出来る様になるのでは。

データサイエンティストを目指して半年で学んだことまとめ

軽い気持ちで機械学習に手を出してて苦労してるところで非常に参考になった。

プログラミング、フレームワークの力で、機械学習ができるのは事実ですが、作ったモデルや予測結果の説明ができなければ価値がありません。

が特に心に響いた。極端に言えば、機械学習は魔法のようなモノとまで認識していたようだ。

モデルの精度を上げるために、「とりあえず、Deep Learningで!」や「分類問題を解くならSVM!」等、アプローチだけに気をとられていると、すぐに精度があがらなくなります。勿論、納得できる良い精度は出ません。

そして早い段階でこれにぶち当たった。精度が出ない。何故?何も知らないから。

[Ruby] Twitterから画像を自動収集する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
ruby 2.4.1
gem 2.6.11
twitter_oauth 0.4.94

また、記事内で必要となるTwitterAPI用のトークンは既に取得済みであることを前提とする

概要

Twitter上に特定ワードと共に添付された画像ファイルを、任意の枚数自動でダウンロードをするCUIツールをRubyで実装したお話。
画像収集方法として他に以下を検討したが、それぞれ以下の理由で見送った。

  • Bing Image Search API
    — これまで画像収集でお世話になっていたAPIだが、Azureに統合(?)され、実質有料化したので見送り
  • Instagram API
    — イケると思って調べたが、どうやら他のユーザの投稿写真を取得するためには承認を得る必要がある模様
  • Tumblr API
    — Tumblrがよくわからなかったため
  • Google画像検索などのクローリング
    — Pythonでクローリングしようとも検討したが、あまり美しく無いと思ったので見送り

他にもいくつか調査したが、最終的に普段から利用しているTwitterAPIを使うことにした。TwitterのSearchAPIを用いると、ツイートに添付されている画像に関してもまとめて取得できるようになっているので、比較的簡単にできると踏んだため。

ライブラリのインストール

今回はRubyによるCUIツールの形式で実装する。RubyからTwitterAPIを叩くために、普段から利用しているGemライブラリであるtwitter_oauthを今回も使用する。

以下のコマンドを叩くか、あるいは同様のGemfileを作成する。

$ gem install twitter_oauth

TwitterAPI認証

Twitterクラスを実装し、TwitterAPI認証を行うauthメソッドを実装する。API認証をコンストラクタにしなかったのは、インスタンス生成のタイミングで認証が行われるのが気持ち悪いと個人的に感じたため。

authメソッドでは、前項でインストールしたtwitter_oauthモジュールに含まれるClientクラスのインスタンスを生成する。その際にトークンが必要になるので、今回は環境変数から読み込むようにした。

require 'twitter_oauth'
require 'net/http'
require 'uri'
class Twitter
  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end
end

検索ワードに合致する写真一覧を取得

特定ワードでツイートを検索し、その中に画像ファイルが添付されている場合にはURLを配列に追加。画像ファイルの総数が指定した枚数に達するまで、再帰的にツイートの検索を繰り返す。という一連の処理を行うsearch_picturesメソッドを実装する。

また、以下ページを参考に、TwitterAPIのレスポンスから添付画像のURLを取得するextract_pictures_from_tweetsメソッドを実装し、ツイート取得ごとに実行する。
GET search/tweets – ツイートを検索する

@@SLEEP_TIMEはAPI呼び出し毎に挟む待機時間。APIは時間あたりの利用回数が決まっているので、それに引っかからないように調整する。

#
# 検索ワードに合致する写真一覧を取得
#
def search_pictures(word, num = 10, opt = {})
  @twitter or self.auth
  params = {
    lang:        'ja',
    locale:      'ja',
    result_type: 'mixed',
    count:       200,
  }.merge(opt)
  puts "画像検索中(残り#{num}枚)"

  tweets = @twitter.search(word, params)['statuses']
  max_id = tweets[-1]['id']
  pictures = extract_pictures_from_tweets(tweets)

  if num <= pictures.count
    return pictures.take(num)
  else
    sleep @@SLEEP_TIME
    return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
  end
end
#
# TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
#
def extract_pictures_from_tweets(tweets)
  pictures = tweets.map do |t|
    if media = t['entities']['media']
      media.map {|m| m['media_url']}
    else
      []
    end
  end
  pictures.flatten.uniq
end

画像のダウンロード

前項のsearch_picturesメソッドを用いて取得した画像URL一覧を元に、指定したディレクトリにダウンロードするためのdownload_picturesメソッドを以下のように実装する。

こちらは単純に、個々の画像URLに対してNet::HTTPを用いてダウンロードしている。こちらもマナーとして、ダウンロード1件毎に@@SLEEP_TIME秒待機する。


#
# ツイッター上の画像をまとめてダウンロードする
#
def download_pictures(word, download_dir, num = 10)
  pictures = self.search_pictures(word, num)
  pictures.each_with_index do |picture, idx|
    filename = File.basename(picture)
    filepath = "#{download_dir}/#{filename}"
    open(filepath, 'wb') do |file|
      puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
      file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
    end
    sleep @@SLEEP_TIME
  end
end

動作確認

実行方法

irbなんかを起動して、

Twitter.new.download_pictures('#デグー', '/share/images', 20)

と実行することで、”#デグー”が含まれたツイートに含まれている画像20件をダウンロードする。もちろん’#デグー’が含まれたツイートに画像が添付されてない場合もあるので、写真数が20に達するまで再帰的にツイートを検索し続ける。

デモ

ソースコード

今回は諸事情でGithubにリポジトリを上げていないので、以下に全ソースコードを掲載する

require 'twitter_oauth'
require 'net/http'
require 'uri'

class Twitter2

  @@SLEEP_TIME = 1

  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end

  #
  # 検索ワードに合致するツイート一覧を取得
  #
  def search_pictures(word, num = 10, opt = {})
    @twitter or self.auth
    params = {
      lang:        'ja',
      locale:      'ja',
      result_type: 'mixed',
      count:       200,
    }.merge(opt)
    puts "画像検索中(残り#{num}枚)"

    tweets = @twitter.search(word, params)['statuses']
    max_id = tweets[-1]['id']
    pictures = extract_pictures_from_tweets(tweets)

    if num <= pictures.count
      return pictures.take(num)
    else
      sleep @@SLEEP_TIME
      return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
    end
  end

  #
  # ツイッター上の画像をまとめてダウンロードする
  #
  def download_pictures(word, download_dir, num = 10)
    pictures = self.search_pictures(word, num)
    pictures.each_with_index do |picture, idx|
      filename = File.basename(picture)
      filepath = "#{download_dir}/#{filename}"
      open(filepath, 'wb') do |file|
        puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
        file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
      end
      sleep @@SLEEP_TIME
    end
  end

  private

    #
    # TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
    #
    def extract_pictures_from_tweets(tweets)
      pictures = tweets.map do |t|
        if media = t['entities']['media']
          media.map {|m| m['media_url']}
        else
          []
        end
      end
      pictures.flatten.uniq
    end

end

[Python3] GithubAPI/TwitterAPIを用いて、最新のコミットログをツイートする

前提

以下の環境で実装、動作確認

要素 バージョン
debian 8.6
python 3.6.2

概要

GithubAPIを用いて、自身がGithub上のリモートリポジトリに対してpushした内容からコミットログを取得し、それに関する情報をTwitterAPIを用いてツイートしたお話。

本記事ではTwitterAPIを利用するためのコンシューマキー、アクセストークンは取得済みであることを前提としている。

同じことはGithubのWebhookを使ってできるしむしろ自然だが、その場合サーバーを用意する必要があるので、今回はあえてpull型で非効率なやり方を採用

動作イメージ

リモートリポジトリがGithub上にあるリポジトリに対して適当にコミット/プッシュ

$ git commit -m "コミットテスト"
[master e6bb9e4] コミットテスト
 1 file changed, 1 insertion(+)
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.

今回実装したコマンドを実行する

$ python main.py

以下のように、リポジトリ名、コミットメッセージ、diffへのリンクがツイートされる

GithubAPIについて

GithubAPIは、名前の通りGithubの各種読み書きを行うためのAPI。
公開されているデータの取得に関しては認証無しで手軽に利用できる。

例えば、curlコマンドを用いて以下のURLに対してGETリクエストを送信すると

curl -i https://api.github.com/users/sa2knight/events

以下のように、該当ユーザのGithubでのイベントログを取得することができる

[
  {
    "id": "6516862394",
    "type": "PushEvent",
    "actor": {
      "id": 16274215,
      "login": "Sa2Knight",
      "display_login": "Sa2Knight",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Sa2Knight",
      "avatar_url": "https://avatars.githubusercontent.com/u/16274215?"
    },
    "repo": {
      "id": 96615971,
      "name": "Sa2Knight/degulog2",
      "url": "https://api.github.com/repos/Sa2Knight/degulog2"
    },
    "payload": {
      "push_id": 1946817475,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
      "before": "afd8b8647c2b810ceb731a74bad20c7388c19974",
      "commits": [
        {
          "sha": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
          "author": {
            "email": "shingo.sasaki.0529@gmail.com",
            "name": "shingo sasaki"
          },
          "message": "バックアップ追加",
          "distinct": true,
          "url": "https://api.github.com/repos/Sa2Knight/degulog2/commits/4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29"
        }
      ]
    },
    "public": true,
    "created_at": "2017-08-27T15:22:08Z"
  },
  {
    "id": "6516846556",
    "type": "PushEvent",
(以下省略)

ここで取得できるイベントは、Webで言う以下のような、そのユーザのリポジトリに対する操作全般の情報なので、今回はこのAPIを用いることにする

ライブラリの導入

今回はPython3を用いて実装するので、Pythonのパッケージ管理ツールであるpipを用いて、以下の二種類のライブラリを導入する。

  • requests
    — HTTPライブラリ
  • twitter
    — TwitterAPI用

TwitterAPIの利用には認証も絡んでくるので、専用のライブラリを利用する。GithubAPIは特定URLにGETするだけなのでHTTPライブラリで直接実行することに。

$ pip install requests
$ pip install twitter

ソースコード

最新版はこちら

import twitter
import requests
import json
import os

FILE_NAME      = "last_id"
EVENTS_URL     = "https://api.github.com/users/sa2knight/events"
REPOSITORY_URL = "https://api.github.com/repos"

def tweet(text):
  auth = twitter.OAuth(consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
                       consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
                       token=os.environ['TWITTER_ACCESS_TOKEN'],
                       token_secret=os.environ['TWITTER_ACCESS_SECRET'])
  t = twitter.Twitter(auth=auth)
  t.statuses.update(status=text)

def tweet_event(event):
  tweet_text = f"""
  @null
  Githubにコミットをプッシュしました。
  [{event['repository']}]
  「{event['commits'][0]['message']}」
  """.strip()
  if 1 < len(event['commits']):
    tweet_text += f"ほか{len(event['commits']) - 1}件"
  tweet_text += f"\n\n{event['commits'][0]['url']}"
  tweet(tweet_text)

def save_id(id):
  with open(FILE_NAME, 'w') as f:
    f.write(id)

def load_id():
  if os.path.exists(FILE_NAME):
    with open(FILE_NAME, 'r') as f:
      return f.readline()
  else:
    return ''

def is_new_id(id):
  return id != load_id()

def parse_commit_log(repo_name, commit):
  return {
    'url': f"https://github.com/{repo_name}/commit/{commit['sha']}",
    'message': commit['message']
  }

def get_repository_description(repo_name):
  url = f"{REPOSITORY_URL}/{repo_name}"
  response = requests.get(url)
  repository = json.loads(response.text)
  return repository['description']

def get_recent_push_event():
  response = requests.get(EVENTS_URL)
  events   = json.loads(response.text)
  recent_event = list(filter(lambda e: e['type'] == 'PushEvent', events))[0]
  repo_name    = recent_event['repo']['name']
  commits      = list(map(lambda c: parse_commit_log(repo_name, c), recent_event['payload']['commits']))
  return {
    'id':         recent_event['id'],
    'repository': get_repository_description(recent_event['repo']['name']),
    'commits':    commits,
  }

event = get_recent_push_event()
if is_new_id(event['id']):
  tweet_event(event)
  save_id(event['id'])
  print('ツイートを投稿しました')
else:
  print('ツイートは不要です')

スクリプトを実行すると概ね以下の手順でツイートされる
1. GithubAPIを叩いて、自身の最新のコミットを含んだイベントログを取得
2. 直近で取得したイベントIDをファイルに保存しておき、そのIDと比較、一致していた場合変更なしとして終了
3. イベントログを元にツイートするテキストを生成
4. TwitterAPIを用いてツイート

所感

  • 本スクリプトを15分おきぐらいにcronで実行すれば、概ねコミット内容がツイートされる
  • Webhookと違ってpull型になるので、リアルタイムにはできない
  • 単にGithubAPIが使いたくて遊んだだけで実用性はあんまり無い

チャットワークAPIをRubyで利用する

前提

以下の環境で実装及び動作確認を行った

要素 バージョン
debian 8.6
ruby 2.2.2

概要

チャットワークのAPIを使える状態にし、Rubyからチャットワークの各種操作を行う。今回の成果物についてはGithubから閲覧、ダウンロード可能。

チャットワークとは

ビジネス向けのチャットツールで以下の特徴がある

  • 基本無料
  • ルームを個別に作成し、複数人でチャット可能
  • ルームごとにタスクの管理が可能
  • ファイルの共有が可能
  • 音声通話/ビデオ通話も可能

チャットワークAPIとは

チャットワークの基本的な機能をプログラムから利用するためのRestFullなAPI。
現在(2017/05/20)は、プレビュー版のみの限定公開となっており、APIを利用するためには申請する必要がある

チャットワークAPIの利用準備

チャットワークAPIの利用申請

チャットワークにログインしているブラウザで、利用申請ページにアクセスし、APIの利用申請を行う。利用が可能な状態になると、チャットワークで登録しているメールアドレスに、以下のような通知メールが届くのでそれまで待機(1営業日程度?)

いつもお世話になっております。
チャットワーク サポートデスクです。

チャットワークAPI(プレビュー版)のご利用が可能となりましたので
お知らせいたします。

APIトークンの確認

メールが届いたら、ブラウザでチャットワークを開き、「動作設定」を開くと、以下のようにAPI発行メニューが追加されていることが確認できる。

パスワードを入力することで、以下のようにAPIトークンを確認することができる(画像のトークンはダミー)
トークンはこの画面で更新可能なので、必要に応じて更新する。

APIトークンはプログラムでAPIを利用するのに必須のため、環境変数に保存しておく。以下のコマンドをbashrcに追記するのがベスト

$ export CHATWORKAPI=hogehogehogehogehogehogehogehogehoge

APIの仕様確認

APIドキュメントを確認すれば、APIのシンプルな仕様を確認できるが、ここでは以下さえ抑えておけば問題ない

  • APIのエンドポイントは https://api.chatwork.com/v2
  • リクエストは必ずHTTPSを用いる
  • リクエストヘッダに”X-ChatWorkToken”というキーでAPIトークンを指定する必要がある
  • APIの制限は5分で100回程度
  • レスポンスは必ずJSONで返却される
  • ただし、APIの成否判定の多くはレスポンスヘッダに含まれている

APIの動作確認

さっそくプログラムを作っても良いが、API及びAPIトークンが正しく機能しているかを確認するため、curlコマンドで試しにAPIを呼び出してみる。

$ curl https://api.chatwork.com/v2/me GET -H "X-ChatWorkToken: hogehoge"

上記curlコマンドは、以下のHTTPリクエストを送信する

  • リクエスト先は https://api.chatwork.com/v2/me (自身のステータスを取得するAPI)
  • メソッドにGETを用いる
  • リクエストヘッダに”X-ChatWorkToken: hogehoge”を含める

すると、以下のレスポンスが返却される

{"account_id":2091543,"room_id":59255776,"name":"笹木信吾","chatwork_id":"","organization_id":1118988,"organization_name":"","department":"","title":"","url":"https://github.com/Sa2Knight","introduction":"","mail":"sasaki@91932.com","tel_organization":"","tel_extension":"","tel_mobile":"090-7067-7376","skype":"","facebook":"","twitter":"","avatar_image_url":"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}

レスポンスを見る限り、無事にチャットワークAPIを用いて、自身のユーザ情報を取得できていることがわかる。

RubyからチャットワークAPIを利用する

Chatworkクラスの作成

とりあえずチャットワークAPIをRubyで叩くためのエンドポイントとなるChatworkクラスを作成する。

require 'net/http'
require 'uri'
require 'json'
require 'date'

class Chatwork
  @@API_BASE = 'https://api.chatwork.com/v2'

  # tokenを指定してオブジェクトを生成
  # tokenを省略した場合、環境変数を参照する
  def initialize(token = nil)
    @token = token || ENV['CHATWORKAPI']
  end

end

例によって、APIトークンを指定してオブジェクトを生成する。トークンの指定を省略した場合に、環境変数”CHATWORKAPI”を参照するようにしてあるので、今回はそれを前提にする。APIのエンドポイントは全てのAPIで共通なので、クラス定数@@API_BASEで定義しておく。

HTTP通信するメソッドの実装

どのAPIでも共通となる、HTTPリクエストを投げてレスポンスを戻すメソッドを作成する。

createRequestObjectは、そのヘルパーメソッドで、指定したHTTPメソッド(:get,:post,:put,:delete)に応じた、空のHTTPRequestオブジェクトを返却する。

createHttpObject、対象のURL,HTTPメソッド,パラメータを指定すると、以下のことをしてくれる。
1. APIのエンドポイントと各APIのURLを結合し、URIオブジェクトを生成する
2. URIを元にHTTPオブジェクトを生成し、SSLを有効にする
3. (メソッドがGETの場合) URIにパラメータを付与する
4. メソッドに応じたRequestオブエクトを取得
5. リクエストヘッダにAPIトークンを付与
6. (メソッドがGET以外の場合) リクエストボディにパラメータを付与する
7. リクエストを送信し、レスポンスを戻す

## HTTPリクエストを送信する
def createHttpObject(url, method, params = {})
  api_uri = URI.parse(@@API_BASE + url)
  https = Net::HTTP.new(api_uri.host, api_uri.port)
  https.use_ssl = true
  api_uri.query = URI.encode_www_form(params) if method == :get
  req = createRequestObject(method, api_uri)
  req["X-ChatWorkToken"] = @token
  req.set_form_data(params) unless method == :get
  https.request(req)
end
## リクエストオブジェクトを生成する
def createRequestObject(method, uri)
  case method
    when :get
      return Net::HTTP::Get.new(uri.request_uri)
    when :post
      return Net::HTTP::Post.new(uri.request_uri)
    when :put
      return Net::HTTP::Put.new(uri.request_uri)
    when :delete
      return Net::HTTP::Delete.new(uri.request_uri)
  end
end

上記のメソッドを用いて、各APIに対応したメソッドを実装する。

各APIごとのメソッドを実装する

あとはAPIドキュメントを参考に、各種APIを呼び出すためのメソッドを順に実装する。全て実装するのは疲れるので、とりあえず以下を実装した。残りもおいおい実装する予定

メソッド名 内容
me 自身のユーザ情報を取得
myStatus 自身の未読数、未読To数、未完了タスク数を取得する
myTasks 自身のタスク一覧を取得する
myContact 自身のコンタクト一覧を取得する
myRooms 自信が参加しているチャット一覧を取得する
getRoom チャットの情報を取得する
createRoom 新しいチャットを作成する
updateRoom チャットの情報を更新する
getRoomMembers チャットに参加しているユーザ一覧を取得する
getRoomMessages チャットのメッセージ一覧を取得する
sendMessage チャットに新規メッセージを投稿する
getRoomTask チャット内のタスク一覧を取得する
getTask タスク情報を取得する
createTask タスクを新規登録する
getRoomFiles アップロードされたファイル一覧を取得する
getFile アップロードされたファイルの情報を取得する

※ 何故か既存のタスクを操作するためのAPIが無い。タスクを作ることは出来るのに完了したり編集したりすることがでない謎。

実装例

どのメソッドも基本的に実装内容は同じ(URL、メソッド、パラメータの有無などが異なるだけ)なので、全てを記載しないが、ここでは以下の2種類について記載する。

myStatus(自身の未読数、未読To数、未完了タスク数を取得する)

myStatusは特定のURLに対して、パラメータなしでシンプルにGETするだけなので、以下のように実装した

def myStatus
  url = '/my/status'
  res = createHttpObject(url, :get)
  return JSON.parse(res.body)
end

createTask(タスクを新規登録する)

createTaskは少しだけ複雑で、タスクを新規登録するルーム(チャット)IDとタスクの内容、担当者のユーザID一覧を指定して登録する。また、オプションでタスクの期限を指定することができる。

APIが要求するパラメータでは、担当者のユーザID一覧はカンマ区切りで、期限はUNIXTIMEで指定する必要がある。これはそのままだと使いづらいので、メソッド側では担当者のユーザID一覧を配列で、期限をDateTimeオブジェクトで受け取るようにしている。メソッド内でそれをAPI用に変換して実行している。

def createTask(room_id, body, to_ids = [], params = {})
  url = '/rooms/' + room_id + '/tasks'
  params[:body] = body
  params[:to_ids] = to_ids.join(',')
  params[:limit] = params[:limit].to_i if params[:limit].class == Time
  res = createHttpObject(url, :post, params)
  return res.body ? JSON.parse(res.body) : []
end

動作確認

本項では、以下の手順で動作確認する

  1. 動作確認用の新しいチャットルームを作成する(自分のみ参加)
  2. チャット内で適当に発言する
  3. チャット内にタスクを追加する
  4. チャットルームの情報を取得する
  5. チャットルームの発言一覧を取得する
  6. チャットルームのタスク一覧を取得する

なお、動作確認中に使用するユーザのIDを’2091543′,及び作成するチャットルームのIDを’76097933’とし、以下のようにChatworkオブジェクトを生成済みとする。

cw = Chatwork.new

チャットルームを作成

動作確認用に、「API動作確認ルーム」というチャットルームを作成する。参加者は、自身のみを管理者ユーザとして設定する。

cw.createRoom('API動作確認ルーム', ['2091543'], :description => '動作確認用のルームです')

実行すると、以下のように新規チャットルームが作成されたことがわかる。

チャット内で適当に発言する

「こんにちは,APIからの投稿です」という発言を、先ほど作成したルームに対して送信する

cw.sendMessage('76097933', 'こんにちは、APIからの投稿です')

実行すると、発言が送信されていることがわかる

チャット内にタスクを追加する

「WordPressを更新する」というタスクを、本日(5/21)を締め切りにして登録する

cw.createTask('76097933', 'WordPressを更新する', ['2091543'], :limit => Time.now)

実行すると、タスクが登録されたことが確認できる

チャットの情報を取得する

先程作成したチャットルームの情報を取得する。

cw.getRoom('76097933')

実行すると、チャットルームの基本的な情報を確認することができる

{"room_id"=>76097933, "name"=>"API動作確認ルーム", "type"=>"group", "role"=>"admin", "sticky"=>false, "unread_num"=>0, "mention_num"=>0, "mytask_num"=>1, "message_num"=>3, "file_num"=>0, "task_num"=>1, "icon_path"=>"https://appdata.chatwork.com/icon/ico_group.png", "description"=>"動作確認用のルームです", "last_update_time"=>1495307674}

チャットルーム内のメッセージ一覧を取得する

メッセージ一覧(と言っても先程の1件のみだが)を取得する

cw.getRoomMessages('76097933')

メッセージ一覧及び個々のメッセージの発言者の情報が取得できることがわかる

[{"message_id"=>"2020162066", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"[info][title][dtext:chatroom_groupchat_created][/title][dtext:chatroom_chatname_is]API動作確認ルーム[dtext:chatroom_set]\n\n[dtext:chatroom_description_is]動作確認用のルームです[dtext:chatroom_set]\n\n[dtext:chatroom_member_is][piconname:2091543][dtext:chatroom_added][/info]", "send_time"=>1495307261, "update_time"=>0}, {"message_id"=>"2020162308", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"こんにちは、APIからの投稿です", "send_time"=>1495307439, "update_time"=>0}, {"message_id"=>"2020162647", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"[info][title][dtext:task_added][/title][task aid=2091543 st=open lt=1495378799]WordPressを更新する[/task][/info]", "send_time"=>1495307674, "update_time"=>0}]

チャットルーム内のタスク一覧を取得

こちらも先程登録した1件のみだが、タスクの情報を取得する

p cw.getRoomTasks('76097933')

実行すると、タスク及びそのメタ情報が確認できる

[{"task_id"=>68592616, "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "assigned_by_account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "message_id"=>"2020162647", "body"=>"WordPressを更新する", "limit_time"=>1495378799, "status"=>"open"}]

所感

  • チャットワークのAPIはそれは見事なRestFull設計で感動を覚えるほど使いやすかった
  • だがまだプレビュー版で、既存タスクの更新ができないなど、痒いところに手が届かないので正式リリースが待たれる
  • APIを叩くライブラリを作ったはいいが、問題はこれをどう活用するか。そこは全然考えてない。おそらくサーバを監視して報告をチャットワークに流すとか、プロジェクト管理ツールの更新をチャットワークでも通知するとか、読み込みより書き込みがメインになるとは思う

参考

[Ruby] codicAPIを使って英語での命名で楽する

前提

以下のシステム構成で開発、動作確認済み

要素 バージョン
debian 8.6
ruby 2.2.2

成果物

今回開発したツールは以下のリポジトリから取得可能。導入方法はReadme参照
https://github.com/Sa2Knight/codic-ruby

概要

コーディングに特化した翻訳サービスであるcodicのAPIを用いて、Rubyスクリプトから翻訳を行う。翻訳作業をコマンド化し、手軽に利用できるようにする。

codicには各種エディタから利用できるプラグインなどが既にいくつか存在するが、個人的にどれもしっくり来なかったり、機能過剰だったりしたので、自分でAPIを利用して作ってみることにする。

codicについて

codicとは

codicは、プログラマー・システムエンジニアのためのネーミングツール。私含め英語弱者なプログラマは、クラス名、関数名、変数名などの識別子の命名に苦労することが多々ある。

codicはそれをサポートするツールで、日本語で識別子の概要を入力すると、それなりに適した英語に翻訳してくれる。

一見すると、Google翻訳などの機械翻訳と変わらないが、codicはコーディングに特化した翻訳を行い、コード内で頻出する単語に特に力を入れていたり、キャメルケースやスネークケースなどのフォーマットを指定することもできるなど、まさにプログラマ向けの翻訳ツールである

codic APIとは

名の通り、codicのサービスをプログラムから実行するためのAPIである。ユーザ登録こそ必要なものの、OAuth認証などの手続きは必要なく、ユーザ登録時に取得できるアクセストークンをリクエストヘッダに付与するだけで手軽に利用できる。本記事では、日本語を投げると英語が返ってくるEngineAPIのみ利用する。

Engine APIとは

Engine APIは、codicのネーミング生成エンジンにアクセスし、ネーミング変換(和英変換)を行う。

シンプルなリクエストとレスポンスは以下の通り

request(日本語はURLエンコードするものとする)

https://api.codic.jp/v1/engine/translate.json?text=得点を取得する&casing=camel

response

[{"successful"=>true,
  "text"=>"得点を取得する",
  "translated_text"=>"getScore",
  "words"=>
   [{"successful"=>true,
     "text"=>"取得する",
     "translated_text"=>"get",
     "candidates"=>
      [{"text"=>"get"},
       {"text"=>"retrieve"},
       {"text"=>"fetch"},
       {"text"=>"obtain"},
       {"text"=>"acquire"},
       {"text"=>"getting"}]},
    {"successful"=>true,
     "text"=>"得点",
     "translated_text"=>"score",
     "candidates"=>[{"text"=>"score"}, {"text"=>"point"}]},
    {"successful"=>true,
     "text"=>"を",
     "translated_text"=>nil,
     "candidates"=>
      [{"text"=>nil},
       {"text"=>"that"},
       {"text"=>"to"},
       {"text"=>"for"},
       {"text"=>"from"},
       {"text"=>"is"},
       {"text"=>"of"}]}]}]

以上のように、エンジンAPIはGETパラメータで翻訳対象の日本語文字列(text)と、出力フォーマット(casing)を指定するだけで利用できる。

レスポンスは、全体の変換結果及び単語ごとの変換結果、さらに他の変換候補なども返却してくれるが、本記事では単純な変換結果のみ欲しいので、

“translated_text”=>”getScore”

のみ抜き出して利用する。

codic APIの利用準備

codic APIを利用するためには、codic APIのアクセストークンを取得する必要がある。以下の手順で簡単に取得できるので詳細は割愛
1. codicにユーザ登録するする
2. codicにログインし、アカウント設定ページに移動
3. APIをクリックし、以下の画面よりアクセストークンを確認(画像はダミー)

Rubyスクリプトからアクセストークンを利用する必要があるので、実行環境の環境変数にアクセストークンをエクスポートしておく

$ export CODICAPI hogehogehogehgoehgehgheogheohgehog

エクスポートを毎回するのが面倒なので、上記コードをbashrcに追加しておくことをオススメする

Rubyスクリプトの実装

今回は実装内容もシンプルで、OAuthする必要もないため、gemライブラリを利用せず、Rubyの標準モジュールのみで実装を行う。

Codicクラスを作成

スクリプトのベースとなるCodicクラスを以下のように作成する

require 'net/http'
require 'uri'
require 'json'

class Codic
  @@API_URL = "https://api.codic.jp/v1/engine/translate.json"

  def initialize(api_key = nil)
    @api_key = api_key || ENV['CODICAPI']
  end
end
  • APIURLをクラス定数に持たせる
  • アクセストークンを指定してインスタンスを生成する。指定がない場合環境変数を参照する

HTTPリクエストを送信するメソッドを実装

# HTTPリクエストを送信し、レスポンスJSONを戻す
def request(url, params = {})
  uri = URI.parse(appendQueryString(url, params))
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = true
  req = Net::HTTP::Get.new(uri.request_uri)
  req["Authorization"] = "Bearer #{@api_key}"
  res = https.request(req)
  JSON.parse(res.body)
end
# URLにクエリストリングを付与する
def appendQueryString(url, params = {})
  query_string = params.map {|key, val| "#{key}=#{URI.escape(val)}"}.join("&")
  url += "?#{query_string}" if query_string
  return url
end

requestメソッド

  • Ruby標準モジュールのnet/httpを用いて、HTTPリクエストの作成、送信を行う
  • codicAPIでは、Authorizationヘッダにアクセスキーを付与する必要があるので、インスタンス変数の@api_keyを設定
  • text及びcasingをURLに付与する必要があるので、それはappendQueryStringメソッドに移譲

appendQueryStringメソッド

  • ハッシュ形式のパラメータを、文字列に置き換えてURLに付与する
  • textは日本語が飛んで来るので、URLエンコードを行う

※ どちらもツールの目的から独立したメソッドなのでこのような実装は望ましくない気がするが、軽量ツールなので妥協する

casingをAPI用に変換するメソッドを実装

casingに指定する文字列と出力フォーマットの対応は以下の通り

casing フォーマット 例(データを取得する)
camel キャメルケース getData
pascal パスカルケース GetData
lower underscore 小文字のスネークケース get_data
upper underscore 大文字のスネークケース GET_DATA
hyphen ハイフン刻み get-data
(指定なし) スペース刻み get data

といっても文字列を直接指定するのは面倒なので、アルファベット1文字でそれぞれに変換できる以下のメソッドを実装した

# casingをAPI用に変換する
def convertCasing(casing)
  casingList = {
    'c' => 'camel',
    'p' => 'pascal',
    'l' => 'lower underscore',
    'u' => 'upper underscore',
    'h' => 'hyphen',
  }
  new_casing = casingList[casing]
  new_casing ? new_casing : ''
end
  • c/p/l/u/hをそれぞれの文字列に変換する
  • 該当が無い場合、空文字(指定なし)を返却

翻訳を実行するメソッドを実装

ここまで実装したメソッドを利用して、APIを通して翻訳結果を出力する以下のメソッドを実装した

def translate(text, casing = '')
  casing = convertCasing(casing)
  result = request(@@API_URL, {:text => text, :casing => casing})
  result[0]["translated_text"] if result.class == Array && result[0].include?("translated_text")
end
  • responseは色々返ってくるが、その一部のみしか利用しないのでそこだけ抜き出して返却

エントリポイントの実装

コマンドライン引数を用いてCodicクラスのtranslateメソッドを実行するエントリポイントを実装

codic = Codic.new
word = ARGV[0] || exit
casing = ARGV[1] || ''
puts codic.translate(word, casing)

動作確認

$ ruby codic.rb データを取得
acquisition data
$ ruby codic.rb データを設定 c
settingData
$ ruby codic.rb データを削除 h
deleting-data

シェルスクリプトの実装

これでRubyコマンドを用いた簡易的な翻訳ツールを作成することができた。蛇足かもしれないが、より汎用的に使えるように、このツール自体をコマンド化したい。そこで、シェルスクリプトでcodicコマンドを作成し、それ経由でcodic.rbを呼び出すようにする。

シェルスクリプトの仕様

  • 第一引数に変換する日本語文字列を指定する
  • 第二引数で以下のcasingを指定する(省略可)
    • -c(デフォルト)
    • -p
    • -l
    • -u
    • -h
    • -n
  • Rubyスクリプトを実行し、結果を標準出力

スクリプトパスの定義

先程作成したスクリプトのパスを定義しておく

#!/bin/bash
SCRIPTPATH="/home/vagrant/codicRuby/codic.rb"

ヘルプ関数を作成

よくあるヘルプ関数を以下のように実装する。本関数はコマンド引数が存在しなかった場合に表示する

usage() {
  echo "Codic APIに基いて日本語の翻訳を行う"
  echo "  Usage: codic text [-c] [-p] [-l] [-u] [-h] [-n]"
  echo "オプション:"
  echo "  -c キャメルケースで取得(getData)"
  echo "  -p パスカルケースで取得(GetData)"
  echo "  -l 小文字のスネークケースで取得(get_data)"
  echo "  -u 大文字のスネークケースで取得(GET_DATA)"
  echo "  -h ハイフン刻みで取得(get-data)"
  echo "  -n スペース刻みで取得(get data)"
  exit 1
}

第一引数をチェック

第一引数(翻訳文字列)の存在を確認。存在しない場合前項のヘルプ関数を実行してスクリプトを終了

if [ -n "$1" ]; then
  text=$1
  shift 1
else
  usage
  exit 1
fi

第二引数をチェック

第二引数をチェックし、引数に応じてcasingを設定する

casing="c" #デフォルト設定をここに記述
while getopts cpluhn: OPT
do
  case $OPT in
  c)  casing="c"
      ;;
  p)  casing="p"
      ;;
  l)  casing="l"
      ;;
  u)  casing="u"
      ;;
  h)  casing="h"
      ;;
  n)  casing="n"
      ;;
  esac
done

Rubyスクリプトを実行する

text及びcasingを用いて、Rubyスクリプトを実行する

command="$SCRIPTPATH $text $casing"
ruby $command

コマンドをパスに含める

パスを確認

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

今回は/usr/local/binにハードリンクを作成する

sudo ln codic /usr/local/bin

パスが通っていることを確認

$ which codic
/usr/local/bin/codic

動作確認

text指定なし

$ codic
Codic APIに基いて日本語の翻訳を行う
  Usage: codic text [-c] [-p] [-l] [-u] [-h] [-n]
オプション:
  -c キャメルケースで取得(getData)
  -p パスカルケースで取得(GetData)
  -l 小文字のスネークケースで取得(get_data)
  -u 大文字のスネークケースで取得(GET_DATA)
  -h ハイフン刻みで取得(get-data)
  -n スペース刻みで取得(get data)
$

casing指定なし

$ codic データを取得する
getData

casing指定あり

$ codic データを設定する -u
SET_DATA

所感

  • 今までで一番手軽に利用できるAPIだったので使っていて楽しかった
  • 英語弱者なので命名が苦手だったので使えそう(あくまで参考程度に)
  • Vim上で効率的に利用したいので、これを元にvimプラグインを作ってみたい

NodeによるZaimAPIの利用

本記事で使用したZaim.jsは、最終更新が2013年で、最新のAPIを利用することができません。そのため、Zaim.jsのソースコードを直接編集し、最新のAPIを利用できるように書き換えたものを利用しています。

書き換えについては、Zaim.jsをAPI ver2.xが使えるように改良してみたをご参考ください

前提

  • node/npmが使える状態
  • Zaimのアカウントを保有しており、最低限の利用記録がある状態
  • Zaim API用のカスタマーキー、アクセストークンを何らかの手段(OAuth)で取得している状態

かなり限定的な状態が対象なので限りなく個人備忘録用

概要

クラウド型の家計簿サービスであるZaimを、Node上でAPI経由で読み書きする。
本記事ではZaim API用のカスタマーキー及びアクセストークンを取得済みで、APIを利用できる状態にあることを前提とする

Zaimとは

Zaimは、おそらく国内シェアナンバー1のクラウド型家計簿サービスであり、Web/iOS/Androidそれぞれから利用することができる。他の家計簿ソフトと同様に、会計簿の記入、集計、検索、予算管理など、家計簿サービスとしての基本的な機能を備えている他、以下の特徴的な機能を持っている。

  • レシートを撮影することで全自動で入力が行われる。精度はお察し
  • 各金融機関と連携し、入出金を自動入力。金融機関の認証情報を民間企業に渡すとかどうなんだろ
  • 年末調整とかそれなりに自動でやってくれる。正確に家計簿を記入していないと無意味

などと特徴的な機能はあんまり使いものにならないので、私は普通に家計簿の入出力にしか使っていない。(※上記は私的意見です)

Zaim API とは

OAuthにてZaimのアカウントの認証を受け、API経由で家計簿のCRUDを行うことができる。

Zaim APIはRESTFULLに実装されているので、そのまま利用することも簡単であるが、今回はZaim APIをラッピングしたNodeモジュールであるZaim.jsを利用する。

Zaim.jsのインストール

Nodeモジュールなのでnpmを用いてインストール

npm install zaim -D

Zaimオブジェクトの作成

なんらかの手段で取得したカスタマーキーとアクセスキーを指定して、Zaimオブジェクトを作成する

var Zaim = require('zaim');

var zaim = new Zaim({
  consumerKey: 'hogehoge',
  consumerSecret: 'fugafuga',
  accessToken: 'foofoo',
  accessTokenSecret: 'barbar',
});

とりあえず入力履歴を取得

getMoneyメソッドで入力履歴を取得。デフォルトでは最新20件を取得するので、とりあえず2/17の記録を全て取得する。
ちょっと恥ずかしいが、実行結果は生データ。

ソースコード

zaim.getMoney({
  start_date: '2017-02-17',
  end_date: '2017-02-17'
} , function(data , err) {
  console.log(data.money);
});

実行結果

$ node zaim.js
[ { id: 1581817347,
    user_id: 3061609,
    date: '2017-02-17',
    category_id: '101',
    genre_id: '10105',
    comment: 'サラダ、ドレッシング、卵とじ丼',
    name: '',
    active: 1,
    created: '2017-02-17 19:43:22',
    currency_code: 'JPY',
    type: 'pay',
    price: '-341' },
  { id: 1581291695,
    user_id: 3061609,
    date: '2017-02-17',
    category_id: '101',
    genre_id: '10104',
    comment: '月見大盛り',
    name: '',
    active: 1,
    created: '2017-02-17 14:06:50',
    currency_code: 'JPY',
    type: 'pay',
    price: '-350' } ]

入力データの構成

前項では、2/17の記録2件の入力データを取得した。入力データはオブジェクト形式で取得でき、以下の構成になっている。

キー 内容
id 入力ID(ユニーク)
user_id ユーザID(ユニーク)
date 記録日時
category_id カテゴリ(食費/日用雑貨などの大分類)のID
genre_id ジャンル(朝食/昼食/夕食などの小分類)のID
comment 記録に対する自由記入欄(購入したモノなどを記入する)
name 不明(外部連携とかで使う??)
active 多分論理削除すると0になる
created データの登録日
currency_code お金の種類
type 支出(pay) or 収入(income)
price 金額(支出の場合負数)

特定の入力データを取得する

ここでは、2016年に「ゲーム」で使った金額の一覧を取得する

ジャンル一覧を取得

Zaimでは、「ゲーム」は、「エンターテイメント」カテゴリに属するジャンルとしてデフォルトで登録されている。
Zaim.jsでは、genre_idで指定する必要があるので、「ゲーム」のジャンルIDがいくつなのかを調べるために、以下のコードで支出のジャンル一覧を取得する。

ソースコード

zaim.getPayGenres({
  lang: 'ja',
} , function(data) {
  console.log(data);

実行結果

$ node zaim.js
{ genres:
   [ { id: 10101, category_id: 101, title: '食料品' },
     { id: 10102, category_id: 101, title: 'カフェ' },
     { id: 10103, category_id: 101, title: '朝ご飯' },
     { id: 10104, category_id: 101, title: '昼ご飯' },
     { id: 10105, category_id: 101, title: '晩ご飯' },
     { id: 10199, category_id: 101, title: 'その他' },
(中略)
     { id: 9047786, category_id: 108, title: 'ゲーム' },
(以下省略)

ゲームの支出履歴を取得

「ゲーム」のジャンルIDが9047786であることがわかったので、これを指定して2016年のゲームの支出一覧を取得する。
全部出力してしまうと長いので、価格とコメントのみmapして出力する。

ソースコード

zaim.getMoney({
  start_date: '2016-01-01',
  end_date: '2016-12-31',
  genre_id: '9047786',
} , function(data , err) {
  console.log(data.money.map(function(m) { return [m.price , m.comment]}));
});

実行結果

$ node zaim.js
[ [ '-1080', '桃鉄' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-950', 'ポケモン空の探検隊' ],
  [ '-706', 'マジカルバケーション' ] ]

ポケモンGOにほどよく課金していたのがわかる。

参考