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つ):
- 出力を a(追記)→ w(上書き)に変更(再実行でファイルが増殖するのを防ぐ)
- open() を行ごとに開くのをやめて、1回だけ開く(速度も安定)
- m波 を削除せず、m 波 に分割(10m波でも波が消えない)
- 本題:風 北西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 を出す
- あるいは連結が残ってたら例外で止める(早期検知)