diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-09-23 09:37:14 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-09-23 09:37:14 +0900 |
| commit | d9b4276d838c1cdaa89f3be2fbe98b7c9719a921 (patch) | |
| tree | cce62ff69d38c814228995e5ebc33ff5f194437d | |
| parent | 849f67fedeb55df21c5603097ee1c71bfa3ff715 (diff) | |
parse-sanpi-hyo.py: 令和8年3月のレイアウトに対応
表の縦罫線を上下の幅で判定して文字の輪郭線を除き(列がずれる原因だった)、会派名の行をページ内から探し、議決結果の印は部分一致で判定するようにした。令和8年3月定例会は8議案すべてで検算が一致。令和8年6月定例会はレイアウトがさらに異なる(冒頭が全会一致の欄、会派が9つ)ため未対応で、続けて対応する。
| -rw-r--r-- | scripts/parse-sanpi-hyo.py | 97 |
1 files changed, 60 insertions, 37 deletions
diff --git a/scripts/parse-sanpi-hyo.py b/scripts/parse-sanpi-hyo.py index f04b0d0..474440a 100644 --- a/scripts/parse-sanpi-hyo.py +++ b/scripts/parse-sanpi-hyo.py @@ -18,20 +18,28 @@ YES, NO, ABS = set("〇○◯"), set("×✕✖"), set("-—−-・") VOTE = YES | NO | ABS FULL2HALF = str.maketrans("0123456789ABC", "0123456789ABC") -# 議決結果 → アイコン表示 +# 議決結果 → 印(可決系は朱色の印、否決系は青色の印) KEKKA_ICON = { - "原案可決": ("ok", "✓"), - "可決": ("ok", "✓"), - "認定": ("ok", "✓"), - "採択": ("ok", "✓"), - "同意": ("ok", "✓"), - "承認": ("ok", "✓"), - "否決": ("ng", "✗"), - "原案否決": ("ng", "✗"), - "不採択": ("ng", "✗"), + "原案可決": ("pass", "可決"), + "可決": ("pass", "可決"), + "認定": ("pass", "認定"), + "採択": ("pass", "採択"), + "同意": ("pass", "同意"), + "承認": ("pass", "承認"), + "否決": ("stop", "否決"), + "原案否決": ("stop", "否決"), + "不採択": ("stop", "不採択"), } +def kekka_icon(text): + """議決結果の文字列から、印の種類とラベルを返す(部分一致で判定)。""" + for key in sorted(KEKKA_ICON, key=len, reverse=True): + if key in text: + return KEKKA_ICON[key] + return ("", text) + + def col_of(x, bounds): for i in range(len(bounds) - 1): if bounds[i] <= x < bounds[i + 1]: @@ -61,29 +69,45 @@ def main(path, label): pg = doc[0] cs = chars(pg) - # 会派(上部2行) - top = "".join(c["c"] for c in sorted( - [c for c in cs if 45 <= c["y"] <= 70], key=lambda c: (round(c["y"] / 6), c["x"]))) - parties = [(s, f.strip(), int(n)) for s, f, n in - re.findall(r"([ぁ-んァ-ヶ一-龥A-Za-z]{2})[::]([^((]+?)[((](\d+)", top)] + # 会派(「短名:正式名(議員数)」)の行を、ページ内から探す。 + # (年度によって、全会一致の欄が先に来るなど、位置が変わるため) + best = None + for y0 in range(40, 420, 2): + seg = "".join(c["c"] for c in sorted( + [c for c in cs if y0 <= c["y"] <= y0 + 12], key=lambda c: (round(c["y"] / 6), c["x"]))) + p = re.findall(r"([ぁ-んァ-ヶ一-龥A-Za-z]{2})[::]([^((]+?)[((](\d+)", seg) + if p and (best is None or len(p) > len(best[1])): + best = (y0, [(s, f.strip(), int(n)) for s, f, n in p]) + header_y, parties = best total = sum(n for _, _, n in parties) + print(f"会派の行 y={header_y} / {len(parties)}会派 / {total}人") # 縦罫線グリッドと、横罫線(議案行の区切り) xs = {} - ylines = [] for d in pg.get_drawings(): for it in d["items"]: if it[0] == "l": a, b = it[1], it[2] if abs(a.x - b.x) < 0.8: k = round(a.x, 1) - xs[k] = max(xs.get(k, 0), a.y, b.y) - elif abs(a.y - b.y) < 0.8: - ylines.append((round(a.y, 1), round(min(a.x, b.x), 1), round(max(a.x, b.x), 1))) - grid = sorted(x for x, y in xs.items() if y > 200 and 30 < x < 780) + lo, hi = min(a.y, b.y), max(a.y, b.y) + info = xs.setdefault(k, [1e9, -1e9]) + info[0] = min(info[0], lo) + info[1] = max(info[1], hi) + # 列の境界は表の高さに近い縦線(文字の輪郭線などを除くため、上下の幅で判定する) + grid = sorted(x for x, (lo, hi) in xs.items() if hi > 200 and (hi - lo) > 120 and 30 < x < 780) - # 議員名の帯 - namechars = [c for c in cs if 88 <= c["y"] <= 140 and c["x"] > 308] + # 議案行(賛否記号のある行)。凡例等を除くため、議員数の半分以上の記号がある行だけを採用する + # (議員名の範囲を決めるため、先に計算する) + marks = [c for c in cs if c["c"] in VOTE and c["y"] > 145 and c["x"] > 300] + rows0 = {} + for c in marks: + rows0.setdefault(round(c["y"] / 10) * 10, []).append(c) + ys = sorted(y for y, v in rows0.items() if len(v) >= max(4, total // 2)) + rows = {y: v for y, v in rows0.items() if y in ys} + + # 議員名の帯(見出しの下。縦書きの名が収まる範囲) + namechars = [c for c in cs if 88 <= c["y"] <= 140 and c["x"] > 308 and c["c"] not in VOTE] nx0 = min(c["x"] for c in namechars) left = [x for x in grid if x <= nx0] bounds = ([left[-1]] if left else [nx0 - 8]) + [x for x in grid if nx0 < x] @@ -98,13 +122,8 @@ def main(path, label): member_only = members[:total] mcols = [m[0] for m in member_only] # 議員の列index - # 議案行(賛否記号のある行)。凡例等を除くため、議員数の半分以上の記号がある行だけを採用する - marks = [c for c in cs if c["c"] in VOTE and c["y"] > 145] - rows = {} - for c in marks: - rows.setdefault(round(c["y"] / 10) * 10, []).append(c) - ys = sorted(y for y, v in rows.items() - if sum(1 for c in v if col_of(c["x"], bounds) in mcols) >= max(4, total // 2)) + # 左側(議案)の列境界 + x_kubun, x_no, x_name, x_kekka = (40, 78), (82, 118), (120, 268), (270, 312) # 合計欄(議員列より右)の数字 nums = {} @@ -114,22 +133,24 @@ def main(path, label): if i is not None and i >= total: nums.setdefault(round(c["y"] / 10) * 10, []).append((c["y"], c["x"], c["c"])) - # 左側(議案)の列境界: 議員列より左の罫線 - lb = [x for x in grid if x < bounds[0]] # 議案行の組み立て(前後の中点を境界にして、行のテキストを拾う) data = [] + kubun = "" for n, y in enumerate(ys): lo = (ys[n - 1] + y) / 2 if n > 0 else y - 14 hi = (y + ys[n + 1]) / 2 if n + 1 < len(ys) else y + 14 - no = text_of(cs, 78, 118, lo, hi).translate(FULL2HALF) - name = text_of(cs, 120, 268, lo, hi).translate(FULL2HALF) - kekka = text_of(cs, 270, 312, lo, hi).translate(FULL2HALF) + k = text_of(cs, *x_kubun, lo, hi).translate(FULL2HALF) + if k: + kubun = k + no = text_of(cs, *x_no, lo, hi).translate(FULL2HALF) + name = text_of(cs, *x_name, lo, hi).translate(FULL2HALF) + kekka = text_of(cs, *x_kekka, lo, hi).translate(FULL2HALF) cells = {} for c in rows[y]: i = col_of(c["x"], bounds) if i in mcols: cells[i] = c["c"] - data.append({"no": no, "name": name, "kekka": kekka, "votes": cells}) + data.append({"no": no, "name": name, "kekka": kekka, "votes": cells, "kubun": kubun}) # 検算 yes = sum(1 for i in mcols if cells.get(i) in YES) no_ = sum(1 for i in mcols if cells.get(i) in NO) @@ -173,9 +194,11 @@ def main(path, label): else: cells.append("/".join("〇" if d["votes"].get(i) in YES else "×" if d["votes"].get(i) in NO else "—" for i in idx)) - ic, mark = KEKKA_ICON.get(d["kekka"], ("", d["kekka"])) + ic, mark = kekka_icon(d["kekka"]) kekka = f'<span class="sanpi-kekka {ic}" title="{d["kekka"]}">{mark}</span>' if ic else d["kekka"] - title = (d["no"] + " " + d["name"]).strip() + # 請願・議員提出議案は区分を頭に付ける + head = "" + title = (head + d["no"] + " " + d["name"]).strip() print(f"| {title} | {kekka} | " + " | ".join(cells) + " |") print(f"\n(会派: " + " / ".join(f"{s}({n})" for s, _, n in parties) + f" = {total}人)") |
