【GAS】Yahooニュースをスクレイピング【サンプルソース付】

快速ワーク
スポンサーリンク

スクレイピングを使いこなせば、インターネット上のデータを簡単に拾っていろいろやること拡がりますね。

例えば、最新のYahoo!ニュースの全カテゴリを一発で一覧に出力できたら便利です。

スクレイピングはいろいろな言語で対応可能だと思いますが、とりあえずGASで実装してみました。

Googleスプレッドシートであれば、割と簡単に自分好みのわかりやすい形にカスタマイズできます。

いろんな方法があると思いますが、正規表現によるゴリゴリの置換で実現してます。
スマートな方法ではないかもしれませんが、なるべく直感的にわかりやすいように実装してます。

もしよかったら参考にしてください。スクレイピング技術を使って必要な情報を効率的に取得しましょう。

いつのまにかYahooニュースページの仕様が変わっていたので、変更後のデータが取得できるように対応しました。※ただし、操作イメージ映像は旧のままです。ご了承ください。


スポンサーリンク

Yahoo!ニュースページの取得箇所とスクレイピング操作イメージ映像

スクレイピングで取得するYahooニュースページソースの項目と実際にマクロを起動してスプレッドシートに反映するまでの映像です。

やるべきことのイメージを沸かせてみてください。


Yahoo!ニューススクレイピングGASマクロの主な仕様

細かい処理はコメントで補足してるので、ザックリとした仕様を箇条書きで記載します。

  • Yahoo!ニュースからカテゴリ単位でページリストを取得
  • ページ番号範囲指定で複数ページを一度に取得(ただしページ制限制御あり)
  • 実行判断の確認メッセージ制御(フラグon/offあり)
  • 既に取得済データかをタイトルでチェック(既に存在している場合は反映しない)
  • ソート・枠線・フィルタ処理を追加



Yahoo!ニューススクレイピングGASマクロのソースコード

下記のソースを全てコピーして起動してください。

【GAS】Googleスプレッドシートでマクロの使い方入門【動画付】
Googleの「Google Apps Script」通称GAS。GoogleマップやGメールなど、いろんなGoogle関連のサービスと自由に連携できたり、独自のWebアプリを開発することもできる、いろんな可能性を秘めたワクワクの...

目的に合っていない場合は適当にカスタマイズしてみてください。GASマクロの勉強にも少しは役立つと思います。

