コンテンツにスキップ

利用者:Hatukanezumi/仮リンクの整理/aggregateTentativeLinks.py

  1. -*- python -*-
  2. -*- coding: utf-8 -*-

"""

aggregateTentativeLinks.pyは、{{仮リンク}}テンプレートの使用状況を調査し、結果を特定のページ (複数) に投稿するボットです。通常ボットとしてイメージされるプログラムとは異なり、このボットは大量のページからデータを取得しながら、あらかじめ決められたごくわずかのページしか変更しません。

インストール[編集]

必要なソフトウェア

  • pywikipedia。2010年11月ころのtrunkでテストしていますが、最近のバージョンならたいてい大丈夫だと思います。
  • Pythonインタプリタ。pywikipediaが動くバージョンのもの。

手順

  1. pywikipediaを、自分のボット用アカウントでログインできるように設定します。
  2. 当ページのソースをダウンロードして保存します (画面をコピー・ペーストしてもうまく動かないかもしれません)。保存する際の文字コードはUTF-8、改行はpywikipediaをインストールしたオペレーティングシステムの改行コードにします。
  3. 保存したファイルの名前を「aggregateTentativeLinks.py」にして、pywikipediaのディレクトリに複写します。

設定[編集]

  1. 下記「コード」の「基本設定」の箇所を適当に修正します。
  2. OUTPUTDIRで設定したディレクトリがなければ、作ります。

実行[編集]

aggregateTentativeLinks.pyは、つぎの二段階に分けて実行できます。

情報を取得して解析し、OUTPUTDIR下に保存する。

python aggregateTentativeLinks.py -retrieve

保存した情報を投稿する。

python aggregateTentativeLinks.py -put

「-retrieve」と「-put」のいずれかは、かならず指定する必要があります。両方指定すると、情報の取得・解析と投稿を続けて実行します。

ほかのオプション。

-max:数
-retrieveの場合、仮リンクのあるページのうち、指定した数だけ処理します。数を制限するだけで、どのページを処理するかは選べません。
-comment:テキスト
-put の場合、投稿時の要約欄の内容。
-always
-put の場合、投稿するかどうかを確認せずに実行する。

まず、オプションに「-retrieve -max:小さな数」を指定して、このボットがどんなふうに情報を収集するかを見てください。つぎに「-put」を指定すれば、収集した情報がどのように投稿されるかがわかります。本格的に運用するには、「-max:」オプションを指定せずに動かします。完全に自動化してもよいと思ったらはじめて、「-always」オプションを追加して投稿します。

制限等[編集]

retrieve処理では、処理の途中結果を外部記憶などに保存しません。そのため、なんらかの原因で実行が中断すると、最初からやりなおしです。処理にかかる時間の大半はメディアウィキサーバとの通信が占めるので、実行するコンピュータの性能はあまり関係ありません。

  • 仮リンクテンプレートを使ったページ約2500に対して、実測で3-5時間程度かかりました。
  • メモリは、AMD64 Linux上のpythonでおよそ200MB必要でした。

pywikipediaの現時点での制限により、仮リンクテンプレートを使っているページが5000を超えると、すべての項目の情報を取得できなくなります (と思います)。

ライセンス等[編集]

aggregateTentativeLinks.pyは、ウィキペディアの記事と同じライセンスにしたがって配布、利用、変更、再配布、二次著作物の作成等を行えます。

オリジナルの版は、このページのこの版です。

コード[編集]

"""

###
### 基本設定。LANG、FAMILY、TEMPLATENAMEは通常は変更不要。
###

LANG = 'ja'                     # 対象プロジェクトの言語
FAMILY = None                   # プロジェクトファミリ (Noneならuser-config.py
                                # の設定にしたがう)
TEMPLATENAME = '仮リンク'       # 仮リンクテンプレートのページ名 (名前空間なし)
LISTPAGES = [
    # 報告先ページの名前空間番号とページ名のプリフィクス。
    # これらで始まる名前のすべてのページから{{jareq}}テンプレートを抽出する。
    (4, '多数の言語版にあるが日本語版にない記事'),
]
OUTPUTDIR = '/var/tmp/wiki'     # 結果を出力するディレクトリ。存在すること。
                                # 結果を投稿する先のメインページ。
                                # 複数のサブページに投稿する。
