TwitterのツイートにURLがあったら「はてなブックマーク」にブックマーク追加する仕組みをPythonで実装してAWS Lambdaで動してみた

2018年末、はてなブックマークで便利だった外部サービス連携「Twitterからブックマークを追加」サービスが終了しました。Qiita投稿時にツイートするとはてなブックマークにもブックマーク追加してくれて良かったのに。。。悲しい。。。

はてなブックマークがIDコール廃止。外部サービス連携も順次終了 - Impress Watch
https://www.watch.impress.co.jp/docs/news/1150477.html
>「Twitterからブックマークを追加」や「mixi連携」、「Evernote連携」、「Facebook連携」などの外部サービス連携も11月初旬に終了。また、毎日、指定の時刻にマイホットエントリーをメールで送信する「マイホットエントリーメール」、お気に入りフィードを「はてなメッセージ」で送信する「お気に入りレポート」も終了する。

終了してからは手動でブックマーク追加していたのですが、他にも気になったサイトがあったときにツイートするかブックマークするか無駄に悩んだりすることが増えてきて面倒になったので、連携する仕組みを実装してみました。


## はてなブックマークAPIがつかえる

はてなブックマークにAPIがあるのか調べてみたらありました。下記記事でブックマーク追加するサンプルもあげてくれているので参考にしました。(感謝

はてなブックマークを #API で追加する #python スクリプトの例 - Qiita
https://qiita.com/YumaInaura/items/0641702719fef0260d8e


APIに関しては公式ドキュメントが参考になります。

はてなブックマーク ブックマーク API - Hatena Developer Center
http://developer.hatena.ne.jp/ja/documents/bookmark/apis/rest/bookmark#post_my_bookmark_parameter_comment

OAuth認証を利用するには`Consumer key` を取得する必要がありますので、下記ドキュメントを参考にします。

Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center
http://developer.hatena.ne.jp/ja/documents/auth/apis/oauth/consumer

APIを利用して自分のアカウント情報にアクセスするにはOAuth認証でアクセストークンを取得する必要があります。
下記ライブラリでさくっと取得できそうでしたがエラー発生したため、公式にあるRuby実装サンプルを利用してアクセストークンを取得しました。

はてなブックマーク Web APIを手軽に使えるgemを作った - Qiita
https://qiita.com/t__nabe/items/7918b6c6232eb14c248e

```console:ダメだった(´・ω・`)
> gem install hatena_bookmark_client_for_ruby

> get_hatebu_access_token <YOUR_CONSUMER_KEY> <YOUR_CONSUMER_SECRET>

/Library/Ruby/Gems/2.3.0/gems/oauth-0.5.4/lib/oauth/consumer.rb:197:in `request': parameter_rejected (OAuth::Problem)
        from /Library/Ruby/Gems/2.3.0/gems/oauth-0.5.4/lib/oauth/consumer.rb:214:in `token_request'
        from /Library/Ruby/Gems/2.3.0/gems/oauth-0.5.4/lib/oauth/consumer.rb:155:in `get_request_token'
        from /Library/Ruby/Gems/2.3.0/gems/hatena_bookmark_client_for_ruby-0.1.3/exe/get_hatebu_access_token:27:in `<top (required)>'
        from /usr/local/bin/get_hatebu_access_token:22:in `load'
        from /usr/local/bin/get_hatebu_access_token:22:in `<main>'
```

```console
> touch oauth_consumer.rb
# 公式の「サンプルコード Ruby版」をコピペ
# YOUR_CONSUMER_KEYとYOUR_CONSUMER_SECRETを書き換える

> gem install sinatra

> ruby oauth_consumer.rb