/* Yahoo!ニューススクレイピング */
function yahooNewsScraping() {
  
  // 確認メッセージフラグ
  var confirmMsgFlg = 0; // 0:確認メッセージを表示しない、1:確認メッセージを表示する
  
  // 開始位置
  var startRow = 4;
  var startColumn = 1;
  
  // 目次シートと各シート左上に目次リンク作成処理
  yahooNewsScrapingShori(confirmMsgFlg,
                         startRow,
                         startColumn);
  
}
////////////////////////////////////////////////////////////////////////////////
/* スクレイピング処理 */
function yahooNewsScrapingShori(confirmMsgFlg,
                                startRow,
                                startColumn) {
  
  var spSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = SpreadsheetApp.getActiveSheet(); // アクティブシート
  var pageRange = 10; // ページ制限範囲
  var outColCnt = 5; // 出力列数
  var categoryCol = 1; // カテゴリ
  var titleCol    = 2; // タイトル
  var upDtTmCol   = 3; // 掲載日時
  var urlCol      = 4; // URL
  var addDtTmCol  = 5; // 追加日時
  
  var inCategory = sheet.getRange(1,2).getValue();
  var inPageNo = sheet.getRange(2,2).getValue();
  var inPageNoTo = sheet.getRange(3,2).getValue();
  
  //////////チェック//////////
  if (inCategory == "") {
    Browser.msgBox("カテゴリが入力されていません。\\n"+"処理を終了します。"+"処理を終了します。");
    return;
  }
  if (inPageNo == "") {
    Browser.msgBox("ページ番号が入力されていません。\\n"+"処理を終了します。");
    return;
  } else {
    if (isNaN(Number(inPageNo))) {
      Browser.msgBox("ページ番号が数値ではありません。\\n"+"処理を終了します。");
      return; 
    } else {
      inPageNo = Number(inPageNo);
    }
  }
  if (inPageNoTo == "") {
    inPageNoTo = Number(inPageNo);
  } else {
    if (isNaN(inPageNo)) {
      Browser.msgBox("ページ番号Toが数値ではありません。\\n"+"処理を終了します。");
      return;
    } else {
      inPageNoTo = Number(inPageNoTo);
    }
    if (inPageNo > inPageNoTo) {
      Browser.msgBox("ページ範囲指定が逆です。\\n"+"処理を終了します。");
      return;
    }
    if (inPageNoTo-inPageNo+1 > pageRange) {
      Browser.msgBox("ページ制限範囲「"+pageRange+"」を超えてます。\\n"+"処理を終了します。");
      return;
    }
  }
  
  //////////確認処理//////////
  if (confirmMsgFlg == 1) {
    var msg = "";

    if (inPageNoTo == inPageNo) {
      msg = msg
      +"・Yahooニュース「"+inCategory+"」"+inPageNo+"ページ目のリストを出力します。\\n";
    } else {
      msg = msg
      +"・Yahooニュース「"+inCategory+"」"+inPageNo+"ページ目から"+inPageNoTo+"ページ目のリストを出力します。\\n";
    }
    
    msg = msg
     +"・タイトルで存在チェックし、既に存在している場合は追加しません。\\n";
    
    // 実行確認
    if (msg != "") {
      var msgRtn = Browser.msgBox(msg+"\\n実行してよろしいですか?",Browser.Buttons.OK_CANCEL);
      if (msgRtn == 'cancel') {return;}
    }
    
  }

  //////////コンテンツ取得//////////
  var Data = [];
  for (var p=inPageNo;p<=inPageNoTo;p++) {
  
    // 対象URL取得
    switch(inCategory) {
      case '全て':
        var inUrlArr = [
          ["国内",   "https://news.yahoo.co.jp/topics/domestic?page=" + p]
         ,["国際",   "https://news.yahoo.co.jp/topics/world?page=" + p]
         ,["経済",   "https://news.yahoo.co.jp/topics/business?page=" + p]
         ,["エンタメ","https://news.yahoo.co.jp/topics/entertainment?page=" + p]
         ,["スポーツ","https://news.yahoo.co.jp/topics/sports?page=" + p]
         ,["IT",     "https://news.yahoo.co.jp/topics/it?page=" + p]
         ,["科学",   "https://news.yahoo.co.jp/topics/science?page=" + p]
         ,["地域",   "https://news.yahoo.co.jp/topics/local?page=" + p]
        ];
        break;
      case '主要':
        var inUrl = "https://news.yahoo.co.jp/topics/top-picks?page=" + p;
        break;
      case '国内':
        var inUrl = "https://news.yahoo.co.jp/topics/domestic?page=" + p;
        break;
      case '国際':
        var inUrl = "https://news.yahoo.co.jp/topics/world?page=" + p;
        break;
      case '経済':
        var inUrl = "https://news.yahoo.co.jp/topics/business?page=" + p;
        break;
      case 'エンタメ':
        var inUrl = "https://news.yahoo.co.jp/topics/entertainment?page=" + p;
        break;
      case 'スポーツ':
        var inUrl = "https://news.yahoo.co.jp/topics/sports?page=" + p;
        break;
      case 'IT':
        var inUrl = "https://news.yahoo.co.jp/topics/it?page=" + p;
        break;
      case '科学':
        var inUrl = "https://news.yahoo.co.jp/topics/science?page=" + p;
        break;
      case '地域':
        var inUrl = "https://news.yahoo.co.jp/topics/local?page=" + p;
        break;
      default:
        Browser.msgBox("対象外のカテゴリです。\\n"+"処理を終了します。");
        return;
    }
    
    // HTML結果取得
    if (inCategory == "全て") {
      for (var i=0;i<inUrlArr.length;i++) {
        inCategory = inUrlArr[i][0];
        inUrl = inUrlArr[i][1];
        var request = UrlFetchApp.fetch(inUrl);
        var content = request.getContentText();
        //////////ページ内リスト取得//////////
        Data = yahooNewsPageListGet(inCategory,content,Data);
      }
    } else {
      var request = UrlFetchApp.fetch(inUrl);
      var content = request.getContentText();
      //////////ページ内リスト取得//////////
      Data = yahooNewsPageListGet(inCategory,content,Data);
    }
  
  }
  
  //////////データ存在チェック(「タイトル」で判定)//////////
  var outData = []; // 詰め替え用配列
  if (sheet.getRange(startRow+1,titleCol).getValue() != "") {
    var chkData = sheet.getRange(startRow+1,titleCol,sheet.getLastRow()-startRow,1).getValues();
    for (var i=0;i<Data.length;i++) {
      var chkCategory = Data[i][categoryCol-1];
      var chkDataTgt  = Data[i][titleCol-1];
      var excludeFlg  = false;
      for (var j=0;j<chkData.length;j++) {
        if (chkData[j].indexOf(chkDataTgt) !== -1 ) {
          excludeFlg = true;
          Logger.log("「"+chkDataTgt+"」"+"は既に存在する");
          break;
        }
      }
      // 既に存在するデータを除外して配列に詰め替え
      if (!excludeFlg) {outData.push(Data[i]);}
    }
  } else {
    outData = Data;
  }

  //////////スプレッドシート書込//////////
  sheet.getRange(startRow,categoryCol).setValue("カテゴリ");
  sheet.getRange(startRow,titleCol).setValue("タイトル");
  sheet.getRange(startRow,upDtTmCol).setValue("掲載日時");
  sheet.getRange(startRow,urlCol).setValue("URL");
  sheet.getRange(startRow,addDtTmCol).setValue("追加日時");
  if (outData != "" ) {
    // 最終行に追記
    sheet.getRange(sheet.getLastRow()+1,startColumn,outData.length,outColCnt).setValues(outData);
    if (confirmMsgFlg == 1) {
      Browser.msgBox("抽出した"+Data.length+"件のニュースのうち、"+outData.length+"件を追加しました。");
    }
  } else {
    if (confirmMsgFlg == 1) {
      Browser.msgBox("抽出した"+Data.length+"件のニュースは既に存在するため、追加しませんでした。");
    }
  }
  
  /////ソート/////
  // 掲載日時(降順)
  sheet.getRange(startRow+1,1,sheet.getLastRow()-startRow,outColCnt)//.activate()
  .sort({column: upDtTmCol, ascending: false});

  /////罫線付加/////
  // タイトル部
  sheet.getRange(startRow,1,1,outColCnt)
     // 上・左・下・右・垂直・水平を全て実線
    .setBorder(true,true,true,true,true,true,'#000000',SpreadsheetApp.BorderStyle.SOLID);
  // データ部
  sheet.getRange(startRow+1,1,sheet.getMaxRows(),outColCnt)
    .setBorder(false,false,false,false,false,false);
  sheet.getRange(startRow+1,1,sheet.getLastRow()-startRow,outColCnt)
    // 上・左・下・右・垂直は実線
    .setBorder(true,true,true,true,true,null,'#000000',SpreadsheetApp.BorderStyle.SOLID)
    // 水平は点線
    .setBorder(null,null,null,null,null,true,'#000000',SpreadsheetApp.BorderStyle.DOTTED);

  /////フィルタ/////
  // 一旦フィルタをオフ(フィルタが存在しない場合はエラーキャッチしてスルー)
  try{ sheet.getFilter().remove(); } catch(e) { Logger.log(e); }
  // フィルタを作成
  sheet.getRange(startRow,1,sheet.getLastRow()-startRow+1,outColCnt).createFilter();
  
}
////////////////////////////////////////////////////////////////////////////////
/* ページ内リスト取得処理 */
function yahooNewsPageListGet(inCategory,content,Data) {
  
  //////////ページ内リスト取得//////////
  // 「<li class="newsFeed_item"><a class="newsFeed_item_link" href="」と「</time></div></div></div></a></li>」に囲まれたデータを取得(改行なし)
  var liTags = content.match(/<li\sclass="newsFeed_item"><a\sclass="newsFeed_item_link"\shref=".*?<\/time><\/div><\/div><\/div><\/a><\/li>/g);
  var liTag,aTag,url,dlTag,title,category,timeTag,upDtTm,upDate,upTime,addDtTm;
  for (var i=0;i<=liTags.length-1;i++) {
    // 空の配列作成
    var data = [];
    
    /////htmlタグからコンテンツを抽出/////
    liTag = liTags[i];
    // 「<a class="newsFeed_item_link" href="」と「" data-ylk="rsec:st_topics;slk:title;pos:」に囲まれたデータを取得
    url = String(liTag.match(/<a\sclass="newsFeed_item_link"\shref=".*?"\sdata-ylk="rsec:st_topics;slk:title;pos:/g))
                    .replace(/<a\sclass="newsFeed_item_link"\shref="/,"").replace(/"\sdata-ylk="rsec:st_topics;slk:title;pos:/,""); //"
    // 「<div class="newsFeed_item_title">」と「</div>」に囲まれたデータを取得(改行なし)
    title = String(liTag.match(/<div\sclass="newsFeed_item_title">.*?<\/div>/g))
                    .replace(/<div\sclass="newsFeed_item_title">/,"").replace(/<\/div>/,"");
    category = inCategory; // カテゴリーは記載がない!!
    // 「<time」と「</time>」に囲まれたデータを取得
    timeTag = String(liTag.match(/<time.*?<\/time>/g));
    Logger.log(timeTag);
    // 「<time class="newsFeed_item_date">」と「(」に囲まれたデータを取得
    upDate = String(timeTag.match(/<time\sclass="newsFeed_item_date">.*?\(/g))
                         .replace(/<time\sclass="newsFeed_item_date">/,"").replace(/\(/,"");
    // 「)」と「</time>」に囲まれたデータを取得
    upTime = String(timeTag.match(/\).*?<\/time>/g))
                       .replace(/\)/,"").replace(/<\/time>/,"");
    // 日時整形
    if ((upDate.match(/\//g )||[]).length == 1) { // "/"の数が1の場合はmm/dd、2の場合はyyyy/mm/ddと判断
      // 最新のYahoo!ニュース上の日付は年が付いていないので付加して変換
      upDtTm = formatDate(new Date(new Date().getFullYear()+"/"+upDate+" "+upTime),'yyyy/mm/dd(aaa) HH:MM');
    } else {
      upDtTm = formatDate(new Date(upDate+" "+upTime),'yyyy/mm/dd(aaa) HH:MM'); 
    }
    // 現在日時取得
    var now = Utilities.formatDate(new Date(),'Asia/Tokyo','yyyy/MM/dd HH:mm:ss');
    addDtTm = formatDate(new Date(now),'yyyy/mm/dd(aaa) HH:MM:ss');
    
    // 2次元配列に格納
    data.push(category); // カテゴリ
    data.push(title);    // タイトル
    data.push(upDtTm);   // 掲載日時
    data.push(url);      // URL
    data.push(addDtTm);  // 追加日時
    Data.push(data);     // 1行データ格納
    
  }

  return Data;
  
}
////////////////////////////////////////////////////////////////////////////////
/* 日時フォーマット変換処理 */
function formatDate(date, format) {
  
  format = format.replace(/yyyy/g, date.getFullYear());
  format = format.replace(/mm/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/dd/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/aaa/g, ['日','月','火','水','木','金','土'][date.getDay()])
  format = format.replace(/AAA/g, ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()])
  format = format.replace(/HH/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/MM/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  format = format.replace(/SSS/g, ('00' + date.getMilliseconds()).slice(-3));
  
  return format;
  
};
////////////////////////////////////////////////////////////////////////////////
/* クリア処理 */
function yahooNewsClear() {
  
  // 開始位置
  var spSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = SpreadsheetApp.getActiveSheet(); // アクティブシート
  var startRow = 4;
  var startColumn = 1;
  var outColCnt = 6;
  
  /////罫線削除/////
  // データ部
  sheet.getRange(startRow+1,1,sheet.getMaxRows(),outColCnt)
    .setBorder(false,false,false,false,false,false);
  
  // クリア処理(開始位置から最終行まで一旦クリアし再作成する)
  sheet.getRange(startRow,startColumn,sheet.getMaxRows()-startRow+1,outColCnt).clearContent();
  try{ sheet.getFilter().remove(); } catch(e) { Logger.log(e); }
                       
}
変更履歴

2019/8/13
・いつのまにかYahooニュースページの仕様が変わっていたので、変更後のデータが取得できるように対応。ページ内からはカテゴリが取得できないようになっていたのでインプット情報から設定。よって「主要」で抽出するとカテゴリが不明になるので注意。その代わり、「全て」のカテゴリを取得する処理を追加。


最後に

Googleスプレッドシートの使い方は人それぞれ、いろんなやり方があると思いますが、一例としてご紹介させていただきました。

GASマクロはちょっとした向上心さえあれば、取っ付きやすいプログラムなのでショートカットキーなどと組み合わせてぜひ活用してみてください。

Googleスプレッドシート全ショートカットキー一覧はこちら↓↓↓

Googleスプレッドシートの使い方や機能がわかるショートカットキー全まとめ一覧【初心者こそ必見】
表計算ソフトとして、まだまだExcelのシェア率は高いですが、GoogleスプレッドシートにはExcelにはない超強力な関数があったり、Excelとは違って常に最新版を無料で使用できます。なにより、インターネットを使ったオンライン...

ちょっと工夫すれば、ちょっとした操作に1分かかっていた作業を10秒でこなすことができるようになる可能性があります。

それだけでも、積み上げれば相当の工数を削減できるはずなので、ぜひ自分に合ったやり方を模索していきましょう。


コメント

//▼2023/04/08追加 //https://lovagelab.com/posts/3406/ //▲2023/04/08追加