OUTPUTPAGEBASE = '利用者:Hatukanezumi/仮リンクの整理'

###
### ここから後は変更の必要はありません。
###

import os
import sys
import re
from wikipedia import Site, Page, handleArgs, inputChoice, output, stopme
#from catlib import Category

SITE = Site(LANG, FAMILY)
TEMPLATENAME = Page(SITE, 'Template:'+unicode(TEMPLATENAME, 'utf-8')).titleWithoutNamespace()

class ProposedArticles:
    """
    解析結果を保持するためのクラス。クラスにした意味があまりない。
    """

    def __init__(self):
        self.hint = {}
        self.ref = {}
        #self.cat = {}
        self.pages = []

    def addHint(self, proposed, project, pagename):
        p = self.hint.get(proposed, set())
        p.update([Page(Site(project, FAMILY), pagename).aslink().replace('[[', '').replace(']]', '')])
        self.hint[proposed] = p

    #def addCat(self, proposed, category):
    #    p = self.cat.get(proposed, {})
    #    p[category] = p.get(category, 0) + 1
    #    self.cat[proposed] = p

    def addRef(self, proposed, referer):
        p = self.ref.get(proposed, set())
        p.update([referer.title()])
        self.ref[proposed] = p


def getListedPages():
    """
    WP:JAREQから、報告ずみの項目名を取得
    """
    listedPages = {}
    for ns, pfx in LISTPAGES:
        for page in SITE.prefixindex(unicode(pfx, 'utf-8'), ns, False):
            output('Getting: ' + page.aslink().encode('utf-8'))
            for tname, args in page.templatesWithParams(get_redirect=True):
                if tname.lower() <> 'jareq':
                    continue

                alt = [x[4:] for x in args if x.startswith('alt=')]
                args = [x for x in args if x.find('=') < 0]

                reqPage = None
                try:
                    reqPage = Page(SITE, args[1])
                except:
                    continue
                p = listedPages.get(reqPage.title(), set())
                p.update([reqPage.title()])
                listedPages[reqPage.title()] = p
                for al in alt:
                    for a in re.split(r'(?<=\]\])/|/(?=\[\[)', al):
                        if re.match(r'\[\[.+\]\]', a) and \
                        not re.match(r'.+\]\].+', a):
                            try:
                                a = Page(SITE, a.replace('[[','').replace(']]',''))
                            except:
                                continue
                            p = listedPages.get(a.title(), set())
                            p.update([reqPage.title()])
                            listedPages[a.title()] = p
    output('listed: %d' % len(listedPages))
    return listedPages

def aggregate(proposedArticles, maxCount):
    """
    {{仮リンク}}の使用情報を取得する。
    同テンプレートを使用しているすべてのページからテンプレートのマークアップ
    を抽出し、推奨項目名、参考リンク情報を取得する。
    """
    templatePage = Page(SITE, 'Template:'+TEMPLATENAME)
    count = 0
    for page in templatePage.getReferences(follow_redirects=False,
                                           onlyTemplateInclusion=True):
        # 標準名前空間のページのみを走査する
        if page.namespace() <> 0:
            continue

        # DEBUG
        output('Analyzing ' + page.title().encode('utf-8'))

        ## 呼び出し元ページからカテゴリを取得する
        #cats = [c.titleWithoutNamespace() for c in page.categories()]

        # テンプレートを処理する
        for tname, args in page.templatesWithParams(get_redirect=True):
            if tname <> TEMPLATENAME:
                continue

            args = [arg.strip() for arg in args
                    if not arg.strip().startswith('label=')]

            if not len(args):           # 引数が必要
                continue

            try:
                proposed = args[0]
                args = args[1:]
                proposed = proposed.split('{{!}}')[0] # 誤用への対応
                if not proposed.strip():
                    raise
                proposed = Page(SITE, proposed).title()
            except:
                output('Bad name of proposed article: %r' % proposed)
                continue

            # 呼び出し元ページ
            proposedArticles.addRef(proposed, page)

            ## 呼び出し元ページのカテゴリを仮項目名に対応づける
            #for cat in cats:
            #    proposedArticles.addCat(proposed, cat)

            # 参考リンクを取得する
            try:
                while len(args):
                    proposedArticles.addHint(proposed, args[0], args[1])
                    args = args[2:]
            except:
                output('Bad args: %r' % args)
                continue

        count += 1
        if 0 < maxCount and maxCount <= count:
            break

