Spaces:
Sleeping
Sleeping
File size: 13,226 Bytes
7c5a3be 7a6ea38 7c5a3be 7a6ea38 7c5a3be 395503f 7a6ea38 1012ab1 7a6ea38 7c5a3be 7a6ea38 7c5a3be 1012ab1 7c5a3be 395503f 7c5a3be 395503f 7c5a3be aa937e5 7c5a3be 1012ab1 3fa0fec 1012ab1 bd5a765 3fa0fec bd5a765 1012ab1 3fa0fec bd5a765 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 395503f bd5a765 1012ab1 395503f 1012ab1 395503f 1012ab1 395503f 1012ab1 395503f 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 3fa0fec 1012ab1 9e10dc0 1012ab1 9e10dc0 7c5a3be 395503f 1012ab1 395503f 1012ab1 395503f 9e10dc0 1012ab1 395503f 7c5a3be 1012ab1 7c5a3be 1012ab1 7c5a3be 1012ab1 7c5a3be 1012ab1 7c5a3be 395503f 7c5a3be 395503f 1012ab1 395503f 7c5a3be 395503f 1012ab1 395503f 7c5a3be 395503f 7c5a3be 395503f 7c5a3be 1012ab1 7c5a3be 1012ab1 7c5a3be 395503f 7c5a3be 1012ab1 7c5a3be 18f56a2 1012ab1 18f56a2 7c5a3be 1012ab1 7c5a3be 1012ab1 7c5a3be 18f56a2 1012ab1 18f56a2 7c5a3be |
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
import os
import re
import time
import zipfile
import unicodedata
from urllib.parse import urljoin, urlparse, parse_qs, unquote
import gradio as gr
import requests
import pandas as pd
from bs4 import BeautifulSoup
PUBLIC_URL = "https://www.fit-portal.go.jp/PublicInfo"
OUTDIR = "data_fit"
# -------------------- ユーティリティ --------------------
def normalize_filename(name: str) -> str:
name = unicodedata.normalize("NFKC", name)
name = re.sub(r'[\\/:*?"<>|]+', "_", name)
name = name.strip()
return name or "file"
def guess_filename_from_headers(resp: requests.Response, fallback: str) -> str:
cd = resp.headers.get("Content-Disposition", "")
m = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^";]+)"?', cd, flags=re.IGNORECASE)
if m:
try:
fn = unquote(m.group(1))
except Exception:
fn = m.group(1)
return normalize_filename(fn)
return normalize_filename(fallback)
def is_pref_link(a_tag) -> bool:
href = a_tag.get("href") or ""
return "servlet.FileDownload" in href and "file=" in href
def extract_pref_name(a_tag) -> str:
txt = (a_tag.get_text() or "").strip()
return txt or "pref"
def pick_sheet_name(xls_path: str, preferred: str | None) -> str | None:
try:
xl = pd.ExcelFile(xls_path)
if preferred and preferred in xl.sheet_names:
return preferred
# 一般的に「代表地番」を優先
for candidate in ["代表地番", "代表地番のみ", "代表地番シート"]:
if candidate in xl.sheet_names:
return candidate
return xl.sheet_names[0] if xl.sheet_names else None
except Exception:
return None
def collect_pref_links(session: requests.Session) -> list[dict]:
r = session.get(PUBLIC_URL, timeout=60)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
links = []
for a in soup.find_all("a"):
if is_pref_link(a):
links.append({"pref": extract_pref_name(a), "href": urljoin(PUBLIC_URL, a.get("href"))})
# 重複除去
seen, uniq = set(), []
for item in links:
key = (item["pref"], item["href"])
if key not in seen:
seen.add(key)
uniq.append(item)
return uniq
def download_one(session: requests.Session, url: str, outdir: str, pref: str) -> str:
os.makedirs(outdir, exist_ok=True)
qs = parse_qs(urlparse(url).query)
file_id = (qs.get("file", ["unknown"])[0])[:18]
with session.get(url, timeout=180, stream=True) as r:
r.raise_for_status()
fname = guess_filename_from_headers(r, f"{pref}_{file_id}.xlsx")
path = os.path.join(outdir, fname)
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 15):
if chunk:
f.write(chunk)
return path
# -------------------- 列名選択: 小分類 > 中分類 > 大分類 --------------------
def _clean_cell(x) -> str:
if x is None:
return ""
s = str(x).strip()
if s.lower() == "nan":
return ""
return s
def choose_names_from_multiindex(mi: pd.MultiIndex) -> list[str]:
"""
3段ヘッダ(MultiIndex)から列名を選ぶ。
優先順: 中分類(第2段) → 小分類(第3段) → 大分類(第1段)。
すべて空なら 'col'。重複は .1, .2… を付与。
"""
def _clean_cell(x) -> str:
if x is None:
return ""
s = str(x).strip()
return "" if s.lower() == "nan" else s
names = []
for tpl in mi:
# tpl は (大, 中, 小) を想定
a = _clean_cell(tpl[0]) if len(tpl) >= 1 else ""
b = _clean_cell(tpl[1]) if len(tpl) >= 2 else ""
c = _clean_cell(tpl[2]) if len(tpl) >= 3 else ""
name = b or c or a or "col" # ★ 中 > 小 > 大
names.append(name)
# 重複解消
seen = {}
out = []
for n in names:
if n not in seen:
seen[n] = 0
out.append(n)
else:
seen[n] += 1
out.append(f"{n}.{seen[n]}")
return out
# -------------------- 読み込みルール --------------------
# 0行目は削除し、1/2/3行目をヘッダ(= header=[1,2,3])
HEADER_ROWS = [1, 2, 3]
# 2枚目以降は 0〜3行目をスキップ(= skiprows=4)、header=None でデータのみ
SKIP_ROWS_OTHERS = 4
def load_excel_first(xls_path: str, sheet_pref: str | None) -> tuple[pd.DataFrame, list[str]]:
"""
1枚目:
- header=[1,2,3] で3段ヘッダを読み込み(0行目は自動的に使われない)
- 左端の列を削除
- MultiIndex から列名を「小>中>大」の優先で単一行に変換
戻り値: (df, chosen_names)
"""
sheet = pick_sheet_name(xls_path, sheet_pref)
if not sheet:
raise RuntimeError("シートが見つかりません")
df = pd.read_excel(
xls_path,
sheet_name=sheet,
engine="openpyxl",
header=HEADER_ROWS,
dtype=str
)
# 左端の列を削除
df = df.iloc[:, 1:]
# 前後空白トリム
for c in df.select_dtypes(include=["object"]).columns:
df[c] = df[c].str.strip()
# 列名を選択
if isinstance(df.columns, pd.MultiIndex):
chosen = choose_names_from_multiindex(df.columns)
else:
# 念のため単層だった場合もクリーニング&重複解消
raw = [_clean_cell(c) or "col" for c in df.columns]
seen = {}
chosen = []
for n in raw:
if n not in seen:
seen[n] = 0
chosen.append(n)
else:
seen[n] += 1
chosen.append(f"{n}.{seen[n]}")
df.columns = chosen
return df, chosen
def load_excel_other(xls_path: str, sheet_pref: str | None, target_cols: list[str]) -> pd.DataFrame | None:
"""
2枚目以降:
- skiprows=4, header=None でデータのみ
- 左端の列を削除
- 列数が合わなければ切り詰め/ダミー列追加で合わせる
- 列名を 1枚目の chosen に置換
"""
sheet = pick_sheet_name(xls_path, sheet_pref)
if not sheet:
return None
df = pd.read_excel(
xls_path,
sheet_name=sheet,
engine="openpyxl",
header=None,
skiprows=SKIP_ROWS_OTHERS,
dtype=str
)
# 左端の列を削除
df = df.iloc[:, 1:]
# 前後空白トリム
for c in df.select_dtypes(include=["object"]).columns:
df[c] = df[c].str.strip()
# 列数調整
if df.shape[1] != len(target_cols):
print(f"[WARN] 列数不一致: file={os.path.basename(xls_path)} "
f"read={df.shape[1]} vs target={len(target_cols)} -> 自動調整")
if df.shape[1] > len(target_cols):
df = df.iloc[:, :len(target_cols)]
else:
# 足りないときは None 列を追加
for k in range(len(target_cols) - df.shape[1]):
df[f"_pad_{k}"] = None
df = df.iloc[:, :len(target_cols)]
df.columns = target_cols
return df
def zip_paths(paths: list[str], out_zip: str) -> str:
with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as z:
for p in paths:
if os.path.exists(p):
z.write(p, arcname=os.path.basename(p))
return out_zip
# -------------------- メイン実行(Gradioから呼ぶ) --------------------
def run_job(sheet_name, sleep_sec, limit, re_download, progress=gr.Progress(track_tqdm=False)):
progress(0, desc="初期化中…")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (compatible; FITCollector/1.3; +https://huggingface.co/spaces)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})
# 1) リンク収集
links = collect_pref_links(session)
if not links:
return ("都道府県ファイルのリンク検出に失敗しました。ページ構成の変更/一時的な制限の可能性があります。",
None, None, None, None)
if limit and limit > 0:
links = links[:int(limit)]
progress(0.1, desc=f"リンク検出 {len(links)} 件")
# 2) ダウンロード
downloaded = []
for i, item in enumerate(links, start=1):
progress(0.1 + 0.6 * i / max(1, len(links)),
desc=f"ダウンロード {i}/{len(links)}: {item['pref']}")
try:
existing = None
if not re_download and os.path.isdir(OUTDIR):
for fn in os.listdir(OUTDIR):
if fn.lower().endswith(".xlsx") and item["pref"] in fn:
existing = os.path.join(OUTDIR, fn)
break
if existing and os.path.exists(existing):
path = existing
else:
path = download_one(session, item["href"], OUTDIR, item["pref"])
time.sleep(float(sleep_sec))
downloaded.append(path)
except Exception as e:
print(f"[WARN] ダウンロード失敗: {item['pref']} {e}")
if not downloaded:
return ("ダウンロードに失敗しました。", None, None, None, None)
# 3) 読み込み(1枚目で列名確定)
progress(0.75, desc="1枚目を読み込み(列名を確定)")
first_path = downloaded[0]
try:
df0, cols0 = load_excel_first(first_path, sheet_name if sheet_name else None)
except Exception as e:
return (f"1枚目の読み込みに失敗しました: {os.path.basename(first_path)} / {e}",
None, None, None, None)
frames = [df0]
# 4) 読み込み(2枚目以降)
for j, p in enumerate(downloaded[1:], start=2):
progress(0.75 + 0.25 * (j - 1) / max(1, len(downloaded) - 1),
desc=f"{j}枚目を読み込み")
df = load_excel_other(p, sheet_name if sheet_name else None, cols0)
if df is not None and len(df) > 0:
frames.append(df)
else:
print(f"[WARN] 読み込みスキップ: {os.path.basename(p)}")
# 5) 縦結合
combined = pd.concat(frames, ignore_index=True)
# 6) 出力
os.makedirs(OUTDIR, exist_ok=True)
out_xlsx = os.path.join(OUTDIR, "combined_fit.xlsx")
out_parq = os.path.join(OUTDIR, "combined_fit.parquet")
with pd.ExcelWriter(out_xlsx, engine="openpyxl") as w:
combined.to_excel(w, index=False, sheet_name="combined")
combined.to_parquet(out_parq, index=False)
# 7) ZIP(取得ファイル一式)
raw_zip = os.path.join(OUTDIR, "raw_excels.zip")
zip_paths(downloaded, raw_zip)
# 8) プレビュー
preview_csv = os.path.join(OUTDIR, "combined_head.csv")
combined.head(1000).to_csv(preview_csv, index=False)
progress(1.0, desc=f"完了({len(combined):,} 行)")
msg = (
f"✅ 結合完了: 行数 = {len(combined):,}\n"
f"・Excel: combined_fit.xlsx\n"
f"・Parquet: combined_fit.parquet\n"
f"・Raw ZIP: raw_excels.zip\n"
f"・プレビュー: combined_head.csv\n"
f"・列名は『小分類>中分類>大分類』の優先で単一行化(結合は不実施)"
)
return (msg, out_xlsx, out_parq, raw_zip, preview_csv)
# -------------------- Gradio UI --------------------
with gr.Blocks(title="FIT 公表(都道府県別Excel)一括取得&結合") as demo:
gr.Markdown(
"""
# FIT 公表(都道府県別Excel)一括取得 & 結合
**列名ポリシー**:
- 1枚目: 0行目を使わず、1/2/3行目をヘッダとして読み込み(3段)。
- 列名は **小分類に値があれば小分類、無ければ中分類のみ**(結合しません)。
- 2枚目以降: 0〜3行目をスキップし、データのみ読み込み。
- すべてのファイルで **左端の列は削除**。
- ファイル名/シート名などのメタ列は付与しません。
"""
)
with gr.Row():
sheet = gr.Textbox(label="読み込むシート名(空欄=自動)", placeholder="例)代表地番 / 全地番")
sleep = gr.Slider(0.0, 5.0, value=1.0, step=0.1, label="ダウンロード間隔(秒)")
with gr.Row():
limit = gr.Number(value=None, precision=0, label="先頭N県のみ(テスト用・空欄は全県)")
reget = gr.Checkbox(label="既存ファイルがあっても再ダウンロードする", value=False)
run_btn = gr.Button("実行", variant="primary")
out_msg = gr.Markdown()
out_xlsx = gr.File(label="結合Excel(combined_fit.xlsx)")
out_parq = gr.File(label="結合Parquet(combined_fit.parquet)")
out_zip = gr.File(label="取得した都道府県Excel一式(zip)")
out_preview = gr.File(label="先頭1000行プレビュー(CSV)")
run_btn.click(
fn=run_job,
inputs=[sheet, sleep, limit, reget],
outputs=[out_msg, out_xlsx, out_parq, out_zip, out_preview]
)
if __name__ == "__main__":
demo.queue(max_size=20).launch()
|