You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyInstaller打包后Gmail API报错:OAuth客户端已删除(未删客户端)

Gmail垃圾邮件过滤器PyInstaller打包后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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 17:24:52