LoginSignup
7
1

More than 3 years have passed since last update.

【Qiita x COTOHA APIプレゼント企画】の記事から Happy Comment賞 を勝手に決定する

Last updated at Posted at 2020-03-16

【Qiita x COTOHA APIプレゼント企画】 が昨日終了しました。

本キャンペーンでは

Cool!賞
Omoshiro Idea賞
Technology賞
fOr Beginner賞

が決定されますが、今回は、COTOHA API の 感情分析API で、キャンペーン対象のコメントを分析し、

HAppy Comment賞 を勝手に決定します。

HAppy Comment賞

記事についた、執筆者以外のコメントを 感情分析API で評価し、 感情分析の結果(sentiment) が Positive のコメントの、センチメントスコア(score) の総計 で評価します。

集計スクリプト
// for COTOHA API
const COTOHA_DEVELOPER_API_BASE_URL   = "https://api.ce-cotoha.com/api/dev/";
const COTOHA_ACCESS_TOKEN_PUBLISH_URL = "https://api.ce-cotoha.com/v1/oauth/accesstokens";
const CLIENT_ID     = "ココニアイデイカク";
const CLIENT_SECRET = "ココニシークレットカク";

// for QIITA_API
const QIITA_AUTHORIZATION = 'Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd' // token

const request = require('request');
const fs = require("fs");

const sleep = delay  => new Promise(resolve => setTimeout(resolve, delay));

const getItems = page => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `https://qiita.com/api/v2/items?page=${page}&per_page=100&query=created:>2020-02-06 created:<2020-03-15 tag:COTOHA`,
        headers: { Authorization: QIITA_AUTHORIZATION },
      },
      (error, response, body) => {
        if (error) {
          console.error('ERROR!', error);
          reject(error);
        } else {
          let res = JSON.parse(body);
          if (!Array.isArray(res)) {
            console.log(`INVALID!!! response is not array!!! ${JSON.stringify(res)}`);
            reject(res);
          } else {
            resolve(res.map( r => {
              return {
                id: r.id,
                title: r.title,
                user_id: r.user.id,
                likes_count: r.likes_count,
              }
            }));
          }
        }
      }
    );
  });
}

const getComments = item_id => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `https://qiita.com/api/v2/items/${item_id}/comments?page=1&per_page=100`, // 100件以上のコメントはない想定
        headers: { Authorization: QIITA_AUTHORIZATION },
      },
      (error, response, body) => {
        if (error) {
          console.error('ERROR!', error);
          reject(error);
        } else {
          let res = JSON.parse(body);
          if (!Array.isArray(res)) {
            console.log(`INVALID!!! response is not array!!! ${JSON.stringify(res)}`);
            reject(res);
          } else {
            resolve(res.map( r => {
              return {
                body: r.body,
                user_id: r.user.id,
              }
            }));
          }
        }
      }
    );
  });
}

const getAccessToken = () => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: COTOHA_ACCESS_TOKEN_PUBLISH_URL,
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        json: {
          grantType: "client_credentials",
          clientId: COTOHA_CLIENT_ID,
          clientSecret: COTOHA_CLIENT_SECRET,
        },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          resolve(body.access_token);
        } else {
          if (error) {
            console.log(`request fail. error: ${error}`);
          } else {
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${body}`);
          }
          reject(body);
        }
      },
    );
  });
}

const getSentiment = (accessToken, sentence) => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: `${COTOHA_DEVELOPER_API_BASE_URL}nlp/v1/sentiment`,
        method: 'POST',
        headers: { 'Content-Type': 'application/json;charset=UTF-8', Authorization: `Bearer ${accessToken}`},
        json: { sentence: sentence },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          if (body.status === 0) {
            resolve({
              sentiment: body.result.sentiment,
              score: body.result.score,
              emotional_phrase: body.result.emotional_phrase,
            });
          } else {
            console.log(sentence);
            console.log(`request fail. error: ${body.message}`);
            reject(body);
          }
        } else {
          if (error) {
            console.log(sentence);
            console.log(`request fail. error: ${error}`);
          } else {
            msg = (typeof body !== 'object') ? body : JSON.stringify(body);
            console.log(sentence);
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${msg}`);
          }
          reject(body);
        }
      }
    );
  });
}


