import gradio as gr from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import Select from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup import pandas as pd import time import re import os def parse_credits(credits_str): """精準拆解國圖的作品職者字串""" c_str = re.sub(r'\s+', '', credits_str) singer_match = re.search(r'表演者:?([^作編混]*?)(?:作詞|作曲|編曲|$)', c_str) lyricist_match = re.search(r'作詞者:?([^作表編混]*?)(?:作曲|編曲|表演|$)', c_str) composer_match = re.search(r'作曲者:?([^作表編混]*?)(?:作詞|編曲|表演|$)', c_str) singer = singer_match.group(1).strip() if singer_match else "" lyricist = lyricist_match.group(1).strip() if lyricist_match else "" composer = composer_match.group(1).strip() if composer_match else "" return singer.replace(':', '').replace(':', ''), lyricist.replace(':', '').replace(':', ''), composer.replace(':', '').replace(':', '') def process_single_song(driver, song_name, artist_name): """處理單一歌曲的爬取核心邏輯""" try: url = "https://isrc-web.ncl.edu.tw/C200/C200" driver.get(url) # 1. 切換分頁 song_tab = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "profile-tab"))) song_tab.click() time.sleep(0.5) advanced_tab = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "v-pills-settings-tab"))) advanced_tab.click() time.sleep(1.2) # 2. 輸入歌名 field1_element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "songField1"))) Select(field1_element).select_by_value("NAME") if song_name: keyword1_box = driver.find_element(By.ID, "songKeyword1") keyword1_box.clear() keyword1_box.send_keys(str(song_name).strip()) # 3. 輸入歌手 field2_element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "songField2"))) Select(field2_element).select_by_value("ACTION") if artist_name and not pd.isna(artist_name): keyword2_box = driver.find_element(By.ID, "songKeyword2") keyword2_box.clear() keyword2_box.send_keys(str(artist_name).strip()) # 4. 送出與等待渲染 search_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, '//button[@data-action="C20004"]'))) search_button.click() time.sleep(3.5) # 5. 解析 soup = BeautifulSoup(driver.page_source, 'html.parser') result_table = soup.find('table', id='dataTable2') data_list = [] if result_table and result_table.find('tbody'): rows = result_table.find('tbody').find_all('tr') for row in rows: cols = row.find_all('td') if len(cols) >= 8: raw_credits = cols[4].text.strip() release_date = cols[7].text.strip() extracted_singer, lyricist, composer = parse_credits(raw_credits) data_list.append({ "主要歌手": extracted_singer, "作詞者": lyricist, "作曲者": composer, "發行日期": release_date }) if data_list: df = pd.DataFrame(data_list) df['datetime_parsed'] = pd.to_datetime(df['發行日期'], errors='coerce') df = df.sort_values(by='datetime_parsed', ascending=True).reset_index(drop=True) earliest_release_date = df.iloc[0]['發行日期'] if not df.empty else "未記載" final_lyricist, final_composer = "", "" target_artist = str(artist_name).strip() if (artist_name and not pd.isna(artist_name)) else "" df_artist_match = df[df['主要歌手'] == target_artist].reset_index(drop=True) if target_artist else pd.DataFrame() loop_df = df_artist_match if not df_artist_match.empty else df for _, row in loop_df.iterrows(): if not final_lyricist and row['作詞者']: final_lyricist = row['作詞者'] if not final_composer and row['作曲者']: final_composer = row['作曲者'] if final_lyricist and final_composer: break return final_lyricist if final_lyricist else "未記載", final_composer if final_composer else "未記載", earliest_release_date else: return "查無資料", "查無資料", "查無資料" except Exception as e: print(f"處理歌曲 {song_name} 時發生問題: {str(e)}") return "錯誤", "錯誤", "錯誤" def create_driver(): """建立並初始化 Chrome 瀏覽器的捷徑函式""" chrome_options = Options() chrome_options.add_argument('--headless') chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') return webdriver.Chrome(options=chrome_options) # 🎯 關鍵改動:加入 progress=gr.Progress() 參數來啟用進度條 def batch_process_file(file_obj, progress=gr.Progress()): if file_obj is None: return None, "請先上傳檔案!" file_path = file_obj.name if file_path.endswith('.csv'): input_df = pd.read_csv(file_path, header=None) else: input_df = pd.read_excel(file_path, header=None, engine='openpyxl') if input_df.shape[1] < 2: return None, "錯誤:表格格式不正確,至少需要有兩欄資料(左邊歌名、右邊歌手)!" song_col_idx = 0 artist_col_idx = 1 # 初始化第一個瀏覽器 driver = create_driver() lyricists = [] composers = [] release_dates = [] total_songs = len(input_df) # 🎯 核心進度條啟動:設定總步數 progress(0, desc="🚀 正在準備啟動自動化系統...") for index, row in input_df.iterrows(): song = row[song_col_idx] artist = row[artist_col_idx] # 🎯 即時更新網頁上的進度條狀態與文字提示 progress((index / total_songs), desc=f"🎵 正在處理 ({index + 1}/{total_songs}): {song if not pd.isna(song) else ''}") # 🎯 記憶體防爆優化:每查 30 首歌,就關掉重開一次瀏覽器清空快取 if index > 0 and index % 30 == 0: driver.quit() driver = create_driver() if index == 0 and any(keyword in str(song) for keyword in ["歌名", "歌曲", "曲目", "Title", "Name"]): lyricists.append("作詞者") composers.append("作曲者") release_dates.append("最早發行時間") continue if pd.isna(song): lyricists.append("歌名空白") composers.append("歌名空白") release_dates.append("歌名空白") continue lyr, comp, rel = process_single_song(driver, song, artist) lyricists.append(lyr) composers.append(comp) release_dates.append(rel) # 🎯 在這裡加入「溫柔限速機制」:每查完一首,強迫機器人休息 1.5 秒 # 這樣查 100 首歌雖然會多花 150 秒,但可以確保 100% 穩當不被國圖封鎖! time.sleep(1.5) driver.quit() progress(0.95, desc="💾 正在將清洗後的數據打包成 Excel 檔案...") # 重新結合成全新表格 output_df = pd.DataFrame() output_df['歌曲'] = input_df[song_col_idx] output_df['歌手'] = input_df[artist_col_idx] output_df['作詞者'] = lyricists output_df['作曲者'] = composers output_df['最早發行時間'] = release_dates if any(keyword in str(output_df.iloc[0, 0]) for keyword in ["歌名", "歌曲", "曲目", "Title"]): output_df.columns = ["歌曲", "歌手", "作詞者", "作曲者", "最早發行時間"] output_df = output_df.drop(output_df.index[0]) output_file_path = "isrc_batch_results.xlsx" output_df.to_excel(output_file_path, index=False, engine='openpyxl') progress(1.0, desc="🎉 大功告成!") return output_file_path, f"🎉 成功處理完成!共處理 {len(output_df)} 筆歌曲資料。" # Gradio 批量介面保持不變 with gr.Blocks(title="ISRC 歌曲資料批量洗滌器") as demo: gr.Markdown("# 🗂️ 臺灣 ISRC 歌曲詞曲資料【批量】查詢系統") gr.Markdown("請上傳一個 Excel 或 CSV 表格檔。規定:**第一欄為「歌名」,第二欄為「歌手」**(有無標題列皆相容)。") with gr.Row(): file_input = gr.File(label="請上傳 Excel (.xlsx) 或 CSV (.csv) 檔案", file_types=[".xlsx", ".csv"]) process_btn = gr.Button("開始批量轉換", variant="primary") with gr.Row(): status_output = gr.Textbox(label="處理狀態", placeholder="尚未開始...") file_output = gr.File(label="下載轉換完成的 Excel 表格") process_btn.click( fn=batch_process_file, inputs=file_input, outputs=[file_output, status_output] ) demo.launch()