# 自分だけのニュースアプリを作ってみよう! ニュースアプリの機能をつくってみる

自分だけのニュースアプリ

# この記事

  1. ニュースアプリが「こうだったらいいな」を考える
  2. ニュースアプリの画面を考える
  3. ニュースアプリに機能を考える
  4. ニュースアプリの画面をつくってみる
  5. ニュースアプリの機能をつくってみる ←⭐この記事
  6. ニュースアプリを使ってみる

# 目次

# 機能作成の進め方

はじめに ニュースアプリに機能を考える#機能一覧 で検討した機能を再掲します。

ニュースアプリを実際に使うユースケースを整理すると、

  1. ユーザがアプリの画面でキーワードを登録する
  2. システム(アプリ)は、登録されたキーワードをもとにニュース情報を取得する
  3. システム(アプリ)は、取得したニュース情報を登録する
  4. ユーザはアプリ画面でニュース情報を確認する

となります。

今回は、この順番で処理を作成していきます。

# 処理フローの全体図

自分だけのニュースアプリ:処理フロー全体図

# 機能一覧

処理 処理ステップ
①キーワード登録/削除 画面で登録されたキーワードを登録
画面で登録されたキーワードを削除
②ニュース取得 NewsAPIへのリクエスト情報を用意する
NewsAPIにリクエストする
NewsAPIからの戻り値を取得する
③ニュース登録/削除 一定期間経過した過去のニュースを削除する
取得したニュースを登録する
④ツイート Twitterのインスタンスを取得する
ニュースをツイートする

# Google Apps Script の準備

これから処理を実装するために、Google Apps Scriptの開発環境を立ち上げていきます。 Glideを作成した際のGoogleアカウントで、Google Driveにアクセスします。

Google Drive (opens new window)

Google Driveの右上にある「新規+」を押し、ドロップダウンメニューから 「Google Apps Script」を選択します。

自分だけのニュースアプリ:処理フロー全体図

新しく「Google Apps Script」が作成されます。 myFunctionは新規作成時にデフォルトで作成されます。 (使ってもいいですし、削除しても問題ないです。)

自分だけのニュースアプリ:処理フロー全体図

これから作成する機能の記載個所にコメントを追加しておきます。

自分だけのニュースアプリ:処理フロー全体図

# メイン関数 myFunction

これから、機能を関数(Function)に分割して実装してきます。 そのメリットの1つが、全体処理を見通しやすくなり、保守性が向上するためです。

「分割して統治せよ」

ですね。今回はメインの処理をmyFunctionに書きました。

  1. 必要な設定情報「NewsAPIのアクセスキー」「Google Spread SheetのID」を設定
  2. get_keywords:検索キーワードを取得
  3. deleteNews:過去記事の削除(7日分は残こす)
  4. GetNewsFromAPI:検索キーワードを毎にニュースを取得
  5. addNews:検索キーワードを毎にスプレッドシートに保存

メイン処理としては20行ほどですので、1つずつ理解すれば何とかなるコード量かと思います。

今回のソースは全体で300行弱になります。それを上から全てコードリーディングするよりは、 機能ごとに関数化して、メイン処理で全体像が見渡せるほうが「この処理が何をしたいのか」が 理解しやすいと思います。

//**********************************************************************************
//グローバル変数
//**********************************************************************************

//NewsAPIのアクセスキー
NEWSAPIKEY = "*******************************";

//Google Spread Sheet ID
GoogleSpreadSheetID = "*****************************************************"

//①ニュース検索キーワード
var NEWS_KEYWORDS = get_keywords(); 
PropertiesService.getScriptProperties().setProperty('NEWS_KEYWORDS', NEWS_KEYWORDS);

function myFunction() {
  //③記事削除(7日分は残す)
  deleteNews(7);
  // 検索キーワード分ループ
  for (var i in NEWS_KEYWORDS) {
    //②ニュースを取得
    let newsJson = GetNewsFromAPI(NEWS_KEYWORDS[i]);
    //③レスポンスが正常で、記事数が0件でなければスプレッドシートに記事追加
    if (newsJson.status =="ok" && newsJson.totalResults != 0){
      addNews(newsJson);
    }
  }
}

以下では、メイン処理から呼ばれる関数(function)の実装について、説明をしていきたいと思います。

# ①キーワード登録/削除

# 画面からキーワードを登録/削除する

早速なのですが、 ニュースを検索するときのキーワードの登録と削除ですが、実装する必要がありません。

本来なら、データベースのデータ更新はロジックとしてコーディングするものですが Glideを使うと自動でやってくれます。

この機能は、4.ニュースアプリの画面をつくってみる で作成済みとなります。

機能の動きは、以下の動画の通りです。

# ②ニュース取得

ニュースを取得する機能を実装していきたいと思います。

# NewsAPIの申請(アカウント作成)

まずはNewsAPI (opens new window)を利用するためのアカウントを作成します。申請すると直ぐに使えるようになります。