def dump(proposedArticles, listedPages):
    """
    取得した情報を整理して、ファイルに出力する。
    
    * listed.wiki - JAREQ掲載ずみ
    * redirect.wiki - ページは存在するがリダイレクト
    * disambig.wiki - 曖昧さ回避ページとして存在する
    * empty.wiki - 存在するが内容がない
    * exists.wiki - 以上以外で立項ずみ
    * synonym.wiki - 未立項だが、言語間リンク中にホームウィキの項目がある
    * unknown.wiki - 未立項。言語間リンクが取得できない
    * 1.wiki, 2.wiki, ... - 以上のどれでもない。未立項。
                            参考リンクからたどれる言語間リンクの数により分類
    """
    outputs = {}
    for proposed in proposedArticles.ref.keys():

        page = Page(SITE, proposed)

        # 分類する
        g = 'unknown'
        synonyms = []
        if listedPages.has_key(page.title()):
            # WP:JAREQに報告ずみのもの。別名があればそれも追加
            g = 'listed'
            synonyms = [Page(SITE, x) for x in listedPages[page.title()]
                        if x <> page.title()]
        elif page.exists():
            # ページが存在する場合。リダイレクト、曖昧さ回避、白紙は分ける
            if page.isRedirectPage():
                g = 'redirect'
            elif page.isDisambig():
                g = 'disambig'
            elif page.isEmpty():
                g = 'empty'
            else:
                g = 'exists'
        else:
            # 参考リンクのページから言語間リンクを抽出
            interwiki = set()
            for hint in proposedArticles.hint.get(proposed, set()):
                try:
                    hintPage = Page(SITE, hint)
                    if hintPage.isRedirectPage():
                        hintPage = hintPage.getRedirectTarget()
                    interwiki.update([x.aslink().replace('[[','').replace(']]','') for x in hintPage.interwiki()])
                    interwiki.update([hintPage.aslink().replace('[[','').replace(']]','')])
                except:
                    output('Failed to get interwiki: %r' % hint)
            # 言語間リンクにホームウィキの項目があればシノニムとして抽出
            synonyms = [Page(SITE, p) for p in interwiki
                        if Page(SITE, p).site().language() == LANG]
            # シノニムがないものは言語間リンク数で分類
            if len(synonyms):
                g = 'synonym'
            elif len(interwiki):
                g = len(interwiki)

        out = outputs.get(g, [])

        # DEBUG
        output('Dump: %s: %s' % (g, proposed.encode('utf-8')))

        # 整形する
        o = ['[[:%s|%s]]' % (h, h.split(':')[0]) for h in proposedArticles.hint.get(proposed, set())]
        o.sort()
        hints = '/'.join(o)
        o = ['[[%s]]' % r for r in proposedArticles.ref.get(proposed, set())]
        o.sort()
        refs = '/'.join(o)
        o = [s.aslink() for s in synonyms]
        o.sort()
        syns = '/'.join(o)
        f = (page.aslink().encode('utf-8'),
             hints.encode('utf-8'),
             refs.encode('utf-8'),
             page.aslink().replace('[[', '[[special:whatLinksHere/').replace(']]', '|...]]').encode('utf-8'))
        if g == 'synonym' or len(synonyms):
            f += (syns.encode('utf-8'),)
            out.append('* %s<small>(%s)</small> ←%s%s<br/>≈%s' % f)
        else:
            out.append('* %s<small>(%s)</small> ←%s%s' % f)
        outputs[g] = out

    # 以前のファイルを消す
    for path in os.listdir(OUTPUTDIR):
        if not path.endswith('.wiki'):
            continue
        try:
            os.unlink(os.path.join(OUTPUTDIR, path))
        except:
            output('Failed to remove: %s' % path)
    # ファイルを出力する
    for k, out in outputs.items():
        fp = open(os.path.join(OUTPUTDIR, '%s.wiki' % k), 'w')
        out.sort()
        print >>fp, "\n".join(out),
        fp.close()

