Lambdaを使って特定サイトのはてなブックマーク総数と増加数をSlackに通知する

6/14にはてなブックマークのブクマ数取得APIが公開された. bookmark.hatenastaff.com

ブックマーク総数が取得できるということで,例えば「ブログのブクマ数が今日どのくらい増えたのか」を確認できると便利だと思い作ってみた. ブクマ総数前回の実行時からどのくらい増加したかを通知してくれる. f:id:cohalz:20180616163803p:plain

CloudWatch Eventsでeventにbookmark_urlslack_urlを渡すことを想定している.

import boto3
import os
import json
import urllib.request
import re

def lambda_handler(event, context):
    bookmark_url = event['bookmark_url']
    message = bookmark_url + ' のブックマーク総数: '
    total_count = get_total_count(bookmark_url)
    total = total_count['total_bookmarks']
    env_key = re.sub(r'^[^a-zA-Z]|[^a-zA-Z0-9\_]', '', bookmark_url)
    
    prev_tmp = os.getenv(env_key)
    if prev_tmp != None:
        prev_total = int(prev_tmp)
        message  += str(total) + '(' + get_delta_str(total, prev_total) + ')'
        if total != prev_total:
            update_variables(env_key, total, context.function_name)
    else:
        message += str(total)
        update_variables(env_key, total, context.function_name)
    
    print(message)
    post_to_slack(event['slack_url'], message)

    return total_count
    
def get_total_count(bookmark_url):
    params = urllib.parse.urlencode({'url': bookmark_url})
    api_url = 'http://api.b.st-hatena.com/entry.total_count?' + params
    
    with urllib.request.urlopen(api_url) as response:
        return json.loads(response.read().decode('utf-8'))

def update_variables(env_key, total, function_name):
    env = {}
    for key, value in os.environ.items():
        if key.startswith('http'):
            env[key] = value
    env[env_key] = str(total)

    client = boto3.client('lambda')

    return client.update_function_configuration(
        FunctionName=function_name,
        Environment={
            'Variables': env
        })

def post_to_slack(slack_url, text):
    json_data = json.dumps({'text': text}).encode('utf-8')
    request = urllib.request.Request(slack_url, data=json_data, method='POST')
    with urllib.request.urlopen(request) as response:
        return response.read().decode('utf-8')

def get_delta_str(x, y):
    delta = x - y
    if delta >= 0:
        return '+' + str(delta)
    else:
        return str(delta)

前回の状態をLambdaに記憶させるために,AWS SDK経由で環境変数に書き込む方法を取った. 環境変数を使うことで別のサービスに依存しない簡易的なKVSとして扱える.

ただし,環境変数に使える文字は限られているのでkeyを工夫する必要がある.

env_key = re.sub(r'^[^a-zA-Z]|[^a-zA-Z0-9\_]', '', bookmark_url)

docs.aws.amazon.com

また,環境変数の変更にはLambdaに対してlambda:UpdateFunctionConfigurationの権限追加が必要になる.

LambdaおよびCloudWatch Eventsで定期実行するためのCloudFormationテンプレートはこちらから. github.com

IFTTTを使ってSwitchのSNS投稿をGoogleフォトにアップロードする

手順はこちら

  1. IFTTTからTwitterを選ぶ

  2. New tweet by you with hashtagを選び #NintendoSwitch を入力f:id:cohalz:20180527001758p:plain

  3. 連携先からGoogle Photosを選び,投稿するアルバム名を入力してPhoto URLに {{FirstLinkUrl}} を入力 f:id:cohalz:20180527002042p:plain

  4. Finishを押せば連携完了

これでSNS投稿をすると自動でGoogleフォトにアップロードされるようになる

問題点

画質が良くない

680 × 383程度になってしまう

動画はアップロードできない

サムネイル画像のみが投稿される

タイムゾーンがおかしい

13時間前にズレる f:id:cohalz:20180527002454p:plain

この3つの問題点は割と致命的なのでIFTTT以外の類似サービスで解決できるんだったらそっちに変えたい.

