16 def get_score_data(score_id):
20 score_id: 取得するスコアのスコアID。
25 指定のスコアIDに該当するスコアが見つからない場合None。
28 cond = 'ORDER BY score_id DESC LIMIT 1'
30 cond = 'WHERE score_id = :score_id'
32 with sqlite3.connect(config.config['ScoreDB']['path']) as con:
33 con.row_factory = sqlite3.Row
38 WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
42 WHEN killer = 'ripe' THEN '勝利の後引退'
43 WHEN killer = 'Seppuku' THEN '勝利の後切腹'
44 ELSE killer || 'に殺された'
59 c = con.execute(sql, {'score_id': score_id})
62 return score[0] if len(score) == 1 else None
65 def get_daily_score_stats(year, month, day):
66 '''DBから指定した日付のスコア統計データを得る
75 'total_count': 総スコア件数, 'winner_count': 勝利スコア件数
77 with sqlite3.connect(config.config['ScoreDB']['path']) as con:
78 con.row_factory = sqlite3.Row
81 count(*) AS total_count,
82 count(winner = 1 OR NULL) AS winner_count
86 date >= date('{target_date}') AND date < date('{target_date}', '+1 day')
87 '''.format(target_date=datetime.date(year, month, day).isoformat())
88 c = con.execute(sql, {})
94 def get_death_reason_detail(score_id):
95 '''ダンプファイル内から詳細な死因を取得する。
98 score_id: ダンプファイルのスコアID。
102 ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
104 subdir = (score_id // 1000) * 1000
106 with gzip.open("dumps/{0}/{1}.txt.gz"
107 .format(subdir, score_id), 'r') as f:
112 # NOTE: 死因の記述は31行目から始まる
113 death_reason = unicode(''.join([l.strip() for l in dump[30:33]]), "UTF-8")
114 match = re.search(u"…あなたは、?(.+)。", death_reason)
116 return match.group(1) if match else None
119 def create_score_tweet(score_id):
123 score_id: 指定するスコアID。Noneの場合最新のスコアを取得する。
127 なんらかの理由により生成できなかった場合None。
129 score_data = get_score_data(score_id)
130 if score_data is None:
133 death_reason_detail = get_death_reason_detail(score_data['score_id'])
134 if death_reason_detail is None:
135 death_reason_detail = (u"{0} {1}階"
136 .format(score_data['death_reason'],
137 score_data['depth']))
139 summary = (u"【新着スコア】{personality_name}{name} Score:{score}\n"
140 u"{race_name} {class_name}{realms_name}\n"
141 u"{death_reason_detail}"
142 ).format(death_reason_detail=death_reason_detail, **score_data)
144 dump_url = ("https://hengband.osdn.jp/score/show_dump.php?score_id={}"
145 ).format(score_data['score_id'])
146 screen_url = ("https://hengband.osdn.jp/score/show_screen.php?score_id={}"
147 ).format(score_data['score_id'])
149 tweet = (u"{summary}\n\n"
150 u"dump: {dump_url}\n"
151 u"screen: {screen_url}\n"
153 ).format(summary=summary,
155 screen_url=screen_url)
159 def create_daily_stats_tweet(year, month, day):
160 '''デイリースコア統計データのツイートを生成する
169 なんらかの理由により生成できなかった場合None。
171 daily_stats = get_daily_score_stats(year, month, day)
174 u"https://hengband.osdn.jp/score/score_ranking.php"
175 u"?fd={target_date}&td={target_date}"
176 .format(target_date=datetime.date(year, month, day).isoformat())
179 tweet = (u"{year}年{month}月{day}日のスコア一覧 "
180 u"(全 {total_count} 件, 勝利 {winner_count} 件)\n"
183 ).format(year=year, month=month, day=day, score_url=score_url,
193 パースした結果を表す辞書。OptionParser.parse_args()のドキュメント参照。
195 from optparse import OptionParser
196 parser = OptionParser()
199 type="int", dest="score_id",
200 help="Tweet score with specified id.\n"
201 "When this option and -d are not set, latest score is specified.")
203 "-d", "--daily-stats",
204 type="string", dest="stats_date",
205 help="Tweet statistics of the score of the specified day.")
206 parser.add_option("-c", "--config",
207 type="string", dest="config_file",
208 default="tweet_score.cfg",
209 help="Configuration INI file [default: %default]")
210 parser.add_option("-l", "--log-file",
211 type="string", dest="log_file",
212 help="Logging file name")
213 parser.add_option("-n", "--dry-run",
214 action="store_true", dest="dry_run",
216 help="Output to stdout instead of posting to Twitter.")
217 return parser.parse_args()
220 def setup_logger(log_file):
224 log_file: ロガーの出力先ファイル名。
225 Noneの場合、ファイルには出力せず標準エラー出力のみに出力する。
227 from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
228 logger = getLogger(__name__)
229 logger.setLevel(INFO)
231 logger.addHandler(sh)
233 formatter = Formatter('[%(asctime)s] %(message)s')
234 fh = FileHandler(log_file)
235 fh.setFormatter(formatter)
236 logger.addHandler(fh)
239 if __name__ == '__main__':
240 (options, arg) = parse_option()
241 setup_logger(options.log_file)
242 from logging import getLogger
243 logger = getLogger(__name__)
246 config.parse(options.config_file)
247 if 'Python' in config.config:
248 sys.path.append(config.config['Python']['local_lib_path'])
250 if options.stats_date:
251 target_datetime = datetime.datetime.strptime(
252 options.stats_date, "%Y-%m-%d")
253 tweet_contents = create_daily_stats_tweet(
254 target_datetime.year,
255 target_datetime.month,
259 tweet_contents = create_score_tweet(options.score_id)
261 if tweet_contents is None:
262 logger.warning('No score data found.')
265 if (options.dry_run):
266 if isinstance(tweet_contents, basestring):
267 tweet_contents = [tweet_contents]
268 for tw in tweet_contents:
269 print(tw.encode("UTF-8"))
271 twitter = twitter.Twitter(
273 **config.config['TwitterOAuth']
275 twitter.post_tweet(tweet_contents)
277 from traceback import format_exc
278 logger.critical(format_exc())