0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【GAS】ニュースサイトの記事を定期実行でスクレイピングしてスプレッドシートに保存 その1

Last updated at Posted at 2023-04-02

はじめに

友人の依頼であるニュースサイトの最新記事データを毎日取得できないか?と相談を受けました。

スクレイピングはあまりした事がなかったので、良い機会だと思い挑戦してみました!!!

コードの例は全てあるニュースサイトからデータを取得する想定で書いています:bow_tone1:

全体像

以下のステップで分けて考えると良いかと思います。

①データ取得
まずはこちらの記事を見てくださいw

これでデータ取得の全体像は掴めるかと。

ただ、今回の場合はデータを取得するだけではダメで、、、改行コードなど不要な文字列が紛れているので、取得した後に編集する工程が結構大事だったりします。

②今日の日付でシートを作成しアクティブにする
「スプレッドシート」と「シート」は違うのでご注意ください!

③取得したデータを②で作成したシートに保存
Screenshot 2023-03-30 at 20.50.53.png
こんな感じで保存されます。

データ取得

基本こちらの記事を参考にしているので、当然Parserライブラリも使います!

まずはトップページから最新ニュースを取得するfunctionを作りましょう

function getValues() {
  let response = UrlFetchApp.fetch("https://jp.news.gree.net/media/scoopie-news");
  let text = response.getContentText("utf-8");

  //トップページから必要なブロックを取得
  let topic_block = Parser.data(text).from('class="unit"').to('</section>').build();

  //ulタグで囲まれている記述(トップニュース)を抽出
  let content_block = Parser.data(topic_block).from('<ul>').to('</ul>').build();

  // ニュースリスト用の配列変数を宣言
  let newsList= new Array();

  // content_blockの要素のうち、<li class="cst news-list-item">に囲まれている記述を抽出
  topics = Parser.data(content_block).from('<li class="cst news-list-item">').to('</li>').iterate();

  // <li class="cst news-list-item">に囲まれたブロックの回数分、URL/タイトル/画像リンク/日付/本文/名前を抽出する
  for (news of topics) {
    
    //記事のURL
    const newsUrl = Parser.data(news).from('href="').to('#"').build();

    //記事のタイトル
    const newsTitle = Parser.data(news).from('<p class="newsLead">').to('</p>').build();

    //記事の見出し画像
    const newsImage = Parser.data(news).from('<img src="').to('"').build();

    //記事の公開日
    const newsDate = Parser.data(news).from('<span class="minor lineL">').to('</span>').build();

    //記事のURLを渡し、個別の記事から本文と名前を取得
    const newsTextAndName = getNewsTextAndName(newsUrl);

    //配列を作る
    const newsInfo = [newsDate, newsUrl, newsTitle, newsImage, newsTextAndName[0], newsTextAndName[1]];

    //作った配列newsInfoをあらかじめ用意した配列newsListに入れる
    newsList.push(newsInfo);
   }
  return newsList;
}

//記事のURLを渡し、個別の記事から本文と名前を取得
function getNewsTextAndName(newsUrl) {
  let response = UrlFetchApp.fetch(newsUrl);
  let text = response.getContentText("utf-8");
  let news_block = Parser.data(text).from('<article id="news-entry">').to('</article>').build();
  let news_text_block = Parser.data(news_block).from('<div class="newsArticle">').to('<ul class="snb">').build();
  let textItems = new Array();
  const regex = /^(?!&nbsp;).*/;
  textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();

  let textItemsRemovedNewLine = textItems.filter(function (value) {
    return value.match(regex);
  });
  const startNum = textItemsRemovedNewLine.length -2;
  textItemsRemovedNewLine.splice(startNum, 2);

  const joinedTextItem = textItemsRemovedNewLine.join();
  const textItem = joinedTextItem.replace('<strong>', '').replace('</strong>', '').replace(',', '');

  let name = Parser.data(news_text_block).from('<strong>').to('</strong>').build();
  
  return [textItem, name];
}

データ編集の詳細

取得したデータは色々と編集しないととても見にくいです。。。
なのでgetNewsTextAndName(newsUrl)では、めっちゃコネコネして編集しています。。。:sweat_smile:

<p>で分割して配列にする

  let textItems = new Array();
  textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();

これをしないと改行コードを省きにくいです。。。!

改行コードを省く

 let textItems = new Array();
 textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();

  const regex = /^(?!&nbsp;).*/;
  let textItemsRemovedNewLine = textItems.filter(function (value) {
    return value.match(regex);
  });