# 利用規約

ちなみにNewsAPIは無料プランでも1日100リクエストまで利用可能ですので、今回の利用用途であれば十分です。 他の利用規約は下記の通りです(2021/3現在)。

NewsAPI 利用規約 (opens new window)

  • Developerプランであれば無料で始められます。
  • 個人または商用プロジェクトで利用可能です。
  • すべての記事を検索できます。(有償版と同じ)
  • リアルタイムではありません。最大1時間くらい遅いです。(有償版はリアルタイム)
  • 過去1か月分の記事まで検索可能(有償版は3年)
  • 1日あたり100件のリクエストまで(有償版は2.5万)
  • 利用可能な追加のリクエストはありませんヘルプ
  • サービス品質保証契約はなし(有償版はあり)

ニュースアプリの機能をつくってみる#ニュースAPI準備

# アカウントを作成

では早速、アカウントを作成していきます。

NewsAPI (opens new window)にアクセスして表示される「Get API Key」をクリックします。

ニュースアプリの機能をつくってみる#ニュースAPI準備

以下の入力を求められるので、記入していき最後に「Submit」を押します。

  • First Name
  • Email address
  • password
  • you are ... (利用用途かと)

ニュースアプリの機能をつくってみる#ニュースAPI準備

画面に「Registration Complete」が表示されれば完了です。

この時、画面上に表示されるapiKeyは、この後利用することになるので、メモしておいてください。

ニュースアプリの機能をつくってみる#ニュースAPI準備

ちなみに「My account」をクリックすると、以下の画面が表示されます。 この画面で、APIキーや利用プラン、利用回数などを確認できます。

ニュースアプリの機能をつくってみる#ニュースAPI準備

# ニュース取得機能の実装

NewsAPIのAPI KEYも手に入ったので、実装を進めたいと思います。

詳細を説明する前に、今回作成するNewsAPIの呼び出すソースを掲載します。 以下のソースで、大体の内容が分かる場合は、詳細は割愛しても良いと思います。

akiKeyだけはご自身のキーに設定しなおす必要がありますので、差し替えて、 次の ③ニュース登録/削除 から進めてください。

ここでは、以下の3つのポイントを説明していきます。

  • NewsAPIの呼び出しURL組み立て
  • UrlFetchAppを使ってNewsAPIへアクセス
  • レスポンスのチェック
//**********************************************************************************
//グローバル変数
//**********************************************************************************
NEWSAPIKEY = "*******************************";

function myFunction() {
  GetNewsFromAPI("GIGAスクール")
}

//**********************************************************************************
//NewsAPIからニュースを取得
//**********************************************************************************

/**
 * 指定されたキーワードでNewsAPIからニュースを取得する
 * @param {string} [str] - 検索キーワード
 * @return {Object} - ニュース情報
 */
function GetNewsFromAPI(query){

  //NewsAPIのアカウント作成時に取得したAPIキーを設定
  let apiKey = NEWSAPIKEY;

  // 最新記事を対象としたいため、ニュース記事の投稿日を1日前までを指定する準備
  // 今日の日付を取得
  let today = new Date(); 
  // 1日前を設定
  let qday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); 
  // フォーマットをyyyy-MM-ddに設定
  let queryDay = Utilities.formatDate( qday, 'Asia/Tokyo', 'yyyy-MM-dd');
  
  // 呼び出しURL
  // パラメータの詳細はNewsAPIのリファレンス参照
  // https://newsapi.org/docs/endpoints/everything
  let url = 'http://newsapi.org/v2/everything?' +
          'q='+query+'&' +
          'from='+queryDay+'&' +
          'sortBy=relevancy&' +
          'language=jp&' + 
          'pageSize=3&' + 
          'apiKey=' + apiKey;
  
  // WEBサービスの呼び出にはUrlFetchAppを利用する
  // https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app
  // UrlFetchAppのオプション情報を作成
  let options = {
    "method" : "GET",
    "muteHttpExceptions" : true
  }
  
  // UrlFetchAppを使ってNewsAPIへアクセス
  let res = UrlFetchApp.fetch(url, options);
  //レスポンスをJSON形式に変換
  let jsonData = JSON.parse(res);
  let jsonDataRes = jsonData;
  
  // レスポンスのチェック
  if(jsonData.status !="ok"){
    console.error("[ERROR][Get Google News]レスポンスがOKでありません。");
  }else{
    if(jsonData.totalResults == 0){
      console.info("[WARN][Get Google News]ニュース記事jが0件です。");
    }else{
      // NewsAPIの戻り値に、今日の日付と検索キーワードを追加
      jsonDataRes.today = today;
      jsonDataRes.query = query;
      console.log(JSON.stringify(jsonDataRes))
    }
  }

  return jsonDataRes;
}

# NewsAPIの呼び出しURL組み立て

