我朋友对你的回复如下:
答案 2 ("修复"的 V2 答案):
这个答案是 100% 错误的,它会让你再次 404。
它的核心论点是:"你用的URL是 .../styles_batch_update,而正确的URL是 .../styles_batch_update。" —— 这两串地址一模一样!
它建议你使用的 API_V2_STYLE_URL 就是你现在这个 404 错误的 URL。
虽然它提到的 V2 Payload 结构 {"data": [...]} 可能是对的(你用的是 {"styleRanges": [...]}),但这根本不重要,因为你的 404 错误发生在 URL 层面,服务器根本没去看你的 Payload 就报错了。
我现在的代码如下:
import os
import json
import requests
import sys
from typing import Dict, Any, List, Tuple, Set, Optional
import urllib.parse
import time
import datetime
import re
from dotenv import load_dotenv
from collections import defaultdict
# === 辅助工具 ===
class Timer:
def __init__(self): self._start = None
def start(self): self._start = time.perf_counter()
def stop(self, msg=""):
if self._start: print(f" - [计时] {msg}耗时: {time.perf_counter() - self._start:.4f}s"); self._start = None
# --- 加载配置 ---
try: script_dir = os.path.dirname(os.path.realpath(__file__))
except: script_dir = os.path.abspath(os.getcwd())
dotenv_path = os.path.join(script_dir, '.env')
load_dotenv(dotenv_path) if os.path.exists(dotenv_path) else None
APP_ID = os.getenv("APP_ID")
APP_SECRET = os.getenv("APP_SECRET")
SPREADSHEET_TOKEN = os.getenv("SPREADSHEET_TOKEN")
REDIRECT_URI = os.getenv("REDIRECT_URI", "http://localhost:8080/auth")
TOKEN_FILE = "feishu_token.json"
# 列配置
ATTR_COL = os.getenv("ATTRIBUTE_COL", "B") # (新) 属性列
NAME_COL = os.getenv("MATERIAL_NAME_COL", "A") # 物料名称
S1_CODE_COL = os.getenv("SUPPLIER_1_CODE_COL", "C")
S1_SUP_COL = os.getenv("SUPPLIER_1_SUPPLIER_COL", "D")
S2_CODE_COL = os.getenv("SUPPLIER_2_CODE_COL", "E")
S2_SUP_COL = os.getenv("SUPPLIER_2_SUPPLIER_COL", "F")
START_ROW = int(os.getenv("START_ROW", "2"))
END_ROW = int(os.getenv("END_ROW", "500"))
SUMMARY_SHEET_NAME = os.getenv("SUMMARY_SHEET_NAME", "零部件比对汇总(新)")
# === 1. 认证模块 (保持不变) ===
def save_token(data):
try:
with open(os.path.join(script_dir, TOKEN_FILE), 'w') as f: json.dump(data, f, indent=4)
except Exception as e: print(f"Error saving token: {e}")
def load_token():
path = os.path.join(script_dir, TOKEN_FILE)
if not os.path.exists(path): return {}
try:
with open(path, 'r') as f:
d = json.load(f)
if 'expires_at' in d: d['expires_at'] = datetime.datetime.fromisoformat(d['expires_at'])
return d
except: return {}
def get_access_token():
data = load_token()
if data.get("access_token") and data.get("expires_at", datetime.datetime.min) > datetime.datetime.now():
return data["access_token"]
if data.get("refresh_token"):
try:
url = "https://open.feishu.cn/open-apis/authen/v2/oauth/token"
resp = requests.post(url, json={"grant_type": "refresh_token", "client_id": APP_ID, "client_secret": APP_SECRET, "refresh_token": data["refresh_token"]})
resp.raise_for_status()
res = resp.json()
if res.get("code") == 0:
new_data = {"access_token": res["access_token"], "refresh_token": res["refresh_token"],
"expires_at": (datetime.datetime.now() + datetime.timedelta(seconds=res["expires_in"]-300)).isoformat()}
save_token({**new_data, "expires_at": new_data["expires_at"]})
return res["access_token"]
except: pass
scope = "sheets:spreadsheet sheets:spreadsheet:read sheets:spreadsheet:write_only sheets:spreadsheet.meta:read sheets:spreadsheet.meta:write"
url = f"https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id={APP_ID}&response_type=code&redirect_uri={urllib.parse.quote(REDIRECT_URI)}&scope={scope}"
print(f"请打开链接授权:\n{url}")
code = input("请输入Code: ").strip()
resp = requests.post("https://open.feishu.cn/open-apis/authen/v2/oauth/token", json={
"grant_type": "authorization_code", "client_id": APP_ID, "client_secret": APP_SECRET, "code": code, "redirect_uri": REDIRECT_URI})
res = resp.json()
if res.get("code") != 0: raise Exception(f"Auth failed: {res}")
token_data = {"access_token": res["access_token"], "refresh_token": res.get("refresh_token"),
"expires_at": (datetime.datetime.now() + datetime.timedelta(seconds=res["expires_in"]-300)).isoformat()}
save_token(token_data)
return res["access_token"]
# === 2. 飞书 API 基础操作 ===
def call_api(method, url, json_data=None, params=None):
token = get_access_token()
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json; charset=utf-8"}
resp = requests.request(method, url, headers=headers, json=json_data, params=params)
resp.raise_for_status()
res = resp.json()
if res.get("code", 0) != 0: raise Exception(f"API Error: {res}")
return res
def get_sheet_id_by_name(sp_token, name):
res = call_api("GET", f"https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/{sp_token}/sheets/query")
for sheet in res["data"]["sheets"]:
if sheet["title"] == name: return sheet["sheet_id"]
return None
def create_sheet(sp_token, name):
exist = get_sheet_id_by_name(sp_token, name)
if exist: return exist
# create_sheet 仍然使用 .../sheets_batch_update (这没错, V2的批量更新)
res = call_api("POST", f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{sp_token}/sheets_batch_update",
json_data={"requests": [{"addSheet": {"properties": {"title": name}}}]})
return res["data"]["replies"][0]["addSheet"]["properties"]["sheetId"]
def read_range(sp_token, sheet_id, col, s_row, e_row):
rng = f"{sheet_id}!{col}{s_row}:{col}{e_row}"
# read_range 使用 .../values_batch_get (这也没错, V2的读取)
res = call_api("GET", f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{sp_token}/values_batch_get", params={"ranges": rng})
vals = res["data"]["valueRanges"][0]["values"]
result = []
for i, row in enumerate(vals):
val = ""
if row:
cell = row[0]
if isinstance(cell, dict): val = cell.get("text", "")
else: val = str(cell)
result.append({"row": s_row + i, "val": val.strip()})
return result
# === 3. 工具函数:(保持不变) ===
def _col_str_to_idx(col_str: str) -> int:
idx = 0
for char in col_str:
idx = idx * 26 + (ord(char) - ord('A')) + 1
return idx - 1
def parse_range(range_str: str) -> Dict[str, Any]:
try:
sheet_id, coords = range_str.split('!')
match = re.match(r"([A-Z]+)(\d+):([A-Z]+)(\d+)", coords)
if match:
start_col_str, start_row_str, end_col_str, end_row_str = match.groups()
start_row_idx = int(start_row_str) - 1
end_row_idx = int(end_row_str)
start_col_idx = _col_str_to_idx(start_col_str)
end_col_idx = _col_str_to_idx(end_col_str) + 1
else:
match_single = re.match(r"([A-Z]+)(\d+)", coords)
if not match_single:
raise ValueError(f"Invalid range coordinate format: {coords}")
col_str, row_str = match_single.groups()
start_row_idx = int(row_str) - 1
end_row_idx = start_row_idx + 1
start_col_idx = _col_str_to_idx(col_str)
end_col_idx = start_col_idx + 1
return {
"sheetId": sheet_id,
"startRowIndex": start_row_idx,
"endRowIndex": end_row_idx,
"startColumnIndex": start_col_idx,
"endColumnIndex": end_col_idx
}
except Exception as e:
print(f"ERROR: 解析范围字符串 '{range_str}' 失败: {e}")
return {}
def num_to_col_char(n):
string = ""
while n > 0:
n, remainder = divmod(n - 1, 26)
string = chr(65 + remainder) + string
return string
# === 4. 核心逻辑:数据抓取与对齐 (保持不变) ===
ProjectData = Dict[str, Dict[str, Tuple[str, str]]]
def fetch_all_data(sp_token, sheets):
aligned_data = defaultdict(lambda: {"attr": "", "projects": {}})
for sheet in sheets:
title, sid = sheet["title"], sheet["sheet_id"]
print(f" - 读取项目: {title} ...")
names = read_range(sp_token, sid, NAME_COL, START_ROW, END_ROW)
attrs = read_range(sp_token, sid, ATTR_COL, START_ROW, END_ROW)
s1_codes = read_range(sp_token, sid, S1_CODE_COL, START_ROW, END_ROW)
s1_sups = read_range(sp_token, sid, S1_SUP_COL, START_ROW, END_ROW)
s2_codes = read_range(sp_token, sid, S2_CODE_COL, START_ROW, END_ROW)
s2_sups = read_range(sp_token, sid, S2_SUP_COL, START_ROW, END_ROW)
for i in range(len(names)):
name = names[i]["val"]
if not name: continue
attr = attrs[i]["val"] if i < len(attrs) else ""
s1_c = s1_codes[i]["val"] if i < len(s1_codes) else ""
s1_s = s1_sups[i]["val"] if i < len(s1_sups) else ""
s2_c = s2_codes[i]["val"] if i < len(s2_codes) else ""
s2_s = s2_sups[i]["val"] if i < len(s2_sups) else ""
if not aligned_data[name]["attr"] and attr:
aligned_data[name]["attr"] = attr
aligned_data[name]["projects"][title] = {
"s1": (s1_s, s1_c), # (供应商, 料号)
"s2": (s2_s, s2_c)
}
return aligned_data
def main():
print("=== 开始执行:多项目零部件比对与样式生成 ===")
# 1. 获取所有 Sheet
all_sheets_meta = call_api("GET", f"https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/{SPREADSHEET_TOKEN}/sheets/query")["data"]["sheets"]
valid_sheets = [s for s in all_sheets_meta if s["title"] != SUMMARY_SHEET_NAME]
print("发现以下项目:")
for i, s in enumerate(valid_sheets): print(f" [{i+1}] {s['title']}")
sel_idx = input("请输入要比对的项目序号 (如 1,2,3 或 all): ").strip()
if sel_idx.lower() == "all": selected = valid_sheets
else: selected = [valid_sheets[int(x)-1] for x in sel_idx.split(",") if x.strip().isdigit()]
if len(selected) < 2: print("至少选择两个项目!"); return
proj_names = [s["title"] for s in selected]
# 2. 读取并对齐数据
timer = Timer()
timer.start()
data_map = fetch_all_data(SPREADSHEET_TOKEN, selected)
timer.stop("数据读取与对齐")
# 3. 准备写入数据矩阵
sorted_names = sorted(data_map.keys())
matrix = []
# --- 构建表头行 ---
row1 = ["项目", "属性"]
for p in proj_names:
row1.extend([p, "", "", ""])
matrix.append(row1)
row2 = ["", ""]
for _ in proj_names:
row2.extend(["一供", "", "二供", ""])
matrix.append(row2)
row3 = ["物料名称", ""]
for _ in proj_names:
row3.extend(["供应商", "物料料号", "供应商", "物料料号"])
matrix.append(row3)
# --- 构建数据行与样式计算 ---
COLOR_GREEN = "#b7e1cd"
COLOR_RED = "#f4c7c3"
green_cells = []
red_cells = []
current_row_idx = 4
for name in sorted_names:
item = data_map[name]
attr = item["attr"]
row_data = [name, attr]
pair_counts = defaultdict(int)
for p_name in proj_names:
p_data = item["projects"].get(p_name, {"s1":("",""), "s2":("","")})
s1 = p_data["s1"]
s2 = p_data["s2"]
if s1[0] and s1[1]: pair_counts[s1] += 1
if s2[0] and s2[1]: pair_counts[s2] += 1
col_idx = 3
for p_name in proj_names:
p_data = item["projects"].get(p_name, {"s1":("",""), "s2":("","")})
s1 = p_data["s1"]
s2 = p_data["s2"]
row_data.extend([s1[0], s1[1]])
if s1[0] and s1[1]:
cell_sup = f"{num_to_col_char(col_idx)}{current_row_idx}"
cell_code = f"{num_to_col_char(col_idx+1)}{current_row_idx}"
if pair_counts[s1] > 1:
green_cells.extend([cell_sup, cell_code])
else:
red_cells.extend([cell_sup, cell_code])
col_idx += 2
row_data.extend([s2[0], s2[1]])
if s2[0] and s2[1]:
cell_sup = f"{num_to_col_char(col_idx)}{current_row_idx}"
cell_code = f"{num_to_col_char(col_idx+1)}{current_row_idx}"
if pair_counts[s2] > 1:
green_cells.extend([cell_sup, cell_code])
else:
red_cells.extend([cell_sup, cell_code])
col_idx += 2
matrix.append(row_data)
current_row_idx += 1
# 4. 写入表格
timer.start()
sum_sid = create_sheet(SPREADSHEET_TOKEN, SUMMARY_SHEET_NAME)
print(f"目标Sheet: {SUMMARY_SHEET_NAME} (ID: {sum_sid})")
if matrix and len(matrix) > 0:
num_rows = len(matrix)
num_cols = len(matrix[0])
end_col_letter = num_to_col_char(num_cols)
full_range = f"{sum_sid}!A1:{end_col_letter}{num_rows}"
print(f" - GangZaiXieRuFanWei: {full_range} ...")
call_api("PUT", f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
json_data={"valueRange": {"range": full_range, "values": matrix}})
else:
print(" - 警告: 没有数据需要写入。")
print(" - 数据写入完成")
# 5. 准备合并请求
print(" - 正在准备单元格合并 (表头)...")
merge_list = []
merge_list.append(f"{sum_sid}!A1:A2") # 项目 (FIX: A1:A2, 之前是 A1:A3)
merge_list.append(f"{sum_sid}!B1:B3") # 属性
current_col = 3 # C列
for _ in proj_names:
start = num_to_col_char(current_col)
end = num_to_col_char(current_col + 3)
merge_list.append(f"{sum_sid}!{start}1:{end}1") # Proj Name
s1_start = num_to_col_char(current_col)
s1_end = num_to_col_char(current_col + 1)
merge_list.append(f"{sum_sid}!{s1_start}2:{s1_end}2") # 一供
s2_start = num_to_col_char(current_col + 2)
s2_end = num_to_col_char(current_col + 3)
merge_list.append(f"{sum_sid}!{s2_start}2:{s2_end}2") # 二供
current_col += 4
# ★★★ 使用专用的 .../merge_cells API (V2, 这个没问题) ★★★
API_V2_MERGE_URL = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/merge_cells"
if merge_list:
print(f" - 正在批量执行 {len(merge_list)} 个合并请求...")
# 这个接口不是批量的,我们必须循环调用
for rng_str in merge_list:
payload = {
"range": rng_str, # A1 字符串格式,带 sheetId
"mergeType": "MERGE_ALL"
}
try:
call_api("POST", API_V2_MERGE_URL, json_data=payload)
except Exception as e:
print(f"WARN: 合并 {rng_str} 失败: {e}")
print(" - 表头合并完成")
# 6. 准备上色请求
print(f" - 正在准备应用样式 (红: {len(red_cells)/2}组, 绿: {len(green_cells)/2}组)...")
# ★★★ 核心修复: 切换到 V3 API ★★★
# V3 接口的 URL 包含 sheet_id (sum_sid)
# 这将替换掉 404 的 V2 .../styles_batch_update 接口
API_V3_STYLE_URL = f"https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/{SPREADSHEET_TOKEN}/sheets/{sum_sid}/batch_update_style"
style_requests = []
if red_cells or green_cells:
color_map = {}
if red_cells: color_map[COLOR_RED] = red_cells
if green_cells: color_map[COLOR_GREEN] = green_cells
# 构造 V3 payload
for color, cells in color_map.items():
for cell in cells:
style_requests.append({
"range": cell, # V3 的 range 不带 sheet_id! 前缀 (修复点 1)
"style": {"backColor": color}
})
if style_requests:
print(f" - 正在批量执行 {len(style_requests)} 个样式请求...")
chunk_size = 100 # V3 API 批次限制通常比 100 高,但 100 绝对安全
for i in range(0, len(style_requests), chunk_size):
chunk = style_requests[i:i + chunk_size]
print(f" - 正在提交样式批次 {i//chunk_size + 1}/{ (len(style_requests) // chunk_size) + 1 }...")
# 构造正确的 V3 payload (修复点 2)
payload = {"requests": chunk}
call_api("POST", API_V3_STYLE_URL, # 使用 V3 URL
json_data=payload) # 使用 V3 payload
else:
print(" - 没有样式需要更新。")
timer.stop("写入、合并与上色")
print("任务完成!请查看飞书文档。")
if __name__ == "__main__":
try: main()
except Exception as e:
import traceback
traceback.print_exc()
print(f"Error: {e}")
运行后报错如下:
目标Sheet: R500C&R510超薄&R520半包物料差异比对-2 (ID: lUYLu)
- GangZaiXieRuFanWei: lUYLu!A1:N72 ...
- 数据写入完成
- 正在准备单元格合并 (表头)...
- 正在批量执行 11 个合并请求...
- 表头合并完成
- 正在准备应用样式 (红: 87.0组, 绿: 227.0组)...
- 正在批量执行 628 个样式请求...
- 正在提交样式批次 1/7...
Traceback (most recent call last):
File "D:\python-code\PROJECT-DATE\关键零部件清单差异化比对\关键零部件清单差异化比对-格式优化2.py", line 419, in <module>
try: main()
^^^^^^
File "D:\python-code\PROJECT-DATE\关键零部件清单差异化比对\关键零部件清单差异化比对-格式优化2.py", line 410, in main
call_api("POST", API_V3_STYLE_URL, # 使用 V3 URL
File "D:\python-code\PROJECT-DATE\关键零部件清单差异化比对\关键零部件清单差异化比对-格式优化2.py", line 97, in call_api
resp.raise_for_status()
File "C:\Program Files\Python311\Lib\site-packages\requests\models.py", line 1026, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/EYRysBZWkhbA4vts3yLcwitNn1g/sheets/lUYLu/batch_update_style
Error: 404 Client Error: Not Found for url: https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/EYRysBZWkhbA4vts3yLcwitNn1g/sheets/lUYLu/batch_update_style
进程已结束,退出代码为 0
最新发布