Python プログラミング ボートレース 予想システム

ボートレース予想システム】バグ#49:2024/01/04(戸田4R等)で race_id が NULL になる原因と前処理だけ”で直した話

1. 何が起きた?

2024/01/04 のデータ取り込みで、特定レースだけ race_id が採番できず NULL になり、DB登録で停止した。

発生レース(戸田)

  • 4R
  • 5R
  • 7R
  • 9R
  • 10R
  • 11R

症状(Excel出力で確認)

  • race_id / race_date / course_code / rno が空白(欠損)
  • MySQL側で race_id NOT NULL に引っかかる(例:IntegrityError: Column 'race_id' cannot be null)

2. 「2R/3Rと4R、見た目ほぼ同じやのに何で?」問題

1着行を見ると、内容はほぼ同じに見える。ただし“風向と風速”だけが微妙に違う。

2R/3R のイメージ

… 晴 風 北西 8m 波 …

4R のイメージ

… 晴 風 北西10m 波 …

人間から見たら誤差レベルやけど、機械にとっては致命的やった。


3. 根本原因:パースより前の“前処理”で、区切りが消えてた

提供元の TXT(例:K240104.TXT)段階では、風向と風速の間は 全角スペース で区切られている。

ところが、次のステップ(03_SHTXT の pre01.txt)を作る前処理で、全角スペース「 」を 無条件で削除していた。

その結果、例えば

北西 10m

(全角スペースで区切り)

が、

北西10m

(くっつく)

に変化する。

一方で 8m のような 1桁風速は、元データによっては半角スペースが混ざっているケースがあり、

北西  8m

→ 全角だけ消しても半角が残る →

北西 8m

となり、“たまたま助かる”。

結論:2桁風速(10mなど)のときだけ区切りが消えて、トークン数が1個減るのが真犯人。


4. それで何が壊れる?(なぜ race_id が NULL になる?)

後段のパーサは「レーサー1行を split して末尾15トークンをレース共通情報として抜く」前提で動いていた。

しかし「北西10m」でトークン数が1個減ると、末尾15の境界がズレる。

ズレるとどうなるかというと、例えばこんな“列ズレ事故”が起きる:

  • Place(場名)が「戸田」ではなく「1.51.8」みたいな値になる
  • Round が「4R」ではなく「戸田」みたいな値になる
  • 結果として course_code や rno が作れず、race_id も NULL になる

要するに:Excelで壊れたのではなく、pre01.txt の時点で壊れていた。


5. 方針:「パース側で頑張らず、前処理でさばく」

今回の方針はこれ:

  • 全角スペース削除は維持(選手名や場名の「戸 田」を詰める目的がある)
  • その代わり、削除後に「風向+風速(m)がくっついたら分割」を追加
  • ついでに危ない処理(m波 を削除)を“分割”に変更(波を殺さない)

6. 修正内容(text_processing の改修)

やったこと(主に4つ):

  1. 出力を a(追記)→ w(上書き)に変更(再実行でファイルが増殖するのを防ぐ)
  2. open() を行ごとに開くのをやめて、1回だけ開く(速度も安定)
  3. m波 を削除せず、m 波 に分割(10m波でも波が消えない)
  4. 本題:風 北西10m を 風 北西 10m に直す正規表現を追加

7. 修正後コード

※PRC_FILE_DIR は既存コードと同じ定義を利用する前提

import os
import re
from os import path

def text_processing(pln_txt_file_list: list):
    """
    02_TXTDown のTXT(shift_jis) → 03_SHTXT の pre01.txt(utf-8) を生成する前処理。
    全角スペースを潰して氏名/場名を詰めつつ、風向+風速の「連結だけ」救済する。
    """

    for pln_file_name in pln_txt_file_list:
        file_name_with_extension = os.path.basename(pln_file_name)
        file_name_without_extension = os.path.splitext(file_name_with_extension)[0]

        output_raw = path.join(PRC_FILE_DIR, file_name_without_extension + "pre01.txt")

        with open(pln_file_name, mode="r", encoding="shift_jis", errors="replace") as fin, \
             open(output_raw,  mode="w", encoding="utf-8", newline="\\n") as fout:

            for line in fin:
                s = line.rstrip("\\r\\n")

                # 既存置換(意図は維持)
                s = (s.replace("選  手  名", "選手名")
                       # "m波" は削除せず分割(波が消えるのを防ぐ)
                       .replace("m波", "m 波")
                       .replace("cm", "")
                       .replace("進入固定", "")
                       .replace("〜", "")
                       .replace("!", "")
                       .replace("(", "")
                       .replace(")", "")
                       .replace("・", "")
                       .replace("−", "")
                     )

                # 全角スペース削除(場名/選手名を詰める目的)
                s = s.replace(" ", "")

                # 本件の核心:風向+風速が連結してたら分割(北西10m → 北西 10m)
                s = re.sub(r"(風\\s*)([北南東西]{1,3})(\\d{1,2}m)", r"\\1\\2 \\3", s)

                # 保険:風速と波が連結してたら分割(10m波 → 10m 波)
                s = re.sub(r"(\\d{1,2}m)(波)", r"\\1 \\2", s)

                # 空白正規化
                s = re.sub(r"[ \\t]{2,}", " ", s).lstrip()

                fout.write(s + "\\n")

    print("テキスト整形作業を終了しました")

8. 直ったこと(確認結果)

  • pre01.txt 段階で「北西10m」が残らない(「北西 10m」になる)
  • 戸田 4R/5R/7R/9R/10R/11R でも列ズレしない
  • race_id 欠損が消える → DB登録が止まらない
  • バグ#49 はクローズ(完了)

9. 学び(今回の教訓)

  • 「空白削除」は見た目を整えるけど、構造も壊す
  • テキストは“空白区切りデータ”ではなく“帳票(固定幅)”のことが多い
  • split前提のパーサは、区切りが1個消えただけで崩れる

→ なので、前処理で “トークン構造” を安定化させるのが一番コスパええ。


(次回の改善案)

  • pre01.txt に「風向+風速の連結(北西10m等)」が残ってたら WARN を出す
  • あるいは連結が残ってたら例外で止める(早期検知)

-Python, プログラミング, ボートレース, 予想システム