NewsAPIの利用ガイドを参照し、APIの使い方を確認しながら、 呼び出し用のURLを組み立てていく処理です。

  //NewsAPIのアカウント作成時に取得したAPIキーを設定
  let apiKey = NEWSAPIKEY;

  // 最新記事を対象としたいため、ニュース記事の投稿日を1日前までを指定する準備
  // 今日の日付を取得
  let today = new Date(); 
  // 1日前を設定
  let qday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); 
  // フォーマットをyyyy-MM-ddに設定
  let queryDay = Utilities.formatDate( qday, 'Asia/Tokyo', 'yyyy-MM-dd');
  
  // 呼び出しURL
  // パラメータの詳細はNewsAPIのリファレンス参照
  // https://newsapi.org/docs/endpoints/everything
  let url = 'http://newsapi.org/v2/everything?' +
          'q='+query+'&' +
          'from='+queryDay+'&' +
          'sortBy=relevancy&' +
          'language=jp&' + 
          'pageSize=3&' + 
          'apiKey=' + apiKey;

NewsAPIリファレンス (opens new window)

外部のAPIを利用する際の確認ポイントは3つです。

  1. リクエスト方法(呼び出し方)
  2. パラメータ(呼び出し時の引数)
  3. レスポンスデータ(戻り値の形)

# 1.リクエスト方法

WEBAPIを使う上でのポイントの1つがGET/POSTのどちらになるかです。 今回はGetの呼び出しでOKのようです。 ざっくりと説明すると、GetはURLに「&key=value」で情報を付与してリクエストを行います。

例:https://newsapi.org/v2/everything?q=bitcoin&apiKey=API_KEY

# 2.パラメータ

リファレンスに利用可能な呼び出しパラメータの情報がいっぱい載っています。 今回は以下を利用したいと思います。

  • apiKey
    • APIキー:必須です。アカウント作成時に取得した認証キーです。
  • q
    • 記事のタイトルと本文で検索するキーワードまたはフレーズです。
    • 高度な検索はここでサポートされています
    • 今回は使わない高度な機能もサポートしています。詳細はNewsAPIリファレンス (opens new window)
  • from
    • どこまで古い記事を検索対象とするか指定します。
    • ISO8601形式である必要があります(例:2021-03-06または2021-03-06T06:15:22)
  • language
    • 見出しを取得したい言語の2文字のISO-639-1コード
    • 日本語は「jp」
  • sortBy
  • 事を並べ替える順序
  • relevancy :気のある情報源や出版社からの記事が最初に来ます
  • pageSize
  • 1ページに返される結果の数

# 3.レスポンス

レスポンスはJSON形式で返却されます。

例:レスポンスサンプル

Example response
{
"status": "ok",
"totalResults": 9527,
"articles": [
{
"source": {
"id": "engadget",
"name": "Engadget"
},
"author": "Jon Fingas",
"title": "Tesla buys $1.5 in Bitcoin, will soon accept it as payment",
"description": "Elon Musk’s cryptocurrency hype was more than just idle talk. CNBC reports that Tesla not only bought $1.5 billion worth of Bitcoin to help “diversify and maximize” its returns, but will start taking payments using the digital asset sometime in the “near futu…",
"url": "https://www.engadget.com/tesla-to-take-bitcoin-payments-140109988.html",
"urlToImage": "https://s.yimg.com/os/creatr-uploaded-images/2021-02/389f89e0-6a11-11eb-b5c5-309f2241e56a",
"publishedAt": "2021-02-08T14:01:09Z",
"content": "Elon Musks cryptocurrency hype was more than just idle talk. CNBCreports that Tesla not only bought $1.5 billion worth of Bitcoin to help diversify and maximize its returns, but will start taking pay… [+1182 chars]"
},
]
}

大きく分けると以下の3つの情報が含まれています。

  • status: NewsAPIの呼び出し結果
  • totalResults: ニュース記事の件数
  • articles: ニュース記事情報

また、articles自体もJSON形式になっており、以下の情報が含まれています。

  • source: ニュースサイト情報(ここもJSON形式)
  • author: 記事の削除
  • title: ニュースのタイトル
  • description: ニュースの概要
  • url: 記事のURL
  • urlToImage: 記事のアイキャッチ画像
  • publishedAt: 記事の投稿日
  • content: 記事の詳細内容

# UrlFetchAppを使ってNewsAPIへアクセス

UrlFetchApp (opens new window)は、Google Apps Scriptが提供するサービスクラスです。

以下は、サービス説明の抜粋です。つまり、外部のWebAPIを呼べます。

このサービスを使用すると、スクリプトはURLをフェッチして、他のアプリケーションと通信したり、 Web上の他のリソースにアクセスしたりできます。 スクリプトは、URLフェッチサービスを使用して、HTTPおよびHTTPS要求を発行し、応答を受信できます。

