Download export_utils.py from ssboost/fg54wre: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/spaces/ssboost/fg54wre/resolve/main/export_utils.py
- Command line
-
hf download hf://spaces/ssboost/fg54wre/export_utils.py
-
curl -L -o export_utils.py https://huggingface.co/spaces/ssboost/fg54wre/resolve/main/export_utils.py
16 kB
| """ | |
| ๊ฒฐ๊ณผ ์ถ๋ ฅ ๊ด๋ จ ์ ํธ๋ฆฌํฐ ํจ์ ๋ชจ์ - ์นดํ ๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ฑฐ + ํค์๋ ํด๋ฆญ ์ ๋ค์ด๋ฒ ์ผํ ์ด๋ ๊ธฐ๋ฅ ์ถ๊ฐ | |
| - HTML ํ ์ด๋ธ ์์ฑ | |
| - ์์ ํ์ผ ์์ฑ | |
| - ํค์๋ ํด๋ฆญ ์ ๋ค์ด๋ฒ ์ผํ ๋งํฌ ๊ธฐ๋ฅ | |
| """ | |
| import pandas as pd | |
| import tempfile | |
| import os | |
| import threading | |
| import time | |
| import logging | |
| import urllib.parse # URL ์ธ์ฝ๋ฉ์ ์ํด ์ถ๊ฐ | |
| # ๋ก๊น ์ค์ | |
| logger = logging.getLogger(__name__) | |
| logger.setLevel(logging.INFO) | |
| formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') | |
| handler = logging.StreamHandler() | |
| handler.setFormatter(formatter) | |
| logger.addHandler(handler) | |
| # ์์ ํ์ผ ์ถ์ ๋ฆฌ์คํธ | |
| _temp_files = [] | |
| def create_table_without_checkboxes(df): | |
| """DataFrame์ HTML ํ ์ด๋ธ๋ก ๋ณํ - ํค์๋ ํด๋ฆญ ์ ๋ค์ด๋ฒ ์ผํ ์ด๋ ๊ธฐ๋ฅ ์ถ๊ฐ""" | |
| if df.empty: | |
| return "<p>๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.</p>" | |
| # === ์์ ๋ ๋ถ๋ถ: ์นดํ ๊ณ ๋ฆฌ ๊ด๋ จ ์ด ์ ๊ฑฐ === | |
| df_display = df.copy() | |
| # "์ํ ๋ฑ๋ก ์นดํ ๊ณ ๋ฆฌ(์์100์)" ๋๋ "๊ด๋ จ ์นดํ ๊ณ ๋ฆฌ", "์นดํ ๊ณ ๋ฆฌ ํญ๋ชฉ" ์ด์ด ์์ผ๋ฉด ์ ๊ฑฐ | |
| columns_to_remove = ["์ํ ๋ฑ๋ก ์นดํ ๊ณ ๋ฆฌ(์์100์)", "๊ด๋ จ ์นดํ ๊ณ ๋ฆฌ", "์นดํ ๊ณ ๋ฆฌ ํญ๋ชฉ"] | |
| for col in columns_to_remove: | |
| if col in df_display.columns: | |
| df_display = df_display.drop(columns=[col]) | |
| logger.info(f"ํ ์ด๋ธ์์ '{col}' ์ด ์ ๊ฑฐ๋จ") | |
| # HTML ํ ์ด๋ธ ์คํ์ผ ์ ์ - Z-INDEX ์์ | |
| html = ''' | |
| <style> | |
| .table-container { | |
| position: relative; | |
| width: 100%; | |
| margin: 0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); | |
| } | |
| .header-wrap { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; /* z-index ์ฆ๊ฐ */ | |
| background-color: #009879; | |
| } | |
| .styled-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| table-layout: fixed; | |
| margin: 0; | |
| padding: 0; | |
| font-size: 14px; | |
| } | |
| .styled-table th, | |
| .styled-table td { | |
| padding: 12px 15px; | |
| text-align: left; | |
| border-bottom: 1px solid #dddddd; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| /* ๊ธด ํ ์คํธ๊ฐ ์ ์์ ์ค๋ฐ๊ฟ๋๋๋ก ์์ */ | |
| .styled-table td.col-rank { | |
| white-space: normal; | |
| word-break: break-word; | |
| line-height: 1.3; | |
| } | |
| /* ๊ทธ ์ธ ์ด์ ํ ์ค๋ก ํ์ */ | |
| .styled-table td.col-seq, | |
| .styled-table td.col-keyword, | |
| .styled-table td.col-pc, | |
| .styled-table td.col-mobile, | |
| .styled-table td.col-total, | |
| .styled-table td.col-range, | |
| .styled-table td.col-count { | |
| white-space: nowrap; | |
| } | |
| .styled-table th { | |
| background-color: #009879; | |
| color: white; | |
| font-weight: bold; | |
| position: sticky; | |
| top: 0; | |
| white-space: nowrap; | |
| z-index: 50; /* ํค๋ z-index ์ฆ๊ฐ */ | |
| } | |
| .styled-table tbody tr:nth-of-type(even) { | |
| background-color: #f3f3f3; | |
| } | |
| .styled-table tbody tr:hover { | |
| background-color: #f0f0f0; | |
| } | |
| .styled-table tbody tr:last-of-type { | |
| border-bottom: 2px solid #009879; | |
| } | |
| /* ๋ฐ์ดํฐ ์ z-index ์ค์ */ | |
| .styled-table tbody td { | |
| position: relative; | |
| z-index: 1; /* ๋ฐ์ดํฐ ์ ์ ๋ฎ์ z-index */ | |
| } | |
| .data-container { | |
| max-height: 600px; | |
| overflow-y: auto; | |
| position: relative; /* position ์ถ๊ฐ */ | |
| } | |
| /* ์คํฌ๋กค๋ฐ ์คํ์ผ */ | |
| .data-container::-webkit-scrollbar { | |
| width: 10px; | |
| } | |
| .data-container::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 5px; | |
| } | |
| .data-container::-webkit-scrollbar-thumb { | |
| background: #888; | |
| border-radius: 5px; | |
| } | |
| .data-container::-webkit-scrollbar-thumb:hover { | |
| background: #555; | |
| } | |
| /* ํค์๋ ๋งํฌ ์คํ์ผ - ์๋ก ์ถ๊ฐ */ | |
| .keyword-link { | |
| color: #2c5aa0; | |
| text-decoration: none; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: inline-block; | |
| padding: 2px 4px; | |
| border-radius: 3px; | |
| position: relative; | |
| z-index: 5; /* ๋งํฌ z-index ์ค์ */ | |
| } | |
| .keyword-link:hover { | |
| color: #ffffff; | |
| background-color: #2c5aa0; | |
| text-decoration: none; | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 4px rgba(44, 90, 160, 0.3); | |
| } | |
| .keyword-link:active { | |
| transform: translateY(0px); | |
| } | |
| /* ํค์๋ ์ ํน๋ณ ์คํ์ผ */ | |
| .col-keyword { | |
| position: relative; | |
| } | |
| .keyword-tooltip { | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: #333; | |
| color: white; | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s ease; | |
| z-index: 1000; /* ํดํ์ ๊ฐ์ฅ ๋์ z-index */ | |
| pointer-events: none; | |
| margin-bottom: 5px; | |
| } | |
| .keyword-tooltip::after { | |
| content: ''; | |
| position: absolute; | |
| top: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| border: 4px solid transparent; | |
| border-top-color: #333; | |
| } | |
| .keyword-link:hover .keyword-tooltip { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| /* === ์์ ๋ ๋ถ๋ถ: ์ด ๋๋น ์ ์ - ์นดํ ๊ณ ๋ฆฌ ์ด ์ ๊ฑฐ ํ ์กฐ์ === */ | |
| .col-seq { width: 8%; } | |
| .col-keyword { width: 25%; } | |
| .col-pc { width: 12%; } | |
| .col-mobile { width: 12%; } | |
| .col-total { width: 12%; } | |
| .col-range { width: 12%; } | |
| .col-rank { width: 15%; } | |
| .col-count { width: 10%; } | |
| .truncated-text { | |
| position: relative; | |
| cursor: pointer; | |
| z-index: 2; /* ํ ์คํธ z-index ์ค์ */ | |
| } | |
| .truncated-text:hover::after { | |
| content: attr(data-full-text); | |
| position: absolute; | |
| left: 0; | |
| top: 100%; | |
| z-index: 99; | |
| min-width: 200px; | |
| max-width: 400px; | |
| padding: 8px; | |
| background-color: #fff; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| white-space: normal; | |
| } | |
| /* ํค์๋ ํ๊ทธ ์คํ์ผ */ | |
| .keyword-tag-container { | |
| margin-top: 20px; | |
| padding: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 5px; | |
| background-color: #f9f9f9; | |
| } | |
| .keyword-tag { | |
| display: inline-block; | |
| background-color: #009879; | |
| color: white; | |
| padding: 5px 10px; | |
| margin: 5px; | |
| border-radius: 15px; | |
| font-size: 12px; | |
| } | |
| .category-tag { | |
| display: inline-block; | |
| background-color: #2c7fb8; | |
| color: white; | |
| padding: 5px 10px; | |
| margin: 5px; | |
| border-radius: 15px; | |
| font-size: 12px; | |
| } | |
| /* ๋ถ์ ๊ฒฐ๊ณผ ํ ์ด๋ธ ์คํ์ผ */ | |
| .analysis-result { | |
| margin-top: 30px; | |
| border: 1px solid #ddd; | |
| border-radius: 5px; | |
| padding: 15px; | |
| background-color: #f9f9f9; | |
| } | |
| .result-header { | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| color: #009879; | |
| } | |
| .match-item { | |
| margin: 5px 0; | |
| padding: 5px; | |
| border-bottom: 1px solid #eee; | |
| } | |
| .match-keyword { | |
| font-weight: bold; | |
| color: #2c7fb8; | |
| } | |
| .match-count { | |
| display: inline-block; | |
| background-color: #009879; | |
| color: white; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| font-size: 12px; | |
| margin-left: 10px; | |
| } | |
| </style> | |
| ''' | |
| # === ์์ ๋ ๋ถ๋ถ: ์ด ์ด๋ฆ๊ณผ ํด๋์ค ๋งคํ - ์นดํ ๊ณ ๋ฆฌ ๊ด๋ จ ์ ๊ฑฐ === | |
| col_mapping = { | |
| "์๋ฒ": "col-seq", | |
| "์กฐํฉ ํค์๋": "col-keyword", | |
| "์ฐ๊ด ํค์๋": "col-keyword", # ์ฐ๊ด๊ฒ์์ด ๋ถ์์ฉ ์ถ๊ฐ | |
| "ํค์๋": "col-keyword", # ์ผ๋ฐ ํค์๋์ฉ ์ถ๊ฐ | |
| "PC๊ฒ์๋": "col-pc", | |
| "๋ชจ๋ฐ์ผ๊ฒ์๋": "col-mobile", | |
| "์ด๊ฒ์๋": "col-total", | |
| "๊ฒ์๋๊ตฌ๊ฐ": "col-range", | |
| "ํค์๋ ์ฌ์ฉ์์์": "col-rank", | |
| "ํค์๋ ์ฌ์ฉํ์": "col-count" | |
| # ์นดํ ๊ณ ๋ฆฌ ๊ด๋ จ ๋งคํ ์ ๊ฑฐ๋จ | |
| } | |
| # ๋ค์ด๋ฒ ์ผํ ๋งํฌ ์์ฑ ํจ์ | |
| def create_naver_shopping_link(keyword): | |
| """ํค์๋๋ฅผ ๋ค์ด๋ฒ ์ผํ ๋งํฌ๋ก ๋ณํ""" | |
| # URL ์ธ์ฝ๋ฉ (ํ๊ธ ํค์๋ ์ฒ๋ฆฌ) | |
| encoded_keyword = urllib.parse.quote(keyword.strip()) | |
| naver_shopping_url = f"https://search.shopping.naver.com/search/all?where=all&frm=NVSCTAB&query={encoded_keyword}" | |
| # ๋งํฌ๊ฐ ํฌํจ๋ HTML ๋ฐํ | |
| return f'''<a href="{naver_shopping_url}" target="_blank" class="keyword-link" title="๋ค์ด๋ฒ ์ผํ์์ '{keyword}' ๊ฒ์ํ๊ธฐ"> | |
| {keyword} | |
| <span class="keyword-tooltip">ํด๋ฆญํ๋ฉด ๋ค์ด๋ฒ ์ผํ์ผ๋ก ์ด๋</span> | |
| </a>''' | |
| # ํ ์ด๋ธ ์ปจํ ์ด๋ ์์ | |
| html += '<div class="table-container">' | |
| # ๋จ์ผ ํ ์ด๋ธ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝ (ํค๋๋ position: sticky๋ก ๊ณ ์ ) | |
| html += '<div class="data-container">' | |
| html += '<table class="styled-table">' | |
| # colgroup์ผ๋ก ์ด ๋๋น ์ ์ | |
| html += '<colgroup>' | |
| html += f'<col class="{col_mapping["์๋ฒ"]}">' | |
| for col in df_display.columns: | |
| col_class = col_mapping.get(col, "") | |
| html += f'<col class="{col_class}">' | |
| html += '</colgroup>' | |
| # ํ ์ด๋ธ ํค๋ | |
| html += '<thead>' | |
| html += '<tr>' | |
| html += f'<th class="{col_mapping["์๋ฒ"]}">์๋ฒ</th>' | |
| for col in df_display.columns: | |
| col_class = col_mapping.get(col, "") | |
| html += f'<th class="{col_class}">{col}</th>' | |
| html += '</tr>' | |
| html += '</thead>' | |
| # ํ ์ด๋ธ ๋ณธ๋ฌธ | |
| html += '<tbody>' | |
| for idx, row in df_display.iterrows(): | |
| html += '<tr>' | |
| # ์๋ฒ ํ์ - 1๋ถํฐ ์์ํ๋ ์์ฐจ์ ๋ฒํธ | |
| html += f'<td class="{col_mapping["์๋ฒ"]}">{idx + 1}</td>' | |
| # ๋ฐ์ดํฐ ์ ์ถ๊ฐ | |
| for col in df_display.columns: | |
| col_class = col_mapping.get(col, "") | |
| value = str(row[col]) | |
| # === ์๋ก ์ถ๊ฐ: ํค์๋ ์ด์ ๋งํฌ ์ ์ฉ === | |
| if col in ["์กฐํฉ ํค์๋", "์ฐ๊ด ํค์๋", "ํค์๋"]: | |
| # ํค์๋ ์ ์ ๋ค์ด๋ฒ ์ผํ ๋งํฌ ์ ์ฉ | |
| keyword_with_link = create_naver_shopping_link(value) | |
| html += f'<td class="{col_class}">{keyword_with_link}</td>' | |
| elif col == "ํค์๋ ์ฌ์ฉ์์์": | |
| # ๊ธด ํ ์คํธ์ ์ ์ ๊ทธ๋๋ก ํ์ (์ค๋ฐ๊ฟ ํ์ฉ) | |
| html += f'<td class="{col_class}">{value}</td>' | |
| elif len(value) > 30: | |
| # ๋ค๋ฅธ ๊ธด ํ ์คํธ๋ hover๋ก ์ ์ฒด ํ์ | |
| html += f'<td class="{col_class}"><div class="truncated-text" data-full-text="{value}">{value[:30]}...</div></td>' | |
| else: | |
| # ์ผ๋ฐ ํ ์คํธ | |
| html += f'<td class="{col_class}">{value}</td>' | |
| html += '</tr>' | |
| html += '</tbody>' | |
| html += '</table>' | |
| html += '</div>' # data-container ๋ซ๊ธฐ | |
| html += '</div>' # table-container ๋ซ๊ธฐ | |
| # ์ฌ์ฉ๋ฒ ์๋ด ์ถ๊ฐ | |
| html += ''' | |
| <div style="margin-top: 15px; padding: 12px; background: #e8f5e8; border-radius: 8px; border-left: 4px solid #009879;"> | |
| <div style="font-weight: bold; color: #155724; margin-bottom: 5px;">๐ก ์ฌ์ฉํ</div> | |
| <div style="font-size: 14px; color: #155724;"> | |
| ํค์๋๋ฅผ ํด๋ฆญํ๋ฉด ๋ค์ด๋ฒ ์ผํ์์ ํด๋น ํค์๋๋ก ๊ฒ์ํ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฐฝ์์ ํ์ธํ ์ ์์ต๋๋ค. | |
| </div> | |
| </div> | |
| ''' | |
| return html | |
| def cleanup_temp_files(delay=300): | |
| """์์ ํ์ผ ์ ๋ฆฌ ํจ์""" | |
| global _temp_files | |
| def cleanup(): | |
| time.sleep(delay) # ์ง์ ๋ ์๊ฐ ๋๊ธฐ | |
| temp_files_to_remove = _temp_files.copy() | |
| _temp_files = [] | |
| for file_path in temp_files_to_remove: | |
| try: | |
| if os.path.exists(file_path): | |
| os.remove(file_path) | |
| logger.info(f"์์ ํ์ผ ์ญ์ : {file_path}") | |
| except Exception as e: | |
| logger.error(f"ํ์ผ ์ญ์ ์ค๋ฅ: {e}") | |
| # ์ ์ค๋ ๋ ์์ | |
| threading.Thread(target=cleanup, daemon=True).start() | |
| def download_keywords(df, auto_cleanup=True, cleanup_delay=300): | |
| """ํค์๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ์ผ๋ก ๋ค์ด๋ก๋ - ์นดํ ๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ฑฐ""" | |
| global _temp_files | |
| if df is None or df.empty: | |
| return None | |
| # ์์ ํ์ผ๋ก ์ ์ฅ | |
| temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') | |
| temp_file.close() | |
| filename = temp_file.name | |
| # ์์ ํ์ผ ์ถ์ ๋ชฉ๋ก์ ์ถ๊ฐ | |
| _temp_files.append(filename) | |
| # === ์์ ๋ ๋ถ๋ถ: ์นดํ ๊ณ ๋ฆฌ ๊ด๋ จ ์ด ์ ๊ฑฐ === | |
| df_export = df.copy() | |
| # ์นดํ ๊ณ ๋ฆฌ ๊ด๋ จ ์ด๋ค ์ ๊ฑฐ | |
| columns_to_remove = ["์ํ ๋ฑ๋ก ์นดํ ๊ณ ๋ฆฌ(์์100์)", "๊ด๋ จ ์นดํ ๊ณ ๋ฆฌ", "์นดํ ๊ณ ๋ฆฌ ํญ๋ชฉ"] | |
| for col in columns_to_remove: | |
| if col in df_export.columns: | |
| df_export = df_export.drop(columns=[col]) | |
| logger.info(f"์์ ๋ด๋ณด๋ด๊ธฐ์์ '{col}' ์ด ์ ๊ฑฐ๋จ") | |
| # ํค์๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ์ผ๋ก ์ ์ฅ | |
| with pd.ExcelWriter(filename, engine='xlsxwriter') as writer: | |
| # ํค์๋ ๋ชฉ๋ก ์ํธ | |
| df_export.to_excel(writer, sheet_name='ํค์๋ ๋ชฉ๋ก', index=False) | |
| # ์ด ๋๋น ์กฐ์ - ์นดํ ๊ณ ๋ฆฌ ์ด ์ ๊ฑฐ ํ ์กฐ์ | |
| worksheet = writer.sheets['ํค์๋ ๋ชฉ๋ก'] | |
| worksheet.set_column('A:A', 20) # ์กฐํฉ ํค์๋ ์ด | |
| worksheet.set_column('B:B', 12) # PC๊ฒ์๋ ์ด | |
| worksheet.set_column('C:C', 12) # ๋ชจ๋ฐ์ผ๊ฒ์๋ ์ด | |
| worksheet.set_column('D:D', 12) # ์ด๊ฒ์๋ ์ด | |
| worksheet.set_column('E:E', 12) # ๊ฒ์๋๊ตฌ๊ฐ ์ด | |
| worksheet.set_column('F:F', 20) # ํค์๋ ์ฌ์ฉ์์์ ์ด | |
| worksheet.set_column('G:G', 12) # ํค์๋ ์ฌ์ฉํ์ ์ด | |
| # ์นดํ ๊ณ ๋ฆฌ ์ด๋ค ์ ๊ฑฐ๋ก H, I ์ด ์ค์ ์ ๊ฑฐ๋จ | |
| # ํค๋ ํ์ ์ค์ | |
| header_format = writer.book.add_format({ | |
| 'bold': True, | |
| 'bg_color': '#009879', | |
| 'color': 'white', | |
| 'border': 1 | |
| }) | |
| # ํค๋์ ํ์ ์ ์ฉ | |
| for col_num, value in enumerate(df_export.columns.values): | |
| worksheet.write(0, col_num, value, header_format) | |
| logger.info(f"์์ ํ์ผ ์์ฑ: {filename}") | |
| # ํ์ผ ์๋ ์ ๋ฆฌ ์ต์ | |
| if auto_cleanup: | |
| # ๋ณ๋ ์ ๋ฆฌ ์์ ์์ฒญ ์์ด ์ถ์ ๋ชฉ๋ก์ ์ถ๊ฐ๋ง ํ์ฌ ์ผ๊ด ์ฒ๋ฆฌ | |
| pass | |
| return filename | |
| def register_cleanup_handlers(): | |
| """์ฑ ์ข ๋ฃ ์ ์ ๋ฆฌ๋ฅผ ์ํ ํธ๋ค๋ฌ ๋ฑ๋ก""" | |
| import atexit | |
| def cleanup_all_temp_files(): | |
| global _temp_files | |
| for file_path in _temp_files: | |
| try: | |
| if os.path.exists(file_path): | |
| os.remove(file_path) | |
| logger.info(f"์ข ๋ฃ ์ ์์ ํ์ผ ์ญ์ : {file_path}") | |
| except Exception as e: | |
| logger.error(f"ํ์ผ ์ญ์ ์ค๋ฅ: {e}") | |
| _temp_files = [] | |
| # ์ฑ ์ข ๋ฃ ์ ์คํ๋ ํจ์ ๋ฑ๋ก | |
| atexit.register(cleanup_all_temp_files) |