Spaces:
Sleeping
Sleeping
class SheetService:
Browse files- app.py +261 -131
- sheet_service.py +134 -77
app.py
CHANGED
@@ -2164,7 +2164,7 @@ def get_sheet_data(sheet_url, range_name):
|
|
2164 |
def update_sheet_data(sheet_url, qa_result, video_id):
|
2165 |
# 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
|
2166 |
sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
|
2167 |
-
print(f"sheet_data: {sheet_data}")
|
2168 |
|
2169 |
if not sheet_data or len(sheet_data) < 1:
|
2170 |
print("錯誤:工作表資料為空或缺少標頭列。")
|
@@ -2175,7 +2175,7 @@ def update_sheet_data(sheet_url, qa_result, video_id):
|
|
2175 |
|
2176 |
# 直接指定要尋找的欄位名稱
|
2177 |
target_column_name = '均一平台 YT Readable ID'
|
2178 |
-
target_qa_column_name = "QA"
|
2179 |
|
2180 |
try:
|
2181 |
# 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
|
@@ -2190,89 +2190,221 @@ def update_sheet_data(sheet_url, qa_result, video_id):
|
|
2190 |
for i, row in enumerate(data_rows):
|
2191 |
# 確保該列有足夠的欄位,並且該欄位的值等於 video_id
|
2192 |
if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
|
2193 |
-
target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1
|
2194 |
break # 找到後即可跳出迴圈
|
2195 |
|
2196 |
if target_row_index != -1:
|
2197 |
-
|
|
|
|
|
|
|
2198 |
|
2199 |
-
# --- 以下是更新邏輯
|
2200 |
try:
|
2201 |
-
# 找到 QA 欄位的索引
|
2202 |
qa_col_index = header_row.index(target_qa_column_name)
|
2203 |
|
2204 |
-
#
|
2205 |
-
|
2206 |
-
|
2207 |
-
|
2208 |
-
|
2209 |
-
#
|
2210 |
-
|
2211 |
-
#
|
2212 |
-
|
|
|
|
|
|
|
|
|
|
|
2213 |
|
|
|
|
|
|
|
2214 |
else:
|
2215 |
-
|
|
|
2216 |
|
2217 |
except ValueError:
|
2218 |
-
print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{
|
2219 |
# --- 更新邏輯結束 ---
|
2220 |
|
2221 |
else:
|
2222 |
# 如果找不到 video_id,印出錯誤訊息
|
2223 |
print(f"錯誤:��欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
|
2224 |
|
2225 |
-
def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column,
|
2226 |
-
|
2227 |
-
|
2228 |
-
|
2229 |
-
success_video_ids = []
|
2230 |
-
failed_video_ids = []
|
2231 |
-
|
2232 |
sheet_qa_success_tag = "OO"
|
2233 |
sheet_qa_failed_tag = "XX"
|
|
|
2234 |
|
2235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2236 |
try:
|
2237 |
-
# 確認 GCS 中是否存在 ID_transcript.json in GCS
|
2238 |
-
# 如果存在就 pass
|
2239 |
-
# 如果不存在就 refresh_video_LLM_all_content_by_id
|
2240 |
-
bucket_name = 'video_ai_assistant'
|
2241 |
-
file_name = f'{video_id}_transcript.json'
|
2242 |
-
blob_name = f"{video_id}/{file_name}"
|
2243 |
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
2244 |
-
if
|
2245 |
-
print(f"{video_id}
|
2246 |
-
|
2247 |
else:
|
2248 |
-
print(f"{video_id}
|
2249 |
-
|
2250 |
-
qa_result = sheet_qa_success_tag
|
2251 |
-
time.sleep(1)
|
2252 |
-
# 更新呼叫,不再傳遞 sheet_video_column
|
2253 |
-
update_sheet_data(sheet_url, qa_result, video_id)
|
2254 |
-
success_video_ids.append(video_id) # 假設 update_sheet_data 成功就加入 success
|
2255 |
except Exception as e:
|
2256 |
-
print(f"
|
2257 |
-
|
2258 |
-
|
2259 |
-
|
2260 |
-
|
2261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2262 |
try:
|
2263 |
-
|
2264 |
-
|
2265 |
-
|
2266 |
-
|
2267 |
-
|
2268 |
-
|
2269 |
-
|
2270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2271 |
|
|
|
|
|
|
|
2272 |
|
2273 |
result = {
|
2274 |
-
"success_video_ids":
|
2275 |
-
"failed_video_ids":
|
2276 |
}
|
2277 |
return result
|
2278 |
|
@@ -3599,81 +3731,79 @@ def create_app():
|
|
3599 |
with gr.Row():
|
3600 |
with gr.Tab("學習單"):
|
3601 |
with gr.Row():
|
3602 |
-
|
3603 |
-
|
3604 |
-
|
3605 |
-
|
3606 |
-
|
3607 |
-
|
3608 |
-
|
3609 |
-
|
3610 |
-
|
3611 |
-
|
3612 |
-
|
3613 |
-
|
3614 |
-
|
3615 |
-
|
3616 |
-
|
3617 |
-
|
3618 |
-
|
3619 |
-
|
3620 |
-
|
3621 |
-
|
3622 |
-
|
3623 |
-
|
3624 |
-
|
3625 |
-
|
3626 |
-
|
3627 |
-
|
3628 |
-
|
3629 |
-
|
3630 |
-
|
3631 |
-
|
3632 |
-
|
3633 |
-
|
3634 |
-
|
3635 |
-
|
3636 |
-
|
3637 |
-
|
3638 |
-
|
3639 |
-
|
3640 |
-
|
3641 |
-
|
3642 |
-
|
3643 |
-
|
3644 |
-
|
3645 |
-
|
3646 |
-
|
3647 |
-
|
3648 |
-
|
3649 |
-
|
3650 |
-
|
3651 |
-
|
3652 |
-
|
3653 |
-
|
3654 |
-
|
3655 |
-
|
3656 |
-
|
3657 |
-
|
3658 |
-
|
3659 |
-
|
3660 |
-
exit_ticket_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
3661 |
-
exit_ticket_result_word_link = gr.File(label="Download Word")
|
3662 |
|
3663 |
|
3664 |
-
|
3665 |
-
|
3666 |
-
|
3667 |
|
3668 |
-
|
3669 |
-
|
3670 |
-
|
3671 |
-
|
3672 |
-
|
3673 |
-
|
3674 |
-
|
3675 |
-
|
3676 |
-
|
3677 |
|
3678 |
with gr.Accordion("免責聲明", open=True):
|
3679 |
gr.Markdown("""
|
@@ -3780,8 +3910,8 @@ def create_app():
|
|
3780 |
refresh_btn = gr.Button("refresh", variant="primary")
|
3781 |
with gr.Tab("by sheets"):
|
3782 |
sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
|
3783 |
-
sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="
|
3784 |
-
sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="
|
3785 |
sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
|
3786 |
sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
|
3787 |
sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
|
|
|
2164 |
def update_sheet_data(sheet_url, qa_result, video_id):
|
2165 |
# 根據 url 找到 sheet ,根據 video_id 找到 sheet 中的 video_id 的 row number
|
2166 |
sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
|
2167 |
+
# print(f"sheet_data: {sheet_data}")
|
2168 |
|
2169 |
if not sheet_data or len(sheet_data) < 1:
|
2170 |
print("錯誤:工作表資料為空或缺少標頭列。")
|
|
|
2175 |
|
2176 |
# 直接指定要尋找的欄位名稱
|
2177 |
target_column_name = '均一平台 YT Readable ID'
|
2178 |
+
target_qa_column_name = "QA" # 假設 QA 欄位的標題是 "QA"
|
2179 |
|
2180 |
try:
|
2181 |
# 1. 找到 '均一平台 YT Readable ID' 所在的欄位索引 (column index)
|
|
|
2190 |
for i, row in enumerate(data_rows):
|
2191 |
# 確保該列有足夠的欄位,並且該欄位的值等於 video_id
|
2192 |
if len(row) > video_id_col_index and row[video_id_col_index] == video_id:
|
2193 |
+
target_row_index = i + 1 # 找到匹配的列,其在原始 sheet_data 中的索引是 i + 1 (因為 data_rows 從 sheet_data[1] 開始)
|
2194 |
break # 找到後即可跳出迴圈
|
2195 |
|
2196 |
if target_row_index != -1:
|
2197 |
+
# target_row_index 是 video_id 在 sheet_data (含標頭) 中的 0-based 索引
|
2198 |
+
# 例如,第一筆資料的 target_row_index 是 1
|
2199 |
+
actual_target_row_number = target_row_index + 1 # 這是實際的 1-based 工作表列號 (例如 2)
|
2200 |
+
print(f"找到 video_id '{video_id}' 於欄位 '{target_column_name}' 的第 {actual_target_row_number} 列 (工作表列號)")
|
2201 |
|
2202 |
+
# --- 以下是更新邏輯 ---
|
2203 |
try:
|
2204 |
+
# 找到 QA 欄位的索引
|
2205 |
qa_col_index = header_row.index(target_qa_column_name)
|
2206 |
|
2207 |
+
# 取得目標列的實際資料
|
2208 |
+
target_row_data = sheet_data[target_row_index]
|
2209 |
+
|
2210 |
+
# 檢查目標列是否有足夠的欄位可以更新
|
2211 |
+
if len(target_row_data) > qa_col_index:
|
2212 |
+
# *** 修正點:傳遞給 update_sheet_cell 的列號減 1 ***
|
2213 |
+
# 因為觀察到 update_sheet_cell(N) 會更新到 N+1 列
|
2214 |
+
# 所以我們傳遞 N-1,讓它內部 +1 後剛好是 N
|
2215 |
+
row_index_to_pass = actual_target_row_number - 1 # 將 1-based 列號減 1
|
2216 |
+
|
2217 |
+
# 為了 log 清晰,仍然顯示 1-based 的目標列號
|
2218 |
+
print(f"準備更新 第 {actual_target_row_number} 列, 欄位 '{target_qa_column_name}' (索引 {qa_col_index}) 為 '{qa_result}'")
|
2219 |
+
# 添加一行 log 來確認傳遞的值
|
2220 |
+
print(f" (傳遞給 update_sheet_cell 的 row_index: {row_index_to_pass})")
|
2221 |
|
2222 |
+
# --- 實際更新 Google Sheet 的程式碼 ---
|
2223 |
+
# 傳遞調整後的索引 row_index_to_pass
|
2224 |
+
SHEET_SERVICE.update_sheet_cell(sheet_url, row_index_to_pass, qa_col_index, qa_result)
|
2225 |
else:
|
2226 |
+
# 如果目標列的長度不足以包含 qa_col_index
|
2227 |
+
print(f"錯誤:第 {actual_target_row_number} 列 (資料長度 {len(target_row_data)}) 沒有足夠的欄位來更新 QA (需要索引 {qa_col_index})。")
|
2228 |
|
2229 |
except ValueError:
|
2230 |
+
print(f"錯誤:在標頭列 {header_row} 中找不到 QA 欄位名稱 '{target_qa_column_name}'")
|
2231 |
# --- 更新邏輯結束 ---
|
2232 |
|
2233 |
else:
|
2234 |
# 如果找不到 video_id,印出錯誤訊息
|
2235 |
print(f"錯誤:��欄位 '{target_column_name}' 中找不到 video_id '{video_id}'。")
|
2236 |
|
2237 |
+
def refresh_video_LLM_all_content_by_sheet(sheet_url, sheet_qa_column, video_ids_input): # 參數名稱改為 video_ids_input 避免混淆
|
2238 |
+
print("=== 開始批次處理工作表刷新 ===")
|
2239 |
+
target_id_column_name = '均一平台 YT Readable ID'
|
2240 |
+
target_qa_column_name = "QA"
|
|
|
|
|
|
|
2241 |
sheet_qa_success_tag = "OO"
|
2242 |
sheet_qa_failed_tag = "XX"
|
2243 |
+
bucket_name = 'video_ai_assistant'
|
2244 |
|
2245 |
+
# 1. 解析輸入的 video_ids
|
2246 |
+
video_ids_list = video_ids_input.replace('\n', ',').split(',')
|
2247 |
+
video_ids_list = [vid.strip() for vid in video_ids_list if vid.strip() and vid.strip() != target_id_column_name]
|
2248 |
+
print(f"準備處理的 Video IDs: {video_ids_list}")
|
2249 |
+
|
2250 |
+
if not video_ids_list:
|
2251 |
+
print("沒有有效的 Video ID 需要處理。")
|
2252 |
+
return {"success_video_ids": [], "failed_video_ids": []}
|
2253 |
+
|
2254 |
+
# 2. 一次性讀取工作表資料
|
2255 |
+
try:
|
2256 |
+
print(f"正在從 {sheet_url} 讀取工作表資料...")
|
2257 |
+
sheet_data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url)
|
2258 |
+
if not sheet_data or len(sheet_data) < 1:
|
2259 |
+
print("錯誤:工作表資料為空或缺少標頭列。")
|
2260 |
+
return {"success_video_ids": [], "failed_video_ids": []}
|
2261 |
+
print(f"成功讀取 {len(sheet_data)} 列資料。")
|
2262 |
+
header_row = sheet_data[0]
|
2263 |
+
data_rows = sheet_data[1:]
|
2264 |
+
except Exception as e:
|
2265 |
+
print(f"讀取工作表時發生錯誤: {e}")
|
2266 |
+
return {"success_video_ids": [], "failed_video_ids": []}
|
2267 |
+
|
2268 |
+
# 3. 找到目標欄位索引並建立 video_id 到 row_number 的映射
|
2269 |
+
try:
|
2270 |
+
video_id_col_index = header_row.index(target_id_column_name)
|
2271 |
+
qa_col_index = header_row.index(target_qa_column_name)
|
2272 |
+
# Google Sheet 的欄位字母 (A=0, B=1, ...)
|
2273 |
+
qa_col_letter = chr(ord('A') + qa_col_index)
|
2274 |
+
print(f"'{target_id_column_name}' 欄位索引: {video_id_col_index}")
|
2275 |
+
print(f"'{target_qa_column_name}' 欄位索引: {qa_col_index} (欄位 {qa_col_letter})")
|
2276 |
+
except ValueError as e:
|
2277 |
+
print(f"錯誤:在標頭列 {header_row} 中找不到必要的欄位: {e}")
|
2278 |
+
return {"success_video_ids": [], "failed_video_ids": []}
|
2279 |
+
|
2280 |
+
video_id_to_row_map = {}
|
2281 |
+
for i, row in enumerate(data_rows):
|
2282 |
+
if len(row) > video_id_col_index and row[video_id_col_index]:
|
2283 |
+
# 儲存 1-based 的工作表列號
|
2284 |
+
video_id_to_row_map[row[video_id_col_index]] = i + 2 # 資料列從第 2 列開始 (1-based)
|
2285 |
+
|
2286 |
+
# 4. 批次檢查 GCS 並分類
|
2287 |
+
ids_to_set_oo_phase1 = [] # (video_id, row_number)
|
2288 |
+
ids_to_refresh = [] # video_id
|
2289 |
+
processed_ids = set() # 追蹤已處理的ID,避免重複
|
2290 |
+
|
2291 |
+
print("開始檢查 GCS 檔案狀態...")
|
2292 |
+
for video_id in video_ids_list:
|
2293 |
+
if video_id in processed_ids:
|
2294 |
+
continue
|
2295 |
+
processed_ids.add(video_id)
|
2296 |
+
|
2297 |
+
if video_id not in video_id_to_row_map:
|
2298 |
+
print(f"警告:輸入的 video_id '{video_id}' 在工作表的 '{target_id_column_name}' 欄位中找不到,將跳過。")
|
2299 |
+
continue
|
2300 |
+
|
2301 |
+
row_number = video_id_to_row_map[video_id]
|
2302 |
+
file_name = f'{video_id}_transcript.json'
|
2303 |
+
blob_name = f"{video_id}/{file_name}"
|
2304 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
2305 |
is_file_exists = GCS_SERVICE.check_file_exists(bucket_name, blob_name)
|
2306 |
+
if is_file_exists:
|
2307 |
+
print(f" - {video_id} (第 {row_number} 列): GCS 檔案存在,標記為 OO。")
|
2308 |
+
ids_to_set_oo_phase1.append((video_id, row_number))
|
2309 |
else:
|
2310 |
+
print(f" - {video_id} (第 {row_number} 列): GCS 檔案不存在,需要刷新。")
|
2311 |
+
ids_to_refresh.append(video_id)
|
|
|
|
|
|
|
|
|
|
|
2312 |
except Exception as e:
|
2313 |
+
print(f"檢查 GCS 檔案 {blob_name} 時發生錯誤: {e},將嘗試刷新。")
|
2314 |
+
ids_to_refresh.append(video_id) # 如果檢查出錯,也歸類為需要刷新
|
2315 |
+
|
2316 |
+
# 5. 批次更新 "OO" (Phase 1: GCS 已存在)
|
2317 |
+
if ids_to_set_oo_phase1:
|
2318 |
+
print(f"\n準備批次更新 {len(ids_to_set_oo_phase1)} 個 Video ID 的 QA 為 '{sheet_qa_success_tag}' (GCS 已存在)...")
|
2319 |
+
update_data_oo1 = []
|
2320 |
+
sheet_name = SHEET_SERVICE.get_sheet_name_by_url(sheet_url) # 需要 SHEET_SERVICE 提供此方法
|
2321 |
+
if not sheet_name:
|
2322 |
+
print("錯誤:無法獲取工作表名稱,無法執行批次更新。")
|
2323 |
+
# 或者可以嘗試從 sheet_data 推斷,但不夠通用
|
2324 |
+
else:
|
2325 |
+
for vid, row_num in ids_to_set_oo_phase1:
|
2326 |
+
update_data_oo1.append({
|
2327 |
+
'range': f"{sheet_name}!{qa_col_letter}{row_num}",
|
2328 |
+
'value': sheet_qa_success_tag
|
2329 |
+
})
|
2330 |
+
try:
|
2331 |
+
# *** 假設 SHEET_SERVICE.batch_update_cells 存在 ***
|
2332 |
+
SHEET_SERVICE.batch_update_cells(sheet_url, update_data_oo1)
|
2333 |
+
print(f"成功批次更新 {len(update_data_oo1)} 個儲存格為 '{sheet_qa_success_tag}'。")
|
2334 |
+
except Exception as e:
|
2335 |
+
print(f"批次更新 QA 為 '{sheet_qa_success_tag}' 時發生錯誤: {e}")
|
2336 |
+
# 這裡可以考慮是否要將這些 ID 移到失敗列表
|
2337 |
+
|
2338 |
+
# 6. 處理需要刷新的 ID
|
2339 |
+
successfully_refreshed_ids = [] # (video_id, row_number)
|
2340 |
+
failed_refresh_ids = [] # (video_id, row_number)
|
2341 |
+
|
2342 |
+
if ids_to_refresh:
|
2343 |
+
print(f"\n開始處理 {len(ids_to_refresh)} 個需要刷新的 Video ID...")
|
2344 |
+
for video_id in ids_to_refresh:
|
2345 |
+
row_number = video_id_to_row_map[video_id] # 必定存在,前面已檢查
|
2346 |
+
print(f" 正在刷新 {video_id} (第 {row_number} 列)...")
|
2347 |
try:
|
2348 |
+
refresh_video_LLM_all_content_by_id(video_id)
|
2349 |
+
print(f" - {video_id} 刷新成功。")
|
2350 |
+
successfully_refreshed_ids.append((video_id, row_number))
|
2351 |
+
time.sleep(1) # 避免過於頻繁觸發其他 API (例如 GCS 刪除/上傳)
|
2352 |
+
except Exception as e:
|
2353 |
+
print(f" - {video_id} 刷新失敗: {str(e)}")
|
2354 |
+
failed_refresh_ids.append((video_id, row_number))
|
2355 |
+
time.sleep(5) # 失敗時稍作停頓
|
2356 |
+
|
2357 |
+
# 7. 批次更新刷新結果 (OO for success, XX for failure)
|
2358 |
+
update_data_oo2 = []
|
2359 |
+
update_data_xx = []
|
2360 |
+
sheet_name = SHEET_SERVICE.get_sheet_name_by_url(sheet_url) # 再次獲取或使用之前獲取的
|
2361 |
+
|
2362 |
+
if sheet_name:
|
2363 |
+
for vid, row_num in successfully_refreshed_ids:
|
2364 |
+
update_data_oo2.append({
|
2365 |
+
'range': f"{sheet_name}!{qa_col_letter}{row_num}",
|
2366 |
+
'value': sheet_qa_success_tag
|
2367 |
+
})
|
2368 |
+
for vid, row_num in failed_refresh_ids:
|
2369 |
+
update_data_xx.append({
|
2370 |
+
'range': f"{sheet_name}!{qa_col_letter}{row_num}",
|
2371 |
+
'value': sheet_qa_failed_tag
|
2372 |
+
})
|
2373 |
+
|
2374 |
+
if update_data_oo2:
|
2375 |
+
print(f"\n準備批次更新 {len(update_data_oo2)} 個 Video ID 的 QA 為 '{sheet_qa_success_tag}' (刷新成功)...")
|
2376 |
+
try:
|
2377 |
+
SHEET_SERVICE.batch_update_cells(sheet_url, update_data_oo2)
|
2378 |
+
print(f"成功批次更新 {len(update_data_oo2)} 個儲存格為 '{sheet_qa_success_tag}'。")
|
2379 |
+
except Exception as e:
|
2380 |
+
print(f"批次更新 QA 為 '{sheet_qa_success_tag}' (刷新成功) 時發生錯誤: {e}")
|
2381 |
+
|
2382 |
+
if update_data_xx:
|
2383 |
+
print(f"\n準備批次更新 {len(update_data_xx)} 個 Video ID 的 QA 為 '{sheet_qa_failed_tag}' (刷新失敗)...")
|
2384 |
+
try:
|
2385 |
+
SHEET_SERVICE.batch_update_cells(sheet_url, update_data_xx)
|
2386 |
+
print(f"成功批次更新 {len(update_data_xx)} 個儲存格為 '{sheet_qa_failed_tag}'。")
|
2387 |
+
except Exception as e:
|
2388 |
+
print(f"批次更新 QA 為 '{sheet_qa_failed_tag}' 時發生錯誤: {e}")
|
2389 |
+
else:
|
2390 |
+
if successfully_refreshed_ids or failed_refresh_ids:
|
2391 |
+
print("錯誤:無法獲取工作表名稱,無法批次更新刷新結果。")
|
2392 |
+
|
2393 |
+
|
2394 |
+
# 8. 整理最終結果
|
2395 |
+
final_success_ids = [item[0] for item in ids_to_set_oo_phase1] + [item[0] for item in successfully_refreshed_ids]
|
2396 |
+
final_failed_ids = [item[0] for item in failed_refresh_ids]
|
2397 |
+
# 將在工作表中找不到的 ID 也視為失敗
|
2398 |
+
initial_not_found = [vid for vid in video_ids_list if vid not in video_id_to_row_map]
|
2399 |
+
final_failed_ids.extend(initial_not_found)
|
2400 |
|
2401 |
+
print("\n=== 批次處理完成 ===")
|
2402 |
+
print(f"成功處理 (或 GCS 已存在): {len(final_success_ids)} 個")
|
2403 |
+
print(f"處理失敗 (或未找到/刷新失敗): {len(final_failed_ids)} 個")
|
2404 |
|
2405 |
result = {
|
2406 |
+
"success_video_ids": final_success_ids,
|
2407 |
+
"failed_video_ids": list(set(final_failed_ids)) # 去重
|
2408 |
}
|
2409 |
return result
|
2410 |
|
|
|
3731 |
with gr.Row():
|
3732 |
with gr.Tab("學習單"):
|
3733 |
with gr.Row():
|
3734 |
+
worksheet_content_type_name = gr.Textbox(value="worksheet", visible=False)
|
3735 |
+
worksheet_algorithm = gr.Dropdown(label="選擇教學策略或理論", choices=["Bloom認知階層理論", "Polya數學解題法", "CRA教學法"], value="Bloom認知階層理論", visible=False)
|
3736 |
+
worksheet_content_btn = gr.Button("生成學習單 📄", variant="primary", visible=True)
|
3737 |
+
with gr.Accordion("微調", open=False):
|
3738 |
+
worksheet_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
3739 |
+
worksheet_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
3740 |
+
worksheet_result_retrun_original = gr.Button("返回原始結果")
|
3741 |
+
with gr.Accordion("prompt", open=False) as worksheet_accordion:
|
3742 |
+
worksheet_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
3743 |
+
with gr.Column(scale=2):
|
3744 |
+
# 生成對應不同模式的結果
|
3745 |
+
worksheet_result_prompt = gr.Textbox(visible=False)
|
3746 |
+
worksheet_result_original = gr.Textbox(visible=False)
|
3747 |
+
worksheet_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
|
3748 |
+
worksheet_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
3749 |
+
worksheet_result_word_link = gr.File(label="Download Word")
|
3750 |
+
with gr.Tab("教案"):
|
3751 |
+
with gr.Row():
|
3752 |
+
with gr.Column(scale=1):
|
3753 |
+
with gr.Row():
|
3754 |
+
lesson_plan_content_type_name = gr.Textbox(value="lesson_plan", visible=False)
|
3755 |
+
lesson_plan_time = gr.Slider(label="選擇課程時間(分鐘)", minimum=10, maximum=120, step=5, value=40)
|
3756 |
+
lesson_plan_btn = gr.Button("生成教案 📕", variant="primary", visible=True)
|
3757 |
+
with gr.Accordion("微調", open=False):
|
3758 |
+
lesson_plan_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
3759 |
+
lesson_plan_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
3760 |
+
lesson_plan_result_retrun_original = gr.Button("返回原始結果")
|
3761 |
+
with gr.Accordion("prompt", open=False) as lesson_plan_accordion:
|
3762 |
+
lesson_plan_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
3763 |
+
with gr.Column(scale=2):
|
3764 |
+
# 生成對應不同模式的結果
|
3765 |
+
lesson_plan_result_prompt = gr.Textbox(visible=False)
|
3766 |
+
lesson_plan_result_original = gr.Textbox(visible=False)
|
3767 |
+
lesson_plan_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
|
3768 |
+
|
3769 |
+
lesson_plan_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
3770 |
+
lesson_plan_result_word_link = gr.File(label="Download Word")
|
3771 |
+
with gr.Tab("出場券"):
|
3772 |
+
with gr.Row():
|
3773 |
+
with gr.Column(scale=1):
|
3774 |
+
with gr.Row():
|
3775 |
+
exit_ticket_content_type_name = gr.Textbox(value="exit_ticket", visible=False)
|
3776 |
+
exit_ticket_time = gr.Slider(label="選擇出場券時間(分鐘)", minimum=5, maximum=10, step=1, value=8)
|
3777 |
+
exit_ticket_btn = gr.Button("生成出場券 🎟️", variant="primary", visible=True)
|
3778 |
+
with gr.Accordion("微調", open=False):
|
3779 |
+
exit_ticket_result_fine_tune_prompt = gr.Textbox(label="根據結果,輸入你想更改的想法")
|
3780 |
+
exit_ticket_result_fine_tune_btn = gr.Button("微調結果", variant="primary")
|
3781 |
+
exit_ticket_result_retrun_original = gr.Button("返回原始結果")
|
3782 |
+
with gr.Accordion("prompt", open=False) as exit_ticket_accordion:
|
3783 |
+
exit_ticket_prompt = gr.Textbox(label="worksheet_prompt", show_copy_button=True, lines=40)
|
3784 |
+
with gr.Column(scale=2):
|
3785 |
+
# 生成對應不同模式的結果
|
3786 |
+
exit_ticket_result_prompt = gr.Textbox(visible=False)
|
3787 |
+
exit_ticket_result_original = gr.Textbox(visible=False)
|
3788 |
+
exit_ticket_result = gr.Markdown(label="初次生成結果", latex_delimiters = [{"left": "$", "right": "$", "display": False}])
|
3789 |
+
|
3790 |
+
exit_ticket_download_button = gr.Button("轉成 word,完成後請點擊右下角 download 按鈕", variant="primary")
|
3791 |
+
exit_ticket_result_word_link = gr.File(label="Download Word")
|
|
|
|
|
3792 |
|
3793 |
|
3794 |
+
# with gr.Tab("素養導向閱讀題組"):
|
3795 |
+
# literacy_oriented_reading_content = gr.Textbox(label="輸入閱讀材料")
|
3796 |
+
# literacy_oriented_reading_content_btn = gr.Button("生成閱讀理解題")
|
3797 |
|
3798 |
+
# with gr.Tab("自我評估"):
|
3799 |
+
# self_assessment_content = gr.Textbox(label="輸入自評問卷或檢查表")
|
3800 |
+
# self_assessment_content_btn = gr.Button("生成自評問卷")
|
3801 |
+
# with gr.Tab("自我反思評量"):
|
3802 |
+
# self_reflection_content = gr.Textbox(label="輸入自我反思活動")
|
3803 |
+
# self_reflection_content_btn = gr.Button("生成自我反思活動")
|
3804 |
+
# with gr.Tab("後設認知"):
|
3805 |
+
# metacognition_content = gr.Textbox(label="輸入後設認知相關問題")
|
3806 |
+
# metacognition_content_btn = gr.Button("生成後設認知問題")
|
3807 |
|
3808 |
with gr.Accordion("免責聲明", open=True):
|
3809 |
gr.Markdown("""
|
|
|
3910 |
refresh_btn = gr.Button("refresh", variant="primary")
|
3911 |
with gr.Tab("by sheets"):
|
3912 |
sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
|
3913 |
+
sheet_video_column = gr.Textbox(label="輸入要讀取的 youtube_id 欄位", value="D2:D")
|
3914 |
+
sheet_QA_column = gr.Textbox(label="輸入要讀取的 QA 欄位", value="F2:F")
|
3915 |
sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
|
3916 |
sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
|
3917 |
sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
|
sheet_service.py
CHANGED
@@ -5,6 +5,7 @@ import json
|
|
5 |
from urllib.parse import urlparse, parse_qs
|
6 |
import logging # 建議使用 logging 而非 print
|
7 |
import string # 導入 string 模組用於轉換欄位索引
|
|
|
8 |
|
9 |
# 設定基本的 logging
|
10 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -44,69 +45,62 @@ class SheetService:
|
|
44 |
self.service = None
|
45 |
self.sheet = None
|
46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
|
48 |
"""
|
49 |
從 Google Sheets URL 中提取試算表 ID。
|
50 |
"""
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
except ValueError:
|
63 |
-
pass # 'd' 不在路徑中
|
64 |
-
|
65 |
-
logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
|
66 |
-
return None
|
67 |
|
68 |
def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
|
69 |
"""
|
70 |
從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
|
71 |
-
返回整數型別的 gid 或 None。
|
72 |
"""
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
gid_str = None
|
78 |
-
if 'gid' in query_params:
|
79 |
-
gid_str = query_params['gid'][0]
|
80 |
-
elif 'gid' in fragment_params:
|
81 |
-
gid_str = fragment_params['gid'][0]
|
82 |
-
|
83 |
-
if gid_str:
|
84 |
-
try:
|
85 |
-
return int(gid_str)
|
86 |
-
except ValueError:
|
87 |
-
logging.warning(f"URL 中的 gid 不是有效的整數: {gid_str}")
|
88 |
-
return None
|
89 |
-
else:
|
90 |
-
# logging.info(f"URL 中未找到 gid 參數,將嘗試使用第一個工作表: {sheet_url}")
|
91 |
-
# 如果 URL 沒有 gid,通常表示是第一個工作表,其 gid 通常是 0
|
92 |
-
# 但我們在這裡返回 None,讓後續邏輯決定如何處理
|
93 |
-
return None
|
94 |
|
95 |
def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
|
96 |
"""
|
97 |
使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
|
98 |
-
如果 gid 為 None
|
99 |
-
|
100 |
-
Args:
|
101 |
-
spreadsheet_id (str): Google 試算表的 ID。
|
102 |
-
gid (int | None): 目標工作表分頁的 ID。如果為 None,則獲取第一個工作表。
|
103 |
-
|
104 |
-
Returns:
|
105 |
-
str | None: 工作表的名稱 (title),如果找不到或發生錯誤則返回 None。
|
106 |
"""
|
107 |
if not self.service:
|
108 |
logging.error("Sheet API 服務未成功初始化。")
|
109 |
return None
|
|
|
|
|
|
|
110 |
try:
|
111 |
# 使用 spreadsheets.get 獲取試算表的中繼資料
|
112 |
# fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
|
@@ -120,39 +114,36 @@ class SheetService:
|
|
120 |
logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
|
121 |
return None
|
122 |
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
return None
|
138 |
-
else:
|
139 |
-
# 如果 gid 為 None,返回第一個工作表的名稱
|
140 |
-
first_sheet_properties = sheets[0].get('properties', {})
|
141 |
-
first_sheet_title = first_sheet_properties.get('title')
|
142 |
-
first_sheet_gid = first_sheet_properties.get('sheetId', '未知')
|
143 |
-
if first_sheet_title:
|
144 |
-
logging.info(f"未提供 gid,使用第一個工作表 (gid={first_sheet_gid}): '{first_sheet_title}'")
|
145 |
-
return first_sheet_title
|
146 |
-
else:
|
147 |
-
logging.warning(f"第一個工作表 (gid={first_sheet_gid}) 缺少 title 屬性。")
|
148 |
-
return None
|
149 |
-
|
150 |
-
except googleapiclient.errors.HttpError as error:
|
151 |
-
logging.error(f"獲取工作表名稱時發生 API 錯誤: {error}")
|
152 |
return None
|
|
|
153 |
except Exception as e:
|
154 |
-
logging.error(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
return None
|
|
|
|
|
156 |
|
157 |
def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
|
158 |
"""
|
@@ -392,4 +383,70 @@ class SheetService:
|
|
392 |
return False
|
393 |
except Exception as e:
|
394 |
logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
|
395 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
from urllib.parse import urlparse, parse_qs
|
6 |
import logging # 建議使用 logging 而非 print
|
7 |
import string # 導入 string 模組用於轉換欄位索引
|
8 |
+
import re # 導入 re 模組
|
9 |
|
10 |
# 設定基本的 logging
|
11 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
45 |
self.service = None
|
46 |
self.sheet = None
|
47 |
|
48 |
+
def _extract_ids_from_url(self, sheet_url):
|
49 |
+
"""
|
50 |
+
(私有方法) 從 Google Sheets URL 中提取 spreadsheetId 和 gid。
|
51 |
+
"""
|
52 |
+
spreadsheet_id = None
|
53 |
+
gid = None
|
54 |
+
# 嘗試從 URL 中提取 spreadsheetId (通常在 /d/ 和 /edit 之間)
|
55 |
+
match_id = re.search(r'/d/([a-zA-Z0-9-_]+)', sheet_url)
|
56 |
+
if match_id:
|
57 |
+
spreadsheet_id = match_id.group(1)
|
58 |
+
# 嘗試從 URL 中提取 gid (通常在 gid= 後面)
|
59 |
+
match_gid = re.search(r'[#&]gid=(\d+)', sheet_url)
|
60 |
+
if match_gid:
|
61 |
+
gid = int(match_gid.group(1)) # gid 是數字
|
62 |
+
else:
|
63 |
+
# 如果 URL 沒有 gid,通常指向第一個工作表,gid 為 0
|
64 |
+
# logging.info(f"URL 中未找到 gid,假設為第一個工作表 (gid=0): {sheet_url}")
|
65 |
+
gid = 0
|
66 |
+
return spreadsheet_id, gid
|
67 |
+
|
68 |
def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
|
69 |
"""
|
70 |
從 Google Sheets URL 中提取試算表 ID。
|
71 |
"""
|
72 |
+
spreadsheet_id, _ = self._extract_ids_from_url(sheet_url)
|
73 |
+
if spreadsheet_id:
|
74 |
+
# 可以添加一些基本檢查,確保它看起來像一個 ID
|
75 |
+
if len(spreadsheet_id) > 30: # Google Sheet ID 通常很長
|
76 |
+
return spreadsheet_id
|
77 |
+
else:
|
78 |
+
logging.warning(f"從 URL 提取的 ID '{spreadsheet_id}' 看起來太短,可能無效: {sheet_url}")
|
79 |
+
return None
|
80 |
+
else:
|
81 |
+
logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
|
82 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
|
85 |
"""
|
86 |
從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
|
87 |
+
返回整數型別的 gid 或 None (如果未指定則返回 0)。
|
88 |
"""
|
89 |
+
_, gid = self._extract_ids_from_url(sheet_url)
|
90 |
+
# 即使 gid 是 0 (預設),也將其視為有效的 gid
|
91 |
+
return gid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
|
94 |
"""
|
95 |
使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
|
96 |
+
如果 gid 為 None,則返回第一個工作表的名稱 (gid=0)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
"""
|
98 |
if not self.service:
|
99 |
logging.error("Sheet API 服務未成功初始化。")
|
100 |
return None
|
101 |
+
# 如果傳入的 gid 是 None,我們將其視為 0
|
102 |
+
target_gid = gid if gid is not None else 0
|
103 |
+
|
104 |
try:
|
105 |
# 使用 spreadsheets.get 獲取試算表的中繼資料
|
106 |
# fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
|
|
|
114 |
logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
|
115 |
return None
|
116 |
|
117 |
+
for sheet in sheets:
|
118 |
+
properties = sheet.get('properties', {})
|
119 |
+
if properties.get('sheetId') == target_gid:
|
120 |
+
sheet_title = properties.get('title')
|
121 |
+
logging.info(f"找到 gid={target_gid} 對應的工作表名稱: '{sheet_title}'")
|
122 |
+
return sheet_title
|
123 |
+
|
124 |
+
# 如果循環結束還沒找到
|
125 |
+
logging.warning(f"在試算表 {spreadsheet_id} 中未找到 gid={target_gid} 對應的工作表。")
|
126 |
+
# 如果 gid 是 0 但找不到,可能試算表是空的或有問題,返回第一個工作表的名稱作為備選
|
127 |
+
if target_gid == 0 and sheets:
|
128 |
+
first_sheet_title = sheets[0].get('properties', {}).get('title')
|
129 |
+
logging.warning(f"返回第一個工作表的名稱 '{first_sheet_title}' 作為 gid=0 的備選。")
|
130 |
+
return first_sheet_title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
return None
|
132 |
+
|
133 |
except Exception as e:
|
134 |
+
logging.error(f"獲取工作表名稱時發生 API 錯誤 (ID: {spreadsheet_id}, GID: {target_gid}): {e}")
|
135 |
+
return None
|
136 |
+
|
137 |
+
def get_sheet_name_by_url(self, sheet_url: str) -> str | None:
|
138 |
+
"""
|
139 |
+
從 Google Sheets URL 中提取 gid 並獲取對應的工作表名稱。
|
140 |
+
"""
|
141 |
+
spreadsheet_id, gid = self._extract_ids_from_url(sheet_url)
|
142 |
+
if not spreadsheet_id:
|
143 |
+
logging.error(f"無法從 URL {sheet_url} 中提取 Spreadsheet ID。")
|
144 |
return None
|
145 |
+
# 直接調用已有的 get_sheet_name_by_gid 方法
|
146 |
+
return self.get_sheet_name_by_gid(spreadsheet_id, gid)
|
147 |
|
148 |
def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
|
149 |
"""
|
|
|
383 |
return False
|
384 |
except Exception as e:
|
385 |
logging.error(f"更新儲存格時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
|
386 |
+
return False
|
387 |
+
|
388 |
+
def batch_update_cells(self, sheet_url: str, update_data: list[dict]) -> dict | None:
|
389 |
+
"""
|
390 |
+
批次更新 Google Sheet 中的多個儲存格。
|
391 |
+
|
392 |
+
Args:
|
393 |
+
sheet_url (str): Google Sheet 的 URL。
|
394 |
+
update_data (list[dict]): 一個字典列表,每個字典包含:
|
395 |
+
'range' (str): 要更新的範圍 (例如 '工作表名!A1')
|
396 |
+
'value' (str): 要寫入的值
|
397 |
+
|
398 |
+
Returns:
|
399 |
+
dict | None: Google API 的返回結果,如果成功。否則返回 None。
|
400 |
+
"""
|
401 |
+
spreadsheet_id, _ = self._extract_ids_from_url(sheet_url)
|
402 |
+
if not spreadsheet_id:
|
403 |
+
logging.error(f"錯誤:無法從 URL {sheet_url} 中提取 Spreadsheet ID 進行批次更新。")
|
404 |
+
return None
|
405 |
+
if not self.service:
|
406 |
+
logging.error("Sheet API 服務未成功初始化。")
|
407 |
+
return None
|
408 |
+
if not update_data:
|
409 |
+
logging.warning("沒有需要批次更新的資料。")
|
410 |
+
return None
|
411 |
+
|
412 |
+
# 確保 update_data 中的 range 包含 sheet_name
|
413 |
+
# (調用此方法前應已獲取 sheet_name 並填入 range)
|
414 |
+
# 例如 update_data = [{'range': '測試!F2', 'value': 'OO'}, ...]
|
415 |
+
|
416 |
+
body = {
|
417 |
+
'valueInputOption': 'USER_ENTERED', # 或者 'RAW'
|
418 |
+
'data': []
|
419 |
+
}
|
420 |
+
for item in update_data:
|
421 |
+
# 基本的範圍和值檢查
|
422 |
+
if 'range' not in item or 'value' not in item:
|
423 |
+
logging.warning(f"跳過格式錯誤的更新項目: {item}")
|
424 |
+
continue
|
425 |
+
if '!' not in item['range'] or len(item['range'].split('!')[1]) < 2: # 簡單檢查範圍格式
|
426 |
+
logging.warning(f"跳過範圍格式可能錯誤的更新項目: {item['range']}")
|
427 |
+
continue
|
428 |
+
|
429 |
+
body['data'].append({
|
430 |
+
'range': item['range'],
|
431 |
+
'values': [[item['value']]] # value 需要是二維列表
|
432 |
+
})
|
433 |
+
|
434 |
+
if not body['data']:
|
435 |
+
logging.warning("沒有有效的資料可以進行批次更新。")
|
436 |
+
return None
|
437 |
+
|
438 |
+
try:
|
439 |
+
logging.info(f"準備批次更新 Spreadsheet ID: {spreadsheet_id},共 {len(body['data'])} 個儲存格...")
|
440 |
+
result = self.service.spreadsheets().values().batchUpdate(
|
441 |
+
spreadsheetId=spreadsheet_id,
|
442 |
+
body=body
|
443 |
+
).execute()
|
444 |
+
total_updated = result.get('totalUpdatedCells', 0)
|
445 |
+
logging.info(f"批次更新完成。共更新了 {total_updated} 個儲存格。")
|
446 |
+
return result
|
447 |
+
except Exception as e:
|
448 |
+
logging.error(f"批次更新儲存格時發生 API 錯誤: {e}")
|
449 |
+
# from googleapiclient.errors import HttpError
|
450 |
+
# if isinstance(e, HttpError):
|
451 |
+
# logging.error(f"錯誤詳情: {e.content}")
|
452 |
+
return None
|