BetterTouchToolを使って1アクションでウインドウのスクリーンショットを撮る

BetterTouchToolにはAttach Additional Actionという機能があり,複数のアクションを順に実行することできる.

方法は簡単で,最初に実行するアクションを入力したあとトリガー入力の上にあるAttach Additional Actionを押して追加のアクションを入力する.

ウインドウのスクリーンショットは,

  1. Shift + Command + 4

  2. ウインドウにカーソルを合わせスペース

  3. クリック

の3つの手順になっているため,下の画像のような手順になる.

f:id:cohalz:20180525220813p:plain

アクションを実行する際,カーソルの下にあるウインドウが対象になるので気をつける必要がある.(最前面を撮る方法がわからない...)

ちなみに,ウインドウの影を消したい場合は最後のアクションをLeft ClickではなくOption(⌥)+Clickにすれば良い.

はてなでインフラ研修を受けました

11/20に株式会社はてなにアルバイト入社してちょうど5ヶ月経ちました.

インフラの事をほとんどわかっていない状態からスタートし,実務のタスクをこなしつつ研修を受けていました.

そして先月にそのインフラ研修を終えたのでそのレポートを書いていきます.

Go言語研修

まず最初にやったのはGo言語の研修でした.

はてなではgo言語の採用が進んでおり,mackerel-agentdroot等が実際にgo言語で作られています.

研修としてA Tour of Goを進めました.

defer, slice, goroutineなどを学びすぐ手を動かして試せる環境として良いサイトだと思います.

github.com

ISUCON4予選スコアアップ

次に行った研修はISUCON4の予選を動かしスコアアップを試すというものでした.

そもそも,ミドルウェアの設定について全然知らない状態から始まったため,

実際のISUCON4予選でスコアアップさせた記事を見て進めていきました.

confファイルのパラメータを調整していきながら,

  • どれくらい設定するファイルがあるのか
  • どんなパラメータがあるのか

を知っていきました.

設定を変えたあとはMySQLにインデックスを貼ったり,アプリケーションを変更したりして点数を見ていくと,

インデックスを貼った事による点数の増加が一番大きく,やっぱり「DBのチューニングが大事」というのをよく実感しました.

という感じでISUCONの研修は,模範解答から見て作ったものが多かったですが,

どういうときに何を設定すればいいのか多少見当がつくようになりました.

次回のISUCONは参加してみようと思います.

github.com

ISUCON4予選の環境をChefで再現する

まっさらなamiからChefを適用してISUCON4予選のwebアプリケーションが立ち上がるようにする

という目標で進めていきました.

立ち上がるようになった後は,

と,少しずつ世の中のwebアプリケーションに似た構成を目標に進めていきました.

Chef自体やそれぞれのミドルウェアの動作,およびネットワーク周りでだいぶ苦労しましたが,

研修を受ける以前と比べて相当知識がついたと思います.

またその途中EC2-VPC環境でkeepalivedを簡単に使うcookbookを作成しました.

github.com

github.com

インフラ研修を終えて・これから

以上でインフラ研修が終わり,今は実際のタスクのみをこなしています.

以前と比べて,特に普段使うことになるChefやAWS周りに関してわかることが増えたと感じていて,以前よりもはるかにタスクをこなしたり自分でissueを見つけたりすることができるようになりました.

今はLambdaを使ったタスクを進めていています.

趣味のプロダクトではGASを使っているのもあり,サーバレスがマイブームになっているので短期的な目標として「サーバレスに詳しい人」を目指していきたいです.

そんな感じで,これからもよろしくお願いします!

「定期的な楽しみ」について

先週から「メトロダーツの旅」と自分は呼んでいる遊びを始めている.

「ランダムにメトロの駅を一つ選び,仕事が終わったあとその駅まで行き周辺を少し散策しながら夕食を食べて帰る」というものである.

これが思っていたよりハマっていて,週二で東京に出ているときの楽しみになっている.

それもそのはずで,自分は普段Ingressをプレイしている事に加え,色んなお店を発掘するのが好きだからである.

自分の需要にあった楽しみを定期的に用意できたことになる.

 

