Spaces:
Sleeping
Sleeping
get_sheet_data
Browse files- app.py +26 -2
- sheet_service.py +269 -0
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import urllib.parse
|
|
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -69,6 +70,7 @@ from googleapiclient.http import MediaIoBaseUpload
|
|
| 69 |
|
| 70 |
from educational_material import EducationalMaterial
|
| 71 |
from storage_service import GoogleCloudStorage
|
|
|
|
| 72 |
from google.oauth2.service_account import Credentials
|
| 73 |
import vertexai
|
| 74 |
from vertexai.generative_models import GenerativeModel, Part
|
|
@@ -93,6 +95,7 @@ if is_env_local:
|
|
| 93 |
GCS_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 94 |
DRIVE_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 95 |
GBQ_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
|
|
|
| 96 |
OPEN_AI_KEY = config["OPEN_AI_KEY"]
|
| 97 |
OPEN_AI_ASSISTANT_ID_GPT4_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT4_BOT1"]
|
| 98 |
OPEN_AI_ASSISTANT_ID_GPT3_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT3_BOT1"]
|
|
@@ -142,6 +145,7 @@ GBQ_CLIENT = bigquery.Client.from_service_account_info(json.loads(GBQ_KEY))
|
|
| 142 |
GROQ_CLIENT = Groq(api_key=GROQ_API_KEY)
|
| 143 |
GCS_SERVICE = GoogleCloudStorage(GCS_KEY)
|
| 144 |
GCS_CLIENT = GCS_SERVICE.client
|
|
|
|
| 145 |
PERPLEXITY_CLIENT = OpenAI(api_key=PERPLEXITY_API_KEY, base_url="https://api.perplexity.ai")
|
| 146 |
|
| 147 |
# check open ai access
|
|
@@ -2150,6 +2154,12 @@ def summary_add_markdown_version(video_id):
|
|
| 2150 |
|
| 2151 |
|
| 2152 |
# LLM 強制重刷
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2153 |
def refresh_video_LLM_all_content(video_ids):
|
| 2154 |
# 輸入影片 id,以 , 逗號分隔 或是 \n 換行
|
| 2155 |
video_id_list = video_ids.replace('\n', ',').split(',')
|
|
@@ -3647,8 +3657,22 @@ def create_app():
|
|
| 3647 |
with gr.Row():
|
| 3648 |
gr.Markdown("## 清單影片:重新生成所有內容")
|
| 3649 |
with gr.Row():
|
| 3650 |
-
|
| 3651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3652 |
with gr.Row():
|
| 3653 |
refresh_result = gr.JSON()
|
| 3654 |
|
|
|
|
| 1 |
import urllib.parse
|
| 2 |
+
import re
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
| 70 |
|
| 71 |
from educational_material import EducationalMaterial
|
| 72 |
from storage_service import GoogleCloudStorage
|
| 73 |
+
from sheet_service import SheetService
|
| 74 |
from google.oauth2.service_account import Credentials
|
| 75 |
import vertexai
|
| 76 |
from vertexai.generative_models import GenerativeModel, Part
|
|
|
|
| 95 |
GCS_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 96 |
DRIVE_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 97 |
GBQ_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 98 |
+
SHEET_KEY = json.dumps(config["GOOGLE_APPLICATION_CREDENTIALS_JSON"])
|
| 99 |
OPEN_AI_KEY = config["OPEN_AI_KEY"]
|
| 100 |
OPEN_AI_ASSISTANT_ID_GPT4_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT4_BOT1"]
|
| 101 |
OPEN_AI_ASSISTANT_ID_GPT3_BOT1 = config["OPEN_AI_ASSISTANT_ID_GPT3_BOT1"]
|
|
|
|
| 145 |
GROQ_CLIENT = Groq(api_key=GROQ_API_KEY)
|
| 146 |
GCS_SERVICE = GoogleCloudStorage(GCS_KEY)
|
| 147 |
GCS_CLIENT = GCS_SERVICE.client
|
| 148 |
+
SHEET_SERVICE = SheetService(SHEET_KEY)
|
| 149 |
PERPLEXITY_CLIENT = OpenAI(api_key=PERPLEXITY_API_KEY, base_url="https://api.perplexity.ai")
|
| 150 |
|
| 151 |
# check open ai access
|
|
|
|
| 2154 |
|
| 2155 |
|
| 2156 |
# LLM 強制重刷
|
| 2157 |
+
def get_sheet_data(sheet_url, range_name):
|
| 2158 |
+
data = SHEET_SERVICE.get_sheet_data_by_url(sheet_url, range_name)
|
| 2159 |
+
flattened_data = SHEET_SERVICE.flatten_column_data(data)
|
| 2160 |
+
return flattened_data
|
| 2161 |
+
|
| 2162 |
+
|
| 2163 |
def refresh_video_LLM_all_content(video_ids):
|
| 2164 |
# 輸入影片 id,以 , 逗號分隔 或是 \n 換行
|
| 2165 |
video_id_list = video_ids.replace('\n', ',').split(',')
|
|
|
|
| 3657 |
with gr.Row():
|
| 3658 |
gr.Markdown("## 清單影片:重新生成所有內容")
|
| 3659 |
with gr.Row():
|
| 3660 |
+
# tab refresh_video_ids & by sheets
|
| 3661 |
+
with gr.Tab("refresh_video_ids"):
|
| 3662 |
+
refresh_video_ids = gr.Textbox(label="輸入影片 id,以 , 逗號分隔")
|
| 3663 |
+
refresh_btn = gr.Button("refresh", variant="primary")
|
| 3664 |
+
with gr.Tab("by sheets"):
|
| 3665 |
+
sheet_url = gr.Textbox(label="輸入 Google Sheets 的 URL")
|
| 3666 |
+
sheet_get_value_btn = gr.Button("取得 ids", variant="primary")
|
| 3667 |
+
sheet_get_value_result = gr.Textbox(label="ids", interactive=False)
|
| 3668 |
+
sheet_refresh_btn = gr.Button("refresh by sheets", variant="primary")
|
| 3669 |
+
|
| 3670 |
+
sheet_get_value_btn.click(
|
| 3671 |
+
get_sheet_data,
|
| 3672 |
+
inputs=[sheet_url, gr.Textbox(value="D:D", visible=True)], # 將範圍修改為 D 欄
|
| 3673 |
+
outputs=[sheet_get_value_result]
|
| 3674 |
+
)
|
| 3675 |
+
|
| 3676 |
with gr.Row():
|
| 3677 |
refresh_result = gr.JSON()
|
| 3678 |
|
sheet_service.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.oauth2.credentials
|
| 2 |
+
import googleapiclient.discovery
|
| 3 |
+
from google.oauth2 import service_account
|
| 4 |
+
import json
|
| 5 |
+
from urllib.parse import urlparse, parse_qs
|
| 6 |
+
import logging # 建議使用 logging 而非 print
|
| 7 |
+
|
| 8 |
+
# 設定基本的 logging
|
| 9 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 10 |
+
|
| 11 |
+
class SheetService:
|
| 12 |
+
"""
|
| 13 |
+
一個用於與 Google Sheets API 互動的服務類別。
|
| 14 |
+
"""
|
| 15 |
+
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
|
| 16 |
+
|
| 17 |
+
def __init__(self, service_account_key_string: str, api_service_name: str = 'sheets', api_version: str = 'v4'):
|
| 18 |
+
"""
|
| 19 |
+
初始化 SheetService。
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
service_account_key_string (str): 包含 Google 服務帳戶憑證資訊的 JSON 字串。
|
| 23 |
+
通常是從 JSON 金鑰檔案讀取的內容。
|
| 24 |
+
api_service_name (str): 要使用的 Google API 服務名稱。預設為 'sheets'。
|
| 25 |
+
api_version (str): 要使用的 Google API 版本。預設為 'v4'。
|
| 26 |
+
"""
|
| 27 |
+
try:
|
| 28 |
+
credentials_info = json.loads(service_account_key_string)
|
| 29 |
+
self.credentials = service_account.Credentials.from_service_account_info(
|
| 30 |
+
credentials_info, scopes=self.SCOPES
|
| 31 |
+
)
|
| 32 |
+
self.service = googleapiclient.discovery.build(
|
| 33 |
+
api_service_name, api_version, credentials=self.credentials
|
| 34 |
+
)
|
| 35 |
+
self.sheet = self.service.spreadsheets()
|
| 36 |
+
logging.info("成功連接 Google Sheets API")
|
| 37 |
+
except json.JSONDecodeError as e:
|
| 38 |
+
logging.error(f"解析憑證 JSON 字串時發生錯誤: {e}")
|
| 39 |
+
self.service = None
|
| 40 |
+
self.sheet = None
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logging.error(f"連接 Google Sheets API 時發生錯誤: {e}")
|
| 43 |
+
self.service = None
|
| 44 |
+
self.sheet = None
|
| 45 |
+
|
| 46 |
+
def get_sheet_id_by_url(self, sheet_url: str) -> str | None:
|
| 47 |
+
"""
|
| 48 |
+
從 Google Sheets URL 中提取試算表 ID。
|
| 49 |
+
"""
|
| 50 |
+
parsed_url = urlparse(sheet_url)
|
| 51 |
+
path_parts = parsed_url.path.split('/')
|
| 52 |
+
try:
|
| 53 |
+
# Google Sheet URL 格式通常是 /spreadsheets/d/SPREADSHEET_ID/edit...
|
| 54 |
+
if 'd' in path_parts:
|
| 55 |
+
id_index = path_parts.index('d') + 1
|
| 56 |
+
if id_index < len(path_parts):
|
| 57 |
+
spreadsheet_id = path_parts[id_index]
|
| 58 |
+
# 進行一些基本檢查,確保它看起來像一個 ID
|
| 59 |
+
if len(spreadsheet_id) > 30: # Google Sheet ID 通常很長
|
| 60 |
+
return spreadsheet_id
|
| 61 |
+
except ValueError:
|
| 62 |
+
pass # 'd' 不在路徑中
|
| 63 |
+
|
| 64 |
+
logging.warning(f"無法從 URL 中提取有效的 Spreadsheet ID: {sheet_url}")
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
def get_sheet_gid_by_url(self, sheet_url: str) -> int | None:
|
| 68 |
+
"""
|
| 69 |
+
從 Google Sheets URL 中提取 gid (工作表分頁 ID)。
|
| 70 |
+
返回整數型別的 gid 或 None。
|
| 71 |
+
"""
|
| 72 |
+
parsed_url = urlparse(sheet_url)
|
| 73 |
+
query_params = parse_qs(parsed_url.query)
|
| 74 |
+
fragment_params = parse_qs(parsed_url.fragment) # gid 也可能在 # 後面
|
| 75 |
+
|
| 76 |
+
gid_str = None
|
| 77 |
+
if 'gid' in query_params:
|
| 78 |
+
gid_str = query_params['gid'][0]
|
| 79 |
+
elif 'gid' in fragment_params:
|
| 80 |
+
gid_str = fragment_params['gid'][0]
|
| 81 |
+
|
| 82 |
+
if gid_str:
|
| 83 |
+
try:
|
| 84 |
+
return int(gid_str)
|
| 85 |
+
except ValueError:
|
| 86 |
+
logging.warning(f"URL 中的 gid 不是有效的整數: {gid_str}")
|
| 87 |
+
return None
|
| 88 |
+
else:
|
| 89 |
+
# logging.info(f"URL 中未找到 gid 參數,將嘗試使用第一個工作表: {sheet_url}")
|
| 90 |
+
# 如果 URL 沒有 gid,通常表示是第一個工作表,其 gid 通常是 0
|
| 91 |
+
# 但我們在這裡返回 None,讓後續邏輯決定如何處理
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
def get_sheet_name_by_gid(self, spreadsheet_id: str, gid: int | None) -> str | None:
|
| 95 |
+
"""
|
| 96 |
+
使用 spreadsheetId 和 gid 獲取工作表名稱 (title)。
|
| 97 |
+
如果 gid 為 None,則返回第一個工作表的名稱。
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
spreadsheet_id (str): Google 試算表的 ID。
|
| 101 |
+
gid (int | None): 目標工作表分頁的 ID。如果為 None,則獲取第一個工作表。
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
str | None: 工作表的名稱 (title),如果找不到或發生錯誤則返回 None。
|
| 105 |
+
"""
|
| 106 |
+
if not self.service:
|
| 107 |
+
logging.error("Sheet API 服務未成功初始化。")
|
| 108 |
+
return None
|
| 109 |
+
try:
|
| 110 |
+
# 使用 spreadsheets.get 獲取試算表的中繼資料
|
| 111 |
+
# fields 參數限制只返回我們需要的 sheets.properties (包含 title 和 sheetId)
|
| 112 |
+
sheet_metadata = self.service.spreadsheets().get(
|
| 113 |
+
spreadsheetId=spreadsheet_id,
|
| 114 |
+
fields='sheets(properties(sheetId,title))'
|
| 115 |
+
).execute()
|
| 116 |
+
|
| 117 |
+
sheets = sheet_metadata.get('sheets', [])
|
| 118 |
+
if not sheets:
|
| 119 |
+
logging.warning(f"試算表 {spreadsheet_id} 中沒有找到任何工作表。")
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
if gid is not None:
|
| 123 |
+
# 如果提供了 gid,尋找匹配的工作表
|
| 124 |
+
for sheet in sheets:
|
| 125 |
+
properties = sheet.get('properties', {})
|
| 126 |
+
if properties.get('sheetId') == gid:
|
| 127 |
+
sheet_title = properties.get('title')
|
| 128 |
+
if sheet_title:
|
| 129 |
+
logging.info(f"找到 gid={gid} 對應的工作表名稱: '{sheet_title}'")
|
| 130 |
+
return sheet_title
|
| 131 |
+
else:
|
| 132 |
+
logging.warning(f"找到 gid={gid} 但缺少 title 屬性。")
|
| 133 |
+
return None
|
| 134 |
+
# 如果遍歷完畢沒有找到匹配的 gid
|
| 135 |
+
logging.warning(f"在試算表 {spreadsheet_id} 中未找到 gid={gid} 的工作表。")
|
| 136 |
+
return None
|
| 137 |
+
else:
|
| 138 |
+
# 如果 gid 為 None,返回第一個工作表的名稱
|
| 139 |
+
first_sheet_properties = sheets[0].get('properties', {})
|
| 140 |
+
first_sheet_title = first_sheet_properties.get('title')
|
| 141 |
+
first_sheet_gid = first_sheet_properties.get('sheetId', '未知')
|
| 142 |
+
if first_sheet_title:
|
| 143 |
+
logging.info(f"未提供 gid,使用第一個工作表 (gid={first_sheet_gid}): '{first_sheet_title}'")
|
| 144 |
+
return first_sheet_title
|
| 145 |
+
else:
|
| 146 |
+
logging.warning(f"第一個工作表 (gid={first_sheet_gid}) 缺少 title 屬性。")
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
except googleapiclient.errors.HttpError as error:
|
| 150 |
+
logging.error(f"獲取工作表名稱時發生 API 錯誤: {error}")
|
| 151 |
+
return None
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logging.error(f"獲取工作表名稱時發生未知錯誤: {e}")
|
| 154 |
+
return None
|
| 155 |
+
|
| 156 |
+
def get_sheet_data_by_url(self, sheet_url: str, read_range: str | None = None) -> list | None:
|
| 157 |
+
"""
|
| 158 |
+
通過 Google Sheets URL 自動獲取 Spreadsheet ID 和工作表名稱,並讀取數據。
|
| 159 |
+
如果 URL 中包含 gid,則讀取對應的工作表;否則讀取第一個工作表。
|
| 160 |
+
默認讀取整個工作表的數據。
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
sheet_url (str): Google 試算表的完整 URL。
|
| 164 |
+
read_range (str | None): 可選。指定要讀取的儲存格範圍 (例如 'A1:C10')。
|
| 165 |
+
如果提供,則只讀取此範圍;否則讀取整個工作表。
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
list | None: 包含讀取到的資料的列表 (list of lists),如果發生錯誤則返回 None。
|
| 169 |
+
"""
|
| 170 |
+
spreadsheet_id = self.get_sheet_id_by_url(sheet_url)
|
| 171 |
+
if not spreadsheet_id:
|
| 172 |
+
logging.error("無法從 URL 獲取 Spreadsheet ID。")
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
gid = self.get_sheet_gid_by_url(sheet_url)
|
| 176 |
+
# 無論 gid 是否為 None,都嘗試獲取工作表名稱
|
| 177 |
+
sheet_name = self.get_sheet_name_by_gid(spreadsheet_id, gid)
|
| 178 |
+
|
| 179 |
+
if not sheet_name:
|
| 180 |
+
logging.error(f"無法根據 URL ({sheet_url}) 確定要讀取的工作表名稱。")
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
# 組合 range_name
|
| 184 |
+
if read_range:
|
| 185 |
+
# 如果使用者指定了範圍,將其與工作表名稱結合
|
| 186 |
+
# 需要確保工作表名稱不包含特殊字符,或者正確引用
|
| 187 |
+
# 簡單起見,如果名稱包含空格或特殊符號,用單引號括起來
|
| 188 |
+
if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name:
|
| 189 |
+
range_name = f"'{sheet_name}'!{read_range}"
|
| 190 |
+
else:
|
| 191 |
+
range_name = f"{sheet_name}!{read_range}"
|
| 192 |
+
else:
|
| 193 |
+
# 如果未指定範圍,則讀取整個工作表
|
| 194 |
+
# 只需要提供工作表名稱即可
|
| 195 |
+
if ' ' in sheet_name or '!' in sheet_name or ':' in sheet_name:
|
| 196 |
+
range_name = f"'{sheet_name}'"
|
| 197 |
+
else:
|
| 198 |
+
range_name = sheet_name
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
logging.info(f"準備從試算表 '{spreadsheet_id}' 的 '{range_name}' 範圍讀取數據。")
|
| 202 |
+
# 使用現有的 get_sheet_value 方法讀取數據
|
| 203 |
+
return self.get_sheet_value(spreadsheet_id, range_name)
|
| 204 |
+
|
| 205 |
+
def get_sheet_value(self, spreadsheet_id: str, range_name: str) -> list | None:
|
| 206 |
+
"""
|
| 207 |
+
從指定的試算表和範圍讀取資料。
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
spreadsheet_id (str): Google 試算表的 ID。
|
| 211 |
+
range_name (str): 要讀取的範圍,例如 'Sheet1!A1:B2' 或僅 'Sheet1' (讀取整個工作表)。
|
| 212 |
+
|
| 213 |
+
Returns:
|
| 214 |
+
list | None: 包含讀取到的資料的列表 (list of lists),如果發生錯誤則返回 None。
|
| 215 |
+
"""
|
| 216 |
+
if not self.sheet:
|
| 217 |
+
logging.error("Sheet API 服務未成功初始化。")
|
| 218 |
+
return None
|
| 219 |
+
|
| 220 |
+
try:
|
| 221 |
+
logging.info(f"正在讀取 Spreadsheet ID: {spreadsheet_id}, Range: {range_name}")
|
| 222 |
+
|
| 223 |
+
result = self.sheet.values().get(
|
| 224 |
+
spreadsheetId=spreadsheet_id,
|
| 225 |
+
range=range_name
|
| 226 |
+
).execute()
|
| 227 |
+
values = result.get('values', [])
|
| 228 |
+
logging.info(f"成功從 {spreadsheet_id} 的 {range_name} 讀取 {len(values)} 列資料。")
|
| 229 |
+
# 如果 values 是 None 或空列表,直接返回
|
| 230 |
+
if not values:
|
| 231 |
+
logging.warning(f"在 {spreadsheet_id} 的 {range_name} 範圍內未找到任何資料。")
|
| 232 |
+
return [] # 返回空列表而不是 None,以便後續處理
|
| 233 |
+
return values
|
| 234 |
+
except googleapiclient.errors.HttpError as error:
|
| 235 |
+
# 更詳細地記錄錯誤信息
|
| 236 |
+
error_details = error.resp.get('content', '{}')
|
| 237 |
+
try:
|
| 238 |
+
error_json = json.loads(error_details)
|
| 239 |
+
error_message = error_json.get('error', {}).get('message', str(error))
|
| 240 |
+
except json.JSONDecodeError:
|
| 241 |
+
error_message = str(error)
|
| 242 |
+
logging.error(f"讀取試算表時發生 API 錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {error_message}")
|
| 243 |
+
return None
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logging.error(f"讀取試算表時發生未知錯誤 (ID: {spreadsheet_id}, Range: {range_name}): {e}")
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
@staticmethod
|
| 249 |
+
def flatten_column_data(data: list[list[str]]) -> list[str]:
|
| 250 |
+
"""
|
| 251 |
+
將從 Google Sheets API 獲取的單欄數據(列表的列表)扁平化為單一列表。
|
| 252 |
+
|
| 253 |
+
例如,將 [['A'], ['B'], ['C']] 轉換為 ['A', 'B', 'C']。
|
| 254 |
+
此方法會跳過空的內部列表,並假設每個非空內部列表只取第一個元素。
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
data (list[list[str]]): 從 API 獲取的原始數據,通常是 list of lists。
|
| 258 |
+
|
| 259 |
+
Returns:
|
| 260 |
+
list[str]: 包含所有第一欄元素的單一列表。如果輸入為 None 或空列表,
|
| 261 |
+
則返回空列表。
|
| 262 |
+
"""
|
| 263 |
+
if not data:
|
| 264 |
+
return []
|
| 265 |
+
# 使用列表推導式,提取每個子列表的第一個元素
|
| 266 |
+
# 添加 if sublist and sublist[0] is not None 確保子列表非空且第一個元素存在
|
| 267 |
+
# 並將其轉換為字串 str() 以確保類型一致性
|
| 268 |
+
flattened = [str(sublist[0]) for sublist in data if sublist and sublist[0] is not None]
|
| 269 |
+
return flattened
|