optionsのmethodにはGETメッソドを指定します。 muteHttpExceptionsをtrue に指定しておくとエラーを吐かずに HttpResponse を返してくれます。 エラー時のデバッグの際に、HttpResponseの中身を見ればどんなエラーか詳細にわかります。

  // UrlFetchAppのオプション情報を作成
  let options = {
    "method" : "GET",
    "muteHttpExceptions" : true
  }
  
  // UrlFetchAppを使ってNewsAPIへアクセス
  let res = UrlFetchApp.fetch(url, options);
  //レスポンスをJSON形式に変換
  let jsonData = JSON.parse(res);
  let jsonDataRes = jsonData;

# レスポンスのチェック

NewsAPIのレスポンスのチェック でも確認した通り、成功した場合はstatusが"ok"として返却され、ニュース記事の件数がtotalResultsに入力されます。

そのためレスポンスのチェックでは、statusが"ok"でかつ、ニュース記事の件数が0件でない場合に値を返すようにしています。

  // レスポンスのチェック
  if(jsonData.status !="ok"){
    console.error("[ERROR][Get Google News]レスポンスがOKでありません。");
  }else{
    if(jsonData.totalResults == 0){
      console.info("[WARN][Get Google News]ニュース記事jが0件です。");
    }else{
      // NewsAPIの戻り値に、今日の日付と検索キーワードを追加
      jsonDataRes.today = today;
      jsonDataRes.query = query;
    }
  }

# ニュース取得機能のデバッグを実行する

実装したソースをデバッグモードで実行してみます。

実行結果が分かるように、最後にNewsAPIから取得したデータをコンソールに出力処理を追加しています。 動画の最後にニュース記事がログに出力されており、無事にNewsAPIからデータを取得できたことが分かります。

console.log(JSON.stringify(jsonDataRes))

# 初めてのデバッグ実施時は承認要求が発生

デバッグをはじめて実施すると、外部リソースへのアクセスに対して 本当に実施してよいか許可を求められるため、承認する必要があります。 (悪意のあるプログラムが動作していないかを開発者に確認するためです)

今回はNewsAPIへのアクセスを自分で実装していますので、もちろんOKです。 以下の手順の通り、Google Apps Scriptが実行できるよう承認を実施してください。

ニュースアプリの機能をつくってみる#NewsAPIデバッグ

Glideのアカウントを作成したGoogleアカウントを選択します。

ニュースアプリの機能をつくってみる#NewsAPIデバッグ

分かりづらいのですが、左下にある「詳細」をクリックします。

ニュースアプリの機能をつくってみる#NewsAPIデバッグ

「(安全ではないページ)に移動」をクリックします。

ニュースアプリの機能をつくってみる#NewsAPIデバッグ

「許可」をクリックします。

ニュースアプリの機能をつくってみる#NewsAPIデバッグ

# ③ニュース登録/削除

次は、NewsAPIから取得したニュース記事を Google Spread Sheetに保存していきます。

ここまでの説明が長くなったので、最初に確認したメイン関数「myfunction」の処理の流れを再掲します。

メイン関数-myfunction

//①ニュース検索キーワード
var NEWS_KEYWORDS = get_keywords(); 
PropertiesService.getScriptProperties().setProperty('NEWS_KEYWORDS', NEWS_KEYWORDS);

function myFunction() {
  //③記事削除(7日分は残す)
  deleteNews(7);
  // 検索キーワード分ループ
  for (var i in NEWS_KEYWORDS) {
    //②ニュースを取得
    let newsJson = GetNewsFromAPI(NEWS_KEYWORDS[i]);
    //③レスポンスが正常で、記事数が0件でなければスプレッドシートに記事追加
    if (newsJson.status =="ok" && newsJson.totalResults != 0){
      addNews(newsJson);
    }
  }
}

この「③ニュース登録/削除」では、以下の機能について実装と説明をしていきます。

  1. get_keywords:検索キーワードを取得
  2. deleteNews:過去記事の削除(7日分は残こす)
  3. addNews:検索キーワードを毎にスプレッドシートに保存

# 1. get_keywords:キーワードシートから、検索キーワードを取得

①キーワード登録-削除 では、画面から追加/削除したキーワードを、Glideが自動でGoogle Spread Sheetに反映してくれることを確認しました。

ここでは、Google Spread Sheetのキーワードを取得する処理を作成します。 キーワードは、 newsapiの呼び出しurl組み立て で説明した通り、NewsAPIの呼び出し時に「'q='+query」としてURL組み立てに使用されます。


//**********************************************************************************
//ニュースの登録・削除
//**********************************************************************************

/**
 * キーワードシートから、検索キーワードを取得
 * @param void
 * @return void
 */
function get_keywords() {
  // キーワードシートを準備
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('キーワード');
  // 最終行を取得
  let last_row = sheet.getLastRow();
  // キーワードを取得
  let range = sheet.getRange("A2:A" + last_row)
  let values = range.getValues();
  let array = [];
  //1次元配列に詰めなおし
  for(var i = 0; i < values.length; i++){
    array.push(values[i][0]);
  }
  //重複、空白除いた配列を返却
  var result = Array.from(new Set(array)).filter(String);
  return result
}