ところで普段の生活を見てみると,細かいとこは違えど大枠で見れば毎日及び毎週のルーチンは決まっていることが多い.

その中で毎回違うもの,予測の付かないものを用意するのが人生において大事だと思った.

 

このような 「予測の付かない楽しみ」 の例として準備しやすい物の一つがアニメになるんだと思う.

そう思ったきっかけは普段アニメを見ない自分が毎週見ていた「ポプテピピック」である.

実際,毎週予測の付かない楽しみになっていたし,最終回が終わってしばらくは土曜日の夜に違和感を覚えていた.

ここでアニメの話を続けると,自分が何故普段アニメを見ていないのかというと,「しんどくなったときにやめにくい」からである

これは自分のスタイルが下手ということだと思うが,そのアニメが途中から面白くなくなった場合でも「見るのをやめる」という選択肢が取れず,最後まで見てしまう.

 

定期的なものは楽しみにもなるが,義務感に駆られたり生活を圧迫したりという原因にもなる

なので,苦しさを感じたらすぐやめるべきだし,すぐやめられるようにしておくのが大事という気持ちになっている.

 

「メトロダーツの旅」もいつまでやるかはわからない,けれどもやめたとしてそれは悪いことではない.

そういう意識をもちつつこれからも楽しんでいきたい.

GASを使ってサーバレスな採点アプリケーションを作った

大学の副手でプログラミングの採点をしているが,GASでその手間を減らすアプリケーションを作ることができた

その際にGASのイディオムを結構覚えたのでメモ.

アプリケーションの主な仕様

  • Googleフォームから採点するファイルをアップロードする
  • zipファイルの解凍,採点者ごとの担当を決めフォルダの再配置が自動で行われる
  • 採点用シートを自動作成
  • 採点完了後に踏むことでcsv書き出し,ダウンロードができるリンクを作成
  • 採点シート,各採点者ごとのフォルダ,ダウンロードリンクをSlack通知

Googleフォームから採点するファイルをアップロードする

Googleフォームではファイルのアップロード機能がついており,GASのhookと組み合わせることでフォームの送信ごとに自動でファイル処理を行うことができる.

f:id:cohalz:20180413215603p:plain
ファイルに対しても細かく条件をつけることができる

f:id:cohalz:20180413215655p:plain
フォームのhookを設定できる

hookでフォームから投稿されたファイルを扱うには下のようにすれば良い.

function submitForm(e) {
  const itemResponses = e.response.getItemResponses();

  const fileId = itemResponses.filter(
    function(itemResponse){ 
      return itemResponse.getItem().getTitle() == "質問名";
    })[0].getResponse();
  const file = DriveApp.getFileById(fileId).getName();
}

フォームに投稿されたファイルはフォームが置いてあるディレクトリ以下の,

#{フォーム名}(File responses)/#{質問名}(File responses)に保存されている.

ここで注意しないといけないのは,アップロードされたファイルは元のファイル名に加えアップロードした人の名前が追加されていることである.

fugaというユーザがhoge.zipというファイルをアップした際,

ファイル名はhoge - fuga.zipとなるため注意が必要である.

zipファイルの解凍,採点者ごとの担当を決めフォルダの再配置が自動で行われる / 採点用シートを自動作成

GASにはunzipcreateFileなど,Drive上でファイルを扱うメソッドが用意されていて簡単だった.

振り分け後,担当者と学籍番号のペアを返しスプレッドシートに書き込む.

f:id:cohalz:20180413225147p:plain
残りの点数とコメントを担当者がそれぞれ書き込む

採点完了後に踏むことでcsv書き出し,ダウンロードができるリンクを作成

ここはhookではなくGASから「Webアプリケーションとして公開」を利用した.

固定のURLにファイル名のパラメータを付けてアクセスすることで,対応するシートの情報からcsvをエクスポートするリンクを表示する実装になっている.

ファイルダウンロードはこれを参考にした. googleappsscript.hatenablog.com

これにurlパラメータと埋め込みを利用することで動的にシートやファイルを変更できるようになった.

