えいのうにっき

a-knowの日記です

"完成させられるかどうかを恐れない人" のためのWebサービス・Mikanz(ミカンズ)をつくりました

つくりましたー。

f:id:a-know:20150628095358p:plain

Mikanz -ミカンズ- | みんなの未完成品が集まるばしょ

"完成したものを登録する" ようなところは数あれど、つくりかけのもの、未完成のものを登録して見てもらうようなところってそういえば見たことないな、と思ったのがきっかけ。...それだけ。笑

まぁ、他でもない自分(飽き性)が、次から次へと手をつけるクセがある(=未完成ばっかり)っていうのと、あとは単純に、ほかの人の "完成してないから日の目を見てない" ものをぼくが見てみたい、っていうのもあるし。

というわけで、あなたの未完成品、みせてください (。-人-。) ヨロシクー

http://mikanz.a-know.me/



follow us in feedly

"iTunes での楽曲情報を BigQuery に流し込むスクリプトを Go で書いた" の感想

先日、 iTunes での楽曲情報を BigQuery に流し込むスクリプトを Go で書いた というタイトルで Qiita に投稿した。なにげに Qiita 初投稿。(Qiita:Team は業務で使ってるんだけどね)

GAE/G での写経は細々とやっていたんだけど、純粋にツール的なものを Go で書いたのは初めて。てなわけで(?)、特に今回のスクリプトを書いてみた上での感想をここに綴ってみる。

iTunes が吐く plist のパースは大変

いきなり Go とは関係ない感想なんだけど。。 上述の Qiita の方にも書いたんだけど、iTunes は楽曲情報その他を plist 形式の xml ファイルとして出力してくれている。楽曲をたくさん入れてる人だと100MBを超えるサイズになることも珍しくないみたいなんだけど、こいつのパースには骨が折れた。

go-plist という、golang で書かれた plist を解析するためのライブラリがあるのを知って、最初はこれを使おうとしてみてたんだけど、結局うまくいかなかった。 というのも、通常 plist は下記のような形式であるのが普通なんだけど、

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>band-size</key>
        <integer>8388608</integer>
        <key>bundle-backingstore-version</key>
        <integer>1</integer>
        <key>diskimage-bundle-type</key>
        <string>com.apple.diskimage.sparsebundle</string>
        <key>size</key>
        <integer>4398046511104</integer>
    </dict>
</plist>

key で指定される文字列は、全ての dict element において共通。

一方、iTunes の吐く plist は下記のように、key で指定される文字列が Track ID となっていて、 dict element 毎に変わる。

<key>3013</key>
<dict>
    <key>Track ID</key><integer>3013</integer>
    <key>Name</key><string>01_ハイファイ・ローファイ</string>
    <key>Artist</key><string>スピッツ</string>
    <key>Album</key><string>99ep</string>
    <key>Genre</key><string></string>
    <key>Kind</key><string>MPEG オーディオファイル</string>
    <key>Size</key><integer>3489248</integer>
    <key>Total Time</key><integer>145371</integer>
    <key>Track Number</key><integer>1</integer>
    <key>Date Modified</key><date>2005-09-17T13:26:15Z</date>
    <key>Date Added</key><date>2005-09-16T11:16:43Z</date>
    <key>Bit Rate</key><integer>192</integer>
    <key>Sample Rate</key><integer>44100</integer>
    <key>Persistent ID</key><string>2A0848311638F0A2</string>
    <key>Disabled</key><true/>
    <key>Track Type</key><string>File</string>
    <key>Location</key><string>file://localhost/xxx.mp3</string>
    <key>File Folder Count</key><integer>-1</integer>
    <key>Library Folder Count</key><integer>-1</integer>
</dict>

これに、上述のライブラリは対応していなかった。

最初は、go-plist に対して、これに対応できるようにする PR を出すとかもアリか...? とか思ったんだけど、それをするには Go 力が足りず、断念してしまった(試行錯誤したときのPR)。

結局今回は、SAXを使って地道にパースを行った。

なぜ Go を選択したか

これは簡単で、今なら Go で書けばどんなものでも話題になると思ったから。なんとも不純な動機だけど。

