def opt_out_foreigners(): from constant import foreign_teams f_team_query = session.query(Team.team_id).filter( Team.team_name.in_(foreign_teams)).all() f_team_ids = [t.team_id for t in f_team_query] # リストでフィルターをかけているが、deleteの引数synchronize_sessionのデフォルト値'evaluate'ではこれをサポートしていない(らしい)からFalseを指定する count = session.query(Record).filter( Record.team_id.in_(f_team_ids)).delete(synchronize_session=False) session.commit() notify_line(f'{len(f_team_ids)}件の外国籍チームを検出。{count}件の記録を削除')
def add_records_wrapper(date_min, date_max): target_meets = session.query(Meet.meet_id).filter( Meet.start >= date_min, Meet.start <= date_max).order_by(Meet.start).all() target_meets_ids = [m.meet_id for m in target_meets] if not_up_to_date := imperfect_meets(target_meets_ids): count = session.query(Record).filter( Record.meet_id.in_(not_up_to_date)).delete( synchronize_session=False) session.commit() notify_line(f'大会ID:{not_up_to_date}、記録未納の可能性あり。{count}件の記録を削除')
def raw_timestr_to_timeval(time_str): if time_str in ["", "--:--.--", "-", "ー"]: # リレーで第一泳者以外の失格の場合--:--.--になる。最後のはハイフンではなく半角カタカナ長音 return 0 # 記録なし else: ob = re.match(time_format_ptn, time_str) if ob is None: # 普通じゃ想定していない # 大会0119722の平沼さんの女子2フリはこれが適用されている notify_line(f'<!!>無効なタイム文字列{time_str}を検出しました。とりま-1を返します') return -1 else: min = ob.group(1) if ob.group(1) != "" else 0 # 32.34とか分がないときは分は0 time_val = int(min)*6000 + int(ob.group(2))*100 + int(ob.group(3)) return time_val
def parse_table(self): # 記録一覧のページの表を全部とってくる set_of_args = [] for row, lap_table in zip(self.rows, self.lap_tables): data = row.find_all("td") # 一行の中に複数のtd(順位、氏名…)(リレーと個人で異なる)が格納されている laps_raw = lap_table.find_all("td", width = True) # タイムの書かれたtdのみがwidthの引数を持つ laps = [del_space(l.string) for l in laps_raw] # タグを取り除いて空白も削除 laps_val = ",".join([str(raw_timestr_to_timeval(l)) for l in laps]) if self.is_indivisual: raw_grade = del_space(data[3].string) try: grade = japanese_grades.index(raw_grade) except ValueError: notify_line(f'無効な学年を検出。{self.url} で{raw_grade}(L={len(raw_grade)})を検出。とりま-1を返しました。') grade = -1 time_raw = data[4].a name = del_space(data[1].string) else: grade = 0 # リレーに学年は存在しない time_raw = data[3].a # data[1].contentsはbrタグを含む配列 タグ以外をswimmersに格納 names = [del_numspace(name) for name in data[1].contents if isinstance(name, element.NavigableString)] count = len(names) assert count == 1 or count == 4 if count == 4: name = ','.join(names) else: # リレーを棄権したため氏名の表記が無いとき、改行文字だけが検出され、要素数1となる name = '' rank = del_space(data[0].text) # data[0].stringだとタグを含んだときにNoneが返されてしまう team = del_space(data[2].string) time = '' if time_raw is None else del_space(time_raw.string) time_val = raw_timestr_to_timeval(time) relay = 0 if self.is_indivisual else 5 # レコードインスタンスを作るのに必要な引数をまとめたタプル arguments_for_single_record = ( self.meet_id, self.event_id, relay, rank, name, team, grade, time_val, laps_val) set_of_args.append(arguments_for_single_record) return set_of_args
def add_records(target_meets_ids): # 大会IDのリストから1大会ごとにRecordの行を生成しDBに追加 notify_line( f"目標大会をセット。{target_meets_ids[0]}から{target_meets_ids[-1]}。{len(target_meets_ids)}大会の全記録調査開始" ) record_length = 0 # 追加した行数 erased = 0 # 削除した行数 skipped = 0 # 飛ばした種目数 events_count = 0 # 対象の種目数 for meet_id in Takenoko(target_meets_ids): events_list = scraper.all_events(meet_id) # Eventインスタンスのリスト events_count += (sub_total := len(events_list)) # 既にDBにある同一大会IDの記録を抽出し、それぞれの種目IDが何行あるかを調べる # しかしこれでは記録数変わらずに記録の中身(タイムが空白からアップデートされたとき)に対応できない existing_records_in_meet = session.query( Record.event).filter_by(meet_id=meet_id).all() existing_event_id_list = [e.event for e in existing_records_in_meet] for event in events_list: event.crawl() # print(f'{event.event_id} / {sub_total} in {event.meet_id}') if existing_event_id_list.count(event.event_id) != len( event.rows): # 記録数が一致していなかったら削除して登録し直し erased += session.query(Record).filter_by( meet_id=event.meet_id, event=event.event_id).delete() records = [Record(*args) for args in event.parse_table()] for rc in records: rc.set_team() rc.set_swimmer() session.add_all(records) record_length += len(records) session.commit() else: skipped += 1 notify_line( f'{erased}件を削除 {record_length}件を新規に保存 現在:{format(count_records(), ",")}件\n{events_count}種目中{skipped}をスキップ' )
def analyze_all(year): # statisticsテーブルの行を一行ずつ見ていき、それぞれアップデート notify_line('全記録分析を開始') stats_table = session.query(Stats).all() for stat in Takenoko(stats_table, 20): conditions = set_conditions(stat.pool, stat.event, year, stat.grade) stmt = session.query(Record.time).distinct(Record.swimmer_id).filter( *conditions).order_by(Record.swimmer_id, Record.time).subquery() subq = aliased(Record, stmt) times = session.query(stmt).order_by(subq.time).all() count_ranking = len(times) stat.count_ranking = count_ranking stat.count_agg = session.query(func.count( Record.record_id)).filter(*conditions).scalar() if count_ranking >= 2: stat.border = times[499].time if count_ranking >= 500 else None vals = pd.Series([t.time for t in times]) # 外れ値除くための範囲を決める q1 = vals.quantile(.25) q3 = vals.quantile(.75) iqr = q3 - q1 lower_limit = q1 - iqr * 1.5 upper_limit = q3 + iqr * 1.5 # 外れ値除外したやつの記述統計量を取得 desc = vals[(vals > lower_limit) & (vals < upper_limit)].describe() stat.mean = round(desc['mean'], 2) # 小数点第2位までで四捨五入 stat.std = round(desc['std'], 2) stat.q1 = int(desc['25%']) stat.q2 = int(desc['50%']) stat.q3 = int(desc['75%']) session.commit() notify_line('全記録の分析を完了')
def add_meets(year, force=False): notify_line(f"各地域の大会情報の収集を開始。対象:20{year}年") meet_ids = [] # for area_int in Takenoko(range(14,15)): # ローカル用 for area_int in Takenoko(list(range(1, 54)) + [70, 80]): # 本番用 1から53までと全国70国際80がarea番号になる meet_ids.extend(scraper.find_meet(year, area_int)) saved_meets = session.query(func.sum( Meet.meet_id)).filter_by(year=year).scalar() if force or sum(meet_ids) != saved_meets: # 大会IDの合計値が一致しないか、強制実行の場合 notify_line(f'全{len(meet_ids)}の大会を検出') meets = [] for id in Takenoko(meet_ids, 20): area = id // 100000 year = (id % 100000) // 1000 start, end, name, place, pool = scraper.meet_info(id) meets.append( Meet(meet_id=id, meet_name=name, place=place, pool=pool, start=start, end=end, area=area, year=year)) erased = session.query(Meet).filter_by( year=year).delete() # 同じ年度を二重に登録しないように削除する session.add_all(meets) session.commit() notify_line(f'{erased}件を削除 全{len(meets)}の大会情報を保存') else: notify_line(f'大会情報に更新はありませんでした')
def add_row_for_relay(relay, meet_id, swimmer_id): event = convert_relay_event(relay.event) laps_list = relay.laps.split(',') if (lap_len := len(laps_list)) < 4: notify_line(f'R1_INVALID: 無効なタイム({laps_list}) on {relay.record_id}') return 0
def add_first_swimmer_in_relay(target_meets_ids): # 対象大会内のレコードに一つも1泳者のレコード(relay=1)がなかったらまだ未追加 notify_line(f"リレー第一泳者の記録の追加を開始") record_length = 0 # 追加した行数 skipped = 0 # 飛ばした種目数 for meet_id in Takenoko(target_meets_ids, 50): first_swimmers = session.query(Record.record_id).filter_by( meet_id=meet_id, relay=1).all() if first_swimmers: skipped += 1 # その大会においては既に1泳者追加していた else: relay_results = session.query( Record.record_id, Record.event, Swimmer.name, Record.rank, Record.team_id, Record.laps).filter( Record.meet_id == meet_id, Record.swimmer_id == Swimmer.swimmer_id, Record.relay == 5, ~Record.rank.in_(['失格', '失格1泳者', '棄権', '途中棄権']) # 2~4泳者の失格はよい あと失格、は誰が失格なのかわからないから一応除外 ).all() only_relay_but_add = [] sub_count = 0 for relay in relay_results: swimmers = relay.name.split(',') if len(swimmers) == 1: notify_line( f'R1_INVALID: 無効なリレーオーダー({swimmers}) on {relay.record_id}' ) continue first = swimmers[0] # とりあえず同じ名前の人探す candidates = session.query( Swimmer.swimmer_id).filter_by(name=first).all() if candidates: # 同一大会内の個人種目でその人が出場しているか candidates_in_same_meet = session.query( Record.swimmer_id).filter( Record.meet_id == meet_id, Record.swimmer_id.in_([ c.swimmer_id for c in candidates ])).distinct( Record.swimmer_id # 同姓同名の選手が同じ大会に出場していたらオワオワリ ).all() suggest_s_ids = [ s.swimmer_id for s in candidates_in_same_meet ] if (length := len(suggest_s_ids)) == 1: # これは特定余裕 同じ大会内で同じ名前の選手が一人だけいた sub_count += add_row_for_relay(relay, meet_id, suggest_s_ids[0]) elif length == 0: # 同一大会で出場なし if len(candidates) == 1: sub_count += add_row_for_relay( relay, meet_id, candidates[0]) only_relay_but_add.append( f'{first}, {relay.record_id}') else: notify_line( f'R1_INVALID: リレーのみ出場同姓同名({first}) on {relay.record_id}' ) else: # 同姓同名が同一大会で出場したため、リレー一泳が誰か特定不可 notify_line( f'R1_INVALID: 同一大会内同姓同名({first}) on {relay.record_id}' ) else: # 同じ名前の人がSwimmerテーブルに存在しない notify_line( f'R1_INVALID: テーブルに存在しない名前({first}) on {relay.record_id}' ) if only_relay_but_add: msg = ' '.join(only_relay_but_add) notify_line( f'{meet_id}において、リレーのみ出場の選手かつ同姓同名なしで問題なしとしたのが以下。{msg}') session.commit() print(f'{meet_id}にて{sub_count}件追加') record_length += sub_count
) else: # 同じ名前の人がSwimmerテーブルに存在しない notify_line( f'R1_INVALID: テーブルに存在しない名前({first}) on {relay.record_id}' ) if only_relay_but_add: msg = ' '.join(only_relay_but_add) notify_line( f'{meet_id}において、リレーのみ出場の選手かつ同姓同名なしで問題なしとしたのが以下。{msg}') session.commit() print(f'{meet_id}にて{sub_count}件追加') record_length += sub_count notify_line(f'{record_length}件の第一泳者の記録を新規に保存。{skipped}大会をスキップ') def add_row_for_relay(relay, meet_id, swimmer_id): event = convert_relay_event(relay.event) laps_list = relay.laps.split(',') if (lap_len := len(laps_list)) < 4: notify_line(f'R1_INVALID: 無効なタイム({laps_list}) on {relay.record_id}') return 0 else: assert lap_len % 4 == 0 first_range = lap_len // 4 first_laps = laps_list[:first_range] time = int(first_laps[-1]) # 最後の一つが1泳の正式タイム laps = ','.join(first_laps) first_result = Record(meet_id=meet_id,