Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

忙しい MATLAB 芸人向け Qiita ふり返り舞台裏【COTOHA API x Qiita API x MATLAB】

はじめに

今回は「いそがしい MATLAB 芸人向け Qiita 振返り」作成記です。

2020/01/04 追記
COTOHA API の要約機能は正式版がリリースされた 2020/03/23 以降、無料枠では使えなくなっています。参照:https://api.ce-cotoha.com/contents/reference/releasenote.html
追記ここまで

最近 MATLAB タグのついた記事が増えてきているように思いますので、Qiita API を使って集計しがてら、前回(【COTOHA API x MATLAB】Qiita 投稿記事の要約)に引き続き COTOHA API (詳細:COTOHA API Portal)の要約機能を使って少し遊んでみましょう。

この記事の Livescript 版はこちら Example_QiitaSummarization1 に置いてありますので試したい方は DL してみてください。

実行環境

  • MATLAB R2019b
  • Text Analytics Toolbox

やったこと

timetable 型のデータを retime 関数で集計するところ、そして構造体ベクトルの入ったセル配列ベクトル(!?)の処理などが、マニアックで見どころです。

また、約 5000 文字を超える文章については COTOHA API からエラーが返ってきちゃったので、その辺ざっくり打ち切っていますのでご了承ください。

Qiita API で MATLAB/Simulink の記事取得

早速作業に取り掛かります。

記事のデータ取るコード
clear
loadFlag = true;

if loadFlag % 読み込まない場合は事前読み込み済みのデータを mat ファイルから読み込みます
    % アクセストークン使用
    accessToken = 'Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
    opts = weboptions('HeaderFields',{'Authorization',accessToken});
    % opts = weboptions; % 制限内だったらこれだけでもOK

    % 全件取っても 600 記事なので特に細工は必要なさそう。
    page = 1;
    data = [];
    tags = ["matlab","simulink"];
    for tagid=1:2
        page = 1;
        while true
            url = "https://qiita.com/api/v2/tags/" + tags(tagid) + "/items?page="+page+"&per_page=100";
            tmp = webread(url,opts);
            if isempty(tmp)
                break;
            end
            data = [data;tmp]; %#ok<AGROW>
            page = page + 1;
        end
    end

    % 構造体で得られるので table 型に変換
    data = struct2table(data);

    % 一応重複が無いかだけチェック
    [~,ia,~] = unique(data.id);
    data = data(ia,:);
    save('allArticles.mat','data')
else
    load allArticles.mat %#ok<UNRCH>
end

ちなみにこの時点で変数は 18 個もあるんですがとりあえず、投稿日、記事のタイトル、記事の内容、ユーザー名、いいね数、タグ詳細、URL だけを残しておきます。

data = data(:,{'created_at', 'title','rendered_body','user','likes_count','tags','url'});
head(data)

Capture.PNG

こんな感じ。

timetable 型に変換など、こまごましたところ

MATLAB 的には本記事のメインテーマ:前処理です。

投稿時刻が 2020-02-08T19:10:45+09:00 というようなフォーマットなので、それに合わせて datetime 型に変換、そしてデータ全体を timetable 型に変換しておきます。

created_at = datetime(vertcat(data.created_at),...
    'InputFormat', "uuuu-MM-dd'T'HH:mm:ss'+09:00");
tData = table2timetable(data,'RowTimes', created_at);

文字列関係はすべて string 型にしておきます。ユーザー名については他にも沢山情報が詰まった構造体になっているのですが、ここでは id だけを取り出しておきます。

url (文字列 in セル配列)

これは簡単

tData.url = string(tData.url);

tData.url は cell 配列になっているので、そのまま string 型にサクッと変更されます。title と rendered_body も同様です。

tData.title = string(tData.title);
tData.rendered_body = string(tData.rendered_body);

user (構造体)

構造体はちょっと面倒。欲しいのは user の中の id というメンバーだけ。冒頭だけのぞいてみると

tData.user(1:2).id
ans = 'Alberobellojiro'
ans = 'nHounoki'

という「コンマ区切りリスト」になります。

これは、同じサイズのデータであれば [] (vertcat or horzcat) でベクトル化、もしくは {} で囲めば cell 配列として処理できます。慣れるとどうってことないのですが、結構取り扱いに困った数々の記憶がよみがえります・・。

ここでは id はいろんな長さの文字列のようなので、cell 配列に変換した上で string にかけてみます。

tmp = {tData.user.id}';
tmp(1:2)
ans = 2x1 の cell 配列    
'Alberobellojiro'    
'nHounoki'           