# 2.deleteNews:指定された日数より古いニュース記事を削除する

無料版のGlideは、Google Spread Sheetに保存できるデータ量(行数)に制限があります。 また古いニュースを残しておくの理由もないため、過去記事を削除する機能を作成します。


/**
 * 指定された日数より古いニュース記事を削除する
 * @param {int} ニュース記事を残す日数
 * @return void
 */
function deleteNews(days){

  // google spread sheetのIDを指定して取得
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('ニュース');

  // 指定された日数分過去の日付を取得し、タイムスタンプの形式に変換
  let today = new Date();
  let deleteDay = new Date(today.getFullYear(), today.getMonth(), today.getDate() - days);
  let deleteTimeStamp = Utilities.formatDate( deleteDay, 'Asia/Tokyo', 'yyyy/MM/dd: hhmmss');

 // ニュース記事をアプリに取り込んだ日付列(C列)を取得
  var TIMESTAPM_COL = sheet.getRange('C:C').getValues();  //C列の値を全て取得
  var lastRow = TIMESTAPM_COL.filter(String).length;  //空白の要素を除いた長さを取得

  // 削除行を初期化
  let delete_line_num = 1;
  // 過去日が

  /**
   * ニュースはスプレッドシートの下の行に追加されていくため古い記事は上の行に残ることになる。
   * そのため、指定日数以内の日付が出現する行番号を取得し、
   * 開始行~取得した行までを削除する。
   * 開始行は、1行目はニュース記事のヘッダー行のため2行目からとする。
   */
  for(let j = 2; j <= lastRow; j++) {

    // 3列目(C列)がニュース記事をアプリに取り込んだ日付列
    let getDay = sheet.getRange(j, 3).getValue();
    // フォーマットを整形する
    let getDay_fmt = Utilities.formatDate( getDay, 'Asia/Tokyo', 'yyyy/MM/dd: hhmmss')

    //日付が存在し、削除対象日より新しい日付が現れたら、その行数を取得しループを抜ける
    if(getDay_fmt){ 
      if(deleteTimeStamp < getDay_fmt){
        delete_line_num = j-1;
        break;
      }
    }
  }

  //取得した行数が1より大きれば削除を実行(1行目はヘッダー行)
  //最終行より大きくなる場合はあり得ないが、保険として条件に追加
  if(delete_line_num>1 && delete_line_num < lastRow){
    sheet.deleteRows(2, delete_line_num);
  //すべてが過去記事の場合
  }else if(delete_line_num==1 && lastRow > 2){
    sheet.deleteRows(2, lastRow - 1);
  }
}

# 3.addNews:ニュース記事の追加

NewsAPIから取得したニュース記事をGoogle Spread Sheetに追記します。 同じ記事は追加しないように、ニュースのURLが同じであれば追加しないようにします。 また、記事の削除はGoogle Spread Sheetの上から行われるため、新しい記事は下に追加していきます。

/**
 * ニュース記事の追加
 * @param {Object} ニュース情報
 * @return void
 */
function addNews(newsJson){
  
  // google spread sheetのIDを指定して取得
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('ニュース');

  // ニュースのユニークキーとしてURLを使う。URLはE列に格納している。
  // URLを全て取得し、空白の要素を除いた長さを取得。これを最終行とする。
  let URL_COL = sheet.getRange('E:E').getValues();
  let lastRow = URL_COL.filter(String).length;

  //最終行以降に新しい記事を追加する行
  let count = 1;
  //既存記事URLの配列を作成
  let URL_COL_ARRAY = convertTwoDimensionToOneDimension(URL_COL,1);

  // ニュース記事Jsonを取得
  let articles = newsJson.articles;

  //記事数分ループしてニュースを作成
  for (var i in articles) {
    //既に追加済みの記事は追加しない(同じURLの記事は追加しない)
    if(URL_COL_ARRAY.indexOf(articles[i].url) == -1){
      sheet.getRange(lastRow+count, 1).setValue(lastRow+count-1);
      sheet.getRange(lastRow+count, 2).setValue(0);
      sheet.getRange(lastRow+count, 3).setValue(newsJson.today);
      sheet.getRange(lastRow+count, 4).setValue(articles[i].title);
      sheet.getRange(lastRow+count, 5).setValue(articles[i].url);
      sheet.getRange(lastRow+count, 6).setValue(newsJson.query);
      sheet.getRange(lastRow+count, 7).setValue(articles[i].author);
      sheet.getRange(lastRow+count, 8).setValue(articles[i].description);
      sheet.getRange(lastRow+count, 9).setValue(articles[i].urlToImage);
      sheet.getRange(lastRow+count, 10).setValue(articles[i].publishedAt);
      sheet.getRange(lastRow+count, 11).setValue(articles[i].content);
      // 次の追加行を設定
      count = count + 1;
    }
  }
}

