6 OSDNのサーバでrequests_oauthlibを使う方法のメモ
8 1. 環境変数PYTHONUSERBASEでインストール先をWebコンテンツ上の任意のディレクトリに指定し、
9 pipに--userオプションをつけてインストールを実行
10 以下は /home/groups/h/he/hengband/htdocs/score/local 以下にインストールする例
12 `$ PYTHONUSERBASE=/home/groups/h/he/hengband/htdocs/score/local
13 pip install --user requests_oauthlib`
15 2. パスは通っているはずなのにシステムにインストールされているrequestsとurllib3が何故か読み込みに失敗するので、
16 上でインストールしたディレクトリにコピーする
18 `$ cp -a /usr/lib/python2.7/dist-packages/requests
19 /usr/lib/python2.7/dist-packages/urllib3
20 /home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages`
22 3. sys.path.appendで上でインストールしたディレクトリにパスを通してからrequests_oauthlibをimportする
25 `sys.path.append('/home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages')`
26 `import requests_oauthlib`
37 def get_score_data(score_db_path, score_id):
41 score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
42 score_id: 取得するスコアのスコアID。
47 指定のスコアIDに該当するスコアが見つからない場合None。
50 cond = 'ORDER BY score_id DESC LIMIT 1'
52 cond = 'WHERE score_id = :score_id'
54 with sqlite3.connect(score_db_path) as con:
55 con.row_factory = sqlite3.Row
60 WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
64 WHEN killer = 'ripe' THEN '勝利の後引退'
65 WHEN killer = 'Seppuku' THEN '勝利の後切腹'
66 ELSE killer || 'に殺された'
81 c = con.execute(sql, {'score_id': score_id})
84 return score[0] if len(score) == 1 else None
87 def get_daily_score_stats(score_db_path, year, month, day):
88 '''DBから指定した日付のスコア統計データを得る
91 score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
98 'total_count': 総スコア件数, 'winner_count': 勝利スコア件数
100 with sqlite3.connect(score_db_path) as con:
101 con.row_factory = sqlite3.Row
104 count(*) AS total_count,
105 count(winner = 1 OR NULL) AS winner_count
109 date >= date('{target_date}') AND date < date('{target_date}', '+1 day')
110 '''.format(target_date=datetime.date(year, month, day).isoformat())
111 c = con.execute(sql, {})
117 def get_death_reason_detail(score_id):
118 '''ダンプファイル内から詳細な死因を取得する。
121 score_id: ダンプファイルのスコアID。
125 ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
127 subdir = (score_id // 1000) * 1000
129 with gzip.open("dumps/{0}/{1}.txt.gz"
130 .format(subdir, score_id), 'r') as f:
135 # NOTE: 死因の記述は31行目から始まる
136 death_reason = unicode(''.join([l.strip() for l in dump[30:33]]), "UTF-8")
137 match = re.search(u"…あなたは、?(.+)。", death_reason)
139 return match.group(1) if match else None
142 def create_tweet(score_db_path, score_id):
146 score_db: スコアデータが格納されているDBへのパスを表す文字列。
147 score_id: 指定するスコアID。Noneの場合最新のスコアを取得する。
151 なんらかの理由により生成できなかった場合None。
153 score_data = get_score_data(score_db_path, options.score_id)
154 if score_data is None:
157 death_reason_detail = get_death_reason_detail(score_data['score_id'])
158 if death_reason_detail is None:
159 death_reason_detail = (u"{0} {1}階"
160 .format(score_data['death_reason'],
161 score_data['depth']))
163 summary = (u"【新着スコア】{personality_name}{name} Score:{score}\n"
164 u"{race_name} {class_name}{realms_name}\n"
165 u"{death_reason_detail}"
166 ).format(death_reason_detail=death_reason_detail, **score_data)
168 dump_url = ("https://hengband.osdn.jp/score/show_dump.php?score_id={}"
169 ).format(score_data['score_id'])
170 screen_url = ("https://hengband.osdn.jp/score/show_screen.php?score_id={}"
171 ).format(score_data['score_id'])
173 tweet = (u"{summary}\n\n"
174 u"dump: {dump_url}\n"
175 u"screen: {screen_url}\n"
177 ).format(summary=summary,
179 screen_url=screen_url)
183 def create_daily_stats_tweet(score_db_path, year, month, day):
184 '''デイリースコア統計データのツイートを生成する
187 score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
194 なんらかの理由により生成できなかった場合None。
196 daily_stats = get_daily_score_stats(score_db_path, year, month, day)
198 tweet = (u"{year}年{month}月{day}日のスコア\n"
199 u"全 {total_count} 件, 勝利 {winner_count} 件\n"
201 ).format(year=year, month=month, day=day,
207 def tweet(oauth, tweet_contents):
211 oauth: requests_oauthlib.OAuth1Sessionの引数に渡すOAuth認証パラメータ。
212 tweet_contents: ツイートする内容を表す文字列。
214 from requests_oauthlib import OAuth1Session
215 from requests.adapters import HTTPAdapter
216 from requests import codes
217 from logging import getLogger
218 logger = getLogger(__name__)
220 twitter = OAuth1Session(**oauth)
222 url = "https://api.twitter.com/1.1/statuses/update.json"
224 params = {"status": tweet_contents}
225 twitter.mount("https://", HTTPAdapter(max_retries=5))
227 logger.info("Posting to Twitter...")
228 logger.info(u"Tweet contents:\n{}".format(tweet_contents))
229 res = twitter.post(url, params=params)
231 if res.status_code == codes.ok:
232 logger.info("Success.")
234 logger.warning("Failed to post: {code}, {json}"
235 .format(code=res.status_code, json=res.json()))
242 パースした結果を表す辞書。OptionParser.parse_args()のドキュメント参照。
244 from optparse import OptionParser
245 parser = OptionParser()
248 type="int", dest="score_id",
249 help="Tweet score with specified id.\n"
250 "When this option and -d are not set, latest score is specified.")
252 "-d", "--daily-stats",
253 type="string", dest="stats_date",
254 help="Tweet statistics of the score of the specified day.")
255 parser.add_option("-c", "--config",
256 type="string", dest="config_file",
257 default="tweet_score.cfg",
258 help="Configuration INI file [default: %default]")
259 parser.add_option("-l", "--log-file",
260 type="string", dest="log_file",
261 help="Logging file name")
262 parser.add_option("-n", "--dry-run",
263 action="store_true", dest="dry_run",
265 help="Output to stdout instead of posting to Twitter.")
266 return parser.parse_args()
269 def setup_logger(log_file):
273 log_file: ロガーの出力先ファイル名。
274 Noneの場合、ファイルには出力せず標準エラー出力のみに出力する。
276 from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
277 logger = getLogger(__name__)
278 logger.setLevel(INFO)
280 logger.addHandler(sh)
282 formatter = Formatter('[%(asctime)s] %(message)s')
283 fh = FileHandler(log_file)
284 fh.setFormatter(formatter)
285 logger.addHandler(fh)
288 if __name__ == '__main__':
289 (options, arg) = parse_option()
290 setup_logger(options.log_file)
291 from logging import getLogger
292 logger = getLogger(__name__)
295 config.parse(options.config_file)
296 if 'Python' in config.config:
297 sys.path.append(config.config['Python']['local_lib_path'])
299 if options.stats_date:
300 target_datetime = datetime.datetime.strptime(
301 options.stats_date, "%Y-%m-%d")
302 tweet_contents = create_daily_stats_tweet(
303 config.config['ScoreDB']['path'],
304 target_datetime.year,
305 target_datetime.month,
309 tweet_contents = create_tweet(config.config['ScoreDB']['path'],
312 if tweet_contents is None:
313 logger.warning('No score data found.')
316 if (options.dry_run):
317 print(tweet_contents.encode("UTF-8"))
319 tweet(config.config['TwitterOAuth'], tweet_contents)
321 from traceback import format_exc
322 logger.critical(format_exc())