blueellia commited on
Commit
bb27fc6
Β·
verified Β·
1 Parent(s): 57ffeec

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -0
app.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io, re, time, requests, 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.stDownloadButton>button{background:#10b981!important;color:#fff!important;border:none!important;border-radius:8px!important;font-weight:600!important;width:100%!important}
14
+ </style>""", unsafe_allow_html=True)
15
+
16
+ FSC_KEY = "2446da601452d231f1ff77a4c5ac9598896028fb96e8be586bf7f4be58b6231c"
17
+ KAKAO_KEY = "e8eef7246a2cd0f3d2d55af8a806e3ef"
18
+ 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"}
19
+ WORKERS = 20
20
+
21
+ def kakao_keyword(name, addr):
22
+ try:
23
+ r = requests.get("https://dapi.kakao.com/v2/local/search/keyword.json",
24
+ headers={"Authorization":f"KakaoAK {KAKAO_KEY}"},
25
+ params={"query":f"{name} {addr[:20]}","size":5}, timeout=5)
26
+ docs = r.json().get("documents",[])
27
+ gu = re.search(r'[\w]+ꡬ|[\w]+ꡰ', addr)
28
+ gu = gu.group() if gu else ""
29
+ if gu:
30
+ local = [d for d in docs if gu in d.get("address_name","") or gu in d.get("road_address_name","")]
31
+ if local:
32
+ return next((d for d in local if name[:4] in d.get("place_name","")), local[0])
33
+ return next((d for d in docs if name[:4] in d.get("place_name","")), docs[0] if docs else None)
34
+ except: return None
35
+
36
+ def fsc_info(name, biz_no):
37
+ try:
38
+ r = requests.get(
39
+ "https://apis.data.go.kr/1160100/service/GetCorpBasicInfoService_V2/getCorpOutline_V2",
40
+ params={"serviceKey":FSC_KEY,"pageNo":1,"numOfRows":5,"resultType":"json","corpNm":name}, timeout=5)
41
+ items = r.json().get("response",{}).get("body",{}).get("items",{}).get("item",[])
42
+ if not items: return None
43
+ if isinstance(items, dict): items = [items]
44
+ bc = re.sub(r'\D','',str(biz_no))
45
+ m = next((i for i in items if re.sub(r'\D','',str(i.get("bzno",""))) == bc), items[0])
46
+ return {"phone":m.get("enpTlno",""), "addr":m.get("enpBsadr","") or m.get("enpDtadr","")}
47
+ except: return None
48
+
49
+ def naver_phone(name, addr):
50
+ clean = re.sub(r'μ£Όμ‹νšŒμ‚¬|μœ ν•œνšŒμ‚¬|\(μ£Ό\)|\(유\)|[(){}\[\]]','',name).strip()
51
+ gu = re.search(r'[\w]+ꡬ', addr); gu = gu.group() if gu else ""
52
+ try:
53
+ r = requests.get("https://m.map.naver.com/search2/search.nhn",
54
+ headers=NAV_HDR, params={"query":f"{clean} {gu}","type":"all"}, timeout=8)
55
+ if r.status_code == 429: time.sleep(30); return naver_phone(name, addr)
56
+ 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)']:
57
+ f = re.findall(pat, r.text)
58
+ if f: return f[0]
59
+ except: pass
60
+ return ""
61
+
62
+ def process_one(row_data):
63
+ idx, name, addr, biz_no, is_corp = row_data
64
+ phone=""; full_addr=addr; lat=None; lon=None
65
+ if is_corp:
66
+ info = fsc_info(name, biz_no)
67
+ if info:
68
+ phone = info.get("phone","")
69
+ if info.get("addr"): full_addr = info["addr"]
70
+ time.sleep(0.1)
71
+ kk = kakao_keyword(name, addr)
72
+ if kk:
73
+ if not phone: phone = kk.get("phone","")
74
+ lat = float(kk.get("y") or 0) or None
75
+ lon = float(kk.get("x") or 0) or None
76
+ road = kk.get("road_address_name","")
77
+ if road: full_addr = road
78
+ if not phone: phone = naver_phone(name, full_addr)
79
+ return idx, {"phone":phone, "addr":full_addr, "lat":lat, "lon":lon}
80
+
81
+ def make_excel(df):
82
+ wb = Workbook(); ws = wb.active; ws.title = "λ³΄μ™„μ™„λ£Œ"
83
+ def thin():
84
+ s = Side(style="thin",color="D0D0D0")
85
+ return Border(left=s,right=s,top=s,bottom=s)
86
+ hdrs = list(df.columns)
87
+ widths = {"사업μž₯λͺ…":26,"μ—…μ’…":18,"법인여뢀":8,"μ£Όμ†Œ":42,"μ „ν™”λ²ˆν˜Έ":14,"κ°€μž…μžμˆ˜":8,"λ‹Ήμ›”κ³ μ§€κΈˆμ•‘":14,"μœ„λ„":12,"경도":12}
88
+ CENTER = {"법인여뢀","κ°€μž…μžμˆ˜","λ‹Ήμ›”κ³ μ§€κΈˆμ•‘","μœ„λ„","경도","μ „ν™”λ²ˆν˜Έ"}
89
+ for c,h in enumerate(hdrs,1):
90
+ cell = ws.cell(1,c,h)
91
+ cell.fill = PatternFill("solid",start_color="1F3864")
92
+ cell.font = Font(name="Arial",size=10,bold=True,color="FFFFFF")
93
+ cell.alignment = Alignment(horizontal="center",vertical="center")
94
+ cell.border = thin()
95
+ ws.column_dimensions[get_column_letter(c)].width = widths.get(h,12)
96
+ ws.row_dimensions[1].height = 18
97
+ ws.freeze_panes = "A2"
98
+ ws.auto_filter.ref = f"A1:{get_column_letter(len(hdrs))}1"
99
+ for ri,row in enumerate(df.itertuples(index=False),2):
100
+ for ci,(h,v) in enumerate(zip(hdrs,row),1):
101
+ if pd.isna(v) or str(v) in ("nan","None"): v=""
102
+ cell = ws.cell(ri,ci,v)
103
+ cell.font = Font(name="Arial",size=10)
104
+ cell.border = thin()
105
+ cell.fill = PatternFill("solid",start_color="FFFFFF" if ri%2==0 else "F8FAFC")
106
+ cell.alignment = Alignment(horizontal="center" if h in CENTER else "left",vertical="center")
107
+ buf = io.BytesIO(); wb.save(buf); return buf.getvalue()
108
+
109
+ # ── UI ──────────────────────────────────────────────────
110
+ st.title("πŸ“‹ 사업μž₯ μ£Όμ†ŒΒ·μ „ν™”λ²ˆν˜Έ 보완")
111
+ st.caption("μ§€μ—­ 파일 μ—…λ‘œλ“œ β†’ μžλ™ 처리 β†’ λ‹€μš΄λ‘œλ“œ")
112
+ st.divider()
113
+
114
+ uploaded = st.file_uploader("파일 선택 (κ΅­λ―Όμ—°κΈˆ_XXμ‹œ_XXꡬ_사업μž₯.xlsx)", type=["xlsx"])
115
+
116
+ if uploaded:
117
+ df = pd.read_excel(uploaded)
118
+ df.columns = [c.strip() for c in df.columns]
119
+ gc.collect()
120
+
121
+ name_col = next((c for c in df.columns if "사업μž₯λͺ…" in c or "μƒν˜Έ" in c), None)
122
+ addr_col = next((c for c in df.columns if "μ£Όμ†Œ" in c or "λ„λ‘œλͺ…" in c), None)
123
+ biz_col = next((c for c in df.columns if "μ‚¬μ—…μž" in c and "번호" in c), None)
124
+ type_col = next((c for c in df.columns if "ν˜•νƒœ" in c or "법인" in c), None)
125
+ emp_col = next((c for c in df.columns if "κ°€μž…μž" in c), None)
126
+ pay_col = next((c for c in df.columns if "κ³ μ§€" in c), None)
127
+ ind_col = next((c for c in df.columns if "μ—…μ’…" in c), None)
128
+
129
+ for col in ["μ „ν™”λ²ˆν˜Έ"]:
130
+ if col not in df.columns: df[col] = ""
131
+ else: df[col] = df[col].fillna("").astype(str).replace("nan","")
132
+ for col in ["μœ„λ„","경도"]:
133
+ if col not in df.columns: df[col] = None
134
+ else: df[col] = pd.to_numeric(df[col], errors="coerce")
135
+
136
+ total = len(df)
137
+ fname = uploaded.name.replace(".xlsx","")
138
+ st.success(f"βœ… {fname} β€” {total:,}개 λ‘œλ“œ μ™„λ£Œ")
139
+
140
+ prog = st.progress(0, text="처리 쀑...")
141
+ phone_ok = coord_ok = completed = 0
142
+
143
+ tasks = []
144
+ for idx in df.index:
145
+ row = df.loc[idx]
146
+ tasks.append((idx,
147
+ str(row.get(name_col,"")) if name_col else "",
148
+ str(row.get(addr_col,"")) if addr_col else "",
149
+ str(row.get(biz_col,"")) if biz_col else "",
150
+ str(row.get(type_col,"")) in ["법인","1"] if type_col else False))
151
+
152
+ with ThreadPoolExecutor(max_workers=WORKERS) as executor:
153
+ futures = {executor.submit(process_one, t): t[0] for t in tasks}
154
+ for future in as_completed(futures):
155
+ try:
156
+ idx, res = future.result()
157
+ df.loc[idx,"μ „ν™”λ²ˆν˜Έ"] = str(res["phone"])
158
+ df.loc[idx,"μœ„λ„"] = res["lat"]
159
+ df.loc[idx,"경도"] = res["lon"]
160
+ if addr_col and res["addr"]: df.loc[idx,addr_col] = res["addr"]
161
+ if res["phone"]: phone_ok += 1
162
+ if res["lat"]: coord_ok += 1
163
+ except: pass
164
+ completed += 1
165
+ prog.progress(completed/total, text=f"πŸ“ž{phone_ok} πŸ“{coord_ok} | {completed}/{total}")
166
+
167
+ prog.empty()
168
+
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
+
179
+ out = df[[c for c in col_map if c in df.columns]].rename(columns=col_map)
180
+ out = out[out["μ „ν™”λ²ˆν˜Έ"].notna() & (out["μ „ν™”λ²ˆν˜Έ"]!="") &
181
+ out["μ£Όμ†Œ"].notna() & (out["μ£Όμ†Œ"]!="")].reset_index(drop=True)
182
+ del df; gc.collect()
183
+
184
+ st.success(f"βœ… μ™„λ£Œ! πŸ“ž{phone_ok:,}개 | πŸ“{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)