# convertTwoDimensionToOneDimension:2次元配列を1次元配列に変換する便利関数

Google Spread Sheetの値を「sheet.getRange('E:E').getValues()」すると なぜか2次元配列として返却されてしまうため、1次元配列に詰めなおす処理を毎回書くことになります。 そのため、ちょっとしたユーティリティ関数として作成しています。

/**
 *  第一引数の二次元配列を第二引数のインデックスの値の一次元配列に変換する関数
 * @param {Array} 2次元配列 
 * @param {int} 切り出したい列番号
 * @return void
 */
function convertTwoDimensionToOneDimension(twoDimensionalArray, targetIndex) {
  var oneDimensionalArray = []
  twoDimensionalArray.forEach(function(value) {
    if(value[targetIndex-1] != ""){
      oneDimensionalArray.push(value[targetIndex-1]);
    }
  });
  return oneDimensionalArray;
}

# ニュース削除・登録機能のデバッグを実行する

今回も初めてデバッグを実行しようとすると、Google Spread Sheetにアクセスするために承認を求められるため、 初めてのデバッグ実施時は承認要求が発生 と同じように、許可をする必要があります。

# ④ツイート投稿機能

最後にツイート機能を作成します。

TwitterのAPIを利用する方法もありますが、 複数のニュースを一気に投稿するのではなく、1件毎にツイートを行います。 そのため、今回はURLでTwitterにアクセスし、投稿する方法を取りたいと思います。

# Tweet投稿画面をブラウザで開く

以下のURLでアクセスすることで、ブラウザでツイートの投稿画面を表示させる事ができます。

http://twitter.com/share?url=[シェアするページのurl]&text=[ツイート内テキスト]&via=[ツイート内に含むユーザ名]&related=[ツイート後に表示されるユーザー]&hashtags=[ハッシュタグ]

そのため、Tweet用のリンク項目をGoogle Spread Sheetに追加します。

ニュースアプリの機能をつくってみる#Tweetボタン

# Glideで画面の変更(Tweetボタン追加)

はじめに画面にTweetボタンを配置します。

  1. ニュース記事画面で「button(ボタン)」を追加
  2. ボタンのタイトルを「tweet」に設定
  3. アクションは「Open Link」とし、Tergetには先ほど追加した「Twitter」を指定

ニュースアプリの機能をつくってみる#Tweetボタン

# Tweet用URL組み立て処理の追加

先ほど追加した「Twitter」項目に格納するデータを作成していきます。 下記URLを組み立てるため、{Tweet内容}と{ハッシュタグ}を作成すればOKです。

http://twitter.com/share?text={Tweet内容}&hashtags={ハッシュタグ}

ちなみに、改行文字や空白を使いたい場合は、以下の文字で置換する必要があります。

  • #:%23
  • 半角空白:%20
  • 改行:%0a

# addNewsを修正してTweet内容を登録

/**
 * ニュース記事の追加
 * @param {Object} ニュース情報
 * @return void
 */
function addNews(newsJson){
  
  ...省略...

  //記事数分ループしてニュースを作成
  for (var i in articles) {
    //既に追加済みの記事は追加しない(同じURLの記事は追加しない)
    if(URL_COL_ARRAY.indexOf(articles[i].url) == -1){

      ...省略...

      //Twitter投稿用
      let tweetContents = "http://twitter.com/share?text=";
      tweetContents = tweetContents + "🔵🟡🔴プログラミン%0a~気になるニュースをピックアップ~%0a";
      tweetContents = tweetContents + articles[i].title + "%0a%0a";
      tweetContents = tweetContents + articles[i].url + "%0a%0a";
      tweetContents = tweetContents + "&hashtags="+newsJson.query;

      sheet.getRange(lastRow+count, 12).setValue(tweetContents); 
      
      // 次の追加行を設定
      count = count + 1;
    }
  }
}

# Tweet機能のデバッグを実行する

Tweet機能の動作確認を行います。

# ニュース記事を取得を定期実行させる

ここまでで、必要な機能の実装は完了しました。

ただし、毎回ニュースの取得を手動で実行するのは現実的でありません。 そこで、Google Apps Scriptの「トリガー」機能を使って定期実行をしたいと思います。

# トリガーの登録

Google Apps Scriptの左メニューにある「トリガー」を選択します。 画面が切り替わり、右下に「+トリガーを追加する」ボタンが表示されます。

ニュースアプリの機能をつくってみる#定期実行

下記の通り設定を行い、保存を押します。

  • 実行する関数を選択:myFunction
  • 時間ベースのトリガータイプを選択:日付ベースのタイマー
  • 時間を選択:5:00~6:00

ニュースアプリの機能をつくってみる#定期実行

新たに、トリガーが1行増えていることが分かります。