[2019-05-18 14:07:10] INFO  WEBrick 1.3.1
[2019-05-18 14:07:10] INFO  ruby 2.3.7 (2018-03-28) [universal.x86_64-darwin18]
== Sinatra (v2.0.5) has taken the stage on 4567 for development with backup from WEBrick
[2019-05-18 14:07:11] INFO  WEBrick::HTTPServer#start: pid=29898 port=4567
```

http://localhost:4567 にアクセスして、はてなブックマークでアクセスを許可してトークンを取得します。

<img width="200" alt="スクリーンショット 2019-05-18 14.07.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/8249dfd5-07c8-3648-ffc2-7b6241e27889.png"> <img width="200" alt="スクリーンショット 2019-05-18 14.07.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/b8bd073d-cb36-7371-45a4-b70ce8dfde03.png"> <img width="300" alt="localhost_4567_oauth_callback_oauth_token_UC_2B7f_2BcIDSYrpg_3D_3D_oauth_verifier_ZwugnXywhKzEClwAwarZLkzYのコピー.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/8faa22ae-8fc4-eab9-5701-880b25ee4802.png">


## Twitter APIを利用してツイートを取得する

Twitter APIの利用に関しては下記を参考にしました。

Pythonでサクッと簡単にTwitterAPIを叩いてみる - Qiita
https://qiita.com/ogrew/items/0b267f57b8aaa24f1b73


Twitter APIを利用するために必要となるアプリ登録とアクセストークン取得については以前に行ってたので今回はスキップします。下記が参考になります。

Twitter REST APIの使い方
https://syncer.jp/Web/API/Twitter/REST_API/

以前にもTwitter APIを利用したことがあり、ツイートを検索する際に、`since:` と`until:` を利用して期間指定できることを知ったので、それを利用して取得するツイートを絞り込みます。日時のフォーマットを`2019-05-18_12:34:56_JST` と指定することで秒単位まで指定可能です。

Twitterの検索で時間を含む期間指定をする方法 - Qiita
https://qiita.com/kai_kou/items/336a9d1290c13abba7e2

ユーザーを指定するには`from:` が利用できるので、こんな感じのクエリで検索します。

Twitter検索：特定のユーザー、アカウント内を指定して検索する方法
https://appbu.jp/twitter-search-from-specific-user

`from:k_aik_ou since:2019-05-18_12:34:56_JST until:2019-05-18_13:45:01_JST`


## Pythonで実装する

はてなブックマーク APIとTwitter APIが利用できることと必要となるキーが取得できたので、雑にPythonで実装します。`consumerKey` 、`consumerSecret` 、`accessToken` 、`accessTokenSecret` は各自のに置き換えてください。

```python:main.py
import json
import requests
from requests_oauthlib import OAuth1
from requests_oauthlib import OAuth1Session
import datetime,time,sys
import re


def tweet_to_hatebu():
    # はてなブックマーク
    hatena_auth = OAuth1(
      "consumerKey",
      "consumerSecret",
      "accessToken",
      "accessTokenSecret"
    )

    # Twitter
    twitter_session = OAuth1Session(
      "consumerKey",
      "consumerSecret",
      "accessToken",
      "accessTokenSecret"
    )

    # APIのエンドポイント
    bookmark_api_url = "http://api.b.hatena.ne.jp/1/my/bookmark"
    twitter_api_url = "https://api.twitter.com/1.1/search/tweets.json"


    # ツイート取得する期間を指定する
    # from_atは定期実行する間隔に合わせて調整する
    now = datetime.datetime.now()
    from_at = now - datetime.timedelta(minutes=10)
    from_at_str = from_at.strftime("%Y-%m-%d_%H:%M:%S_JST")
    to_at_str = now.strftime("%Y-%m-%d_%H:%M:%S_JST")

    # 取得件数は適当に指定(最大100ツイート)
    params = {
      "q": "from:ツイッターのユーザー名 since:" + from_at_str + "until:" + to_at_str,
      "lang": "ja",
      "result_type": "recent",
      "count": 100,
    }

    # 指定した条件のツイートが取得できるまでAPIを叩く
    statuses = []
    while True:
      res = twitter_session.get(twitter_api_url, params = params, verify=True)
      res_text = json.loads(res.text)
      if len(res_text["statuses"]) == 0:
        break

      # Twitter APIの利用制限を超えたらヤメ
      if res.headers["X-Rate-Limit-Remaining"] == 0:
        break

      statuses.extend(res_text["statuses"])
      next_max_id = res_text["statuses"][-1]["id"] - 1
      params["max_id"] = next_max_id

    # ツイート時間でソートするなど
    statuses = sorted(statuses, key=lambda x: x["created_at"])

    for tweet in statuses:
        # 画像を含むツイートは無視
        if "media" in tweet["entities"]:
          print("media")
          continue

        # ながいつぶやきは無視
        if tweet["truncated"]:
          print("truncated")
          continue

        # ツイートテキストからURLを取得する
        text = tweet["text"]
        url_pattern = r"(https?)(:\/\/[-_\.!~*\"()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)"
        matchOB = re.search(url_pattern, text)

        # URLを含めずにブックマークにコメントする
        comment = re.sub(url_pattern, "", text)

        # リツイートだったらコメントは無視
        rt_matchOB = re.search(r"RT ", text)
        if rt_matchOB:
          comment = ""

        # URLがあったらコメントを添えてブックマーク追加する
        if matchOB:
          bookmark_url = matchOB.group()
          print(requests.post(bookmark_api_url + "?url=" + bookmark_url + "&comment=" + comment, auth= hatena_auth).content)


