2
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.

【Qiitaポートフォリオ】@j5c8k6m8の活動【2021/8 更新停止】

Last updated at Posted at 2020-04-17

ピックアップ記事用のポートフォリオです。
                                                                                                                                                                                    
※ 本記事はスマホだと一部の表示が崩れます。PC版サイトで表示してください。

直近一年の活動(2020/8/11 ~ 2021/8/10)1

{\rm{\Huge 300}\quad{\Large LGTM}\qquad{\Huge 8}\quad{\Large 記事}}

日 ■̟
月 ■̟
火 
水 
木 ■̟■̟■̟
金 ■̟
土 ■̟
■■■■■■■■1LGTM  10LGTM  100LGTM  1,000LGTM  ■̟ 投稿あり(リンク付)

タグ毎の記事一覧2

【Qiita】 (64記事, 1,820LGTM)

上記以外の記事を確認する

【自動更新】 (50記事, 1,525LGTM)

上記以外の記事を確認する

【今読んでおくべき記事100選】 (46記事, 1,726LGTM)

上記以外の記事を確認する

【QiitaAPI】 (15記事, 67LGTM)

上記以外の記事を確認する

【タグ説明】 (14記事, 155LGTM)

上記以外の記事を確認する

【ポエム】 (14記事, 128LGTM)

上記以外の記事を確認する

【JavaScript】 (13記事, 100LGTM)

上記以外の記事を確認する

【CSS】 (8記事, 893LGTM)

上記以外の記事を確認する

【Qiitaタグ集計記事】 (6記事, 37LGTM)

上記以外の記事を確認する

【Ruby】 (6記事, 24LGTM)

上記以外の記事を確認する

【quine投稿】 (5記事, 11LGTM)

上記以外の記事を確認する

【闇の魔術】 (5記事, 208LGTM)

上記以外の記事を確認する

【COTOHA】 (4記事, 44LGTM)

上記以外の記事を確認する

【pug】 (3記事, 851LGTM)

【DeadCodeCookbook】 (3記事, 67LGTM)

【Rails】 (3記事, 9LGTM)

【Python】 (3記事, 9LGTM)

【quine】 (3記事, 3LGTM)

【自然言語処理】 (3記事, 37LGTM)

【なでしこ】 (3記事, 25LGTM)

【ShellScript】 (3記事, 9LGTM)

【Git】 (3記事, 6LGTM)

【ネタ】 (3記事, 64LGTM)

【Go】 (3記事, 5LGTM)

【AsyncAwait】 (3記事, 21LGTM)

【新人プログラマ応援】 (3記事, 22LGTM)

その他の記事 (1記事, 1LGTM)

この記事を自動更新しているコード