ニュースアプリの機能をつくってみる#定期実行

同じよに、トリガーを増やします。

  • 5:00 ~ 6:00:出勤時に見るニュースを更新
  • 10:00 ~ 11:00:お昼休みに見るニュースを更新
  • 17:00 ~ 18:00:帰宅時に見るニュースを更新

ニュースアプリの機能をつくってみる#定期実行

以上で、トリガーにより毎日ニュースが自動取得されます。

# 完成!! 最後に

今回作成したソースを全文掲載しておきます。


//**********************************************************************************
//グローバル変数
//**********************************************************************************

//NewsAPIのアクセスキー
NEWSAPIKEY = "****************************";

//Google Spread Sheet ID
GoogleSpreadSheetID = "****************************"

//ニュース検索キーワード
var NEWS_KEYWORDS = get_keywords();
PropertiesService.getScriptProperties().setProperty('NEWS_KEYWORDS', NEWS_KEYWORDS);

function myFunction() {
  //記事削除
  deleteNews(7);
  // 検索キーワード分ループ
  for (var i in NEWS_KEYWORDS) {
    //ニュースを取得
    let newsJson = GetNewsFromAPI(NEWS_KEYWORDS[i]);
    //レスポンスが正常で、記事数が0件でなければスプレッドシートに記事追加
    if (newsJson.status =="ok" && newsJson.totalResults != 0){
      addNews(newsJson);
    }
  }
}


//**********************************************************************************
//NewsAPIからニュースを取得
//**********************************************************************************

/**
 * 指定されたキーワードでNewsAPIからニュースを取得する
 * @param {string} [str] - 検索キーワード
 * @return {Object} - ニュース情報
 */
function GetNewsFromAPI(query){

  //NewsAPIのアカウント作成時に取得したAPIキーを設定
  let apiKey = NEWSAPIKEY;

  // 最新記事を対象としたいため、ニュース記事の投稿日を1日前までを指定する準備
  // 今日の日付を取得
  let today = new Date(); 
  // 1日前を設定
  let qday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); 
  // フォーマットをyyyy-MM-ddに設定
  let queryDay = Utilities.formatDate( qday, 'Asia/Tokyo', 'yyyy-MM-dd');
  
  // 呼び出しURL
  // パラメータの詳細はNewsAPIのリファレンス参照
  // https://newsapi.org/docs/endpoints/everything
  let url = 'http://newsapi.org/v2/everything?' +
          'q='+query+'&' +
          'from='+queryDay+'&' +
          'sortBy=relevancy&' +
          'language=jp&' + 
          'pageSize=3&' + 
          'apiKey=' + apiKey;
  
  // WEBサービスの呼び出にはUrlFetchAppを利用する
  // https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app
  // UrlFetchAppのオプション情報を作成
  let options = {
    "method" : "GET",
    "muteHttpExceptions" : true
  }
  
  // UrlFetchAppを使ってNewsAPIへアクセス
  let res = UrlFetchApp.fetch(url, options);
  //レスポンスをJSON形式に変換
  let jsonData = JSON.parse(res);
  let jsonDataRes = jsonData;
  
  // レスポンスのチェック
  if(jsonData.status !="ok"){
    console.error("[ERROR][Get Google News]レスポンスがOKでありません。");
  }else{
    if(jsonData.totalResults == 0){
      console.info("[WARN][Get Google News]ニュース記事jが0件です。");
    }else{
      // NewsAPIの戻り値に、今日の日付と検索キーワードを追加
      jsonDataRes.today = today;
      jsonDataRes.query = query;
      console.log(JSON.stringify(jsonDataRes))
    }
  }

  return jsonDataRes;
}

//**********************************************************************************
//ニュースの登録・削除
//**********************************************************************************

/**
 * キーワードシートから、検索キーワードを取得
 * @param void
 * @return void
 */
function get_keywords() {
  // キーワードシートを準備
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('キーワード');
  // 最終行を取得
  let last_row = sheet.getLastRow();
  // キーワードを取得
  let range = sheet.getRange("A2:A" + last_row)
  let values = range.getValues();
  let array = [];
  //1次元配列に詰めなおし
  for(var i = 0; i < values.length; i++){
    array.push(values[i][0]);
  }
  //重複、空白除いた配列を返却
  var result = Array.from(new Set(array)).filter(String);
  return result
}

/**
 * 指定された日数より古いニュース記事を削除する
 * @param {int} ニュース記事を残す日数
 * @return void
 */