def put(pagename, commentText, data, always):
    count = 0
    comment = commentText
    text = "__TOC__\n"
    for filename, title in data:
        path = os.path.join(OUTPUTDIR, filename)
        if os.path.exists(path):
            lines = [l for l in file(path)]
            if commentText is None:
                if not comment:
                    comment = ''
                else:
                    comment += '; '
                comment += unicode('%s%d件' % (title, len(lines)), 'utf-8')
                count += len(lines)
            text += "== %s ==\n%s\n\n" % (title, ''.join(lines))
    comment = unicode('%d件: %s', 'utf-8') % (count, comment)
    if 200 < len(comment) or 250 <= len(comment.encode('utf-8')):
       comment = unicode(comment[:197].encode('utf-8')[:246], 'utf-8', 'ignore') + u'...'
    page = Page(SITE, unicode(pagename, 'utf-8'))
    if always:
        choice = 'y'
    else:
        output(comment)
        choice = inputChoice(
            'Do you update %s' % page.aslink(),
            ['Yes', 'No', 'Quit'],
            ['y', 'N', 'q'], 'N')
    if choice == 'q':
        sys.exit(0)
    elif choice == 'y':
        page.put(unicode(text, 'utf-8'), comment)
    else:
        return

def main(*argv):
    toDo = {}
    maxCount = 0
    commentText = None
    always = False
    for arg in handleArgs(*argv):
        if arg == '-retrieve':
            toDo['retrieve'] = True
        elif arg == '-put':
            toDo['put'] = True
        elif arg.startswith('-max:'):
            try:
                maxCount = int(arg[5:])
            except:
                output('Illegal argument: %s' % arg)
                sys.exit(1)
        elif arg.startswith('-comment:'):
            commentText = arg[9:]
        elif arg == '-always':
            always = True
        else:
            output('Unknown argument: %s' % arg)
            sys.exit(1)
    if not toDo.has_key('retrieve') and not toDo.has_key('put'):
        output('At least either of -retrieve and -put is required.')
        sys.exit(1)

    if toDo.has_key('retrieve'):
        proposedArticles = ProposedArticles()
        aggregate(proposedArticles, maxCount)
        listedPages = getListedPages()
        dump(proposedArticles, listedPages)
    if toDo.has_key('put'):
        put(OUTPUTPAGEBASE + '/要検討',
            commentText,
            [('unknown.wiki', 'プロジェクト数不明'),
             ('disambig.wiki', '曖昧さ回避ページ'),
             ('redirect.wiki', 'リダイレクト'),
             ('synonym.wiki', 'シノニム'),
             ('empty.wiki', '白紙')],
            always)
        put(OUTPUTPAGEBASE + '/立項・報告ずみ',
            commentText,
            [('exists.wiki', '立項ずみ'),
             ('listed.wiki', 'WP:JAREQに報告ずみ')],
            always)
        put(OUTPUTPAGEBASE + '/少数の言語版',
            commentText,
            [('%d.wiki' % x, '%d言語版' % x) for x in range(4, 0, -1)],
            always)
        put(OUTPUTPAGEBASE + '/10-5言語版',
            commentText,
            [('%d.wiki' % x, '%d言語版' % x) for x in range(10, 4, -1)],
            always)
        put(OUTPUTPAGEBASE + '/15-11言語版',
            commentText,
            [('%d.wiki' % x, '%d言語版' % x) for x in range(15, 10, -1)],
            always)
        put(OUTPUTPAGEBASE + '/20-16言語版',
            commentText,
            [('%d.wiki' % x, '%d言語版' % x) for x in range(20, 15, -1)],
            always)
        put(OUTPUTPAGEBASE + '/21言語版以上',
            commentText,
            [('%d.wiki' % x, '%d言語版' % x) for x in range(100, 20, -1)],
            always)



if __name__ == '__main__':
    try:
        main()
    except:
        raise
        #XXXstopme()