1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
"""賛否一覧PDF(新レイアウト)を読み、Markdown表を出力する。合計欄で検算する。
使い方: python .extract-final.py <pdf> "<定例会名>"
"""
import re
import sys
import pymupdf
sys.stdout.reconfigure(encoding="utf-8")
YES, NO, ABS = set("〇○◯"), set("×✕✖"), set("-—−-・")
VOTE = YES | NO | ABS
DIGITS = set("01234567890123456789")
def col_of(x, bounds):
for i in range(len(bounds) - 1):
if bounds[i] <= x < bounds[i + 1]:
return i
return None
def chars(pg):
out = []
for block in pg.get_text("rawdict")["blocks"]:
for line in block.get("lines", []):
for span in line["spans"]:
for ch in span["chars"]:
if ch["c"].strip():
x0, y0, x1, y1 = ch["bbox"]
out.append({"c": ch["c"], "x": (x0 + x1) / 2, "y": (y0 + y1) / 2})
return out
def main(path, label):
doc = pymupdf.open(path)
pg = doc[0]
cs = chars(pg)
# 会派(上部の2行、y 45〜70。文字は y,x の順に並べる)
topcs = sorted([c for c in cs if 45 <= c["y"] <= 70], key=lambda c: (round(c["y"] / 6), c["x"]))
top = "".join(c["c"] for c in topcs)
parties = [(s, f.strip(), int(n)) for s, f, n in
re.findall(r"([ぁ-んァ-ヶ一-龥A-Za-z]{2})[::]([^((]+?)[((](\d+)", top)]
total = sum(n for _, _, n in parties)
# 縦罫線グリッド
xs = {}
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)
info = xs.setdefault(k, {"maxy": 0})
info["maxy"] = max(info["maxy"], a.y, b.y)
grid = sorted(x for x, i in xs.items() if i["maxy"] > 200 and 250 < x < 770)
# 議員名の帯(y 88〜140、左の区分・番号・件名・議決結果を除く)
namechars = [c for c in cs if 88 <= c["y"] <= 140 and c["x"] > 308]
nx0, nx1 = min(c["x"] for c in namechars), max(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 <= nx1 + 15]
cols = {}
for c in namechars:
i = col_of(c["x"], bounds)
if i is not None:
cols.setdefault(i, []).append(c)
members = [(i, "".join(x["c"] for x in sorted(cols[i], key=lambda c: c["y"]))) for i in sorted(cols)]
# 会派ごとに区切る
groups, k = [], 0
for s, f, n in parties:
groups.append((s, members[k:k + n]))
k += n
ngroups = len(members) - sum(n for _, _, n in parties) # 末尾の合計欄(賛成/反対)
member_only = members[:sum(n for _, _, n in parties)]
# 賛否
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)
# 合計欄(末尾の列)の数字
nums = {}
for c in cs:
if c["c"] in DIGITS:
i = col_of(c["x"], bounds)
if i is not None and i >= len(member_only):
nums.setdefault(round(c["y"] / 10) * 10, []).append((c["y"], c["x"], c["c"]))
# 議案名(左側)
left_txt = {}
for c in cs:
if c["x"] < 300 and c["y"] > 120:
left_txt.setdefault(round(c["y"] / 10) * 10, []).append((c["x"], c["c"]))
print(f"### {label}")
print(f"会派: " + " / ".join(f"{s}({n})" for s, _, n in parties) + f" = {total}人")
print(f"議員抽出数: {len(member_only)}(一致: {len(member_only) == total})")
for s, ms in groups:
print(f" {s}: {', '.join(m[1] for m in ms)}")
print("\n議案行の検算:")
ok = True
for y in sorted(rows):
cells = {}
for c in rows[y]:
i = col_of(c["x"], bounds)
if i is not None:
cells.setdefault(i, []).append(c["c"])
yes = sum(1 for i, _ in member_only if any(v in YES for v in cells.get(i, [])))
no = sum(1 for i, _ in member_only if any(v in NO for v in cells.get(i, [])))
if yes + no == 0:
continue # 賛否の記号がない行(全会一致など)
pdfs = "".join(c for _, _, c in sorted(nums.get(y, [])))
norm = pdfs.translate(str.maketrans("0123456789", "0123456789"))
match = (str(yes) in norm and str(no) in norm)
ok = ok and match
print(f" y={y}: 賛成{yes} 反対{no} / PDF={pdfs} {'✓' if match else '✗ 要確認'}")
print(" ⇒ 検算:", "全行一致" if ok else "不一致あり")
doc.close()
main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "")
|