API利用でMediaWikiの変更情報をTwitterで発信するPythonスクリプト

[MediaWiki](https://www.mediawiki.org/) の API を使って該当する wiki の最近の更新情報を取得し、その内容を切り取ったものを Python のライブラリ Tweepy を使って Twitter でつぶやくスクリプトを紹介します。

この記事は MediaWiki の API クエリを Python をつかって利用することの説明を念頭に置いた記事です。ゆえにツイート部分を無視して、単純に記事タイトルや URL、記事の要約をプログラマブルに取得するときの参考情報になることも期待しています。

ここでは以下の 2 つのスクリプトを紹介します。

* 新しい記事をツイートするスクリプト `get_wiki_newpages.py`
* 内容が増えた記事をツイートするスクリプト `get_wiki_editcount.py`

# 環境
* MediaWiki : 1.35
  * Extension として [TextExtracts](https://www.mediawiki.org/wiki/Extension:TextExtracts/ja) が適用されている必要がある。
* Python : 3.8.5
* Tweepy : 3.9.0

# 動かし方
スクリプトと同じディレクトリに `config.py` を作成し、中に [Twitter Developer Platform](https://developer.twitter.com/) で取得した各 Key 情報を以下のように入れておきます。

```python
CONSUMER_KEY = "xxxxxxxxxxxxxxxx"
CONSUMER_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxx"
```

Twitter API Key の取得は以下のページを参考にしてください。

* [2020年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説 - ITTI STAFF BLOG](https://www.itti.jp/web-direction/how-to-apply-for-twitter-api/)

それぞれを `cron` 使って 5 分おきに動かしたりします。このあたりの動作時間の間隔はそれぞれ用途に応じて調整が必要でしょう。

```shell:crontab
5/* * * * * cd /path/to/workdir && /path/to/python3 get_wiki_newpages.py && /path/to/python3 get_wiki_editcount.py
```

# ソースコード全体
まずはそれぞれのソースコードを貼り付けておきます。その後解説します。

## 新しい記事をツイートするスクリプト
`tweepy` 以外は Python の標準ライブラリで動きます。
また該当する MediaWiki に Extension として [TextExtracts](https://www.mediawiki.org/wiki/Extension:TextExtracts/ja) が入っている必要があります。

<details><summary>`get_wiki_newpages.py`（クリックで展開）</summary><div>

```python:get_wiki_newpages.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# MediaWiki の "特別:最近の更新" からデータを抜き出し、
# json として保存, すでに保存してあるデータと比較し、
# 新しい記事のタイトルをツイートするスクリプト

# The MIT License (MIT)
# 
# Copyright (c) 2020 Murahashi Kuriki
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import io
import sys
import requests
import json
import tweepy
import datetime
import urllib.parse
import time
import random
import config

# 日本語を吐き出すとエラーが出るので追加
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

def main():

    # MediaWiki のルート URL を入れる変数
    wiki_root = "ja.wikipedia.org"
    # 記事情報保存用の json ファイル名指定
    save_filename = "articles.json"

    # json ファイルに保存した記事一覧情報を取得
    with open(save_filename) as f:
        last_articles_json = json.load(f)

    # 保存してある記事一覧のうち最新の記事の pageid を取得
    latest_pageid = last_articles_json[0]["pageid"]
    print(latest_pageid)

    # 統計情報を MediaWiki API から取得
    # list = recentchanges : 特別:最近の更新
    # rctype = new : 新規記事のみ
    rclimit = 100 # 取得リストの最大値 (500 が上限)
    # format = json
    url = "https://%s/w/api.php?action=query&list=recentchanges&rctype=new&rclimit=%d&format=json" % (wiki_root, rclimit)
    response = requests.post(url)

    # json 形式に変換し、必要な部分だけ抜き出し
    raw_json = response.json()
    articles_json = raw_json["query"]["recentchanges"]

    # ツイートするメッセージを入れる配列
    tweet_list = []

    # json ファイルの要素のうち, 各記事についてループ
    for article in reversed(articles_json):
        # 各要素を取得
        title = article["title"]
        pageid = article["pageid"]
        ns = article["ns"]

        # latest_pageid より大きい番号の記事はまだツイートしていない
        if pageid > latest_pageid:
            # 新しい記事の情報を保存用 json リストに追記
            last_articles_json.insert(0, article)
            # ns == 0 の記事が一般ページなのでそれだけを処理
            if ns == 0:
                # TextExtract Extention の API を使って本文の一部を取得
                content = "https://%s/w/api.php?action=query&titles=%s&format=json&prop=extracts&exintro=false&exchars=40&explaintext=true" % (wiki_root, title)
                content = requests.post(content)
                content = content.json()
                content = content["query"]["pages"][str(pageid)]["extract"]
                # URL を取得するためのエンコード
                encode_title = urllib.parse.quote(title)
                article_url = "https://%s/wiki/%s" % (wiki_root, encode_title)
                # Tweet する内容を関数から取得し, Tweet 内容リストに追記
                message = make_massage(title, article_url, content)
                tweet_list.append(message)

    # json ファイルとして保存
    with open(save_filename, 'w') as f:
        json.dump(last_articles_json, f, indent=4, ensure_ascii=False)


    ###################
    #
    # Twitter 投稿部分
    #
    ###################
    # OAuth認証部分
    # 同じディレクトリにある config.py から鍵情報を読み込む
    CK      = config.CONSUMER_KEY
    CS      = config.CONSUMER_SECRET
    AT      = config.ACCESS_TOKEN
    ATS     = config.ACCESS_TOKEN_SECRET
    auth = tweepy.OAuthHandler(CK, CS)
    auth.set_access_token(AT, ATS)
    api = tweepy.API(auth)

    # ツイート内容配列についてループ
    for tweet in tweet_list:
        print(tweet)

        # ツイートを行う
        api.update_status(
                status = tweet, 
                auto_populate_reply_metadata = True, 
                display_coordinates = True, 
                )

        # 連続でツイートするとうっとうしいので何秒か待たせる
        time.sleep(40)


def make_massage(title, article_url, content):
    emoji_list = [
            "😀", "😄", "😁", "😆", "😂", 
            "😊", "😍", "😘", "😋", "😝", 
            "😜", "🤪", "😎", "🤗", "✌️", 
            "🙏", "💪", "💯", "🎉", "🎊", 
            "🌠", "✨", 
            ]

    intro = "新しい記事「%s」が増えました%s" % (title, random.choice(emoji_list))
    greet = random.choice(greet_text) + random.choice(emoji_list)

    message = intro + "\n\n" + content + "\n#新しい記事\n" + article_url

    return message


main()
```
</div>

## 内容が増えた記事をツイートするスクリプト

`tweepy` 以外は Python 標準ライブラリで動きます。

<details><summary>`get_wiki_editcount.py`（クリックで展開）</summary><div>

```python:get_wiki_editcount.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# MediaWiki の "特別:最近の更新" からデータを抜き出し、
# json として保存, すでに保存してあるデータと比較し、
# 新しい記事のタイトルをツイートするスクリプト

# The MIT License (MIT)
# 
# Copyright (c) 2020 Murahashi Kuriki
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import io
import sys
import requests
import json
import tweepy
import datetime
import urllib.parse
import time
import random
import re
import config

# 日本語を吐き出すとエラーが出るので追加
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

def main():

    # MediaWiki のルート URL を入れる変数
    wiki_root = "ja.wikipedia.org"
    # 最後にチェックした記事 ID を保存する txt ファイル名指定
    save_filename = "last_revid.txt"

    # 最後に参照した記事の revid を取得
    with open(save_filename) as f:
        last_revid = f.read()

    # 統計情報を MediaWiki API から取得
    # list = recentchanges : 特別:最近の更新
    # rctype = edit : 編集記事のみ, 新規記事なし
    rclimit = 100 # 取得リストの最大値 (500 が上限)
    # rcnamespace = 0 # ns == 0 : 一般記事の名前空間のみ
    # rcprop = title|sizes|timestamp|ids # 記事タイトル, 記事の大きさ, タイムスタンプ, 記事ID を取得
    # format = json
    url = "https://%s/w/api.php?action=query&list=recentchanges&rcnamespace=0&rctype=edit&rcprop=title|sizes|timestamp|ids&rclimit=%d&format=json" % (wiki_root, rclimit)
    response = requests.post(url)

    # json 形式に変換し、必要な部分だけ抜き出し
    raw_json = response.json()
    recent_articles_json = raw_json["query"]["recentchanges"]

    # ツイートするメッセージを入れる配列
    tweet_list = []

    # 前回チェックしたまでの revid を記録しておく
    latest_revid = last_revid

    # json ファイルの要素のうち, 各記事についてループ
    for article in reversed(recent_articles_json):
        # 各要素を取得
        title = article["title"]
        revid = article["revid"]
        oldid = article["old_revid"]
        oldlen = article["oldlen"]
        newlen = article["newlen"]
        timestamp = article["timestamp"]
        difflen = newlen - oldlen

        timestamp_jst = timestamp_parser_toJST(timestamp)

        # last_pageid より大きい番号の記事はまだツイートしていない
        if revid > int(last_revid):
            latest_revid = revid # 最新の revid を記録する
            # 増減バイト数が 130 以上の記事について処理
            if abs(difflen) >= 130:
                # print(title, difflen, timestamp)
                encode_title = urllib.parse.quote(title)
                article_url = "https://%s/w/index.php?title=%s&diff=%d&oldid=%d" % (wiki_root, encode_title, revid, oldid)
                # print(article_url)
                message = make_massage(timestamp_jst, title, difflen, article_url)
                tweet_list.append(message)

    # 最新の revid をファイルとして保存
    with open(save_filename, 'w') as f:
        f.write(str(latest_revid))


    ###################
    #
    # Twitter 投稿部分
    #
    ###################
    # OAuth認証部分
    # 同じディレクトリにある config.py から鍵情報を読み込む
    CK      = config.CONSUMER_KEY
    CS      = config.CONSUMER_SECRET
    AT      = config.ACCESS_TOKEN
    ATS     = config.ACCESS_TOKEN_SECRET
    auth = tweepy.OAuthHandler(CK, CS)
    auth.set_access_token(AT, ATS)
    api = tweepy.API(auth)

    # ツイート内容配列についてループ
    for tweet in tweet_list:
        print(tweet)

        # ツイートを行う
        api.update_status(
                status = tweet, 
                auto_populate_reply_metadata = True, 
                display_coordinates = True, 
                )

        # 連続でツイートするとうっとうしいので何秒か待たせる
        time.sleep(40)


# ツイート用のメッセージを作成する関数
def make_massage(timestamp, title, difflen, article_url):
    positive_emoji_list = [
            "😀", "😄", "😁", "😆", "😂", 
            "😊", "😍", "😘", "😋", "😝", 
            "😜", "🤪", "😎", "🤗", "✌️", 
            "🙏", "💪", "💯", "🎉", "🎊", 
            "🌠", "✨", 
            ]
    negative_emoji_list = [
            "😅", "🤔", "🤨", "😑", "🙄", 
            "🧐", "😮", "😲", "😯", "😳", 
            "🥺", "💦", "💣", 
            ]

    # difflen を見ることで内容量が増えたのか減ったのか調べ、メッセージを変える
    if difflen < 0:
        emoji = random.choice(negative_emoji_list)
        result_message = "大きく減りました%s (%d バイト)" % (emoji, difflen)
    elif difflen > 0:
        emoji = random.choice(positive_emoji_list)
        result_message = "大きく増えました%s (+%d バイト)" % (emoji, difflen)

    intro = "【%s 編集】\n記事「%s」の内容が%s" % (timestamp, title, result_message)

    message = intro + "\n\nリンクから差分が見られます！\n#大幅な増減\n" + article_url

    return message


# Wiki の GMT タイムスタンプを整形し, 日本時間 (+9:00) で返す関数
def timestamp_parser_toJST(wiki_timestamp):
    # 2020-11-23T14:43:09Z # Wiki デフォルトのタイムスタンプ
    # \d+-\d+-\d+T\d+:\d+:\d+Z
    # 正規表現で年月日時刻情報を取得し, list 型に変換
    match_obj = re.match(r'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z', wiki_timestamp)
    date_list = list(map(int, match_obj.groups()))

    # それぞれを変数に取り込み, datetime 型オブジェクトを作成
    year = date_list[0]
    month = date_list[1]
    day = date_list[2]
    hour = date_list[3]
    minute = date_list[4]
    second = date_list[5]
    date_obj = datetime.datetime(year, month, day, hour, minute, second)

    # GMT なので +9 hours する
    date_obj = date_obj + datetime.timedelta(hours = 9)

    # str 型変換
    timestamp = str(date_obj)

    return timestamp

main()
```
</div>

# スクリプト解説
それぞれのスクリプトについて解説します。

## get_wiki_newpages.py
全体の流れは以下の通り。

1. 前準備
2. API を使って最新記事一覧を取得
3. 最新記事一覧のうち以下の条件にある記事のみ処理
  1. まだツイートしていない記事（ツイート済みの記事情報をファイルから読み出す）
  2. 一般記事である
4. API を使って記事の要約等を取得
5. ツイートしたい内容としてリストに保存
6. リストを順番に（時間間隔をおいて）ツイートする

### 1. 前準備
まず、見に行く MediaWiki ページの URL を指定します。この例では Wikipedia の URL を指定しています。
続いて `save_filename` としてつぶやいた記事を記録しておいた `json` ファイルも読み出します。
このファイルを元にして以前つぶやいたかどうかを判断します。あとで使うので 0 番目の要素の `pageid` を取得しておきます。

```python
# MediaWiki のルート URL を入れる変数
wiki_root = "ja.wikipedia.org/w"
# 記事情報保存用の json ファイル名指定
save_filename = "articles.json"

# json ファイルに保存した記事一覧情報を取得
with open(save_filename) as f:
    last_articles_json = json.load(f)

# 保存してある記事一覧のうち最新の記事の pageid を取得
latest_pageid = last_articles_json[0]["pageid"]
print(latest_pageid)
```

ここで読み込む `articles.json` は以下のようなものです。これは API 経由で取得したうち、`["query"]["recentchanges"]` 要素を抜き出したものです（後述）。

```json:articles.json
[
    {
        "type": "new",
        "ns": 0,
        "title": "あらとん",
        "pageid": 432,
        "revid": 1236,
        "old_revid": 0,
        "rcid": 1225,
        "timestamp": "2020-12-03T08:06:28Z"
    },
    {
        "type": "new",
        "ns": 0,
        "title": "農場",
        "pageid": 430,
        "revid": 1232,
        "old_revid": 0,
        "rcid": 1221,
        "timestamp": "2020-12-03T03:08:29Z"
    },
...
```

### 2. API による取得
ここでは記事を取得していきます。
[MediaWiki API:クエリ](https://www.mediawiki.org/wiki/API:Query/ja) を参照すると「特別:最近の更新」情報は `list=recentchanges`で取得できることがわかります。さらに [MediaWiki API ヘルプ](https://www.mediawiki.org/w/api.php?action=help&modules=query%2Brecentchanges) を参照して指定したいクエリパラメタを選ぶと以下のとおりです。

| property | value | 説明 |
|:--|:--|:--|
| list | recentchanges | 特別:最近の更新 |
| rctype | new | 新規記事のみ取得 |
| rclimit | 100 | 取得リストの最大値 (500 が上限) |
| format | json | 出力フォーマット |

これを Python の `requests` ライブラリを使って読み出します。

```python
# 統計情報を MediaWiki API から取得
# list = recentchanges : 特別:最近の更新
# rctype = new : 新規記事のみ
rclimit = 100 # 取得リストの最大値 (500 が上限)
# format = json
url = "https://%s/w/api.php?action=query&list=recentchanges&rctype=new&rclimit=%d&format=json" % (wiki_root, rclimit)
response = requests.post(url)
```

読みだした結果を `json` ライブラリを使って必要な要素 `["query"]["recentchanges"]` だけ抜き出します。

```python
# json 形式に変換し、必要な部分だけ抜き出し
raw_json = response.json()
articles_json = raw_json["query"]["recentchanges"]
```

スクリプトの初回利用時はこの `artciles_json` が書かれたファイルが存在しないので、これをファイルに書き込んでおく必要があります。

### 3. 最新記事一覧のうち以下の条件にある記事のみ処理
先程取得した json オブジェクトに対して `for` ループをかけます。最初に一通り 1 記事の情報（タイトル、ページ ID、ns）を取得し、最初にファイルから読みだしておいた最新記事のページ ID である `latest_pageid` と比較して ID が大きければ新規記事とみなし処理をします。さらに `ns == 0` (ns は namespace のことのようだ) という条件分岐を用いて新規ページの内、プロジェクトページやカテゴリページを除外しています。

また（クエリを指定せずに）取得した記事は新しい記事から順に並んでいます。古い記事から時系列順に処理したいので `for` ループは `reversed` を使って逆順に回します。

ここで `pageid` が大きい `articles.json` に保存されていない新しい記事は、後でファイルに保存するために、保存用の `json` オブジェクトに `insert` しています。

```python
# ツイートするメッセージを入れる配列
tweet_list = []

# json ファイルの要素のうち, 各記事についてループ
for article in reversed(articles_json):
    # 各要素を取得
    title = article["title"]
    pageid = article["pageid"]
    ns = article["ns"]

    # latest_pageid より大きい番号の記事はまだツイートしていない
    if pageid > latest_pageid:
        # 新しい記事の情報を保存用 json リストに追記
        last_articles_json.insert(0, article)
        # ns == 0 の記事が一般ページなのでそれだけを処理
        if ns == 0:
```

### 4. API を使って記事の要約等を取得
ここまでで「まだつぶやいていない最新の記事」の「タイトル」をつかって新しいページの本文を取得します。ここで Extension の [TextExtracts](https://www.mediawiki.org/wiki/Extension:TextExtracts/ja) を使っています。

クエリそれぞれの内容は以下の通り。

| property | value | 説明 |
|:--|:--|:--|
| titles | title | 取得する記事のタイトル. ここでは変数で与えている |
| format | json | 取得フォーマット |
| prop | extracts | Extracts をつかうことの宣言 |
| exintro | false | 最初の節の前だけ返す |
| exchars | 40 | 取得文字数（厳密ではない、少し長めになる場合がある） |
| explaintext | true | HTML要素ではなくプレーンテキストとして出力する |

```python
# TextExtract Extention の API を使って本文の一部を取得
content = "https://%s/w/api.php?action=query&titles=%s&format=json&prop=extracts&exintro=false&exchars=40&explaintext=true" % (wiki_root, title)
content = requests.post(content)
content = content.json()
content = content["query"]["pages"][str(pageid)]["extract"]
```

### 5. ツイートしたい内容としてリストに保存
タイトルと先ほど取得した要約内容を組み合わせてツイート内容を作ります。タイトルは `urllib` ライブラリを使って UTF エンコードしておきます。またツイート内容は別に定義した `make_message` 関数を使って作成します。

作成したツイート内容はリストとして、`tweet_list` に `append` しておきます（`tweet_list` は記事一覧のループに入る前に作っています）。

```python
# URL を取得するためのエンコード
encode_title = urllib.parse.quote(title)
article_url = "https://%s/wiki/%s" % (wiki_root, encode_title)
# Tweet する内容を関数から取得し, Tweet 内容リストに追記
message = make_massage(title, article_url, content)
tweet_list.append(message)
```

make_message 関数の定義は以下の通りです。記事タイトル、URL, 記事の要約を引数に与えて、メッセージを作ります。テキストだけだと寂しいので絵文字もランダムに入れています。

```python:make_message
def make_massage(title, article_url, content):
    emoji_list = [
            "😀", "😄", "😁", "😆", "😂", 
            "😊", "😍", "😘", "😋", "😝", 
            "😜", "🤪", "😎", "🤗", "✌️", 
            "🙏", "💪", "💯", "🎉", "🎊", 
            "🌠", "✨", 
            ]

    intro = "新しい記事「%s」が増えました%s" % (title, random.choice(emoji_list))
    greet = random.choice(greet_text) + random.choice(emoji_list)

    message = intro + "\n\n" + content + "\n#新しい記事\n" + article_url

    return message
```

ここでこの段階までに取得した最新記事の情報を保存して、次回実行時に読み出せるようにするためファイルに上書き保存します。

```python
# json ファイルとして保存
with open(save_filename, 'w') as f:
    json.dump(last_articles_json, f, indent=4, ensure_ascii=False)
```

### 6. リストを順番に（時間間隔をおいて）ツイートする
ツイートする内容を順番につぶやきます。

まずは同じディレクトリにあるはずの `config.py` から OAuth 認証を行い、API オブジェクトを取得します。

```python
# OAuth認証部分
# 同じディレクトリにある config.py から鍵情報を読み込む
CK      = config.CONSUMER_KEY
CS      = config.CONSUMER_SECRET
AT      = config.ACCESS_TOKEN
ATS     = config.ACCESS_TOKEN_SECRET
auth = tweepy.OAuthHandler(CK, CS)
auth.set_access_token(AT, ATS)
api = tweepy.API(auth)
```

Tweepy ライブラリの `update_status` 関数を使ってつぶやきます。また連続してつぶやくと忙しないと思うので、`time.sleep(40)` して 40 秒間時間を空けます。このあたりは cron 等での実行タイミングに合わせて調整する必要があるでしょう。

```python
# ツイート内容配列についてループ
for tweet in tweet_list:
    print(tweet)

    # ツイートを行う
    api.update_status(
            status = tweet, 
            auto_populate_reply_metadata = True, 
            display_coordinates = True, 
            )

    # 連続でツイートするとうっとうしいので何秒か待たせる
    time.sleep(40)
```

## get_wiki_editcount.py
全体の流れは以下の通り。

1. 前準備
2. API を使って最新の編集記事一覧を取得
3. 最新の編集記事一覧のうち、編集内容の増減数を計算
4. 記事の増減数に応じてツイートしたい内容としてリストに保存
5. リストを順番に（時間間隔をおいて）ツイートする

すでに上の関数で説明した内容は適宜省略します。

### 1. 前準備
先ほどとほとんど同じです。
今度は `save_filename` として `txt` ファイルを読み出します。
これは編集 ID の数値が書いてあるだけのファイルで、この番号より大きい番号の編集 ID がまだつぶやいていない編集情報になります。この数値は後でつかうため、`last_revid` として取得しておきます。

```python
# MediaWiki のルート URL を入れる変数
wiki_root = "ja.wikipedia.org"
# 最後にチェックした記事 ID を保存する txt ファイル名指定
save_filename = "last_revid.txt"

# 最後に参照した記事の revid を取得
with open(save_filename) as f:
    last_revid = f.read()
```

### 2. API を使って最新の編集記事一覧を取得
先ほどと同じように API を使って編集情報を取得します。取得した情報を `json` オブジェクトにしてから必要な要素だけ抜き出します。

| property | value | 説明 |
|:------------|:-------------|:---|
| list        | recentcahges | 特別:最近の更新 |
| rctype      | edit         | 編集記事のみ取得 |
| rclimit     | 100          | 取得リストの最大値 (500 が上限) |
| rcnamespace | 0            | ns == 0 : 一般記事の名前空間のみ |
| rcprop      | title&#124;sizes&#124;timestamp&#124;ids | 記事タイトル, 記事の大きさ, タイムスタンプ, 記事ID を取得 |
| format      | json         | 出力フォーマット |

```python
# list = recentchanges : 特別:最近の更新
# rctype = edit : 編集記事のみ, 新規記事なし
rclimit = 100 # 取得リストの最大値 (500 が上限)
# rcnamespace = 0 # ns == 0 : 一般記事の名前空間のみ
# rcprop = title|sizes|timestamp|ids # 記事タイトル, 記事の大きさ, タイムスタンプ, 記事ID を取得
# format = json
url = "https://%s/w/api.php?action=query&list=recentchanges&rcnamespace=0&rctype=edit&rcprop=title|sizes|timestamp|ids&rclimit=%d&format=json" % (wiki_root, rclimit)
response = requests.post(url)

# json 形式に変換し、必要な部分だけ抜き出し
raw_json = response.json()
recent_articles_json = raw_json["query"]["recentchanges"]
```

### 3. 最新の編集記事一覧のうち、編集内容の増減数を計算
先に取得した編集記事の一覧について `for` ループし、記事のタイトルや編集バイト数を取得します。

具体的に取得する要素は以下の通り。

| property | value | 説明 |
|:------------|:-------------|:---|
| title | article["title"] | 編集のあった記事タイトル |
| revid | article["revid"] | 編集 ID |
| oldid | article["old_revid"] |最新編集の 1 つ前の編集 ID |
| oldlen | article["oldlen"] | 編集前の記事の長さ |
| newlen | article["newlen"] | 編集後の記事の長さ |
| timestamp | article["timestamp"] | 編集があった時刻 |

```python
# 前回チェックしたまでの revid を記録しておく
latest_revid = last_revid

# json ファイルの要素のうち, 各記事についてループ
for article in reversed(recent_articles_json):
    # 各要素を取得
    title = article["title"]
    revid = article["revid"]
    oldid = article["old_revid"]
    oldlen = article["oldlen"]
    newlen = article["newlen"]
    timestamp = article["timestamp"]
```

ここで取得した `newlen` と `oldlen` を用いて、編集によって記事の増減した量として `difflen` を計算します。
さらに編集時間をツイート内容に加えたいため、`timestamp_parser_toJST` 関数を使ってタイムスタンプを日本時間に変換します。

```python
difflen = newlen - oldlen

timestamp_jst = timestamp_parser_toJST(timestamp)
```

MediaWiki の API を使って取得できるタイムスタンプは世界標準時となっているため、Python の `datetime` ライブラリをつかって 9 時間足しておきます。

```python
# Wiki の GMT タイムスタンプを整形し, 日本時間 (+9:00) で返す関数
def timestamp_parser_toJST(wiki_timestamp):
    # 2020-11-23T14:43:09Z # Wiki デフォルトのタイムスタンプ
    # \d+-\d+-\d+T\d+:\d+:\d+Z
    # 正規表現で年月日時刻情報を取得し, list 型に変換
    match_obj = re.match(r'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z', wiki_timestamp)
    date_list = list(map(int, match_obj.groups()))

    # それぞれを変数に取り込み, datetime 型オブジェクトを作成
    year = date_list[0]
    month = date_list[1]
    day = date_list[2]
    hour = date_list[3]
    minute = date_list[4]
    second = date_list[5]
    date_obj = datetime.datetime(year, month, day, hour, minute, second)

    # GMT なので +9 hours する
    date_obj = date_obj + datetime.timedelta(hours = 9)

    # str 型変換
    timestamp = str(date_obj)

    return timestamp
```
### 4. 記事の増減数に応じてツイートしたい内容としてリストに保存
`revid` が保存されていた編集 ID より数値が大きければ、まだツイートしていない編集情報として処理します。

さらに細かい量の編集まですべてツイートすると鬱陶しいので、先程計算した編集による増減量 `difflen` の絶対値が `130` 以上になれば、多かった増減として処理することにします（この 130 という数値に特にこだわりはない）。

先ほどの関数と同じように `title` をエンコードして URL を作成しますが、ここでは編集差分のページへのリンクを作りたいので、先に取得した `revid` と `oldid` を使って差分表示ページへの URL を用意します。クエリは次の通り。

| property | value | 説明 |
|:------------|:-------------|:---|
| title | title | 記事タイトル |
| diff | revid | 編集 ID |
| oldid | oldid | 1 つ前の編集 ID |

この URL とタイトル、記事の変化量、編集時間を使ってツイート内容を `make_message` 関数で作成し、リストに `append` します。

```python
# last_pageid より大きい番号の記事はまだツイートしていない
if revid > int(last_revid):
    latest_revid = revid # 最新の revid を記録する
    # 増減バイト数が 130 以上の記事について処理
    if abs(difflen) >= 130:
        # print(title, difflen, timestamp)
        encode_title = urllib.parse.quote(title)
        article_url = "https://%s/w/index.php?title=%s&diff=%d&oldid=%d" % (wiki_root, encode_title, revid, oldid)
        # print(article_url)
        message = make_massage(timestamp_jst, title, difflen, article_url)
        tweet_list.append(message)
```

`make_message` 関数の定義は以下の通りです。最初の関数と違うのは、内容が減った場合は多少ネガティブな顔文字をランダムに使うようにしています。（編集によって記事の量が減ることは必ずしもネガティブなことではないので、疑問や驚くリアクションを選んでいます）

```python:make_massage
# ツイート用のメッセージを作成する関数
def make_massage(timestamp, title, difflen, article_url):
    positive_emoji_list = [
            "😀", "😄", "😁", "😆", "😂", 
            "😊", "😍", "😘", "😋", "😝", 
            "😜", "🤪", "😎", "🤗", "✌️", 
            "🙏", "💪", "💯", "🎉", "🎊", 
            "🌠", "✨", 
            ]
    negative_emoji_list = [
            "😅", "🤔", "🤨", "😑", "🙄", 
            "🧐", "😮", "😲", "😯", "😳", 
            "🥺", "💦", "💣", 
            ]

    # difflen を見ることで内容量が増えたのか減ったのか調べ、メッセージを変える
    if difflen < 0:
        emoji = random.choice(negative_emoji_list)
        result_message = "大きく減りました%s (%d バイト)" % (emoji, difflen)
    elif difflen > 0:
        emoji = random.choice(positive_emoji_list)
        result_message = "大きく増えました%s (+%d バイト)" % (emoji, difflen)

    intro = "【%s 編集】\n記事「%s」の内容が%s" % (timestamp, title, result_message)

    message = intro + "\n\nリンクから差分が見られます！\n#大幅な増減\n" + article_url

    return message
```

ここでこの段階までに取得した最新編集の情報を保存して、次回実行時に読み出せるようにするためファイルに上書き保存します。

```python
# 最新の revid をファイルとして保存
with open(save_filename, 'w') as f:
    f.write(str(latest_revid))
```

### 5. リストを順番に（時間間隔をおいて）ツイートする
これは先に紹介した関数と同じなので省略。ツイート間隔を開けるため 40 秒間は `sleep` させます。

# 最後に
MediaWiki で編集された情報を取得して発信できたら楽しいかな、と思ったので作ったスクリプトでした。

この記事を書くにあたって、Wikipedia で動作するように多少書き換えましたが、元々は任意のドメインで動く標準の MediaWiki で動くことを想定してつくっているのでひょっとしたらうまく動かないこともあるかもしれません。ご容赦ください。

これらのスクリプトは自前で運用している「[北大Wiki](https://hokudaiwiki.net)」とその Twitter アカウント [@hokudai_wiki](https://twitter.com/hokudai_wiki) で使用しています。

# ライセンス
この記事は [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) ライセンスの元で公開します。ソースコードのライセンスは MIT ライセンスです。