tweet_to_hatebu()
```

必要なライブラリをインストールして実行するとツイート取得してブックマーク追加することができます。

```python
> pip install requests requests_oauthlib
> python main.py
```

<img width="603" alt="スクリーンショット 2019-05-18 14.47.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/54561093-fea6-b344-3e56-d64db2a44c77.png">
<img width="543" alt="スクリーンショット 2019-05-18 13.56.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/dbfb3c8a-eba8-2530-db6a-646946414240.png">


## AWS Lambdaを利用して定期実行させる

実装できましたがこのままだと手動でスクリプト実行しなきゃいけないので、AWS Lambdaに関数を登録して定期実行させます。

### 前提

- AWSアカウントがある
- AWS Lambda関数作成権限がある

### 実装の手直し

AWS Lambdaで実行できるように実装を手直しします。`tweet_to_hatebu` メソッドの実装は変わらずです。

```console
> mv main.py lambda_function.py
```

```python:lambda_function.py
import json
import requests
from requests_oauthlib import OAuth1
from requests_oauthlib import OAuth1Session
import datetime,time,sys
import re

def lambda_handler(event, context):
    tweet_to_hatebu()
    
    return {
        "statusCode": 200,
        "body": json.dumps("Hello from Lambda!")
    }

def tweet_to_hatebu():
(略)
```

### ZIPにまとめてアップロードする

こちらを参考に必要となるライブラリと実装をZIPにまとめてアップロードしてAWS Lambda関数を作成します。

【Python】AWS Lambdaで外部モジュールを使用する - Qiita
https://qiita.com/SHASE03/items/16fd31d3698f207b42c9

```console
> pip install requests requests_oauthlib -t ./
> zip -r tweet2hatebu.zip ./*
```

ZIPファイルアップロード後は、ブラウザ上でファイル編集できるようになります。
<img width="1114" alt="Lambda_Management_Consoleのコピー.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/2b30c91d-471b-0b54-654e-5f21c95cf150.png">

このままだと`datetime.datetime.now()` で日時を取得したときにUTC時間となるので関数の環境設定に`TZ` 変数を追加します。値は`Asia/Tokyo` です。

AWS Lambdaのタイムゾーン変更 - Qiita
https://qiita.com/nullian/items/39ecf1f6d0194b72e8e6
<img width="974" alt="スクリーンショット 2019-05-18 15.12.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/552dcbab-a246-d64e-f20b-f649fd2731b9.png">

保存してからテストして動作する確認します。
<img width="1163" alt="スクリーンショット 2019-05-18 13.56.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/d246dc1f-2b17-5e92-d6c3-28d63eb39665.png">

Amazon CloudWatch Eventを利用して定期的に関数が実行するようにします。ルールはお好みで。
<img width="1305" alt="スクリーンショット 2019-05-18 13.58.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/96c5abb0-d84a-2b94-2e96-d5dbd56be101.png">
<img width="1302" alt="スクリーンショット 2019-05-18 13.59.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/2bf6f9a6-5919-78e5-01ce-a504c84cfac3.png">

これで、定期的にツイートに含まれるURLをはてなブックマークにブックマーク追加する仕組みが出来上がりました。
<img width="1161" alt="スクリーンショット 2019-05-18 14.00.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/bc9aeb84-ac82-ba46-7eb1-941e6f1a2413.png">
やったぜ。


## まとめ

調査から実装してAWS Lambdaで動かせるようにするまで2時間くらいでできました。
安定稼働させるには各APIの利用制限などを考慮した実装にする必要があるかと思いますが、個人で利用する分にはこれくらい雑な実装でも良いかなと思います。


## 参考

はてなブックマークがIDコール廃止。外部サービス連携も順次終了 - Impress Watch
https://www.watch.impress.co.jp/docs/news/1150477.html

はてなブックマークを #API で追加する #python スクリプトの例 - Qiita
https://qiita.com/YumaInaura/items/0641702719fef0260d8e

はてなブックマーク ブックマーク API - Hatena Developer Center
http://developer.hatena.ne.jp/ja/documents/bookmark/apis/rest/bookmark#post_my_bookmark_parameter_comment

Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center
http://developer.hatena.ne.jp/ja/documents/auth/apis/oauth/consumer

はてなブックマーク Web APIを手軽に使えるgemを作った - Qiita
https://qiita.com/t__nabe/items/7918b6c6232eb14c248e

Pythonでサクッと簡単にTwitterAPIを叩いてみる - Qiita
https://qiita.com/ogrew/items/0b267f57b8aaa24f1b73

Twitter REST APIの使い方
https://syncer.jp/Web/API/Twitter/REST_API/

Twitterの検索で時間を含む期間指定をする方法 - Qiita
https://qiita.com/kai_kou/items/336a9d1290c13abba7e2

Twitter検索：特定のユーザー、アカウント内を指定して検索する方法
https://appbu.jp/twitter-search-from-specific-user

AWS Lambdaのタイムゾーン変更 - Qiita
https://qiita.com/nullian/items/39ecf1f6d0194b72e8e6