コンテンツにスキップ
Wikipedia

利用者: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()

AltStyle によって変換されたページ (->オリジナル) /