この記事はQiita アドベントカレンダー 2022 Python 24日目の記事です。
OSS開発の社内Wikiツール GrowiのAPIが公開されてましたので、それを使って遊んでみました。
Growi記事ランキング投稿
ページの取得、投稿を行う、Page
, 更新履歴を閲覧する Revisions
クラスを定義した growi
モジュールは別記事1にしま した。 す。予定
この記事はランキング投稿スクリプトの解説記事です。
完成品イメージ
完成品スクショ
完成品標準出力
完成品標準出力
# :heart:ライクが多いランキングトップ10
1. :arrow_right: [/お試し](https://demo.growi.org/5ee0e945ac9357004883964d) :heart:2 :footprints:17 :speech_balloon:0 :pencil2:2
2. :arrow_right: [/お試し/改善](https://demo.growi.org/5f0d697a7cec480048dba270) :heart:2 :footprints:6 :speech_balloon:1 :pencil2:1
3. :arrow_upper_right: [/お試し1/はじめてのページ777](https://demo.growi.org/62b03e339f17db565044b295) :heart:1 :footprints:1 :speech_balloon:1 :pencil2:1
4. :arrow_upper_right: [/お試し/Ranking](https://demo.growi.org/62acf2940b8a39f163ef243d) :heart:1 :footprints:1 :speech_balloon:0 :pencil2:1
5. :arrow_upper_right: [/お試し/はじめてのページ/入れ子のページ](https://demo.growi.org/6281d16d6fa435d7f5925b6c) :heart:1 :footprints:3 :speech_balloon:0 :pencil2:1
6. :arrow_upper_right: [/お試し/はじめてのページkeeeeeeesuke99/入れ子ページ](https://demo.growi.org/6138b7cc285c8c000c142683) :heart:1 :footprints:3 :speech_balloon:0 :pencil2:1
7. :arrow_upper_right: [/お試し/はじめてのページ8](https://demo.growi.org/60a37ca862e1c30049ddf60b) :heart:1 :footprints:3 :speech_balloon:1 :pencil2:1
8. :arrow_upper_right: [/お試し/はじめてのページ10](https://demo.growi.org/607d9616e73c630049b23776) :heart:1 :footprints:4 :speech_balloon:0 :pencil2:1
9. :arrow_upper_right: [/お試し/はじめてのページぽい/入れ子のページぽい](https://demo.growi.org/607547fa4101e20049da2993) :heart:1 :footprints:2 :speech_balloon:0 :pencil2:1
10. :new: [/お試し/はじめてのページ3](https://demo.growi.org/5fca12dbc5c66700485f1ed4) :heart:1 :footprints:3 :speech_balloon:0 :pencil2:1
# :footprints:足跡が多いランキングトップ10
1. :arrow_right: [/お試し](https://demo.growi.org/5ee0e945ac9357004883964d) :heart:2 :footprints:17 :speech_balloon:0 :pencil2:2
2. :arrow_right: [/お試しです/はじめてのページ](https://demo.growi.org/5db14ee94dc19b0044efe9ea) :heart:0 :footprints:11 :speech_balloon:0 :pencil2:1
3. :arrow_right: [/お試し/改善](https://demo.growi.org/5f0d697a7cec480048dba270) :heart:2 :footprints:6 :speech_balloon:1 :pencil2:1
4. :arrow_right: [/お試しです/はじめてのページ/入れ子のページ](https://demo.growi.org/5db1507c4dc19b0044efe9ef) :heart:0 :footprints:6 :speech_balloon:0 :pencil2:1
5. :arrow_right: [/お試しa/はじめてのページ/おりたたみ](https://demo.growi.org/62148d1287b16dd2e145e757) :heart:0 :footprints:5 :speech_balloon:0 :pencil2:1
6. :arrow_right: [/お試しa/はじめてのページ/入れ子のページ](https://demo.growi.org/62148abc87b16dd2e145e04f) :heart:0 :footprints:5 :speech_balloon:0 :pencil2:1
7. :arrow_right: [/お試し/改善/タグの関係_blockdiag](https://demo.growi.org/5f0e826405904e00485a210a) :heart:0 :footprints:5 :speech_balloon:0 :pencil2:1
8. :arrow_right: [/お試し/改善/入れ子のページ](https://demo.growi.org/5f0d69c87cec480048dba273) :heart:0 :footprints:5 :speech_balloon:0 :pencil2:1
9. :arrow_right: [/お試し/はじめてのページ10](https://demo.growi.org/607d9616e73c630049b23776) :heart:1 :footprints:4 :speech_balloon:0 :pencil2:1
10. :arrow_right: [/お試しです](https://demo.growi.org/5c6b517016763b003f629b9f) :heart:0 :footprints:4 :speech_balloon:0 :pencil2:2
# :speech_balloon:コメントが多いランキングトップ10
1. :arrow_right: [/お試し/改善](https://demo.growi.org/5f0d697a7cec480048dba270) :heart:2 :footprints:6 :speech_balloon:1 :pencil2:1
2. :arrow_right: [/お試し/改善/タグの関係_draw.io](https://demo.growi.org/5f0d6deb7cec480048dba27f) :heart:0 :footprints:4 :speech_balloon:1 :pencil2:1
3. :arrow_right: [/お試し/はじめてのページ8](https://demo.growi.org/60a37ca862e1c30049ddf60b) :heart:1 :footprints:3 :speech_balloon:1 :pencil2:1
4. :arrow_right: [/お試し2/はじめてのページ](https://demo.growi.org/5e2ced5688ba150043d9b4e1) :heart:0 :footprints:3 :speech_balloon:1 :pencil2:1
5. :arrow_right: [/お試し/改善/A1](https://demo.growi.org/5f0d6cea7cec480048dba279) :heart:0 :footprints:2 :speech_balloon:1 :pencil2:1
6. :arrow_right: [/お試しです/初めてのページ/入れ子のページ](https://demo.growi.org/5e0421bf88ba150043d9b35b) :heart:0 :footprints:2 :speech_balloon:1 :pencil2:1
7. :arrow_upper_right: [/お試し1/はじめてのページ777](https://demo.growi.org/62b03e339f17db565044b295) :heart:1 :footprints:1 :speech_balloon:1 :pencil2:1
8. :arrow_upper_right: [/お試し](https://demo.growi.org/5ee0e945ac9357004883964d) :heart:2 :footprints:17 :speech_balloon:0 :pencil2:2
9. :arrow_upper_right: [/お試しです/はじめてのページ](https://demo.growi.org/5db14ee94dc19b0044efe9ea) :heart:0 :footprints:11 :speech_balloon:0 :pencil2:1
10. :new: [/お試しです/はじめてのページ/入れ子のページ](https://demo.growi.org/5db1507c4dc19b0044efe9ef) :heart:0 :footprints:6 :speech_balloon:0 :pencil2:1
# :pencil2:編集者が多いランキングトップ10
1. :arrow_right: [/お試し](https://demo.growi.org/5ee0e945ac9357004883964d) :heart:2 :footprints:17 :speech_balloon:0 :pencil2:2
2. :arrow_right: [/お試しです](https://demo.growi.org/5c6b517016763b003f629b9f) :heart:0 :footprints:4 :speech_balloon:0 :pencil2:2
3. :arrow_right: [/お試し/改善](https://demo.growi.org/5f0d697a7cec480048dba270) :heart:2 :footprints:6 :speech_balloon:1 :pencil2:1
4. :arrow_right: [/お試し/改善/タグの関係_draw.io](https://demo.growi.org/5f0d6deb7cec480048dba27f) :heart:0 :footprints:4 :speech_balloon:1 :pencil2:1
5. :arrow_right: [/お試し/はじめてのページ8](https://demo.growi.org/60a37ca862e1c30049ddf60b) :heart:1 :footprints:3 :speech_balloon:1 :pencil2:1
6. :arrow_right: [/お試し2/はじめてのページ](https://demo.growi.org/5e2ced5688ba150043d9b4e1) :heart:0 :footprints:3 :speech_balloon:1 :pencil2:1
7. :arrow_right: [/お試し/改善/A1](https://demo.growi.org/5f0d6cea7cec480048dba279) :heart:0 :footprints:2 :speech_balloon:1 :pencil2:1
8. :arrow_right: [/お試しです/初めてのページ/入れ子のページ](https://demo.growi.org/5e0421bf88ba150043d9b35b) :heart:0 :footprints:2 :speech_balloon:1 :pencil2:1
9. :arrow_upper_right: [/お試し1/はじめてのページ777](https://demo.growi.org/62b03e339f17db565044b295) :heart:1 :footprints:1 :speech_balloon:1 :pencil2:1
10. :new: [/お試しです/はじめてのページ](https://demo.growi.org/5db14ee94dc19b0044efe9ea) :heart:0 :footprints:11 :speech_balloon:0 :pencil2:1
完成品コード2
クリックでコード全体を表示
#!/usr/bin/env python3
"""Growi記事ランキング投稿
usage:
$ python ranking.py DST [SRC] [TOP]
# ランキングを表示するページパスを指定
$ python ranking.py /Ranking
# ランキングを表示するページパスとランキング集計元の親ページパスを指定
$ python ranking.py /Ranking /From/Root
# ランキングを表示するページパスとランキング集計元の親ページパスとトップ5の集計を指定
$ python ranking.py /Ranking /From/Root 5
"""
import re
import argparse
from typing import Iterator
from operator import attrgetter
from collections import UserList, namedtuple
from typing import Union
from more_itertools import chunked
from growi import Page, Revisions
fields = {
"path": "",
"id": "",
"liker": 0,
"seen": 0,
"commentCount": 0,
"authors": 0
}
Rank = namedtuple("Rank", fields.keys(), defaults=fields.values())
class Ranks(UserList):
"""Growi記事ランキング"""
def __init__(self, *args):
"""List of Rank"""
super().__init__(*args)
def sort(self, key: str, reverse=True):
"""ランキングのリストに対してkeyでソートをかける"""
return self.data.sort(key=attrgetter(key), reverse=reverse)
def convert(self) -> list[str]:
"""ランキングリストをGrowiマークダウン形式のリストに変換する"""
return [
f"[{rank.path}]({Page.origin}/{rank.id}) :heart:{rank.liker} \
:footprints:{rank.seen} :speech_balloon:{rank.commentCount} \
:pencil2:{rank.authors}" for rank in self.data
]
def order(self, top: int, ids: list[str]) -> list[str]:
"""top(数字)のリストを上位topの数でランキングづけする。
過去のランクidsがあれば過去ランクとの比較を行う。
"""
after_ranks: list[str] = self[:top].convert()
# arrows初期値、idsがないとき==初めてランキングを作るとき
arrows = ("" for _ in range(top))
if ids:
after_ids: list[str] = [i.id for i in self[:top]]
arrows: Iterator[str] = Ranks.shift(ids, after_ids)
body = [
f"{i}. {arrow} {elem}"
for i, arrow, elem in zip(range(1, 1 + top), arrows, after_ranks)
]
return body
@staticmethod
def shift(before: list, after: list) -> Iterator[str]:
""" afterのインデックスbeforeに比べて上がってたら上、
下がってたら下、横ばいだったら横の記号をリストで返す。
"""
before_ranks: list[Union[int, float]] = \
(after.index(i) if i in after else float("inf") for i in before)
for after_rank, before_rank in enumerate(before_ranks):
sub: int = before_rank - after_rank
if sub == float("inf"):
yield ":new:"
elif sub > 0:
yield ":arrow_upper_right:"
elif sub < 0:
yield ":arrow_lower_right:"
else:
yield ":arrow_right:"
@staticmethod
def read_ids(paragraph: str) -> list[str]:
"""paragraph からpage idのみをリストで抜き出す"""
return re.findall(r"[a-f0-9]{24}", paragraph)
def init(path: str = "/") -> Ranks:
"""Generate List of Rank"""
page = Page(path)
pages = page.list(prop_access=True, limit=1000).pages
rank_list = Ranks()
for page in pages:
revisions = Revisions(page._id, limit=100)
rank = Rank(page.path, page._id, len(page.liker), len(page.seenUsers),
page.commentCount, len(revisions.authors()))
rank_list.append(rank)
return rank_list
def main(dst: str, src: str = "/", top=10, dryrun=False):
"""Growiページパスsrcからランキング情報を収集し、
Growiページパスdstへランキングを記したマークダウン形式の文字列を投稿する。
第2引数以降省略でき、デフォルトで"/"からランキングを作成する。
"""
ranks: Ranks = init(src)
rank_page = Page(dst)
if rank_page.exist:
before_ids = Ranks.read_ids(rank_page.body)
before_ids_chunk = chunked(before_ids, top)
ranking_element = (
(f"# :heart:ライクが多いランキングトップ{top}\n\n", "liker"),
(f"\n\n# :footprints:足跡が多いランキングトップ{top}\n\n", "seen"),
(f"\n\n# :speech_balloon:コメントが多いランキングトップ{top}\n\n", "commentCount"),
(f"\n\n# :pencil2:編集者が多いランキングトップ{top}\n\n", "authors"),
)
page_body = ""
for title, key in ranking_element:
ranks.sort(key)
try:
chunk = next(before_ids_chunk)
except (StopIteration, NameError):
chunk = None
ranking_md = ranks.order(top, chunk)
page_body += title
page_body += "\n".join(ranking_md)
if dryrun:
# Just print test
print(page_body)
return
# Post page
res = rank_page.post(page_body)
print(res)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("dst", help="Growiのランキング表示先ページパス")
parser.add_argument("src",
nargs="?",
default="/",
help="Growiのランキング集計元ページパス")
parser.add_argument("top",
nargs="?",
type=int,
default=10,
help="ランキング上位数")
parser.add_argument("-n",
"--dryrun",
action="store_true",
help="標準出力へマークダウンをprintするのみで、記事投稿しない。")
args = parser.parse_args()
main(args.dst, args.src, args.top, dryrun=args.dryrun)
出力結果(目標)
print()
したときに下記のような出力を期待するコードを書きます。3
1. ↗[/なんかの/ページ](http://192.168.***.***:3000/8d1235b1293ab3f532cb772b) ❤3 👣5 🗨6 ✏2
2. ➡[/なにかの/サイト](http://192.168.***.***:3000/3cb6a636123ab3f53236ba6e) ❤3 👣5 🗨6 ✏2
3. ↘[/だれかの/ページ](http://192.168.***.***:3000/b473a78d123aba636123ab3f) ❤3 👣5 🗨6 ✏2
4. ↗[/なにげに/ペース](http://192.168.***.***:3000/73a78d123a3ab3f532c3236e) ❤3 👣5 🗨6 ✏2
...
前準備
環境変数にGrowiのURLとアクセストークンを指定する必要があります。
URLはデフォルトでhttp://localhost:3000
が指定されますので、
ローカルで動いているGrowiに対しては指定の必要がありません。
アクセストークンは必須です。
試しに Growiのデモページ にアカウントを作って試した結果が上のスクショです。
"/"ルートからランキングを作るとページ数が5800以上と多くて1分程時間がかかりましたが、"お試し"ページが80ページほどでちょうどよかったので、 "/お試し/Ranking" にランキングを作成してみました。 10秒ほどで作成できます。
要素定義
fields = {
"path": "",
"id": "",
"liker": 0,
"seen": 0,
"commentCount": 0,
"authors": 0
}
Rank = namedtuple("Rank", fields.keys(), defaults=fields.values())
ランキングの一要素を表現する Rank
タプルです。
namedtuple
を使っているので、ドットプロパティメソッドが使えます。
Growi APIの/page
で取得できるJSONオブジェクトの一部を要素としています。
要素名 | 型 | 意味 |
---|---|---|
path | str | ページパス |
id | str | ページID |
liker | int | ライクした人の数 |
seen | int | 足跡の数 |
commentCount | int | コメント数 |
authors | int | 編集者数 |
class Ranks(UserList):
"""Growi記事ランキング"""
def __init__(self, *args):
"""List of Rank"""
super().__init__(*args)
Rank
の要素を並べ、ランキングのリストを表現する Ranks
クラスです。
UserList
を継承していますので、append()
, index()
メソッドを使ってリストのように扱えます。
エントリーポイント
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("dst", help="Growiのランキング表示先ページパス")
parser.add_argument("src",
nargs="?",
default="/",
help="Growiのランキング集計元ページパス")
parser.add_argument("top",
nargs="?",
type=int,
default=10,
help="ランキング上位数")
parser.add_argument("-n",
"--dryrun",
action="store_true",
help="標準出力へマークダウンをprintするのみで、記事投稿しない。")
args = parser.parse_args()
main(args.dst, args.src, args.top, dryrun=args.dryrun)
引数を解析して main()
関数へ渡しています。
$ python ranking.py DST [SRC] [TOP]
のようにして使います。
ranking.py
へ渡す引数は DST = ランキングを表示するページパス, SRC = ランキング集計元の親ページパス、 TOP = 上位数を意味します。
SRC, TOPは省略可能です。
def main(dst: str, src: str = "/", top=10, dryrun=False):
"""Growiページパスsrcからランキング情報を収集し、
Growiページパスdstへランキングを記したマークダウン形式の文字列を投稿する。
第2引数以降省略でき、デフォルトで"/"からランキングを作成する。
"""
ranks: Ranks = init(src)
rank_page = Page(dst)
if rank_page.exist:
before_ids = Ranks.read_ids(rank_page.body)
before_ids_chunk = chunked(before_ids, top)
ranking_element = (
(f"# :heart:ライクが多いランキングトップ{top}\n\n", "liker"),
(f"\n\n# :footprints:足跡が多いランキングトップ{top}\n\n", "seen"),
(f"\n\n# :left_speech_bubble:コメントが多いランキングトップ{top}\n\n",
"commentCount"),
(f"\n\n# :pencil2:編集者が多いランキングトップ{top}\n\n", "authors"),
)
page_body = ""
for title, key in ranking_element:
ranks.sort(key)
try:
chunk = next(before_ids_chunk)
except (StopIteration, NameError):
chunk = None
ranking_md = ranks.order(top, chunk)
page_body += title
page_body += "\n".join(ranking_md)
if dryrun:
# Just print test
print(page_body)
return
# Post page
res = rank_page.post(page_body)
print(res)
main()
ではranking_element
の中身をfor
で回して、タイトルとランキングを組み合わせたマークダウン形式の文字列を作ります。
ページ内容(rank_page.body
) はpage_body
と同じ形式のマークダウン文字列です。
その内容を逆に解析してRanks
と比較するページIDのリストを作成します。
更新前のランキングリストを more_itertools.chunked()
で一定数のリストに分割します。
ここではトップ10のランキングを作りたいので、
タイトル+40行のマークダウンを4x10のリストオブリストに変換しました。
前のランキングがなければ、try
分岐でranks.order()
へNone
を渡します。
すでにアップされているランキングページの内容を取得します。
前回ランキングと現在ランキングを比較して、前回と比べて順位が上がった、下がったを判定するために使います。
最後にページの内容(page_body
)をDST
で指定したページへ投稿し、結果のJSONをprint()
します。
その他関数、メソッドの解説
ランキング取得処理
def init(path: str = "/") -> Ranks:
"""Generate List of Rank"""
page = Page(path)
pages = page.list(prop_access=True, limit=1000).pages
rank_list = Ranks()
init()
関数で各ページのページパス、ID、ライク数、コメント数などを含んだRank
のリスト Ranks
=ランキングを取得します。
この時点では並び替えられていないので、ランキングとは言いませんね。
ランキングを取得する関数です。
ランキングの取得とは、要するにGrowi APIの結果として得たJSONを解析して、
ページIDやライク数などの要素をRank
タプルへ格納します。
ルートページ"/"に対して/page.list
APIを実行し、
各ページの情報をfor
文で操作していきます。
for page in pages:
revisions = Revisions(page._id, limit=100)
rank = Rank(page.path, page._id, len(page.liker), len(page.seenUsers),
page.commentCount, len(revisions.authors()))
rank_list.append(rank)
return rank_list
各ページIDを使って更新履歴(Revisions
)を取得します。
ここの処理がページ数分だけ/revisions/list
APIを実行するので、サーバーに負荷をかける重たい処理です。
上記/page.list
は一回実行するだけでライク数、足跡数、コメント数を取得できるのに比べて、ただ編集者数を把握するために各ページに対して実行して編集者数だけ4を取得するコスパの悪い処理ですね。
Ranks
メソッド
def sort(self, key: str, reverse=True):
"""ランキングのリストに対してkeyでソートをかける"""
return self.data.sort(key=attrgetter(key), reverse=reverse)
ランキングリストをソートします。
リストクラスのsort()
メソッドの上書きです。
keyで並べ替えターゲットを指定して、デフォルトで降順(数値の高いものが最初)になるように並べ替えます。
keyは属性を指定するためにoperator.attrgetter()
を使います5。
def convert(self) -> list[str]:
"""ランキングリストをGrowiマークダウン形式のリストに変換する"""
return [
f"[{rank.path}]({Page.origin}/{rank.id}) :heart:{rank.liker} \
:footprints:{rank.seen} :speech_balloon:{rank.commentCount} \
:pencil2:{rank.authors}" for rank in self.data
]
ランキングリストRanks
をマークダウン形式の文字列を要素としたリストに書き換えます。
リストにしたのはこの後のorder()
で更に加工するためです。
下記のようなリストが返ってきます。
[
"[/なんかの/ページ](http://192.168.***.***:3000/8d1235b1293ab3f532cb772b) ❤3 👣5 🗨6 ✏2",
"[/なにかの/サイト](http://192.168.***.***:3000/3cb6a636123ab3f53236ba6e) ❤3 👣5 🗨6 ✏2",
"[/だれかの/ページ](http://192.168.***.***:3000/b473a78d123aba636123ab3f) ❤3 👣5 🗨6 ✏2",
"[/なにげに/ペース](http://192.168.***.***:3000/73a78d123a3ab3f532c3236e) ❤3 👣5 🗨6 ✏2",
]
def order(self, top: int, ids: list[str]) -> list[str]:
"""top(数字)のリストを上位topの数でランキングづけする。
過去のランクidsがあれば過去ランクとの比較を行う。
"""
after_ranks: list[str] = self[:top].convert()
# arrows初期値、idsがないとき==初めてランキングを作るとき
arrows = ("" for _ in range(top))
if ids:
after_ids: list[str] = [i.id for i in self[:top]]
arrows: Iterator[str] = Ranks.shift(ids, after_ids)
body = [
f"{i}. {arrow} {elem}"
for i, arrow, elem in zip(count(1), arrows, after_ranks)
]
return body
Ranks.sort()
で指定のkey
順にソートし、Ranks.convert()
で上記のリストをafter_ids
へ格納します。
after_ids
はランキングのIDのみを取得しています。
Ranks.shift()
でids
とafter_ids
を比較して、'↗', '➡', '↘' 'new'のリストをarrows
へ格納します。
最後にtitle
とarrows
とafter_ranks
を結合して返します。
順位をつけるために最初はenumerate()
を使っていましたが、arrows
とrankmd
と合わせて3要素をfor
へ渡すので、
zip()
とcount()
を併せて使います。
こんな結果のリストが帰ってきます。
[1. ↗[/なんかの/ページ](http://192.168.***.***:3000/8d1235b1293ab3f532cb772b) ❤3 👣5 🗨6 ✏2,
2. ➡[/なにかの/サイト](http://192.168.***.***:3000/3cb6a636123ab3f53236ba6e) ❤3 👣5 🗨6 ✏2,
3. ↘[/だれかの/ページ](http://192.168.***.***:3000/b473a78d123aba636123ab3f) ❤3 👣5 🗨6 ✏2,
4. ↗[/なにげに/ペース](http://192.168.***.***:3000/73a78d123a3ab3f532c3236e) ❤3 👣5 🗨6 ✏2,
...
@staticmethod
def shift(before: list, after: list) -> Iterator[str]:
""" afterのインデックスbeforeに比べて上がってたら上、
下がってたら下、横ばいだったら横の記号をリストで返す。
"""
before_ranks: list[Union[int, float]] = \
(after.index(i) if i in after else float("inf") for i in before)
for after_rank, before_rank in enumerate(before_ranks):
sub: int = before_rank - after_rank
if sub == float("inf"):
yield ":new:"
elif sub > 0:
yield ":arrow_upper_right:"
elif sub < 0:
yield ":arrow_lower_right:"
else:
yield ":arrow_right:"
リストbefore
に対して、リストafter
の要素のインデックスが上がっていたら上矢印、下がっていたら下矢印、ランクに変動なければ横矢印、ランク外から上がってきた要素にはnew
の絵文字を返すイテレータです。
unittestを記載します。
import unittest
class TestRanks_shift(unittest.TestCase):
"""Ranks.shift() test"""
def test_rank_updown(self):
"""ランク内で順位が変わるケース"""
be, af = "a b c".split(), "c b a".split()
self.assertEqual(
list(ranking.Ranks.shift(be, af)),
[':arrow_upper_right:', ':arrow_right:', ':arrow_lower_right:'])
def test_rank_new(self):
"""ランク外から上がってきたケース"""
be, af = "a b c d".split(), "c b a e".split()
self.assertEqual(list(ranking.Ranks.shift(be, af)), [
':arrow_upper_right:', ':arrow_right:', ':arrow_lower_right:',
':new:'
])
def test_rank_new2(self):
"""ランク外から2位に上がってきたケース"""
be, af = "a b c d".split(), "c e a d".split()
self.assertEqual(list(ranking.Ranks.shift(be, af)), [
':arrow_upper_right:', ':new:', ':arrow_lower_right:',
':arrow_right:'
])
@staticmethod
def read_ids(paragraph: str) -> list[str]:
"""paragraph からpage idのみをリストで抜き出す"""
return re.findall(r"[a-f0-9]{24}", paragraph)
マークダウンからページID(16進数24桁のハッシュ)のみを取得し、リストで返します。
mainから呼ばれます。
使い方
usage: ranking.py [-h] [-n] dst [src] [top]
Growi記事ランキング投稿
usage: $ python ranking.py DST [SRC] [TOP]
# ランキングを表示するページパスを指定
$ python ranking.py /Ranking
# ランキングを表示するページパスとランキング集計元の親ページパスを指定
$ python ranking.py /Ranking /From/Root
# ランキングを表示するページパスとランキング集計元の親ページパスとトップ5の集計を指定
$ python ranking.py /Ranking /From/Root 5
positional arguments:
dst Growiのランキング表示先ページパス
src Growiのランキング集計元ページパス
top ランキング上位数
optional arguments:
-h, --help show this help message and exit
-n, --dryrun 標準出力へマークダウンをprintするのみで、記事投稿しない。
DSTを"/Sidebar"にして常に左側のサイドバーにランキングが表示されるようにしました。(スクショなし)
SRCはデフォルトの"/"、TOPはデフォルトの"10"ですので、指定していません。
Dockerコンテナ上でcron
を使って、毎日定期的にGrowiのランキング集計、投稿を行っています。
Dockerfile6
-
GrowiAPIを使って記事を取得・投稿する (まだ書いてない) ↩
-
Growiのマークダウンは1,2,3,4,...と数字を増やさなくても1,1,1,1,...だけでカウントアップしてくれますが、コンソールへ
print()
したときにわかりやすいという目的で1,2,3,4...とプログラムでカウントアップするようにします。 ↩ -
他にも変更回数とか見れます。pageというクエリパラメータの意味がわかりません。API仕様には呼び出し方は書いてありましたが、結果の例は書いていませんでした。
/revisions/list
↩ -
やっていはいないけど、
docker-compose.yml
にもオーバーライドできそうですね。公式にコミットしてみようかしら。 ↩