| 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) |
| |
| |
| 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) |
| |
| |
| 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()) |
| |
| |
| 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()) |
| |
| |
| search_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, '//button[@data-action="C20004"]'))) |
| search_button.click() |
| time.sleep(3.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) |
|
|
| |
| 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 ''}") |
| |
| |
| 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) |
|
|
| |
| |
| 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)} 筆歌曲資料。" |
|
|
| |
| 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() |