PyInstaller打包后Gmail API报错:OAuth客户端已删除(未删客户端)
我开发了一个Python版Gmail垃圾邮件过滤器,在VSCode中运行完全正常,但使用PyInstaller打包后,程序抛出错误:
"Error accessing Gmail: ('deleted_client: The OAuth client was deleted.', {'error': 'deleted_client', 'error_description': 'The OAuth client was deleted.'})"
我确认并未删除OAuth客户端,且尝试过打包时包含和不包含client secret文件,以下是我的代码:
import tkinter as tk from tkinter import scrolledtext from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.feature_extraction.text import CountVectorizer from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow import base64 import pickle import os import sys # ==== Gmail API Scope ==== SCOPES = ['https://www.googleapis.com/auth/gmail.modify'] # ==== ML Spam Classifier Setup ==== Emails = [ "bafake5232@decodewp.com", "brightsky39@emailondeck.com", "vortex.mirror@fakeinbox.com", "nightowl921@moakt.cc", "zenpanda456@guerrillamail.com", "coolcloud888@10minutemail.com", "flashyduck01@trashmail.com", "lunarwave21@temp-mail.org", "greenowl77@dropmail.me", "fastfox932@maildrop.cc", "davishunterboy@gmail.com", "hunterdavis@uagateway.org", "daishadavis1998@gmail.com", "gryphonnevermind@gmail.com", "info@centercourtclub.com", "resources@hjtep.org", "xiaoxiaolong.crystal@gmail.com", "Seijifank@gmail.com", "ellyh@cpe2.org", "qteepie1212@aol.com", "Budzeyday@aol.com", "laura.brown@meditechgroup.com", "user9321@guerrillamail.com", "test8823@temp-mail.org", "guest1145@10minutemail.com", "demo7432@dropmail.me", "mailbot6590@maildrop.cc", "emily.johnson94@gmail.com", "michael.brooks21@yahoo.com", "sarah.taylor@outlook.com", "daniel.martinez83@hotmail.com", "laura.nguyen01@gmail.com", "googlecloud@google.com","security@getgitguardian.com","no-reply@accounts.google.com","support@github.com","honinghindus@gmail.com","lilaroyjggdgdgrtyrg@gmail.com" ] labels = [1]*10 + [0]*11 + [1]*5 + [0]*10 + [1]*2 v = CountVectorizer() x = v.fit_transform(Emails) x_train, x_test, y_train, y_test = train_test_split(x, labels, test_size=0.01) model = DecisionTreeClassifier() model.fit(x_train, y_train) # ==== Gmail API Auth ==== def get_client_secret_path(): filename = '/Users/huntergoat/Documents/Data Science /client_secret_338974916213-ub39j2ralqoi2a35so9a04u41d3rqkh2.apps.googleusercontent.com.json' if getattr(sys, 'frozen', False): # Running from PyInstaller return os.path.join(sys._MEIPASS, filename) else: # Running from source return os.path.abspath(filename) def authenticate_gmail(): global service creds = None if os.path.exists('token.pickle'): with open('token.pickle', 'rb') as token: creds = pickle.load(token) else: flow = InstalledAppFlow.from_client_secrets_file(get_client_secret_path(), SCOPES) creds = flow.run_local_server(port=0) with open('token.pickle', 'wb') as token: pickle.dump(creds, token) service = build('gmail', 'v1', credentials=creds) return service # ==== Spam Detection + GUI Integration ==== def classify_emails(): result_text.delete(1.0, tk.END) try: num_to_fetch = int(email_count_entry.get()) if num_to_fetch <= 0: result_text.insert(tk.END, "Please enter a number greater than 0.\n") return except ValueError: result_text.insert(tk.END, "Invalid input. Enter a number.\n") return try: service = authenticate_gmail() except Exception as e: result_text.insert(tk.END, f"Auth Error: {e}\n") return try: results = service.users().messages().list(userId='me', labelIds=['INBOX'], maxResults=num_to_fetch).execute() messages = results.get('messages', []) except Exception as e: result_text.insert(tk.END, f"Error accessing Gmail: {e}\n") return if not messages: result_text.insert(tk.END, "No emails found.\n") return email_list = [] full_message_info = [] for msg in messages: msg_data = service.users().messages().get(userId='me', id=msg['id']).execute() headers = msg_data['payload']['headers'] sender = next((h['value'] for h in headers if h['name'] == 'From'), "Unknown") email_list.append(sender) full_message_info.append((msg['id'], sender)) vectors = v.transform(email_list) predictions = model.predict(vectors) for (msg_id, sender), label in zip(full_message_info, predictions): status = "SPAM" if label == 1 else "NOT SPAM" result_text.insert(tk.END, f"{sender} => {status}\n") if label == 1: service.users().messages().modify( userId='me', id=msg_id, body={'addLabelIds': ['SPAM'], 'removeLabelIds': ['INBOX']} ).execute() result_text.insert(tk.END, "→ Moved to SPAM folder.\n") result_text.insert(tk.END, "\n") # ==== GUI Setup ==== root = tk.Tk() root.title("Gmail Spam Classifier") root.geometry("640x500") top_frame = tk.Frame(root) top_frame.pack(pady=10) tk.Label(top_frame, text="Number of emails to scan:", font=("Arial", 12)).pack(side=tk.LEFT, padx=5) email_count_entry = tk.Entry(top_frame, width=5, font=("Arial", 12)) email_count_entry.pack(side=tk.LEFT) email_count_entry.insert(0, "10") # default value scan_btn = tk.Button(top_frame, text="Scan Inbox", command=classify_emails, font=("Arial", 12)) scan_btn.pack(side=tk.LEFT, padx=10) result_text = scrolledtext.ScrolledText(root, wrap=tk.WORD, width=80, height=25, font=("Courier", 10)) result_text.pack(pady=10) root.mainloop()
问题根源及修复方案
1. Client Secret路径处理错误
当前代码中get_client_secret_path函数的filename使用了完整绝对路径,打包后sys._MEIPASS是程序临时运行目录,把绝对路径拼进去会导致文件找不到,进而触发OAuth客户端不存在的错误(因为加载不到合法的client secret)。
修改get_client_secret_path函数,拆分路径和文件名:
def get_client_secret_path(): # 仅保留文件名,路径部分分开处理 filename = 'client_secret_338974916213-ub39j2ralqoi2a35so9a04u41d3rqkh2.apps.googleusercontent.com.json' if getattr(sys, 'frozen', False): # 打包后从临时目录读取 return os.path.join(sys._MEIPASS, filename) else: # 开发环境用原路径 return os.path.join('/Users/huntergoat/Documents/Data Science ', filename)
2. 添加PyInstaller打包参数
必须用--add-data参数将client secret文件包含到打包资源中,否则程序运行时无法访问该文件。执行以下打包命令(替换your_script_name.py为你的脚本文件名):
pyinstaller --onefile --add-data "client_secret_338974916213-ub39j2ralqoi2a35so9a04u41d3rqkh2.apps.googleusercontent.com.json:." your_script_name.py
参数说明:--add-data "源文件:目标目录",.表示打包后文件和程序放在同一目录。
3. Token.pickle路径优化
打包后程序运行目录可能与开发环境不同,建议确保token.pickle生成在程序当前工作目录(代码中已实现,无需修改),避免权限问题导致无法读取或写入授权令牌。
额外注意事项
- 打包前将client secret文件放到脚本同一目录,或调整
--add-data的源文件路径 - 第一次运行打包后的程序时,会弹出浏览器要求授权,授权成功后会在程序运行目录生成
token.pickle,后续运行无需重复授权 - 如果之前在开发环境生成过
token.pickle,可以直接复制到打包后程序的运行目录,跳过授权步骤
内容的提问来源于stack exchange,提问作者Hunter Davis