tData.user = string(tmp);

tag (構造体s in セル配列)

これはまた少し面倒くさい。セル配列の中に構造体が(複数)入っています。例えば

tData.tags{1}
フィールド name versions
1 'R' [ ]
2 'matlab' [ ]

こんな感じ。欲しいのは name だけなんですが、

{tData.tags{1}.name}
ans = 1x2 の cell 配列    
'R'          'matlab'     

という手も全行一気に処理するには使えそうにない・・のでここは潔く諦めて cellfun で行ってみます。

tmp = tData.tags; % cell 配列
cellfun(@(x) string({x.name}), tmp);
エラー: cellfun
Uniform の出力のインデックス 1 にある出力 1 がスカラーでありません。
'UniformOutput' を false に設定してください。

とりあえず出ちゃうエラー。

それぞれのセル配列の中身の構造体から name フィールド取ってきて、いったんセル配列にした上で string 化する、という処理しているんですが、UniformOutput じゃないと怒られます。要は記事ごとに tag の数が違うので、連結できませんという意味です。そらそうだ。

ここはエラーメッセージに従ってオプションを付けて、セル配列として出力しておきます。

tData.tags = cellfun(@(x) string({x.name}), tmp, 'UniformOutput', false);
tData.tags(1:2)
1
1 1x2 string
2 1x3 string
tData.tags{1}
ans = 1x2 の string 配列    
"R"          "matlab"     

こんな感じ。記事ごとの tag がベクトルでとれていますね。

月間投稿数推移

ここでようやくひと段落・・。

セルやら構造体やら構造体のベクトルやら面倒でしたが、ひとまずまとまったので月間投稿数の推移でも見てみます。ここは個人的にお気に入りの(しらんがな) retime 関数の出番です。

tmp = retime(tData(:,'title'),'monthly','count');
bar(tmp.Time,tmp.title)
title('# of post with MATLAB/Simulink tag')
ylabel('# of post')
grid on

attach:cat

Qiita 全体の活性化に引っ張られてか MATLAB 関連記事投稿数も少しづつ伸びてきていますが、昨年後半に大きく伸びていますね。

月間いいね総数

ついでに。今度は記事のカウントではなく、いいねの総数なので 'count' を 'sum' に変えておきます。

tmp = retime(tData(:,"likes_count"),'monthly','sum');
bar(tmp.Time,tmp.likes_count)
title('# of likes with MATLAB/Simulink tag')
ylabel('# of likes')
grid on

attach:cat

2015 年に異常値(?)がありますね。2019 年後半も健闘しているんですが、かき消されてしまっていますね・・。5000 越えはすごい。この記事です:数学を避けてきた社会人プログラマが機械学習の勉強を始める際の最短経路

'monthly' のところを 'yearly' にすれば年間、'weekly' にすれば週間、'secondly' にすれば毎秒ごとの集計が取れる優れもの。他にも使いどころあると思うので、詳細は retime 関数のヘルプページを見てください

さて本題の要約

2018年、2019年に投稿されたいいねが多い順に 50 記事について 2 文で COTOHA に要約してもらいます。

COTOHA API を使う部分は前回(【COTOHA API x MATLAB】Qiita 投稿記事の要約)と同じですが、記事の長さが 5000 文字を超えるとエラーが出る現象に見舞られたので、5000文字を超える大作記事については、ざっくり前半の2500文字、後半の2500文字を使います。

アクセストークンを取得する

まずはこれ。clientid/clientsecret はご自身のものに変えてください。

% clientid = 'input_your_Client_ID';
% clientsecret = 'input_your_Client_secret';

url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens';
options = weboptions('RequestMethod','post', 'MediaType','application/json');
Body = struct('grantType', 'client_credentials', ...
    'clientId', clientid, ...
    'clientSecret', clientsecret);
tokens = webwrite(url, Body, options);

2019年まとめ(いいね順)

2019 年の記事を取り出して、いいね順に並べ替えて上から 50 記事について要約をしてもらいます。

trange = timerange(datetime(2019,1,1),datetime(2020,1,1));
tData2019 = tData(trange,:);
tData2019 = sortrows(tData2019,'likes_count','descend');

COTOHA で 2 文に要約。

COTOHAsummary = strings(height(tData2019),1);
for ii=1:50
    COTOHAsummary(ii) = getSummary(tData2019.rendered_body(ii),tokens,2);
end
tData2019.COTOHAsummary = COTOHAsummary;

