ドシロウトがGASでmicro:bit情報のキュレーションはてなブログを作ってみた

## ０．初めに

私はエンジニアではないただのドシロウトです。

最近、Google Apps Scriptを利用しています。とても簡単で便利。

> Overview of Google Apps Script
> https://developers.google.com/apps-script/overview

便利なので自分が気になる情報のキュレーションに使えないかと考えました。気になっているのは超小型マイコンのmicro:bit。

そこで以下の情報ソースからmicro:bitの検索結果を取得してはてなブログに1日1回自動投稿するGASのスクリプトを作ってみました。

* Googleサーチ
* youtube
* ツイッター
* kobo
* はてなブックマーク

たくさんの方の記事を参考にしたのでツギハギな感じになっています…(^_^;)
しかし、これだけ情報を集められれば、キュレーションサイトとして便利だろうと思って作りました。

## １．作ったもの

最終的に作ったはてなブログのキュレーションサイトが以下です。

> micro:bit情報
> https://micro-bit.hatenablog.com/

![はてなブログ](https://i.imgur.com/h2edBl7.jpg)

基本的なGASの仕組みは以下です。

1. トリガー機能で1日1回指定時間帯に実行
1. 情報源を検索
1. 検索結果をはてな記法で編集　
1. 編集結果をはてなブログ投稿アドレスへメール送信

## ２．コード

コード全体は沢山の情報源を対象にしていてちょっと長いのでGistに貼りました。

> hetenamicoro.gs
> https://gist.github.com/basictomonokai/bbfe4a5f79e6a1f5c93b4511ea66f601

以下に各情報源毎に説明します。

## ３．Googleサーチ

Googleサーチ自体は検索APIを提供していません。但しGoogle Custom Search APIというものがあり、これをGASで利用している記事がありました。

> Google Custom Search APIを使ってGoogle検索をやってみる（GAS）
> https://readmaster.net/think-issue-memo/prog/google-custom-search-api

更にPythonの記事ですがGoogle Custom Searchの登録や設定についての記事もありました。

> Google Custom Search APIを使って画像の自動収集
> https://blog.wackwack.net/entry/2017/12/26/211044 

Google Custom Searchで検索結果を取得するコード（関数）は以下となります。

各情報源とも共通ですが、はてなブログ固有のはてな記法で取得結果を編集しています。また、情報源毎に絵文字を付けたかったので編集の中で数値参照で絵文字を付けています。

> はてな記法一覧
> http://hatenadiary.g.hatena.ne.jp/keyword/%E3%81%AF%E3%81%A6%E3%81%AA%E8%A8%98%E6%B3%95%E4%B8%80%E8%A6%A7


```JavaScript:gcsget
function gcsget(p1){
  //Google カスタム検索
  //検索ワードセット
  var word = p1;
  
  var apiKey = 'ggggggggg'; //apiキー
  var apiUrl = 'https://www.googleapis.com/customsearch/v1/';
  var query  = '?q='+ encodeURIComponent(word);
  query += '&cx=' + 'bbbbbbbb'; //カスタム検索エンジンID
  query += '&key='+apiKey;
  // 日付順
  query += '&sort=date';
  // 日本語
  query += '&lr=lang_ja';
  
  
  apiUrl = apiUrl + query;
  
  var response = UrlFetchApp.fetch(apiUrl);
  if (response.getResponseCode() !== 200) {
    throw new Error('エラーのため取得できませんでした');
  } else {
    var res =  JSON.parse(response);
  }
 
  //データを取得する
  try {

    //検索合計数を取得する
    var totalResults =  res.queries["request"][0]["totalResults"].toString();
   
    //ログ出力
    Logger.log('合計検索数:'+totalResults);
    Logger.log('表示可能検索数:'+res.items.length);
    //タイトル名,URL,概要取得
    var out = '';
    for (var i = 0; i < res.items.length; i++) {
      out += '*'+res.items[i]["title"].toString()+'（&#128269;）\n';
      out += 'URL:'+res.items[i]["link"].toString()+'\n';
      out += '概要:'+res.items[i]["snippet"].toString().replace(/\n/g,'').replace(/</g,'＜').replace(/>/g,'＞')+'\n\n';
    }

    // Logger.log(out);
    return out;

  } catch(e) {
    return e.toString();
  }

}
```

## ４．YouTube

YouTube動画検索のGASの記事は見つけられなかったのでJSの記事を参考にしました。APIキーの取得についても記事中に書かれています。

> YouTube Data API v3.0 + JavaScriptで動画検索
> https://qiita.com/akihiro1977/items/55e0007d3b85f9b95223

YouTubeで検索結果を取得するコード（関数）は以下となります。

```JavaScript:youtubesr
function youtubesr() {
  // youtube検索
  var url1 = 'https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=';
  var url2 = encodeURIComponent('micro:bit');
  var url3 = '&order=date&key=ccccccccccccc';
  var url = url1 + url2 + url3;
  var jsonYoutube = UrlFetchApp.fetch(url).getContentText();
  // Logger.log(jsonYoutube);
  var video = JSON.parse(jsonYoutube);
  // Logger.log(video.items.length);
  youtube = '';
  
  //　取得件数は最大5件
  for (var i=0; i < video.items.length; i++) {

    //タイトル名,画像,URL,概要取得
    // はてな記法　* 見出し　[～:] 画像
    youtube += '*'+video.items[i].snippet.title+'（&#128250;）\n';
    youtube += '[https://i.ytimg.com/vi/' + video.items[i].id.videoId + '/default.jpg:image]'+'\n';
    youtube += 'URL:'+'https://www.youtube.com/watch?v='+video.items[i].id.videoId+'\n';
    youtube += '概要:【'+video.items[i].snippet.channelTitle+'】'+video.items[i].snippet.description+'\n\n';

  }
  return youtube;

}
```

## ５．ツイッター

各情報源の中で一番面倒だったのがツイッターです。GASでツイッターAPIを扱う記事はありましたが検索してる記事はありませんでした。GASのライブラリーを使ってツイッターAPIを利用する方法です。

> Google Apps ScriptでTwitter APIを叩く
https://qiita.com/abeyuya/items/3863171d06b641cac9a8

> Google Apps Script で Twitter を使ってみる
> https://qiita.com/Ikanogo/items/1dce33c26559eac56a03

更にツイッターはアプリケーション登録も厳しくなっています。

> 【2018年】TwitterのAPIに登録し、アクセスキー・トークンを取得する具体的な方法
> https://momokogumi.com/twitter-api

更にツイッターAPI公式ページで検索(search)の使い方を確認しました。

> Search Tweets 
> https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets.html

ツイッターで検索結果を取得するコード（関数）は以下となります。getTwitterServiceで認証してtwsrchで検索する形になります。


```JavaScript:twsrchget+TwitterService
function twsrch() {
  // ツイッター検索
  // oauth1ライブラリ呼び出し
  var service = getTwitterService();
  if (service.hasAccess()) {
    var twitterSearchUrl1 = 'https://api.twitter.com/1.1/search/tweets.json?q=';
    var twitterSearchWord = encodeURIComponent('\"micro:bit\"');
    var twitterSearchUrl2 = '&lang=ja&result_type=recent';
    var twitterSearchUrlAll = twitterSearchUrl1 + encodeURI(twitterSearchWord) + twitterSearchUrl2; 
    var res = service.fetch(twitterSearchUrlAll);
    var json = JSON.parse(res)
    Logger.log(json.statuses.length);
    var twkekka = '';
    for (var i=0; i < json.statuses.length; i++) {

      // ユーザー名,ツイートURL,ツイート取得
      // はてな記法　* 見出し　[～:] 画像
      twkekka += '*'+json.statuses[i].user.name+'（&#128038;）\n';
      twkekka += 'ツイートURL:'+'https://twitter.com/i/web/status/'+json.statuses[i].id_str+'\n';
      twkekka += 'ツイート:'+json.statuses[i].text+'\n\n';
  
  
    };
    return twkekka;
  } else {
    var authorizationUrl = service.authorize();
    Logger.log('Open the following URL and re-run the script: %s',
        authorizationUrl);
  }  
}



function getTwitterService() {
  return OAuth1.createService("Twitter")
    .setAccessTokenUrl("https://api.twitter.com/oauth/access_token")
    .setRequestTokenUrl("https://api.twitter.com/oauth/request_token")
    .setAuthorizationUrl("https://api.twitter.com/oauth/authorize")
    .setCallbackFunction('authCallback')
    .setConsumerKey("ddddddddd")
    .setConsumerSecret("eeeeeeeeeee")
    .setPropertyStore(PropertiesService.getUserProperties())
    .setAccessToken("fffffffff", "gggggggggg"); // これだよ！！
};
```

## ６．kobo

koboの書籍検索のGASの記事は見つけられなかったので自分で調べました。APIの情報は公式ページを参考にしました。

> 楽天Kobo電子書籍検索API
> https://webservice.rakuten.co.jp/api/koboebooksearch/

koboで書籍検索結果を取得するコード（関数）は以下となります。

```JavaScript:kobosr
function kobosr() {
  // kobo書籍検索
  var url1 = 'https://app.rakuten.co.jp/services/api/Kobo/EbookSearch/20140811?format=json&keyword=';
  var url2 = encodeURIComponent('micro:bit');
  var url3 = '&language=JA&hits=5&sort=-releaseDate&applicationId=hhhhhhhhh';
  var url = url1 + url2 + url3;
  var jsonKobo = UrlFetchApp.fetch(url).getContentText();
  // Logger.log(jsonKobo);
  var book = JSON.parse(jsonKobo);
  Logger.log(book.Items.length);
  
  var kobo = '';
  
  //　取得件数は最大5件
  for (var i=0; i < book.Items.length; i++) {

    //タイトル名,画像,URL,著者,概要取得
    // はてな記法　* 見出し　[～:] 画像
    kobo += '*'+book.Items[i].Item.title+'（&#128214;）\n';
    kobo += '['+ book.Items[i].Item.smallImageUrl.replace('?_ex=64x64','') +':image:w64]'+'\n';
    kobo += 'URL:'+book.Items[i].Item.itemUrl+'\n';
    kobo += '概要:【'+book.Items[i].Item.author+' 著】'+book.Items[i].Item.itemCaption+'\n\n';

  }
  return kobo;

}
```

koboの表紙画像のURLは画像サイズパラメータ（?_ex=64x64）付ですが、はてなブログではうまく表示できなかったのでパラメータを外してはてな記法（:image:w64）を追加しています。

## ７．はてなブックマーク

はてなブックマークの検索は検索RSSを取得しています。参考記事は以下です。とても丁寧な説明記事でした。

> TypetalkとGoogle App Scriptを組み合わせて、RSSのBOTを作ってみる。
> https://www.actzero.jp/developer/report-21885.html

fetchTodayPostsでRSSを受信し、hatenabookで受信結果を編集しています。

```JavaScript:hatenabook+fetchTodayPosts
function hatenabook() {
  var rss = 'http://b.hatena.ne.jp/search/text?q=micro%3abit&mode=rss&users=10&sort=recent';
  var resrss = fetchTodayPosts(rss);
  var out = '';
  // Logger.log(resrss[0][0]);
  var reshatenacnt = 5;
  if (resrss.length < 5) {
    reshatenacnt = resrss.length;
  };
  for (var i = 0; i < reshatenacnt; i++) {

    // 件名,URL,概要取得
    // はてな記法　* 見出し
    out += '*'+resrss[i][0].toString()+'（&#128209;）\n';
    out += 'URL:'+resrss[i][1].toString()+'\n';
    out += '概要:'+resrss[i][2].replace(/\n/g,'').replace(/</g,'＜').replace(/>/g,'＞')+'\n\n';
  }
  return out;

}

function fetchTodayPosts(feedUrl) {
  var response = UrlFetchApp.fetch(feedUrl);
  var rssXML   = response.getContentText();
  var document = XmlService.parse(rssXML);
  var root     = document.getRootElement();
  var ns_rss   = XmlService.getNamespace('http://purl.org/rss/1.0/');
  var ns_dc    = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
  var ns_atom  = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  var ns_rdf   = XmlService.getNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
  var rootTagName = root.getName().toLowerCase();
  var entries = [];
 
  // 記事のリストを取得
  switch (rootTagName) {
    case 'rdf': // 1.0
      entries = root.getChildren('item', ns_rss);
      break;
    case 'feed': // atom
      entries = root.getChildren('entry', ns_atom);
      break;
    case 'rss': // 2.0
      entries = root.getChild('channel').getChildren('item');
      break;
    default:
      return false;
  }
  return entries.map(function(entry, index) {
    var title;
    var link;
    var desc;
    var pubDate;
    switch (rootTagName) {
      case 'rdf': // 1.0
        title   = entry.getChild('title', ns_rss).getText();
        link    = entry.getChild('link', ns_rss).getText();
        desc    = entry.getChild('description', ns_rss).getText();
        pubDate = entry.getChild('date', ns_dc).getText();
        break;
      case 'feed': // atom
        title   = entry.getChild('title', ns_atom).getText();
        link    = entry.getChild('link', ns_atom).getAttribute('href').getValue();
        desc    = entry.getChild('description', ns_rss).getText();
        pubDate = entry.getChild('updated', ns_atom).getText(); 
        break;
      case 'rss': // 2.0
        title   = entry.getChild('title').getText();
        link    = entry.getChild('link').getText();
        desc    = entry.getChild('description', ns_rss).getText();
        pubDate = entry.getChild('pubDate').getText();
        break;
    }
    return [title,link,desc];
  }).filter(function(item) {
    return item !== '';
  });
}
```

## ８．まとめ

もう少し、検索条件を見直す必要はあるかもしれませんが300行以下のコードで５つの情報源から情報がキュレーションできるのはお手軽かなと思います。

さらにGASのトリガー機能で1日1回実行すれば毎日更新のキュレーション情報ブログの出来上がりです。

私は以前に全く同じスクリプトでGoogle Apps Scriptのキュレーション情報ブログも作っています。

> Google Apps Script情報
> https://google-apps-script.hatenablog.com/

もっと早く、記事を書きたかったのですがmicro:bitのツイッター検索した際に認証エラーのようなメッセージが返却され原因不明だったので記事にできませんでした。

1か月ほど試行錯誤した結果、認証エラーではなく、単なる構文ミス（encodeURIComponent漏れ）とわかったのでやっと記事にできました。

GASって本当に便利だと思います。


以　上