qiitaPortfolio.js
const authorization_token = 'Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd';
const thisCode = (authorization_token) => {
  const request = require('request');
  const fs = require('fs');

  const ITEM_ID = '45dc28d8204827d1212b';
  const USER_ID = 'j5c8k6m8';
  const LIKES_CACHE_FILE_PATH = 'qiita_portfolio_likes_cache.json';

  const execRequest = (url, method, json) => {
    return new Promise( (resolve, reject) => {
      let request_arg = { url: url, headers: { Authorization: authorization_token } };
      if (method) request_arg.method = method; 
      if (json) {
        request_arg.json = json;
        request_arg.headers['Content-Type'] = 'application/json';
      }
      request(request_arg, (error, response, body) => {
        if (error) {
          console.log(`execRequest fail [${url}]`);
          reject(error);
        } else {
          console.log(`execRequest success [${url}}]`);
          if (method) console.log(body);
          resolve(method ? body : JSON.parse(body));
        }
      });
    });
  }

  const getDateString = date => {
    return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()).toISOString().substring(0, 10);
  }

  const weekTextCalendarHeatmap = (startDate, endDate, dateCallback) => {
    const retRows = [ '', '', '','', '', '', '' ].map( w => [ `<font color="#a6aebb">${w} </font>` ]);
    const tmpDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
    let firstFlg = true;
    let tmpDay = tmpDate.getDay();
    let tmpDateNum;
    while (tmpDate <= endDate) {
      if (firstFlg) {
        for (let i = 0; i < tmpDay; i++) {
          retRows[i].push(dateCallback(null));
        }
        firstFlg = false;
      }
      retRows[tmpDay].push(dateCallback(tmpDate));
      tmpDate.setDate(tmpDate.getDate() + 1);
      tmpDay = tmpDate.getDay();
      tmpDateNum = tmpDate.getDate();
    }
    for (let i = tmpDay; i < 7; i++) {
      retRows[i].push(dateCallback(null));
    }
    return retRows.map( r => r.join('')).join('\n');
  }

  const qiitaTextCalendarHeatmap = (date, items_by_date, likes_num_by_date) => {
    let endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
    let startDate = new Date(date.getFullYear() - 1, date.getMonth(), date.getDate());
    let totalItemNum = 0;
    let totalLikeNum = 0;
    const tmpDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
    while (tmpDate <= endDate) {
      const tmpDateStr = getDateString(tmpDate);
      if (items_by_date[tmpDateStr]) {
        totalItemNum += items_by_date[tmpDateStr].length;
      }
      if (likes_num_by_date[tmpDateStr]) {
        totalLikeNum += likes_num_by_date[tmpDateStr];
      }
      tmpDate.setDate(tmpDate.getDate() + 1);
    }
    const header = `# 直近一年の活動(${startDate.getFullYear()}/${startDate.getMonth() + 1}/${startDate.getDate()}${endDate.getFullYear()}/${endDate.getMonth() + 1}/${endDate.getDate()})[^1]

[^1]: 自身の記事に対して付いたLGTM数、自身の記事投稿数と、自身の記事に対して付いたLGTM数のヒートマップ

\`\`\` math
{\\rm{\\Huge ${totalLikeNum.toLocaleString()}}\\quad{\\Large LGTM}\\qquad{\\Huge ${totalItemNum.toLocaleString()}}\\quad{\\Large 記事}}
\`\`\`
`
    const dateCallback = date => {
      ret = '';
      if (date) {
        const dateStr = getDateString(date);
        if (items_by_date[dateStr]) {
          if (likes_num_by_date[dateStr]) {
            ret += `[<font color="${getColorByNum(likes_num_by_date[dateStr])}">■&#x31f;</font>](https://qiita.com/search?sort=&q=user%3A${USER_ID}+created%3A${dateStr})`;
          } else {
            ret += `[<font color="#a6aebb">■&#x31f;</font>](https://qiita.com/search?sort=&q=user%3A${USER_ID}+created%3A${dateStr})`;
          }
        } else {
          ret += `<font color="${getColorByNum(likes_num_by_date[dateStr])}">■</font>`;
        }
      } else {
        ret += '<font color="#fff">■</font>';
      }
      return ret;
    }
    return header + '\n' + weekTextCalendarHeatmap(startDate, endDate, dateCallback);
  }

  const getColorByNum = num => {
    if (!num) {
      return '#ebedf0'
    } else if (num < 10) {
      return `#${Math.floor(198 - 75 * (num - 1) / 9).toString(16)}${Math.floor(228 - 27 * (num - 1) / 9).toString(16)}${Math.floor(139 - 28 * (num - 1) / 9).toString(16)}`
    } else if (num < 100) {
      return `#${Math.floor(123 - 88 * (num - 10) / 90).toString(16)}${Math.floor(201 - 48 * (num - 10) / 90).toString(16)}${Math.floor(111 - 52 * (num - 10) / 90).toString(16)}`
    } else if (num < 1000) {
      return `#${Math.floor(35 - 10 * (num - 100) / 900).toString(16)}${Math.floor(154 - 57 * (num - 100) / 900).toString(16)}${Math.floor(59 - 20 * (num - 100) / 900).toString(16)}`
    } else {
      return '#196127'
    }
  }

  (async () => {
    const items = [];
    const items_by_tag = {};
    const items_by_date = {};
    for(let i = 1; i <= 100; i++) { // 記事数は最大1万想定
      // 自身以外のユーザの記事も作成できるように
      let response_items = await execRequest(`https://qiita.com/api/v2/items/?page=${i}&per_page=100&query=user:${USER_ID}`);
      response_items.filter( response_item => !response_item.private ).forEach( response_item => {
        let item = {
          id: response_item.id,
          title: response_item.title,
          tags: response_item.tags.map( tag => tag.name ),
          created_at: new Date(response_item.created_at),
          likes_count: response_item.likes_count,
        }
        items.push(item);
        if (!items_by_date[getDateString(item.created_at)]) {
          items_by_date[getDateString(item.created_at)] = [];
        }
        items_by_date[getDateString(item.created_at)].push(item);
        item.tags.forEach( tag => {
          if (!items_by_tag[tag]) items_by_tag[tag] = [];
          items_by_tag[tag].push(item);
        });
      });
      if (response_items.length < 100 || response_items.length === 0) break;
    }
    let items_likes = {};
    const likes_num_by_date = {};
    if (fs.existsSync(LIKES_CACHE_FILE_PATH)) {
      items_likes = JSON.parse(fs.readFileSync(LIKES_CACHE_FILE_PATH, 'utf-8'))
    }
    for (let item of items) {
      let likes = items_likes[item.id];
      if (!likes) {
        likes = [];
        items_likes[item.id] = likes;
      }
      if (item.likes_count === 0) {
        items_likes[item.id] = [];
      } else if (item.likes_count !== likes.length) { // リクエスト数を抑えるために、LGTM数に変動がなければ再取得しない
        if (item.likes_count <= 100) {
          let response_likes = await execRequest(`https://qiita.com/api/v2/items/${item.id}/likes?page=1&per_page=100`);
          likes = response_likes.map(like => { return { created_at: like.created_at, user_id: like.user.id }; });
          items_likes[item.id] = likes;
          fs.writeFileSync(LIKES_CACHE_FILE_PATH, JSON.stringify(items_likes)); // 回数制限でAPI呼び出しに失敗する可能性があるため、都度保存する
        } else { // APIは作成日時の降順で結果を返却する
          let before_likes = likes.concat(); // shiftして破壊していくためコピーしておく
          likes = [];
          for (let i = 1; i <= Math.floor(item.likes_count / 100) + 1; i++) {
            let response_likes = await execRequest(`https://qiita.com/api/v2/items/${item.id}/likes?page=${i}&per_page=100`);
            response_likes.map(like => { return { created_at: like.created_at, user_id: like.user.id }; }).forEach( response_like => {
              if (before_likes.length === 0) {
                likes.push(response_like);
              } else {
                let response_created_at = new Date(response_like.created_at);
                let first_created_at = new Date(before_likes[0].created_at);
                while (response_created_at <= first_created_at) {
                  if (response_created_at < first_created_at || response_like.user_id == before_likes[0].user_id) {
                    before_likes.shift();
                    if (before_likes.length === 0) {
                      break;
                    } else {
                      first_created_at = new Date(before_likes[0].created_at);
                    }
                  } else {
                    break;
                  }
                }
                likes.push(response_like);
              }
            });
            if (likes.length + before_likes.length == item.likes_count) {
              likes = likes.concat(before_likes);
              break;
            }
          }
          items_likes[item.id] = likes;
          fs.writeFileSync(LIKES_CACHE_FILE_PATH, JSON.stringify(items_likes)); // 回数制限でAPI呼び出しに失敗する可能性があるが、差分更新であり、データの整合性を考慮してitem毎の保存とする
        }
      }
    }
    items.forEach( item => {
      items_likes[item.id].forEach( like => {
        let date = getDateString(new Date(like.created_at));
        if (likes_num_by_date[date] == null) {
          likes_num_by_date[date] = 0;
        } else {
          likes_num_by_date[date] += 1;
        }
      });
    });
    let body = 'ピックアップ記事用のポートフォリオです。\n'
    body += '                                                                                                                                                                                    '
    body += '\n※ 本記事はスマホだと一部の表示が崩れます。PC版サイトで表示してください。\n\n'
    body += qiitaTextCalendarHeatmap(new Date(), items_by_date, likes_num_by_date);
    body += `\n<font color="#ffffff">■■■■■■■■</font><font color="#c6e48b">■ </font><font color="#a6aebb">1LGTM</font>  `
    body += `<font color="#7bc96f">■ </font><font color="#a6aebb">10LGTM</font>  `
    body += `<font color="#239a3b">■ </font><font color="#a6aebb">100LGTM</font>  `
    body += `<font color="#196127">■ </font><font color="#a6aebb">1,000LGTM</font>  `
    body += `<font color="#a6aebb">■&#x31f; </font><font color="#a6aebb">投稿あり(リンク付)</font>\n`
    body += `

# タグ毎の記事一覧[^2]

[^2]: 3つ以上の記事で使われているタグが対象。3つ以上の記事で使われているタグが含まれない記事は、「その他の記事」に一覧を表示。
`
    let tags = Object.keys(items_by_tag).sort((a,b) => items_by_tag[b].length - items_by_tag[a].length);
    let other_items = [];
    let other_tags = [];
    tags.forEach( tag => {
      if (items_by_tag[tag].length > 2) {
        let items = items_by_tag[tag].concat().sort((a,b) => b.likes_count - a.likes_count);
        body += `\n## 【${tag}】 (${items.length.toLocaleString()}記事, ${items.reduce( (a, c) => a + c.likes_count, 0).toLocaleString()}LGTM)\n\n`;
        for (let i = 0; i < 3; i++) {
          body += ` - [${items[i].title}](https://qiita.com/j5c8k6m8/items/${items[i].id}) (${getDateString(items[i].created_at).replace(/-/g, '/')}投稿 ${items[i].likes_count.toLocaleString()}LGTM)\n`
        }
        if (items_by_tag[tag].length > 3) {
          body += `\n[上記以外の記事を確認する](https://qiita.com/search?q=user%3A${USER_ID}+tag%3A${tag})\n`
        }
      } else {
        other_items = other_items.concat(items_by_tag[tag].filter( item => !other_items.includes(item)));
        other_tags.push(tag);
      }
    });
    other_items = other_items.filter( item => item.tags.every( tag => other_tags.includes(tag))).sort((a,b) => b.likes_count - a.likes_count);
    if (other_items.length > 0) {
      body += `\n## その他の記事 (${other_items.length.toLocaleString()}記事, ${other_items.reduce( (a, c) => a + c.likes_count, 0).toLocaleString()}LGTM)\n\n`;
      other_items.forEach( item => {
          body += ` - [${item.title}](https://qiita.com/j5c8k6m8/items/${item.id}) (${getDateString(item.created_at).replace(/-/g, '/')}投稿 ${item.likes_count.toLocaleString()}LGTM)\n`
      });
    }

    body += `

# この記事を自動更新しているコード

\`\`\`  javascript:qiitaPortfolio.js
const authorization_token = 'Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd';
const thisCode = ${thisCode.toString()}
thisCode(authorization_token);
\`\`\`
`;

    await execRequest(
      `https://qiita.com/api/v2/items/${ITEM_ID}`,
      'PATCH', {
//      `https://qiita.com/api/v2/items`,
//      'POST', {
        body: body,
        private: false,
        tags: [
          { name: 'Qiita' },
          { name: 'QiitaAPI' },
          { name: 'Qiitaポートフォリオ' },
          { name: 'ポートフォリオ' },
          { name: '自動更新' },
        ],
        title: `【Qiitaポートフォリオ】@${USER_ID}の活動【毎日自動更新】`
      });
  })();
}
thisCode(authorization_token);
  1. 自身の記事に対して付いたLGTM数、自身の記事投稿数と、自身の記事に対して付いたLGTM数のヒートマップ

  2. 3つ以上の記事で使われているタグが対象。3つ以上の記事で使われているタグが含まれない記事は、「その他の記事」に一覧を表示。

2
1
1

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
2
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?