(async () => {
  let items = [];
  for (let i = 1; i <= 3; i++) { // 記事数は138想定だが、念のため3回を上限としてループする
    let tmpItems = await getItems(i);
    if (tmpItems.length === 0) {
      break;
    } else {
      items = items.concat(tmpItems);
      sleep(1000)
    }
  }
  for (let item of items) {
    let comments = await getComments(item.id);
    item.comments = comments.map( c => { return { body: c.body, user_id: c.user_id } })
  }
  fs.writeFileSync(`comments.json`, JSON.stringify(items, null, '  '));
  // let items = JSON.parse(fs.readFileSync(`comments.json`, 'utf-8')); // 一度出力成功した場合はファイルから読み込む
  let accessToken = await getAccessToken();
  for (let item of items) {
    let comments = item.comments.filter( c => c.user_id !== item.user_id);
    for (let comment of comments) {
      let sentiment = await getSentiment(accessToken, comment.body)
      comment.sentiment = sentiment;
      sleep(1000)
    }
    item.comment_num = comments.length;
    item.happy_score = 0;
    item.happy_num = 0;
    for (let comment of comments) {
      if (comment.sentiment.sentiment === 'Positive') {
        item.happy_num += 1;
        item.happy_score += comment.sentiment.score;
      }
    }
  }
  items.sort((a, b) => (b.happy_score - a.happy_score === 0) ? b.likes_count - a.likes_count : b.happy_score - a.happy_score);
  fs.writeFileSync(`happyComments.json`, JSON.stringify(items, null, '  '));
  // let items = JSON.parse(fs.readFileSync(`happyComments.json`, 'utf-8')); // 一度出力成功した場合はファイルから読み込む
  let doc = '|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score|LGTM|\n|---|---|---|---|---|---|\n';
  for (let i = 0; i < items.length; i++) {
    let item = items[i];
    doc += `|${i + 1}|@${item.user_id}|[${item.title}](https://qiita.com/${item.user_id}/items/${item.id})|<font color="red">${item.happy_num}</font>/${item.comment_num}|**<font color="red">${item.happy_score.toFixed(2)}</font>**|${item.likes_count}|\n`;
  }
  fs.writeFileSync(`happyCommentsItemRanking.md`, doc);

  // 平均を求める
  for (let item of items) {
    item.happy_average = item.comment_num === 0 ? 0 : item.happy_score / item.comment_num;
  }
  items.sort((a, b) => (b.happy_average - a.happy_average === 0) ? b.likes_count - a.likes_count : b.happy_average - a.happy_average);
  doc = '|順位|ユーザ|タイトル|Comments (Positive/母数)|<font color="red">Ha</font>ppy Score Avarage |LGTM|\n|---|---|---|---|---|---|\n';
  for (let i = 0; i < items.length; i++) {
    let item = items[i];
    doc += `|${i + 1}|@${item.user_id}|[${item.title}](https://qiita.com/${item.user_id}/items/${item.id})|<font color="red">${item.happy_num}</font>/${item.comment_num}|**<font color="red">${item.happy_average.toFixed(2)}</font>**|${item.likes_count}|\n`;
  }
  fs.writeFileSync(`happyCommentsAvarageItemRanking.md`, doc);

  // コメントごとの集計
  let comments = [];
  for (let item of items) {
    for (let comment of item.comments) {
      if (comment.user_id !== item.user_id) {
        comments.push({
          item_id: item.id,
          item_title: item.title,
          item_user_id: item.user_id,
          user_id: comment.user_id,
          body: comment.body,
          happy_score: comment.sentiment.sentiment === 'Positive' ? comment.sentiment.score : 0,
        })
      }
    }
  }
  comments.sort((a, b) =>  b.happy_score - a.happy_score);
  doc = '|順位|ユーザ|コメント内容|対象記事|<font color="red">Ha</font>ppy Score|\n|---|---|---|---|---|\n';
  for (let i = 0; i < comments.length; i++) {
    let comment = comments[i];
    doc += `|${i + 1}|@${comment.user_id}|${comment.body.replace(/\n/g, '')}|[${comment.item_title}](https://qiita.com/${comment.item_user_id}/items/${comment.item_id})|**<font color="red">${comment.happy_score.toFixed(2)}</font>**|\n`;
  }
  fs.writeFileSync(`happyCommentsRanking.md`, doc);
})();

結果発表