filtermatchで「改行コードを省いた文字列の配列」であるtextItemsRemovedNewLineを作成します。

通常のfilter()関数では、配列から検索してヒットした値を取得できます。逆に「ヒットした値を取り除いた状態」にしたい場合は、正規表現とmatch()関数を使うことで実現できました!

不要な<p>を省く

「最新のグラビア記事はこちら」と「 ※写真はTwitterから」の情報は不要なので、これらを配列から省きます!
配列化した本文の最後の2つの要素は必ず👆になるので、最後の2つを省きます。

  // 最後から2つ目の数値
  const startNum = textItemsRemovedNewLine.length -2;
  // spliceで配列から省く
  textItemsRemovedNewLine.splice(startNum, 2);

配列を合体して1つの文字列にする

  const joinedTextItem = textItemsRemovedNewLine.join();

1つにした文字列から不要な<strong>タグを省く

 const textItem = joinedTextItem.replace('<strong>', '').replace('</strong>', '').replace(',', '');

取得したデータをシートに保存

さぁここからが本番です!
いよいよデータをシートに保存していきます:sunglasses:

function setValues() {
  //アクティブなスプレッドシートを取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  //シートの名前用に今日の日付を取得
  const today = getTodayDate();

  //もし「今日の日付」のシートが存在しなければ、「今日の日付」のシートを作成
  if (ss.getSheetByName(today) == null) {
    ss.insertSheet(today).activate();
    ss.moveActiveSheet(1);
  }
  //今日のシートを取得
   const todaySheet = ss.getSheetByName(today);

   //もしA1が空ならisFirstOfDayはtrueになる。A1が空なら何も書き込まれていない作りたてのシートだという事
   const isFirstOfDay = todaySheet.getRange('A1').isBlank();
   
   //「データ取得」の項目で解説したfunctionでニュースの値を取得
   const newsList = getValues();

  for (news of newsList) {
    if (!isFirstOfDay) {
      // Rowの値(日付)を取得
      const dateRow = todaySheet.getRange(1, 1, todaySheet.getLastRow()).getDisplayValues();
      const date = news[0];
      //includesがfalseならappendRowを実行
      if (!dateRow.flat().includes(date)) {
        todaySheet.appendRow(news);
      }
    } else {
      todaySheet.appendRow(news);
    }
  }
}

function getTodayDate() {
  //Dateオブジェクトからインスタンスを生成
  let today = new Date();
  //formatDateメソッドで日付の表示形式を変換する
  today = Utilities.formatDate(today, "JST", "yyyy/MM/dd");
  return today;
}

シートに保存する処理の解説

スクレイピングは定期実行なので「今回のデータ取得&スプシに保存」が、今日初めて実行されてものか?を主に判別しなければうまくできませんでした。:frowning2:

作成したシートを一番左に置く

  if (ss.getSheetByName(today) == null) {
    ss.insertSheet(today).activate();
    ss.moveActiveSheet(1);
  }

gasで作成したシートはデフォルトのままだと「一番右」に保存されます。
今回は4/2, 4/1, 3/31 のように新しい日付を右に持っていきたいのでmoveActiveSheet(1)で、シート作成後に移動させてます。

今日の2回目以降の実行で重複した記事を保存しないために比較する

  for (news of newsList) {
    if (!isFirstOfDay) {
      // Rowの値(日付)を取得
      const dateRow = todaySheet.getRange(1, 1, todaySheet.getLastRow()).getDisplayValues();
      const date = news[0];
      //includesがfalseならappendRowを実行
      if (!dateRow.flat().includes(date)) {
        todaySheet.appendRow(news);
      }
    } else {
      todaySheet.appendRow(news);
    }
  }

今日初めて実行したかどうか?をisFirstOfDayで判断します。
もし今日初めての実行ではないならA1列にある日付をnews[0]で取得して、重複する日付があるかどうかを確認します。
そして重複していない値だけをtodaySheet.appendRow(news);で保存します。

定期実行に設定する

Screenshot 2023-04-02 at 23.59.43.png
Apps Script>トリガーを開く

Screenshot 2023-04-03 at 0.00.34.png
時間主導型に設定して、自分の好きな時間に定期実行されるようようにする。
画像の場合は毎日6時間ごとに、設定した関数を実行する。

最後に

日毎にシートを作成せずに、1つのシートにずっと追加し続けたい人は👆を参考に!!
一応その2があります

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?