blog.takurinton.dev

日報?を作った

2021-07-21

こんにちは

どうも、僕です。 今回は Twitter API を使って自分のツイートを自動で拾ってきてそれをもとに140文字以内の任意の文章を生成してツイートする bot を作成したのでその様子を記事にします。
なお、 @takurinton ではないアカウント(知ってる人は知ってる)なのでそこはご了承ください。

使用技術

  • JavaScript
    • バンドルするのがめんどくさいので CommonJS べたべた書いてます
    • 手打ちの module.exports 久しぶりに見た
  • twitter
  • heroku
    • 定期実行に使用しています

コード

コードを見ていきます。
コードは5つに分けていて、

  • index.js (実行)
  • createSentence.js (文章を生成する部分)
  • init.js (初期設定、ユーザーネームや API KEY などを初期化する)
  • getTweet.js (ツイートを取得する)
  • postTweet.js (ツイートを投稿する)

といった感じで分割をしています。最終的には index.js を実行すればツイート取得して送信することができるようになっており、140文字を超えていた場合はその場で弾いています。
ここは要リファクタといった感じで、というのも Twitter は確かアルファベットは 1文字ではない換算になってるはずなので Twitter API がもともと仕込んでるエラーを拾ってそれに応じて再実行するか弾くかみたいなことをしたいなと思っています。
また、今のコードだと API KEY がおかしい時などは無限ループになってしまいますし、それを抑えるためにも変更が必要です。今はとりあえずさくっとお試しで作っただけなので適当です。

それでは見ていきます。

index.js

ここでは全体を実行する処理が書かれています。
コード自体はシンプルで、上から getTweet, createSentence, postTweet が流れていってるだけです。
また、ツイートのテキストが 140文字を超えていたら自分自身を呼び出して実行するようにしています。
コードは以下のようになっています。

const createSentence = require('./createSentence');
const getTweet = require('./getTweet');
const postTweet = require('./postTweet');

const tweet = async () => {
    let text = '';
    try {
        const sentence = await getTweet();
        text = createSentence(sentence);
    } catch(err) {
        console.error(err);
    };
    console.log(`text content: ${text}`);
    console.log(`text length: ${text.length}`);
    if (text.length === 0 || text.length > 140) {
        console.log('tweet length is 140 over.');
        console.log('restart programm.');
        console.log('');
        tweet(); // 文字数がおかしい時はここで再帰的に呼んでる
    } else {
        postTweet(text);
    };
}

const main = async () => {
    tweet();
};

main();

createSentence.js

ここが割と肝となる処理で、マルコフ連鎖を用いて文章を生成しています。
ちょっと渡す文字数が少ないとそのまま出力されてしまうことがあるのですが、現状問題なく動作するのでこのままやっています。
詳しくは、以前書いた マルコフ連鎖実装してみた | たくりんとんのブログ を参照してもらいたいのですが、マルコフ連鎖を用いて文章を生成しています。

const TinySegmenter = require('tiny-segmenter');
const segmenter = new TinySegmenter();

// 開始・終了判定
const START = '__START__';
const END   = '__END__';

// 雑音除去
const arrangeText = (text) => {
    text = text.replace(/\n/g, '。');
    text = text.replace(/[\?\!?!]/g, '。');
    text = text.replace(/[-||::・]/g, '。');
    text = text.replace(/[「」()\(\)\[\]【】]/g, ' ');
    text = text.replace(/@.*? /g, '');

    return text;
};

// 文章のリスト(対応)を作ってる
const makeDict = (text) => {
    const sentences = text.split('。');
    const morpheme = {};
    for(var i = 0; i < sentences.length; i++) {
        let tokenized = segmenter.segment(sentences[i]);
        if (!morpheme[START]) morpheme[START] = [];
        if (tokenized[0]) morpheme[START].push(tokenized[0]);
        for (var w = 0; w < tokenized.length; w++) {
            const now = tokenized[w];
            const next = tokenized[w+1] ?? END; 
            
            if (!morpheme[now]) morpheme[now] = [];

            morpheme[now].push(next);

            if (now === '、') morpheme[START].push(next);
        };
    };
    return morpheme;
};

// 文章を作る処理
const makeSentence = (dict) => {
    let now = '';
    let result = '';
    now = dict[START][Math.floor(Math.random() * dict[START].length)];
    result += now;
    while (now !== END) {
        now = dict[now][Math.floor(Math.random() * dict[now].length)];
        result += now;
    };
    result = result.replace(END, '。')
    return result;
};

// 1文を3回生成してる
const createSentence = (text) => {
    let res = '';
    for (var i = 0; i < 3; i++) {
        const _text = arrangeText(text);
        const dict = makeDict(_text);
        const sentence = makeSentence(dict);
        res += sentence;
    }
    return res;
};

module.exports = createSentence;

init.js

初期化をしています。
twitter というライブラリを使用していて、そのコンストラクタに API KEY を渡すことで client を生成して初期化することができます。また、垢バレを防ぐ為に一応ユーザーネームも環境変数に渡しました。セキュリティ意識。

const Twitter = require('twitter');

const SCREEN_NAME = process.env.SCREEN_NAME;
const client = new Twitter({
  consumer_key: process.env.CONSUMER_KEY,
  consumer_secret: process.env.CONSUMER_SECRET,
  access_token_key: process.env.TOKEN,
  access_token_secret: process.env.TOKEN_SECRET,
});

module.exports = {
  client: client, 
  SCREEN_NAME: SCREEN_NAME,
}

getTweet.js

ここではツイートを取得しています。
client.get を使用することで get request を投げることができ、statuses/user_timeline にリクエストを投げることで任意のユーザーのツイートを取得することができます。
詳しくは ドキュメント を見てください。
また、戻り値は Promise を返したりテキストを返したり選ぶことができ、今回は Promise を返すようにしました。

const { client, SCREEN_NAME } = require('./init');

const params = { screen_name: SCREEN_NAME };
const getTweet = async () => {
    try {
        const res = await client.get('statuses/user_timeline', params);
        const text = res.map(tweet => tweet.text).join('')
        return text;
    } catch(err) {
        console.error(err);
    }
};

module.exports = getTweet;

postTweet.js

get と同じく、post を指定することで post request を投げることができます。
後の処理はほぼ同じです。

const { client } = require('./init');

const postTweet = (text) => {
  const content = { status: text };
  client.post('statuses/update', content, (err, tweets, _) => {
    if (!err) console.log('tweet success');
    else console.error(err);
  });
};

module.exports = postTweet;

heroku で 定期実行をする

次に heroku でアプリケーションを作成して定期実行をしていきたいと思います。
以下を参考にして簡単に作成してみました。

定期実行に関してですが、今回は heroku scheduler を使用します。
これは cronのような機能で、定期的にプログラムを実行してくれます。
heroku のアドオンで scheduler を検索するとたくさん出てきますが、heroku scheduler は無料で使えるので良いです。

設定したらこんな感じ。
あとは指定した時間にツイートがされます。
時間については、日本で日が変わるタイミングでツイートして欲しいので UTC の15時にしてあります。
class="content-img

まとめ

思ったよりも簡単に自動ツイート機能ができました。思いついてから実装してやるまで地味に時間がかかってしまいましたが楽しかったです。
おしまい。