function doGet(e) {
  var html = HtmlService.createTemplateFromFile("dialog");
  html.sheetName = e.parameter.fileName;
  html.fileName = e.parameter.fileName + ".csv";

  return html.evaluate();
}
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script type='text/javascript'>    
      function handleDownload() {
        var sheetName = <?= sheetName ?>;
        var content = <?= export(sheetName); ?>;
        var blob = new Blob([, content ], { "type" : "text/csv"});
        document.getElementById("download").href = window.URL.createObjectURL(blob);
      }
  </script>
  </head>
  <body>
    <a id="download" href="#" download="<?= fileName ?>" onclick="handleDownload()">ダウンロード</a>
  </body>
</html>

採点シート,各採点者ごとのフォルダ,ダウンロードリンクをSlack通知

GASにはGETやPOSTを行うメソッドもあるためSlackのwebhookを用意するだけで通知ができる.

qiita.com

スプレッドシートリンクを共有する際,getUrl()では前回アクティブだったシートが開かれてしまうため,指定したいときは下のようにgidも付けてURLにする必要がある.

function getSheetUrl(sheetName){
  const spreadSheet = SpreadsheetApp.openById(globalVariables().spreadSheetId);
  const sheet = spreadSheet.getSheetByName(sheetName);

  return spreadSheet.getUrl() + '#gid=' + String(sheet.getSheetId());
}

また,GASには環境変数がないため,分離したいものを以下のようにまとめて参照している.

function globalVariables(){
  return {
    spreadSheetId: '',
    rootFolderId: '',
    webappUrl: '',
    slackUrl: ''
  }
}

動作

フォームからzipファイルをアップロードすることでこのようなSlack通知が流れるようになる.

f:id:cohalz:20180413224141p:plain
実装したSlack通知

雑感

サーバレスというとLambdaが話題になりがちだけど,スプレッドシートやフォーム,ドライブを簡単に扱えるGASでも色々できそう.

それに加えて引き継ぎが必要な小規模グループだとAWSはアカウントや課金周りで相性悪い気がする. github.com

GASを使ってグルーヴコースター設置店舗マップを半自動で取得・更新できる仕組みを作った

以前からグルコスの設置店舗をGoogle Mapで見られるマイマップを管理していた. drive.google.com

リストの管理はマップ上から手動で行っていて,公式情報と照らし合わせることや,編集が難しいことがあり更新が間に合っていない状態だった.

マイマップのインポートはスプレッドシートから行えることを知り,Google Apps Script(通称GAS)を使うことで,スプレッドシートを自動で更新できるのではないかと考えた.

データは定期的に更新したいので,下の記事を参考にLamdaからAPI経由でGASを叩くようにしてみる.

qiita.com

その途中http://groovecoaster.jp/locationPythonスクレイピングしていると,店舗データがjsonで提供されていることに気付く.

ここでGASでjsonどう扱えるのか調べてみるとGAS自体にUrlFetchApp.fetchというGETを行えるメソッドを発見し,GASのみでリストの取得・更新ができるようになった.

ところがマイマップはインポートしたスプレッドシートと同期するわけではなく更新ができない.更新するには新しくレイヤを作ってインポートをするという手間が挟まる上,レイヤの見た目変更も手間が多く間違えやすい.

しかし,マイマップAPIも昔に終了しているのでここの自働化はひとまず諦め,GASからウェブアプリケーションを作り,

  1. URLにアクセスする
  2. スプレッドシートの更新が走る
  3. 更新終了後,マイマップのリンクと更新手順を表示する

という方法で半自動更新を実現した.アクセスすると下のように表示される.

f:id:cohalz:20180402194141p:plain

手動で更新してた頃に比べ台数の情報も確認できるようになったので自分でも便利に感じる.

ちなみに更新頻度やグルコス公式の鯖の負荷を考え,URL及びスプレッドシートは非公開で自分のみアクセス許可にしている.

作ったGASはこちら.

Groove-Coaster-Tool/reload.gs at master · cohalz/Groove-Coaster-Tool · GitHub