順位 ユーザ タイトル Comments (Positive/母数) Happy Score LGTM
1 @honehoney 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム 12/28 3.47 1376
2 @omiita 「募ってはいるが、募集はしていない」 人たちへ 8/26 1.97 1166
3 @eigs 忙しい MATLAB 芸人向け Qiita ふり返り(2019年版) 3/3 1.47 10
4 @ga_i_sa_i "幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に] 3/3 0.93 24
5 @aikawa4866 感情分析でサボテンは踊るのをやめてしまうか 1/2 0.71 187
6 @tanaken0515 [自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました 1/1 0.68 12
7 @j5c8k6m8 キラやば~っ☆ な「COTOHA DECO」作っちゃった!?よーしっ、さっそくQiitaへ投稿だーっ☆ 1/1 0.59 15
8 @kishiyyyyy 褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話 1/1 0.47 11
9 @yossymura 人間は、そんなに悲しい生き物じゃないけれど 2/5 0.38 183
10 @eigs 【COTOHA API x MATLAB】Qiita 投稿記事の要約 1/1 0.37 15

Cool!賞 を受賞予定の @honehoney さんの 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム見事2冠 を獲得です:trophy:
おめでとうございます:clap:

番外編1(平均で評価)

コメントが付いている方が、いい記事と評価したかったので、総計 で評価しましたが、コメント数に対する 平均 でも評価してみます。

順位 ユーザ タイトル Comments (Positive/母数) Happy Score Avarage LGTM
1 @tanaken0515 [自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました 1/1 0.68 12
2 @j5c8k6m8 キラやば~っ☆ な「COTOHA DECO」作っちゃった!?よーしっ、さっそくQiitaへ投稿だーっ☆ 1/1 0.59 15
3 @eigs 忙しい MATLAB 芸人向け Qiita ふり返り(2019年版) 3/3 0.49 10
4 @kishiyyyyy 褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話 1/1 0.47 11
5 @eigs 【COTOHA API x MATLAB】Qiita 投稿記事の要約 1/1 0.37 15
6 @aikawa4866 感情分析でサボテンは踊るのをやめてしまうか 1/2 0.35 187
7 @ga_i_sa_i "幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に] 3/3 0.31 24
8 @dakikd COTOHA APIで Pen Pineapple Apple Pen 2/2 0.14 19
9 @honehoney 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム 12/28 0.12 1376
10 @omiita 「募ってはいるが、募集はしていない」 人たちへ 8/26 0.08 1166

平均では @tanaken0515さん の [自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました1位 です:medal:
おめでとうございます:clap:

番外編2(コメント自体のランキング)

順位 ユーザ コメント内容 対象記事 Happy Score
1 @perpouh ネガポジ逆にしたら「人間同士の諍いを見て喜ぶ悪魔」も作れますね₍₍⁽⁽👿🔱₎₎⁾⁾ 感情分析でサボテンは踊るのをやめてしまうか 0.71
2 @youwht 丁寧な解説、とても勉強になりました。m(_ _)m拡張固有表現の試行も面白いですね。 [自然言語処理 初心者向け] COTOHA API を Ruby で使いたくて gem を作りました 0.68
3 @youwht 素敵!キラやば~っ☆な1日に、なりますよーに! キラやば~っ☆ な「COTOHA DECO」作っちゃった!?よーしっ、さっそくQiitaへ投稿だーっ☆ 0.59
4 @watawatavoltage サーベイ記事本当に助かります. 忙しい MATLAB 芸人向け Qiita ふり返り(2019年版) 0.54
5 @watawatavoltage 確かに,一時的にバズるだけじゃだめですよね.振り返ることによって,こんなものあったなとかこれよかったななど思い出すきっかけになりますよね. 忙しい MATLAB 芸人向け Qiita ふり返り(2019年版) 0.50
6 @NIPPONPON これは長き戦乱を終わらせる素晴らしいプログラムですね。世に平穏のあらん事を。ところで「おらがはたけのこのさといもはうまい」のようなケースも救済するでしょうか? 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム 0.48
7 @tanaken0515 gem をご活用くださりありがとうございます!ポジティブになれて素敵ですね :clap: 褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話 0.47
8 @kitamin 30年来続くこの国内最大の内戦に対してのスマートなアプローチですが、きのこの里 とか たけのこの山 とか双方巻き込む無差別テロ誤り発言が出てきたらどう対処できるんでしょうね?(*´-`) 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム 0.47
9 @Yasha_Wedyue 非常に面白い記事でした。ただこの方法ですと、きのこの山とたけのこの里両方を好き(どちらかというとたけのこの里派なテロリストも含む)な場合、異分子として排除できないのでは?と思いました。 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム 0.46
10 @Taro_man 「人間勝者」でも名作になりそうです。 "幸せ"の多い生涯を送って来ました。[COTOHA APIを使って「人間失格」を"幸せ"に] 0.45

なんと、 @perpouhさん の ネガポジ逆にしたら「人間同士の諍いを見て喜ぶ悪魔」も作れますね₍₍⁽⁽👿🔱₎₎⁾⁾最もトップの 0.71 をマーク しました!
おめでとうございます:clap:

コメントが、執筆者のモチベーションを保ち、Qiitaを支えているといっても過言ありません。

みなさんの ポジティブなコメントに、感謝いたします:blush:

7
1
4

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
7
1