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

# はじめに
友人の依頼である[ニュースサイト](https://jp.news.gree.net/media/scoopie-news)の最新記事データを毎日取得できないか？と相談を受けました。

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

コードの例は全てある[ニュースサイト](https://jp.news.gree.net/media/scoopie-news)からデータを取得する想定で書いています:bow_tone1:

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

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

https://tetsuooo.net/gas/82/

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

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

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

https://coporilife.com/477/

**③取得したデータを②で作成したシートに保存**
![Screenshot 2023-03-30 at 20.50.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/d3098a32-bcfe-ca04-f5fb-36301ee4ee72.png)
こんな感じで保存されます。

# データ取得
基本[こちらの記事](https://tetsuooo.net/gas/82/)を参考にしているので、当然Parserライブラリも使います！

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

```javascript
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>`で分割して配列にする
```javascript
  let textItems = new Array();
  textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();
```
これをしないと改行コードを省きにくいです。。。！

### 改行コードを省く
```javascript
 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);
  });
```

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


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


### 不要な`<p>`を省く
「最新のグラビア記事はこちら」と「 ※写真はTwitterから」の情報は不要なので、これらを配列から省きます！
配列化した本文の最後の２つの要素は必ず👆になるので、最後の２つを省きます。
```javascript
  // 最後から２つ目の数値
  const startNum = textItemsRemovedNewLine.length -2;
  // spliceで配列から省く
  textItemsRemovedNewLine.splice(startNum, 2);
```

### 配列を合体して１つの文字列にする
```javascript
  const joinedTextItem = textItemsRemovedNewLine.join();
```

### １つにした文字列から不要な`<strong>`タグを省く
```javascript
 const textItem = joinedTextItem.replace('<strong>', '').replace('</strong>', '').replace(',', '');
```

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

```javascript
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:

### 作成したシートを一番左に置く
```javascript
  if (ss.getSheetByName(today) == null) {
    ss.insertSheet(today).activate();
    ss.moveActiveSheet(1);
  }
```

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


### 今日の2回目以降の実行で重複した記事を保存しないために比較する
```javascript
  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/77092e8b-8289-bac5-e9cc-2486fd668712.png)
`Apps Script>トリガー`を開く

![Screenshot 2023-04-03 at 0.00.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/c16dde27-ba7c-2104-c7c2-8c5974b48c11.png)
時間主導型に設定して、自分の好きな時間に定期実行されるようようにする。
画像の場合は毎日６時間ごとに、設定した関数を実行する。


# 最後に

https://qiita.com/kokogento/items/f8739ba049a37dce931f

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