Markdown で書き出して後で Qiita にコピペ。

md = generateMarkdown(tData2019, "# 2019 まとめ");

mdfile = "Qiita2019" + ".md";
fileID = fopen(mdfile,'w');
fprintf(fileID,'%s\n',md);
fclose(fileID);

2018年まとめ(いいね順)

2018 年の記事を取り出して、いいね順に並べ替えて上から 50 記事について要約をしてもらいます。

上とほぼ同じなので折りたたみ
trange = timerange(datetime(2018,1,1),datetime(2019,1,1));
tData2018 = tData(trange,:);
tData2018 = sortrows(tData2018,'likes_count','descend');

COTOHA で 2 文に要約。

COTOHAsummary = strings(height(tData2018),1);
for ii=1:50
    COTOHAsummary(ii) = getSummary(tData2018.rendered_body(ii),tokens,2);
end
tData2018.COTOHAsummary = COTOHAsummary;

Markdown で書き出して後で Qiita にコピペ。

md = generateMarkdown(tData2018, "# 2018 まとめ");

mdfile = "Qiita2018" + ".md";
fileID = fopen(mdfile,'w');
fprintf(fileID,'%s\n',md);
fclose(fileID);

まとめ

Qiita API ではレンダリングされた記事とは別に 'body' で、markdown バージョンも取り出せますが、文字抽出作業には html の方が楽でした。

数式・コード部分はざっくり取り除いて処理をしましたが、タイトル+要約である程度中身も想像がつきますね。大量の記事をざっくり把握して読むものを選ぶという用途にはよさそうな気がしてきました。

COTOHA API を試してみたい!という方のお役に立てたら嬉しいです。

Appendix

getSummary 関数

getSummary 関数
function summary = getSummary(htmlSource, tokens, sent_len)

tree = htmlTree(htmlSource);

selector = "h1,h2,h3,p,li";
subtrees = findElement(tree,selector);

% check if details contained in p
index = false(length(subtrees),1);
for ii=1:length(subtrees)
    tmp = findElement(subtrees(ii),'details');
    index(ii) = isempty(tmp); % <DETAILS> があれば false になる
end

% DETAILS 無しの P, LI だけ
subtreesNoDetails = subtrees(index);

% 入力文
sentence = extractHTMLText(subtreesNoDetails);

sentence = join(sentence);
if strlength(sentence) > 5000
    tmp = char(sentence);
    sentence = string(tmp([1:2500,end-2499:end]));
end
strlength(sentence);

% Call COTOHA API
baseurl = 'https://api.ce-cotoha.com/api/dev/';
accesstoken = tokens.access_token;
Header = {'Content-Type', 'application/json;charset=UTF-8'; 'Authorization', ['Bearer ' accesstoken]};
Body = struct('document', sentence, ...
    'sent_len', num2str(sent_len));
options = weboptions('RequestMethod','post', 'MediaType','application/json','HeaderFields', Header);

response = webwrite([baseurl 'nlp/beta/summary'], Body, options);
summary = string(response.result);

end

generateMarkdown 関数

generateMarkdown
function md = generateMarkdown(tData, header)

md = header + newline + newline;
for ii=1:50
    title = tData.title(ii);
    url = tData.url(ii);
    md = md + "## " + ii + ": [" + title + "]("+url+")" + newline;
    likes = tData.likes_count(ii);
    user = tData.user(ii);
    date = tData.Time(ii);
    date.Format = 'yyyy/MM/dd';

    md = md + "@" + user + " さん (" ...
        + string(date) + " 投稿)" + ": **" + string(likes) + "**" + " いいね" ...
        + newline;

    tags = tData.tags{ii};
    tags = "```" + tags + "```";
    md = md + "**Tags** :" + join(tags) + newline;

    summary = tData.COTOHAsummary(ii);
    if strlength(summary) > 150 % 長い要約は打ち切っちゃいます。
        tmp = char(summary);
        summary = string(tmp(1:150)) + "...(中略)";
    end
    md = md + newline + ...
        "> " + summary + newline + newline;

end

end


  1. Livescript から markdown への変換は livescript2markdown​: MATLAB's live scripts to markdown で実施しています。 

eigs
MATLAB の中の人 MathWorks Japan アプリケーションエンジニア部 データ解析チームリーダー。 公式ブログも書いています。MATLAB の使い方に困ったら MATLAB Answers もどうぞ。 All comments are mine alone and do not necessarily reflect those of my employers.
https://blogs.mathworks.com/japan-community/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away