Google Apps Scriptでnote記事をスクレイピングできなくなった場合の対処法

サークルでDiscordを利用しており、Botに記事を1日に1つ配信してもらう仕組みを作っています。ある日突然Botからの通知がなくなったため、どうしたんだろうと思いつつ、試行錯誤した結果、修正できたので備忘録として記します。海外留学や海外旅行に関する記事を配信してくれるBotです。

- [元のソースコード](#元のソースコード)
- [問題と解決方法](#問題と解決方法)
- [問題解決後のソースコード](#問題解決後のソースコード)
- [一言](#一言)


# 元のソースコード
```
function myFunction(){
  const cache = CacheService.getScriptCache();
  const discordWebHookURL = "";//ここにwebhookURLを貼る

  mute_user_list = [""];//ミュートしたいユーザーネームをまとめるリスト
  mute_hashtag_list = [""];//ミュートしたいハッシュタグをまとめるリスト


  let send_list = new Array;
  let bf_send_list = cache.get("send_list")
  console.log(bf_send_list)
  if(bf_send_list != null && bf_send_list != ""){
    bf_send_list = bf_send_list.split(",")
    bf_send_list.forEach(item =>{
      send_list.push(item)
    }
    );    
  } 

  getNote("https://note.com/hashtag/%E6%B5%B7%E5%A4%96?f=hot&paid_only=false","海外").forEach(item => {
    send_list.push(item);
  });

  let last_list = send_list.filter((item, index) => send_list.indexOf(item) === index);
  console.log(send_list)
  console.log(last_list)
  for (i = 0; i < 1;i++){
    if (last_list.length > 0){
        send_switch = 0
        var article = last_list.pop()
        send_url = "https://note.com" + article
        const message = {
            "content": send_url,
            "tts": false
        }


        const param = {
            "method": "POST",
            "headers": { 'Content-type': "application/json"},
            "payload": JSON.stringify(message)
        }
        var user_name = article.substring(1,article.indexOf("/",2));
        var article_id = article.substring(article.lastIndexOf("/")+1);

        mute_user_list.forEach((value) => {
          if (user_name == value){
            send_switch += 1
          }
        })

        var hashtag_url = `https://note.com/api/v3/notes/${article_id}`;
        let jsonArticleInfo = UrlFetchApp.fetch(hashtag_url, {'method':'get'});
        var parsedData = JSON.parse(jsonArticleInfo);
          
        var hashtags = parsedData.data.hashtag_notes;
        var hashtagNames = hashtags.map(function(item) {
        return item.hashtag.name;
          }
        )

        mute_hashtag_list.forEach((value) => {
          hashtagNames.forEach((value2) => {
            if (value == value2){
              send_switch += 1
            }
          })
        })

        if (send_switch == 0){
          UrlFetchApp.fetch(discordWebHookURL,param);
      }
  }}
  cache.put("send_list",last_list,60 * 60 * 24);
}

function getNote(url,key) {//スクレイピングしてnoteのURLをとってきている
  const cache = CacheService.getScriptCache();
  let bf_url_list = cache.get(key);
  if (bf_url_list != null){
      bf_url_list = bf_url_list.split(",");
  } else{
    bf_url_list = new Array()
  }
  let url_list = new Array();
  let response = UrlFetchApp.fetch(url);
  let content = response.getContentText("utf-8");
  let texts = Parser.data(content).from('<div class="m-largeNoteWrapper__card" data-v-1e6ad5ad>').to('</div>').iterate();
  texts.forEach((value, index) => {
      let i = 0
      url_temp_l = [];
      let url_temp = "";
      let html_list = value.split("");
      html_list.forEach((value2,index) => {
          if (value2 == '"'){
              i += 1
          }
          if (i == 1){
              url_temp_l.push(value2)
          }
      })
      for (i = 1; i < url_temp_l.length; i++){

          url_temp += url_temp_l[i]
      }
      url_list.push(url_temp);
  });
  

  last_url_list = url_list.filter(item => !bf_url_list.includes(item));
  cache.put(key,url_list,60 * 60 * 24);
  
  return(last_url_list);
}
```

# 問題と解決方法

結論から言うと問題個所は、
```
let texts = Parser.data(content).from('<div class="m-largeNoteWrapper__card" data-v-1e6ad5ad>').to('</div>').iterate();
```
でスクレイピングが失敗していることでした。つまりこのdivタグが存在していないということです。では、この問題の解決プロセスを以下に示します。

まずは問題個所を把握するのが重要です。記事が表示されないということはgetNote()関数内で問題が起きている可能性が高いです。console.log()を利用して"url", "response", "content", "texts"などの表示を確認していきます。

console.log(content)までは良い感じに表示されていたのですが、console.log(texts)が正しく表示されていないようでした。（明らかにcontentの中のdivタグ以外の内容もスクレイピングされている。）


contentに関してですが、console.log(content)だと量が多すぎて表示されないので、分割して表示を確認しましょう。そのためには、以下のようにcontentをoutputという単位に分割して表示させます。

HTMLを確認するには、

```
const output = content.split('\n');//改行で分割して1行ごとに要素とした配列
  for(let i=0;i<output.length;i++){
    console.log(output[i]);
  }
```
というコードをcontentのすぐ下に追加します。

そして、表示されたHTMLコードを全てテキストファイルなどにコピペし、ctrl+Fなどで該当するHTMLタグを探しましょう。

noteがアップデートしたのか、HTMLを確認してみると、
```
<div class="m-largeNoteWrapper__card" data-v-1e6ad5ad>
```

というタグが存在していませんでした。

似たタグを探していると、今回は、divタグが

```
<div class="m-largeNoteWrapper__card" data-v-bcbdb926>
```
に置き換わっていることが判明しました。 noteのアップデートによるものなのでしょうか。これこそスクレイピングができなかった根本の理由ですね。

divタグをこれに書き換えれば、問題なく動きました。

# 問題解決後のソースコード

```
function myFunction(){
  const cache = CacheService.getScriptCache();
  const discordWebHookURL = "";//ここにwebhookURLを貼る

  mute_user_list = [""];//ミュートしたいユーザーネームをまとめるリスト
  mute_hashtag_list = [""];//ミュートしたいハッシュタグをまとめるリスト


  let send_list = new Array;
  let bf_send_list = cache.get("send_list")
  console.log(bf_send_list)
  if(bf_send_list != null && bf_send_list != ""){
    bf_send_list = bf_send_list.split(",")
    bf_send_list.forEach(item =>{
      send_list.push(item)
    }
    );    
  } 

  getNote("https://note.com/hashtag/海外?f=hot&paid_only=false","海外").forEach(item => {
    send_list.push(item);
  });

  let last_list = send_list.filter((item, index) => send_list.indexOf(item) === index);
  console.log(send_list)
  console.log(last_list)
  for (i = 0; i < 1;i++){
    if (last_list.length > 0){
        send_switch = 0
        var article = last_list.pop()
        send_url = "https://note.com" + article
        const message = {
            "content": send_url,
            "tts": false
        }


        const param = {
            "method": "POST",
            "headers": { 'Content-type': "application/json"},
            "payload": JSON.stringify(message)
        }
        var user_name = article.substring(1,article.indexOf("/",2));
        var article_id = article.substring(article.lastIndexOf("/")+1);

        mute_user_list.forEach((value) => {
          if (user_name == value){
            send_switch += 1
          }
        })

        var hashtag_url = `https://note.com/api/v3/notes/${article_id}`;
        let jsonArticleInfo = UrlFetchApp.fetch(hashtag_url, {'method':'get'});
        var parsedData = JSON.parse(jsonArticleInfo);
          
        var hashtags = parsedData.data.hashtag_notes;
        var hashtagNames = hashtags.map(function(item) {
        return item.hashtag.name;
          }
        )

        mute_hashtag_list.forEach((value) => {
          hashtagNames.forEach((value2) => {
            if (value == value2){
              send_switch += 1
            }
          })
        })

        if (send_switch == 0){
          UrlFetchApp.fetch(discordWebHookURL,param);
      }
  }}
  cache.put("send_list",last_list,60 * 60 * 24);
}

function getNote(url,key) {//スクレイピングしてnoteのURLをとってきている
  const cache = CacheService.getScriptCache();
  let bf_url_list = cache.get(key);
  if (bf_url_list != null){
      bf_url_list = bf_url_list.split(",");
  } else{
    bf_url_list = new Array()
  }
  let url_list = new Array();
  let response = UrlFetchApp.fetch(url);
  let content = response.getContentText("utf-8");
  // const output = content.split('\n');//改行で分割して1行ごとに要素とした配列
  // for(let i=0;i<output.length;i++){
  //   console.log(output[i]);
  // }
  // スクレイピングができなかった理由はここ
  let texts = Parser.data(content).from('<div class="m-largeNoteWrapper__card" data-v-bcbdb926>').to('</div>').iterate();
  console.log(texts)
  texts.forEach((value, index) => {
      let i = 0
      url_temp_l = [];
      let url_temp = "";
      let html_list = value.split("");
      html_list.forEach((value2,index) => {
          if (value2 == '"'){
              i += 1
          }
          if (i == 1){
              url_temp_l.push(value2)
          }
      })
      for (i = 1; i < url_temp_l.length; i++){

          url_temp += url_temp_l[i]
      }
      url_list.push(url_temp);
  });

  last_url_list = url_list.filter(item => !bf_url_list.includes(item));
  cache.put(key,url_list,60 * 60 * 24);
  return(last_url_list);
}
```
# 一言
スクレイピングの仕組みの理解が深まったので良かった。
# 参考文献
[【誰でも導入可】ShadowverseのnoteをDiscordに投げてくれるbotを作ってみた。※追記,機能追加済](https://note.com/zekito/n/n4ffc1a8a8fd7 "【誰でも導入可】ShadowverseのnoteをDiscordに投げてくれるbotを作ってみた。※追記,機能追加済")
[GASを使ったWebスクレイピング](https://tetsuooo.net/gas/82/# "GASを使ったWebスクレイピング")
[logging output too large. truncating output.の対策](https://tetsuooo.net/gas/237/ "logging output too large. truncating output.の対策")