function deleteNews(days){

  // google spread sheetのIDを指定して取得
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('ニュース');

  // 指定された日数分過去の日付を取得し、タイムスタンプの形式に変換
  let today = new Date();
  let deleteDay = new Date(today.getFullYear(), today.getMonth(), today.getDate() - days);
  let deleteTimeStamp = Utilities.formatDate( deleteDay, 'Asia/Tokyo', 'yyyy/MM/dd: hhmmss');

 // ニュース記事をアプリに取り込んだ日付列(C列)を取得
  var TIMESTAPM_COL = sheet.getRange('C:C').getValues();  //C列の値を全て取得
  var lastRow = TIMESTAPM_COL.filter(String).length;  //空白の要素を除いた長さを取得

  // 削除行を初期化
  let delete_line_num = 1;
  // 過去日が

  /**
   * ニュースはスプレッドシートの下の行に追加されていくため古い記事は上の行に残ることになる。
   * そのため、指定日数以内の日付が出現する行番号を取得し、
   * 開始行~取得した行までを削除する。
   * 開始行は、1行目はニュース記事のヘッダー行のため2行目からとする。
   */
  for(let j = 2; j <= lastRow; j++) {

    // 3列目(C列)がニュース記事をアプリに取り込んだ日付列
    let getDay = sheet.getRange(j, 3).getValue();
    // フォーマットを整形する
    let getDay_fmt = Utilities.formatDate( getDay, 'Asia/Tokyo', 'yyyy/MM/dd: hhmmss')

    //日付が存在し、削除対象日より新しい日付が現れたら、その行数を取得しループを抜ける
    if(getDay_fmt){ 
      if(deleteTimeStamp < getDay_fmt){
        delete_line_num = j-1;
        break;
      }
    }
  }

  //取得した行数が1より大きれば削除を実行(1行目はヘッダー行)
  //最終行より大きくなる場合はあり得ないが、保険として条件に追加
  if(delete_line_num>1 && delete_line_num < lastRow){
    sheet.deleteRows(2, delete_line_num);
  //すべてが過去記事の場合
  }else if(delete_line_num==1 && lastRow > 2){
    sheet.deleteRows(2, lastRow - 1);
  }
}

/**
 * ニュース記事の追加
 * @param {Object} ニュース情報
 * @return void
 */
function addNews(newsJson){
  
  // google spread sheetのIDを指定して取得
  let sheet = SpreadsheetApp.openById(GoogleSpreadSheetID).getSheetByName('ニュース');

  // ニュースのユニークキーとしてURLを使う。URLはE列に格納している。
  // URLを全て取得し、空白の要素を除いた長さを取得。これを最終行とする。
  let URL_COL = sheet.getRange('E:E').getValues();
  let lastRow = URL_COL.filter(String).length;

  //最終行以降に新しい記事を追加する行
  let count = 1;
  //既存記事URLの配列を作成
  let URL_COL_ARRAY = convertTwoDimensionToOneDimension(URL_COL,1);

  // ニュース記事Jsonを取得
  let articles = newsJson.articles;

  //記事数分ループしてニュースを作成
  for (var i in articles) {
    //既に追加済みの記事は追加しない(同じURLの記事は追加しない)
    if(URL_COL_ARRAY.indexOf(articles[i].url) == -1){
      sheet.getRange(lastRow+count, 1).setValue(lastRow+count-1);
      sheet.getRange(lastRow+count, 2).setValue(0);
      sheet.getRange(lastRow+count, 3).setValue(newsJson.today);
      sheet.getRange(lastRow+count, 4).setValue(articles[i].title);
      sheet.getRange(lastRow+count, 5).setValue(articles[i].url);
      sheet.getRange(lastRow+count, 6).setValue(newsJson.query);
      sheet.getRange(lastRow+count, 7).setValue(articles[i].author);
      sheet.getRange(lastRow+count, 8).setValue(articles[i].description);
      sheet.getRange(lastRow+count, 9).setValue(articles[i].urlToImage);
      sheet.getRange(lastRow+count, 10).setValue(articles[i].publishedAt);
      sheet.getRange(lastRow+count, 11).setValue(articles[i].content);

      //Twitter投稿用
      let tweetContents = "http://twitter.com/share?text=";
      tweetContents = tweetContents + "🔵🟡🔴プログラミン%0a~気になるニュースをピックアップ~%0a";
      tweetContents = tweetContents + articles[i].title + "%0a%0a";
      tweetContents = tweetContents + articles[i].url + "%0a%0a";
      tweetContents = tweetContents + "&hashtags="+newsJson.query;

      sheet.getRange(lastRow+count, 12).setValue(tweetContents); 

      // 次の追加行を設定
      count = count + 1;
    }
  }
}


/**
 *  第一引数の二次元配列を第二引数のインデックスの値の一次元配列に変換する関数
 * @param {Array} 2次元配列 
 * @param {int} 切り出したい列番号
 * @return void
 */
function convertTwoDimensionToOneDimension(twoDimensionalArray, targetIndex) {
  var oneDimensionalArray = []
  twoDimensionalArray.forEach(function(value) {
    if(value[targetIndex-1] != ""){
      oneDimensionalArray.push(value[targetIndex-1]);
    }
  });
  return oneDimensionalArray;
}

Last Updated: 3/7/2021, 6:50:18 AM