Python+JS混合方案:破解金山文档批量下载难题

Python+JS混合方案:破解金山文档批量下载难题 1. 项目缘起与核心痛点最近在整理团队资料时遇到了一个非常具体且磨人的需求需要把金山文档里一个包含了几百个文件的协作空间全部下载到本地进行归档和备份。这个需求听起来简单但实际操作起来你会发现金山文档官方并没有提供一个“一键打包下载”的功能。你只能一个个文件点开再手动选择“下载为”某种格式效率低到令人发指而且极其容易出错或遗漏。这让我想起了以前处理类似办公文档批量操作时的经历比如处理Excel表格或者Word文档但金山文档作为一个在线协作平台它的文件存储和访问逻辑又有些不同。这个“坑”的本质在于金山文档的在线文件系统是动态加载的其文件列表和下载链接并非静态呈现而是通过JavaScript动态渲染和接口调用来完成的。这就意味着传统的、简单的HTTP请求抓取页面HTML的方式行不通。你需要模拟浏览器的行为或者直接与它的后端API进行“对话”。我的目标很明确写一个脚本能自动登录或使用已有登录态、遍历指定文件夹下的所有文件、识别文件类型文档、表格、幻灯片等、并批量下载到本地且最好能保持原有的文件名和目录结构。经过一番摸索和尝试我最终组合使用了Python和浏览器开发者工具中的JavaScript形成了一套相对稳定且可复现的解决方案。Python负责整体的流程控制、网络请求和文件操作而JS代码主要通过复制粘贴到浏览器控制台执行则用于在已登录的页面上下文中快速获取那些受保护的API令牌和文件列表信息。下面我就把这套方法的核心思路、踩过的坑以及完整的操作步骤记录下来。2. 技术方案选型与思路拆解面对金山文档这种复杂的单页应用SPA直接上手写Python爬虫往往会碰壁。我们需要先理解它的数据流。2.1 为什么是Python JS的组合纯Python方案如requestsBeautifulSoup在遇到大量JS渲染和反爬机制时会变得非常笨重。你需要处理Cookie、Token、动态参数甚至可能模拟整个登录流程和页面交互复杂度极高。纯浏览器自动化方案如Selenium或Playwright可以完美模拟用户操作但缺点也很明显速度慢、资源占用高、不够稳定且难以进行精细化的错误处理和流程控制。因此我采用的是一种“混合动力”方案JS探路利用浏览器开发者工具F12在已经手动登录的金山文档页面中执行一些简短的JS代码片段。这些代码运行在页面的真实上下文中可以轻松访问到页面内嵌的API令牌、用户信息以及通过XHR/Fetch加载的原始JSON数据。这一步的目的是“侦察”获取那些对Python脚本至关重要的密钥如access_token和数据结构。Python主攻拿到JS侦察兵获取的关键信息后用Python编写主脚本。Python脚本使用requests库携带这些令牌直接调用金山文档的后端API接口进行文件列表的获取和文件下载。这样既绕开了复杂的页面渲染又保证了效率和稳定性。这个组合的核心优势在于JS用于在“信任环境”已登录的浏览器中安全地获取凭证Python利用这些凭证进行高效、批量的自动化操作。两者分工明确各取所长。2.2 核心流程设计整个批量下载流程可以分解为以下几个关键环节我画了一个简单的思维导图来帮助理解开始 │ ├─ 环节1人工介入浏览器登录金山文档 │ (获取登录态Cookie) │ ├─ 环节2JS侦察获取关键令牌(access_token)和空间/文件夹ID │ (在浏览器控制台执行代码) │ ├─ 环节3Python主脚本启动 │ │ │ ├─ 3.1 配置阶段读取JS环节获取的令牌和目标ID │ │ │ ├─ 3.2 遍历阶段调用API递归获取文件列表树 │ │ │ ├─ 3.3 下载阶段根据文件类型构造下载链接并保存 │ │ │ └─ 3.4 归档阶段在本地创建对应文件夹结构 │ └─ 结束本地获得完整文件备份注意此方案的前提是你拥有目标文档空间的访问权限。它解决的是“有权限但操作繁琐”的问题而非绕过权限验证。3. 实操第一步环境准备与信息侦察在写任何代码之前我们需要准备好“战场”和“情报”。3.1 Python环境与依赖库确保你的电脑上安装了Python 3.6及以上版本。我们主要会用到两个库requests用于发送HTTP请求。json用于处理API返回的JSON数据。通常json是Python标准库无需安装。只需要安装requests即可。打开你的终端或命令提示符执行pip install requests如果你需要处理更复杂的网络情况可以考虑安装requests_toolbelt但本项目基础功能不需要。3.2 浏览器端的关键信息获取JS侦察这是整个流程中最关键的一步我们需要从已登录的浏览器页面中“提取”出access_token和space_id或folder_id。登录并打开目标空间用你的浏览器Chrome/Firefox/Edge均可正常登录金山文档并进入你想要批量下载的那个“协作空间”或“文件夹”的页面。打开开发者工具按F12打开开发者工具切换到“网络”(Network)选项卡。为了清晰可以先点击垃圾桶图标清空之前的记录。触发一次文件列表加载在页面左侧的文件列表区域尝试滚动或点击进入某个子文件夹让浏览器发起网络请求。寻找关键请求在“网络”选项卡中你会看到很多请求。寻找一个类型为fetch或xhr名称看起来像list或children的请求其URL可能包含https://www.kdocs.cn/api/v3/这样的路径。点击这个请求查看它的“标头”(Headers)和“响应”(Response)。提取access_token在“标头”部分找到“请求标头”下的Authorization字段。它的值通常是Bearer eyJ...这样一长串字符。eyJ开头的部分就是你的access_token。复制它。重要这个token具有你的账户权限请像保管密码一样妥善保管不要泄露。提取空间或文件夹ID方法A从URL获取。观察浏览器地址栏当你进入一个协作空间时URL可能形如https://www.kdocs.cn/space/SPACE_ID。当你进入一个文件夹时URL可能形如https://www.kdocs.cn/folder/FOLDER_ID。复制这个ID。方法B从API响应获取。在上一步找到的list请求的“响应”(Response)标签页里通常是JSON格式。你可以找到一个id或obj_id字段这就是当前列表的ID。如果是根目录这个ID可能就是space_id。使用JS代码快速提取推荐 为了更准确和方便我们可以直接在“控制台”(Console)标签页执行一段JS代码来获取这些信息。在Console中输入以下代码并回车// 尝试从localStorage或页面全局变量中查找token let token localStorage.getItem(access_token) || window.__NUXT__?.state?.user?.token; // 如果上述方法不行从第一个找到的API请求头里提取需要先触发过请求 if (!token performance.getEntriesByType(resource).length 0) { // 这是一个简化的示例实际可能需要更复杂的查找逻辑 console.log(请先触发一次文件列表加载如滚动然后重试。或手动从Network标签页的请求头中复制Authorization字段值。); } else { console.log(可能的access_token (Bearer部分):, token); } // 获取当前空间或文件夹ID let path window.location.pathname; let id path.split(/).pop(); // 提取路径最后一部分 console.log(当前页面ID:, id); console.log(完整URL:, window.location.href);执行后控制台会输出相关信息。请将获取到的access_token和ID记录下来后面Python脚本会用到。实操心得有时候access_token可能存储在sessionStorage或其他加密变量里。如果上述JS代码没找到最可靠的方法还是从“网络”选项卡里找一个成功的API请求直接复制它的Authorization请求头值。这是万无一失的方法。4. Python核心脚本编写与详解拿到access_token和目标ID后我们就可以开始编写Python主脚本了。我将脚本分为几个功能模块便于理解和修改。4.1 脚本基础结构与配置首先创建一个Python文件比如kdocs_batch_downloader.py。开头部分我们引入库并设置基础配置。import requests import json import os from urllib.parse import unquote import time # 配置区域 # 在这里填入你在浏览器中获取的信息 ACCESS_TOKEN Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1... # 替换成你的token TARGET_ID 1234567890abcdefg # 替换成你的空间ID或文件夹ID BASE_URL https://www.kdocs.cn/api/v3 # 金山文档API基础地址 # 请求头携带认证令牌 HEADERS { Authorization: ACCESS_TOKEN, Content-Type: application/json, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } # 本地保存的根目录 SAVE_ROOT ./金山文档备份 # 配置结束 # 创建保存目录 os.makedirs(SAVE_ROOT, exist_okTrue) def make_request(url, methodGET, paramsNone, dataNone): 封装请求函数添加重试和错误处理 try: if method.upper() GET: resp requests.get(url, headersHEADERS, paramsparams, timeout30) else: resp requests.post(url, headersHEADERS, jsondata, timeout30) resp.raise_for_status() # 如果状态码不是200抛出HTTPError异常 return resp.json() except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) return None代码解读ACCESS_TOKEN和TARGET_ID是核心必须替换为你自己获取的值。User-Agent模拟浏览器避免被简单的反爬机制拦截。make_request函数是对requests的简单封装加入了超时和基础错误处理让后续代码更简洁。4.2 递归获取文件列表树金山文档的文件夹结构是树形的。我们需要一个函数来递归地获取某个文件夹或空间根目录下的所有文件和子文件夹。def get_file_list(node_id, node_typefolder, path): 递归获取文件列表 :param node_id: 当前节点ID空间ID或文件夹ID :param node_type: 节点类型space 或 folder :param path: 当前的本地相对路径用于构建目录结构 :return: 返回一个包含所有文件信息的列表 all_files [] current_path os.path.join(path, str(node_id)) # 临时用ID作为路径名后续会用真实名称替换 # 构建API请求参数 if node_type space: url f{BASE_URL}/spaces/{node_id}/children else: # folder url f{BASE_URL}/folders/{node_id}/children params { limit: 100, # 每页数量最大可能100 order_by: name, asc: 1 } page 1 while True: params[page] page print(f正在获取 {current_path} 的第 {page} 页...) data make_request(url, paramsparams) if not data or data not in data: print(f获取 {node_id} 列表失败或数据为空。) break items data.get(data, []) if not items: break # 没有更多数据了 for item in items: item_type item.get(obj_type) # file, folder item_name item.get(name, 未命名).strip() item_id item.get(obj_id) if not item_name or not item_id: continue # 构建文件/文件夹的完整信息字典 item_info { id: item_id, name: item_name, type: item_type, local_path: os.path.join(path, item_name) # 本地保存路径 } # 如果是文件夹递归获取其内容 if item_type folder: print(f进入文件夹: {item_name}) sub_files get_file_list(item_id, folder, os.path.join(path, item_name)) all_files.extend(sub_files) # 文件夹本身也需要记录吗通常我们只记录文件文件夹在下载时创建即可。 # 这里可以选择将文件夹信息也加入列表或者不加入。我们不加入只通过local_path创建目录。 elif item_type file: # 补充文件详情如下载格式、大小等如果需要 item_info.update({ file_type: item.get(file_type), # doc, sheet, slide等 size: item.get(size), modified_time: item.get(modified_time) }) all_files.append(item_info) print(f找到文件: {item_name}) # 判断是否还有下一页这里根据实际API响应调整有些用has_more字段 # 假设 items 数量小于 limit 就是最后一页 if len(items) params[limit]: break page 1 time.sleep(0.5) # 礼貌性延迟避免请求过快 return all_files关键点解析递归逻辑函数发现一个folder类型的项目时会递归调用自身并将当前路径path加上文件夹名作为新的路径传递下去。分页处理API通常会对列表进行分页。我们使用page和limit参数循环请求直到获取所有数据。limit100是常见的最大值。路径处理我们为每个文件计算了一个local_path这个路径是相对于本地根目录SAVE_ROOT的。这样在下载时就能直接创建对应的目录结构。延迟time.sleep在循环中增加短暂延迟是良好的网络公民行为可以避免对服务器造成过大压力也能减少被风控的风险。4.3 文件下载链接构造与保存获取文件列表后下一步是根据文件类型文档、表格、幻灯片构造正确的下载链接并保存到本地。def download_file(file_info, base_save_dir): 下载单个文件 :param file_info: 文件信息字典 :param base_save_dir: 本地保存的根目录 file_id file_info[id] file_name file_info[name] file_local_path file_info[local_path] file_type file_info.get(file_type, doc) # 默认为文档 # 1. 创建本地目录 local_dir os.path.join(base_save_dir, os.path.dirname(file_local_path)) os.makedirs(local_dir, exist_okTrue) # 2. 根据文件类型决定下载格式和API端点 # 金山文档支持多种导出格式这里我们选择最通用的格式 download_format_map { doc: (docx, documents), # 文档 - .docx sheet: (xlsx, spreadsheets), # 表格 - .xlsx slide: (pptx, presentations), # 幻灯片 - .pptx # 可以根据需要添加其他类型如 pdf 等 } if file_type not in download_format_map: print(f暂不支持的文件类型: {file_type}, 文件: {file_name}) return False file_ext, api_type download_format_map[file_type] # 清理文件名中的非法字符并添加后缀 safe_file_name .join([c for c in file_name if c not in r\/:*?|]).strip() local_file_path os.path.join(local_dir, f{safe_file_name}.{file_ext}) # 3. 构造下载链接并请求 # 注意下载链接可能需要额外的API调用获取临时下载地址 download_url f{BASE_URL}/{api_type}/{file_id}/export payload { format: file_ext.upper(), # 如 DOCX, XLSX csrf_token: 需要从页面获取或某些接口不需要 # 这是一个潜在的坑 } # 重要直接使用 /export 端点可能不行。更常见的是先获取文件下载信息。 # 让我们换一种更可靠的方式模拟“下载为”动作的API。 # 经过分析实际下载往往需要先请求一个/download接口获取临时链接。 download_info_url f{BASE_URL}/files/{file_id}/download download_info make_request(download_info_url) if not download_info or data not in download_info: print(f获取文件 {file_name} 下载信息失败。) # 备选方案尝试使用预览链接转换这里需要根据实际API调整。 # 例如有些文件可以通过 /files/{id}/preview 链接修改参数获得下载流。 return False # 假设返回的data里有 download_url 字段 real_download_url download_info[data].get(download_url) if not real_download_url: print(f文件 {file_name} 的下载链接为空。) return False # 4. 下载文件流 print(f正在下载: {file_name} - {local_file_path}) try: # 注意下载文件的请求可能不需要Authorization头或者需要不同的头。 # 但通常这个download_url是临时的、带签名的可以直接用。 resp requests.get(real_download_url, streamTrue, timeout60) resp.raise_for_status() with open(local_file_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f下载成功: {local_file_path}) return True except Exception as e: print(f下载文件 {file_name} 时出错: {e}) # 如果下载失败可以记录到日志文件稍后重试 return False避坑指南文件名清洗Windows和Mac/Linux系统对文件名中的特殊字符如\/:*?|处理方式不同直接使用可能报错。所以需要清洗。下载链接的获取/export端点可能不是通用的。我在这里遇到了第一个大坑。通过分析浏览器点击“下载为”时的网络请求我发现更常见的流程是先调用一个/files/{id}/download接口该接口返回一个包含临时download_url的JSON响应这个URL才是真正的文件下载地址。务必使用开发者工具跟踪“下载”动作的真实请求。流式下载对于大文件使用resp.iter_content(chunk_size8192)进行流式下载可以避免内存占用过高。Token有效性access_token可能过期通常有几小时到几天的有效期。如果脚本运行中途报错401 Unauthorized就需要重新执行JS侦察步骤获取新的token。4.4 主函数与流程控制最后我们将上述函数串联起来并加入一些简单的进度控制和错误记录。def main(): print( 金山文档批量下载脚本启动 ) print(f目标ID: {TARGET_ID}) print(f保存到: {os.path.abspath(SAVE_ROOT)}) # 步骤1获取所有文件列表 print(\n[步骤1] 正在遍历文件树这可能需要一些时间...) all_files get_file_list(TARGET_ID, folder if folder in TARGET_ID else space, ) # 注意TARGET_ID是空间还是文件夹需要判断这里简单用字符串判断可根据实际情况调整。 # 更严谨的做法是先用一个API测试ID的类型。 if not all_files: print(未获取到任何文件请检查目标ID和Token是否正确。) return print(f\n共发现 {len(all_files)} 个文件。) # 步骤2逐个下载文件 print(\n[步骤2] 开始下载文件...) success_count 0 fail_count 0 fail_list [] for idx, file_info in enumerate(all_files, 1): print(f\n进度: {idx}/{len(all_files)}) if download_file(file_info, SAVE_ROOT): success_count 1 else: fail_count 1 fail_list.append(file_info[name]) # 每个文件下载后稍作停顿避免请求过于密集 time.sleep(1) # 步骤3输出总结报告 print(\n *50) print(下载任务完成) print(f成功: {success_count} 个) print(f失败: {fail_count} 个) if fail_list: print(失败的文件列表:) for name in fail_list: print(f - {name}) print(*50) if __name__ __main__: main()5. 常见问题排查与实战技巧在实际运行中你几乎一定会遇到各种问题。下面是我踩过坑后总结的排查清单和应对技巧。5.1 错误码与解决方案速查表错误现象可能原因排查步骤与解决方案401 Unauthorized1.ACCESS_TOKEN错误或已过期。2.Authorization请求头格式不对。1.重新获取Token按3.2步骤在浏览器新开一个金山文档页面重新获取最新的access_token。2.检查格式确保HEADERS字典中Authorization的值是完整的如Bearer eyJ0eX...注意Bearer后面有一个空格。403 Forbidden1. 没有访问目标空间的权限。2. Token权限不足。3. 请求头缺少必要字段如Referer,Origin。1.确认权限确保当前登录的账号能访问该空间。2.补充请求头在HEADERS中尝试添加Referer: https://www.kdocs.cn/和Origin: https://www.kdocs.cn。3.降低频率可能是触发了风控增加time.sleep的间隔时间。404 Not Found1. API接口地址错误。2. 文件或文件夹ID不存在。1.核对API地址使用开发者工具找到正确的API端点并更新BASE_URL。2.检查ID确认TARGET_ID是否正确是否包含了多余字符。获取的文件列表为空1.node_type参数错误空间/文件夹。2. 分页逻辑有误只获取了第一页。1.区分空间和文件夹空间用/spaces/{id}/children文件夹用/folders/{id}/children。写一个判断函数。2.调试分页打印每次API请求的响应查看data结构和分页字段如has_more,total。下载的文件损坏或为0KB1. 下载链接是预览页而非二进制流。2. 下载请求未携带正确的Cookie或签名参数。1.检查下载链接确保real_download_url是直接指向二进制文件如以.docx、.xlsx结尾或响应头Content-Type为application/octet-stream。2.携带Cookie在下载请求中使用requests.Session()来保持会话或手动设置Cookie头从浏览器复制。脚本中途崩溃1. 网络不稳定。2. 单个文件下载超时。3. 遇到未处理的异常。1.增加异常捕获在download_file函数内用更细的try...except包裹。2.实现重试机制对下载失败的文件记录到列表脚本最后或单独重试。3.使用Session用requests.Session()可以提高连接复用和稳定性。5.2 高级技巧与优化建议使用Session保持连接在脚本开头创建一个requests.Session()对象并用它来发起所有请求。Session会自动处理Cookies且在HTTP长连接上有性能优势。session requests.Session() session.headers.update(HEADERS) # 然后在make_request函数中使用session.get/post实现断点续传对于大批量文件脚本可能因网络或其它原因中断。可以设计一个检查点机制将已成功下载的文件ID记录到一个JSON文件或数据库中。每次运行时先加载这个记录跳过已下载的文件。并发下载提升速度如果文件数量多且单个文件不大可以使用concurrent.futures模块的ThreadPoolExecutor实现多线程并发下载。但务必注意控制并发数如3-5个线程并妥善处理共享资源如文件写入避免被封IP。from concurrent.futures import ThreadPoolExecutor, as_completed def download_worker(file_info): return download_file(file_info, SAVE_ROOT) with ThreadPoolExecutor(max_workers3) as executor: future_to_file {executor.submit(download_worker, f): f for f in all_files} for future in as_completed(future_to_file): file_info future_to_file[future] try: result future.result() # 处理结果 except Exception as e: print(f文件 {file_info[name]} 生成异常: {e})更优雅的Token管理将Token等配置信息放在单独的config.json文件中脚本运行时读取。甚至可以写一个小的辅助脚本用Selenium自动登录并提取Token实现半自动化。处理“蜂窝表格”等特殊类型金山文档特有的“蜂窝表格”类似多维表可能没有标准的.xlsx导出格式。其下载链接或导出API可能不同。需要单独分析其网络请求并可能需要在下载后手动指定后缀名。6. 完整脚本整合与使用步骤将上述所有代码块按顺序整合到一个.py文件中。以下是清晰的、一步一步的操作指南安装Python与依赖确保已安装Python并在终端运行pip install requests。获取凭证浏览器登录金山文档进入目标空间/文件夹。按F12打开开发者工具切换到“网络”(Network)选项卡。触发文件列表加载如滚动。找到一个list或children类型的API请求从其“请求标头”中复制Authorization字段的完整值即ACCESS_TOKEN。从浏览器地址栏或API响应中复制空间ID或文件夹ID即TARGET_ID。配置脚本用文本编辑器打开kdocs_batch_downloader.py找到开头的配置区域将ACCESS_TOKEN和TARGET_ID替换为你自己的值。可以调整SAVE_ROOT设置本地保存路径。运行脚本在终端中切换到脚本所在目录运行python kdocs_batch_downloader.py。监控与处理脚本会打印遍历和下载进度。如果中途失败根据第5部分的排查表检查。常见的首次运行问题是Token失效或API端点不对请仔细对照开发者工具中的实际请求进行修正。这个方案虽然需要一些手动配置和潜在的调试但它提供了对金山文档批量下载过程的深度控制避免了浏览器自动化工具的笨重和不稳定。一旦跑通你就可以轻松地备份任何你有权限的协作空间了。