aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--scripts/parse-sanpi-hyo.py97
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}人)")