blueellia commited on
Commit
fdbdb91
Β·
verified Β·
1 Parent(s): e95ab0c

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -0
app.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io, re, time, random, requests, pickle, gc
2
+ import pandas as pd
3
+ import streamlit as st
4
+ from concurrent.futures import ThreadPoolExecutor, as_completed
5
+ from openpyxl import Workbook
6
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
7
+ from openpyxl.utils import get_column_letter
8
+
9
+ st.set_page_config(page_title="사업μž₯ μ£Όμ†ŒΒ·μ „ν™”λ²ˆν˜Έ 보완", page_icon="πŸ“‹", layout="centered")
10
+ st.markdown("""<style>
11
+ #MainMenu,footer,header{visibility:hidden}
12
+ .block-container{max-width:660px!important;padding:2rem 1rem!important}
13
+ div.stButton>button{background:#3b82f6!important;color:#fff!important;border:none!important;border-radius:8px!important;font-weight:600!important;width:100%!important}
14
+ div.stDownloadButton>button{background:#10b981!important;color:#fff!important;border:none!important;border-radius:8px!important;font-weight:600!important;width:100%!important}
15
+ </style>""", unsafe_allow_html=True)
16
+
17
+ FSC_KEY = "2446da601452d231f1ff77a4c5ac9598896028fb96e8be586bf7f4be58b6231c"
18
+ KAKAO_KEY = "e8eef7246a2cd0f3d2d55af8a806e3ef"
19
+ NAV_HDR = {"User-Agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15","Referer":"https://map.naver.com/","Accept-Language":"ko-KR,ko;q=0.9"}
20
+ WORKERS = 20
21
+
22
+ def kakao_keyword(name, addr):
23
+ try:
24
+ r = requests.get("https://dapi.kakao.com/v2/local/search/keyword.json",
25
+ headers={"Authorization":f"KakaoAK {KAKAO_KEY}"},
26
+ params={"query":f"{name} {addr[:20]}","size":5}, timeout=5)
27
+ docs = r.json().get("documents",[])
28
+ m = next((d for d in docs if name[:4] in d.get("place_name","")), None)
29
+ return m or (docs[0] if docs else None)
30
+ except: return None
31
+
32
+ def fsc_info(name, biz_no):
33
+ try:
34
+ r = requests.get(
35
+ "https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2",
36
+ params={"serviceKey":FSC_KEY,"pageNo":1,"numOfRows":5,"resultType":"json","corpNm":name}, timeout=5)
37
+ items = r.json().get("response",{}).get("body",{}).get("items",{}).get("item",[])
38
+ if not items: return None
39
+ if isinstance(items, dict): items = [items]
40
+ bc = re.sub(r'\D','',str(biz_no))
41
+ m = next((i for i in items if re.sub(r'\D','',str(i.get("bzno",""))) == bc), items[0])
42
+ return {"phone":m.get("enpTlno",""), "addr":m.get("enpBsadr","") or m.get("enpDtadr","")}
43
+ except: return None
44
+
45
+ def naver_phone(name, addr):
46
+ clean = re.sub(r'μ£Όμ‹νšŒμ‚¬|μœ ν•œνšŒμ‚¬|\(μ£Ό\)|\(유\)|[(){}\[\]]','',name).strip()
47
+ gu = re.search(r'[\w]+ꡬ', addr); gu = gu.group() if gu else ""
48
+ try:
49
+ r = requests.get("https://m.map.naver.com/search2/search.nhn",
50
+ headers=NAV_HDR, params={"query":f"{clean} {gu}","type":"all"}, timeout=8)
51
+ if r.status_code == 429: time.sleep(30); return naver_phone(name, addr)
52
+ for pat in [r'tel:(0\d{1,2}-?\d{3,4}-?\d{4})',r'(?<!\d)(0\d{1,2}-\d{3,4}-\d{4})(?!\d)']:
53
+ f = re.findall(pat, r.text)
54
+ if f: return f[0]
55
+ except: pass
56
+ return ""
57
+
58
+ def process_one(row_data):
59
+ idx, name, addr, biz_no, is_corp = row_data
60
+ phone=""; full_addr=addr; lat=None; lon=None
61
+ if is_corp:
62
+ info = fsc_info(name, biz_no)
63
+ if info:
64
+ phone = info.get("phone","")
65
+ if info.get("addr"): full_addr = info["addr"]
66
+ time.sleep(0.1)
67
+ kk = kakao_keyword(name, addr)
68
+ if kk:
69
+ if not phone: phone = kk.get("phone","")
70
+ lat = float(kk.get("y") or 0) or None
71
+ lon = float(kk.get("x") or 0) or None
72
+ road = kk.get("road_address_name","")
73
+ if road: full_addr = road
74
+ if not phone: phone = naver_phone(name, full_addr)
75
+ return idx, {"phone":phone, "addr":full_addr, "lat":lat, "lon":lon}
76
+
77
+ def make_excel(df):
78
+ wb = Workbook(); ws = wb.active; ws.title = "λ³΄μ™„μ™„λ£Œ"
79
+ def thin():
80
+ s = Side(style="thin",color="D0D0D0")
81
+ return Border(left=s,right=s,top=s,bottom=s)
82
+ hdrs = list(df.columns)
83
+ widths = {"사업μž₯λͺ…":26,"μ—…μ’…":18,"법인여뢀":8,"μ£Όμ†Œ":42,"μ „ν™”λ²ˆν˜Έ":14,"κ°€μž…μžμˆ˜":8,"λ‹Ήμ›”κ³ μ§€κΈˆμ•‘":14,"μœ„λ„":12,"경도":12}
84
+ CENTER = {"법인여뢀","κ°€μž…μžμˆ˜","λ‹Ήμ›”κ³ μ§€κΈˆμ•‘","μœ„λ„","경도","μ „ν™”λ²ˆν˜Έ"}
85
+ for c,h in enumerate(hdrs,1):
86
+ cell = ws.cell(1,c,h)
87
+ cell.fill = PatternFill("solid",start_color="1F3864")
88
+ cell.font = Font(name="Arial",size=10,bold=True,color="FFFFFF")
89
+ cell.alignment = Alignment(horizontal="center",vertical="center")
90
+ cell.border = thin()
91
+ ws.column_dimensions[get_column_letter(c)].width = widths.get(h,12)
92
+ ws.row_dimensions[1].height = 18
93
+ ws.freeze_panes = "A2"
94
+ ws.auto_filter.ref = f"A1:{get_column_letter(len(hdrs))}1"
95
+ for ri,row in enumerate(df.itertuples(index=False),2):
96
+ for ci,(h,v) in enumerate(zip(hdrs,row),1):
97
+ if pd.isna(v) or str(v) in ("nan","None"): v=""
98
+ cell = ws.cell(ri,ci,v)
99
+ cell.font = Font(name="Arial",size=10)
100
+ cell.border = thin()
101
+ cell.fill = PatternFill("solid",start_color="FFFFFF" if ri%2==0 else "F8FAFC")
102
+ cell.alignment = Alignment(horizontal="center" if h in CENTER else "left",vertical="center")
103
+ buf = io.BytesIO(); wb.save(buf); return buf.getvalue()
104
+
105
+ # ── UI ──────────────────────────────────────────────────
106
+ st.title("πŸ“‹ 사업μž₯ μ£Όμ†ŒΒ·μ „ν™”λ²ˆν˜Έ 보완")
107
+ st.caption("λŒ€μ‹œλ³΄λ“œμ—μ„œ λ‹€μš΄λ‘œλ“œν•œ μ§€μ—­ νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”")
108
+ st.divider()
109
+
110
+ uploaded = st.file_uploader("파일 선택 (κ΅­λ―Όμ—°κΈˆ_XXμ‹œ_XXꡬ_사업μž₯.xlsx)", type=["xlsx"])
111
+
112
+ if uploaded:
113
+ # ν•„μš”ν•œ 컬럼만 읽기 (λ©”λͺ¨λ¦¬ μ ˆμ•½)
114
+ df = pd.read_excel(uploaded)
115
+ df.columns = [c.strip() for c in df.columns]
116
+ gc.collect() # λ©”λͺ¨λ¦¬ 정리
117
+
118
+ name_col = next((c for c in df.columns if "사업μž₯λͺ…" in c or "μƒν˜Έ" in c), None)
119
+ addr_col = next((c for c in df.columns if "μ£Όμ†Œ" in c or "λ„λ‘œλͺ…" in c), None)
120
+ biz_col = next((c for c in df.columns if "μ‚¬μ—…μž" in c and "번호" in c), None)
121
+ type_col = next((c for c in df.columns if "ν˜•νƒœ" in c or "법인" in c), None)
122
+ emp_col = next((c for c in df.columns if "κ°€μž…μž" in c), None)
123
+ pay_col = next((c for c in df.columns if "κ³ μ§€" in c), None)
124
+ ind_col = next((c for c in df.columns if "μ—…μ’…" in c), None)
125
+
126
+ for col in ["μ „ν™”λ²ˆν˜Έ"]:
127
+ if col not in df.columns: df[col] = ""
128
+ else: df[col] = df[col].fillna("").astype(str).replace("nan","")
129
+ for col in ["μœ„λ„","경도"]:
130
+ if col not in df.columns: df[col] = None
131
+ else: df[col] = pd.to_numeric(df[col], errors="coerce")
132
+
133
+ total = len(df)
134
+ fname = uploaded.name.replace(".xlsx","")
135
+ corp = int((df[type_col].astype(str).str.contains("법인|^1$")).sum()) if type_col else 0
136
+
137
+ st.success(f"βœ… **{fname}** β€” {total:,}개 사업μž₯ (법인 {corp:,}개)")
138
+
139
+
140
+ CHUNK = 300
141
+
142
+ # μ„Έμ…˜ μ΄ˆκΈ°ν™” (파일 λ°”λ€Œλ©΄ 리셋)
143
+ if "df_work" not in st.session_state or st.session_state.get("work_fname") != fname:
144
+ st.session_state["df_work"] = df.copy()
145
+ st.session_state["work_fname"] = fname
146
+ st.session_state["work_done"] = 0
147
+ st.session_state["phone_ok"] = 0
148
+ st.session_state["coord_ok"] = 0
149
+ st.session_state["finished"] = False
150
+
151
+ wdf = st.session_state["df_work"]
152
+ done_cnt = st.session_state["work_done"]
153
+ finished = st.session_state["finished"]
154
+
155
+ # μ§„ν–‰λ°”
156
+ pct = done_cnt / total if total > 0 else 0
157
+ st.progress(pct, text=f"{done_cnt:,}/{total:,}개 μ™„λ£Œ | πŸ“ž{st.session_state['phone_ok']} πŸ“{st.session_state['coord_ok']}")
158
+
159
+ def get_out(wdf):
160
+ col_map = {}
161
+ if name_col: col_map[name_col]="사업μž₯λͺ…"
162
+ if type_col: col_map[type_col]="법인여뢀"
163
+ if addr_col: col_map[addr_col]="μ£Όμ†Œ"
164
+ col_map["μ „ν™”λ²ˆν˜Έ"]="μ „ν™”λ²ˆν˜Έ"
165
+ if emp_col: col_map[emp_col]="κ°€μž…μžμˆ˜"
166
+ if pay_col: col_map[pay_col]="λ‹Ήμ›”κ³ μ§€κΈˆμ•‘"
167
+ if ind_col: col_map[ind_col]="μ—…μ’…"
168
+ col_map["μœ„λ„"]="μœ„λ„"; col_map["경도"]="경도"
169
+ out = wdf[[c for c in col_map if c in wdf.columns]].rename(columns=col_map)
170
+ return out[out["μ „ν™”λ²ˆν˜Έ"].notna() & (out["μ „ν™”λ²ˆν˜Έ"]!="") &
171
+ out["μ£Όμ†Œ"].notna() & (out["μ£Όμ†Œ"]!="")].reset_index(drop=True)
172
+
173
+ if finished:
174
+ out = get_out(wdf)
175
+ st.success(f"βœ… μ™„λ£Œ! πŸ“ž {st.session_state['phone_ok']:,}개 | πŸ“ {st.session_state['coord_ok']:,}개 | μ΅œμ’… {len(out):,}개")
176
+ st.balloons()
177
+ st.download_button("⬇️ 보완 μ™„λ£Œ μ—‘μ…€ λ‹€μš΄λ‘œλ“œ",
178
+ make_excel(out), fname+"_λ³΄μ™„μ™„λ£Œ.xlsx",
179
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
180
+ use_container_width=True)
181
+
182
+ elif done_cnt >= total:
183
+ st.session_state["finished"] = True
184
+ st.rerun()
185
+
186
+ else:
187
+ btn_label = "πŸš€ 보완 μ‹œμž‘" if done_cnt == 0 else f"▢️ 계속 ({done_cnt:,}/{total:,})"
188
+ if st.button(btn_label, type="primary", use_container_width=True):
189
+ chunk_idx = list(wdf.index[done_cnt:done_cnt+CHUNK])
190
+ tasks = []
191
+ for idx in chunk_idx:
192
+ row = wdf.loc[idx]
193
+ tasks.append((idx,
194
+ str(row.get(name_col,"")) if name_col else "",
195
+ str(row.get(addr_col,"")) if addr_col else "",
196
+ str(row.get(biz_col,"")) if biz_col else "",
197
+ str(row.get(type_col,"")) in ["법인","1"] if type_col else False))
198
+
199
+ prog = st.progress(0, text=f"처리 쀑... (0/{len(tasks)})")
200
+ completed = 0
201
+
202
+ with ThreadPoolExecutor(max_workers=WORKERS) as executor:
203
+ futures = {executor.submit(process_one, t): t[0] for t in tasks}
204
+ for future in as_completed(futures):
205
+ try:
206
+ idx, res = future.result()
207
+ wdf.loc[idx,"μ „ν™”λ²ˆν˜Έ"] = str(res["phone"])
208
+ wdf.loc[idx,"μœ„λ„"] = res["lat"]
209
+ wdf.loc[idx,"경도"] = res["lon"]
210
+ if addr_col and res["addr"]: wdf.loc[idx,addr_col] = res["addr"]
211
+ if res["phone"]: st.session_state["phone_ok"] += 1
212
+ if res["lat"]: st.session_state["coord_ok"] += 1
213
+ except: pass
214
+ completed += 1
215
+ prog.progress(completed/len(tasks), text=f"처리 쀑... ({completed}/{len(tasks)})")
216
+
217
+ st.session_state["df_work"] = wdf
218
+ st.session_state["work_done"] = done_cnt + len(chunk_idx)
219
+ prog.empty()
220
+ st.rerun()
221
+
222
+ if done_cnt > 0:
223
+ st.caption(f"μƒˆλ‘œκ³ μΉ¨ λ˜μ–΄λ„ {done_cnt:,}개 μ €μž₯됨. λ²„νŠΌ 눌러 κ³„μ†ν•˜μ„Έμš”.")