blueellia commited on
Commit
aede47f
Β·
verified Β·
1 Parent(s): 8cc2f4d

Upload app.py

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