乗るしか無い、この(ry

一応外向け(?)の理由を付けるとするならば、"Go で書いておけばクロスプラットフォームを実現するのが簡単だから"、っていうのはあるかな。Win でも Mac でも遊べる方が、より多くの人に使ってもらいやすいと思うので。。

でも蓋を開けてみたら、大して伸びてなくて涙目w

Google Cloud Platform との連携に Google Cloud SDK(外部コマンド gcloud)を利用している点

これも、恥を承知で、楽をしたいがために利用してしまったというかんじ。会社の同僚さんにも、「極端な話、外部コマンドの羅列なら shellscript でも出来ますしね」とも言われたので、いつかは直したいと思ってる。 ただ、メリットを無理矢理見出すとすれば、認証周りも Google Cloud SDK にお任せしているので、利用する側からすれば少しは安心できるんじゃないかな、とか思ったり。

ネーミングについて

イキって Goramoph なんていう名前にしたんだけど、これは "蓄音機(Gramophone)" を文字ってみたもの。やろうとしてることのイメージになんとなく近いし、アルファベットの "G" も含まれているし(Go なので...)、ってことで、これにした。 もし "itunes-xml-load-to-bq" みたいな名前だったら、最後までやり通せなかったかもしれない。w

Google APIs Client Library for Ruby を使って BigQuery にデータをロードする

表題の件で躓いてしまったので、ここで書いておく。 "Job configuration must contain exactly one job-specific configuration object (e.g., query, load, extract, spreadsheetExtract), but there were 0: " というエラーが出て、うまくロードできない、という現象。

結論からいうと、ここのStackOverflowでのやりとりが手がかりとなって、下記のようなコードでなんとかロードすることができた。リファクタもなにもしてないコードで、失礼。(gist

require 'bundler/setup'
require 'google/api_client'
require 'yaml'
require 'json'

def multipart_boundary
  'xxx'
end

# load credential yaml
oauth_yaml = YAML.load_file('.google-api.yaml')

# Initialize the client.
client = Google::APIClient.new(
  :application_name => 'Example Ruby Bigquery',
  :application_version => '1.0.0')
client.authorization.client_id = oauth_yaml["client_id"]
client.authorization.client_secret = oauth_yaml["client_secret"]
client.authorization.scope = oauth_yaml["scope"]
client.authorization.refresh_token = oauth_yaml["refresh_token"]
client.authorization.access_token = oauth_yaml["access_token"]

# Initialize Bigquery client.
bq_client = client.discovered_api('bigquery', 'v2')

job_config = {
  'configuration' => {
    'load' => {
      'sourceUris' => ['gs://a-know-test/sample.csv'],
      'schema' => {
        'fields' => [
          {
            'name' => 'id',
            'type' => 'INTEGER'
          },
          {
            'name' => 'name',
            'type' => 'STRING'
          },
          {
            'name' => 'price',
            'type' => 'INTEGER'
          },
        ]
      },
      'destinationTable' => {
        'projectId' => 'test-001',
        'datasetId' => 'test',
        'tableId'   => 'sample'
      },
      'createDisposition' => 'CREATE_NEVER',
      'writeDisposition' => 'WRITE_APPEND'
    }
  }
}

body = "--#{multipart_boundary}\n"
body += "Content-Type: application/json; charset=UTF-8\n"
body += "\n"
body += "#{job_config.to_json}\n"
body += "--#{multipart_boundary}--\n"

# Make an API call.
result = client.execute(
  :api_method => bq_client.jobs.insert,
  :parameters => {
    'projectId' => '234230709110',
    'uploadType' => 'multipart'
  },
  :body => body,
  :headers => { 'Content-Type' => "multipart/related; boundary=#{multipart_boundary}" }
)


puts result.data
puts result.response.body

自分的につまづきポイントだと思った点を、いくつか挙げてみる。

  • "Job configuration must contain〜" のエラーは、リクエストのパラメータになんらかの不備があった場合に出る
    • 特定のパラメータに不備があるときだけに出るようなものではないので厄介
  • body には configuration のjsonだけじゃだめ
    • Content-Type の指定とか multipart boundary とかもいる
  • しかも Content-Type の指定の次に空行が必要
  • headers には以下の2点の指定が必要
    • Content-Type として multipart/related
    • ハイフンのプレフィックスを省いた multipart boundary
  • body末尾の boundary にはサフィックスとしてハイフンが必要
  • parameters に 'uploadType' => 'multipart' の指定が必要(これはここにも書いてある)

この body の書き方って、こういうお作法でしたっけ。今までにも APIリクエストの際のリクエストbody をいじってたことはあったけど、こんな書き方、したっけなぁ。

Ruby で BigQuery」は、ちょっとわけあって今後も少しづつ試していこうと思っているので、同じような境遇の方はこちらwatch して頂ければと。



follow us in feedly