OSS PDF下载到指定文件夹:从原理到批量落地的完整方案

发布时间:2026/9/7 17:22:57
OSS PDF下载到指定文件夹:从原理到批量落地的完整方案 平时处理OSS里的PDF文件最头疼的往往不是上传反而是下载。尤其当你想把某个PDF直接落到本地指定文件夹而不是让浏览器随手丢进“下载”目录时各种小问题就冒出来了文件名乱码、下载一半失败、明明有权限却报403、批量下载时目录结构全乱……我这几年在项目里被这些问题来回折腾过今天把自己沉淀下来的那套完整方案整理出来从原理到代码从单文件到批量一次说清楚。这篇内容适合刚接触对象存储的开发者也适合正在做文件管理系统的朋友。不管你是用Python写脚本、在Java后端做中转还是FastAdmin/PHP项目里集成了OSS都能直接照着操作需要的代码和参数我尽量给全顺便把踩过的坑都标出来。1. 在做之前先把方案想清楚OSS下载PDF的几种落地方式很多人一上来就写代码结果做到一半发现路子不对再回头改很浪费时间。先讲清楚“把OSS里的PDF下载到指定文件夹”在不同场景下到底意味着什么这决定了后面整个技术选型。1.1 不同场景决定了“指定文件夹”的真实含义“指定文件夹”这句话在开发者和普通用户口中指的不是一回事。如果你是用户想的是我打开浏览器点一个按钮PDF直接保存到我电脑的“D:\工作文档\合同”里。如果你是开发者想的是后端调用SDK从OSS拿到文件流写入服务器上的某个目录。这两种诉求有本质区别。浏览器出于安全限制网页脚本是无法任意指定用户本地磁盘文件夹的。你在浏览器里触发下载能控制的大概就是默认下载目录、文件名最多通过浏览器的“下载前询问每个文件的保存位置”选项让用户手动选文件夹。所以如果目标是“纯前端实现任意指定本地目录”那基本是死路必须借助后端或客户端程序的配合。如果目标是“服务端下载到指定目录”那路子就宽了Python、Java、Go、Node.js都有官方SDK直接在代码里指定本地路径自己组装目录结构想放哪就放哪。这也是这篇文章的核心场景。1.2 三种主流实现路径对比根据我实际项目里的经验OSS下载PDF通常就三条路浏览器直链下载前端简单场景通过OSS提供的URL或者生成临时签名URL直接让浏览器下载。优点是实现成本极低缺点是无法精确控制保存位置适合“下载到默认目录就够用”的场景。后端SDK下载后保存到服务器目录精确指定场景服务端调用OSS SDK的get_object_to_file一类的接口把对象流写入指定本地绝对路径。优点是可以精确定位、批量处理、自动维护目录结构缺点是需要写后端代码走一遍服务器IO。客户端程序直连OSS桌面端/运维场景用Python写个命令行工具或小脚本调SDK把文件拉到本地某个固定目录适合批量同步PDF、定期归档这类自动化任务。三条路没有绝对的好坏。我的建议是只是临时下载单个文件用方案1要做批量、定时、目录归档用方案2或3。下文重点展开方案2和3因为方案1太简单网上教程一堆而真正的坑全藏在后面两种里。2. 核心前置OSS文件下载与权限模型跳过这步直接写代码会踩到很多莫名其妙的坑。OSS里的“文件夹”概念、下载权限、签名URL机制这三样东西不搞清楚后面全是坑。2.1 Object Key就是“虚拟文件夹”理解路径与存储实际很多刚用OSS的人会误以为OSS像本地磁盘一样有真正的文件夹层级。其实OSS是扁平架构它只有一个“桶”Bucket概念桶里存的全是对象Object。你看到的“文件夹”其实是Object Key的前缀。举个例子你往OSS上传了一个PDF路径是pdf/contracts/2024/服务合同.pdf。这个完整的字符串就是Object Key它包含了一个模拟的目录层级。OSS并没有真正创建一个叫pdf的文件夹再创建一个叫contracts的文件夹它只是把整个Key作为一个对象的唯一标识存了起来。这个理解非常重要因为下载到本地指定文件夹时你要做的事情本质上是把对象的Key末尾的文件名取出来再拼上一个本地绝对路径的前缀。如果本地想要保留和线上一样的分层结构就得自己按分隔符/拆解Key逐级创建本地目录。我曾见过有人直接用整个Key当本地文件名结果冒出一堆“含路径符”的错误根源就是没理解这个模型。2.2 下载必需的两个前置条件权限与URL任何从OSS下载PDF的操作都必须先过权限这道关。OSS的权限模型主要有三层Bucket级别权限桶是公有读Public Read还是私有Private。如果是公有读PDF可以直接通过https://bucket-name.region.aliyuncs.com/pdf/xxx.pdf访问如果是私有裸URL访问会得到AccessDenied。RAM用户权限如果用AccessKey调用SDK必须确保这个RAM账号有oss:GetObject权限。经常会遇到明明AccessKey是对的但下载时报403十有八九是RAM策略里没加GetObject只加了上传权限。STS临时凭证在Web后端给前端发临时凭证时临时Token的Policy需要包含目标Object前缀的读取权限。如果你走的是“生成URL让浏览器下载”路线那么私有桶要先通过SDK生成一个带签名的URL比如get_signed_urlPython或generatePresignedUrlJava默认有效期一般是15到60分钟。签名URL里会带一堆Expires和Signature参数浏览器直接拿它就能打开PDF。一句话总结SDK下载需要AccessKey具备GetObject权限URL直链下载需要对象可读或生成签名URL。这两条打通了下载动作才从“可能失败”变成“一定成功”。3. 实操Python实现OSS PDF下载到指定文件夹Python是我做这类任务首选的语言SDK设计清晰处理批量任务也很顺手。直接上完整可运行的方案。3.1 环境准备与依赖安装先用pip安装官方SDKpip install oss2然后准备好三个信息Endpoint地域节点、AccessKey ID、AccessKey Secret。如果用的是STS临时凭证还需要SecurityToken。这里要特别提醒一个新人常犯的错误Endpoint填错会导致连接超时。阿里云OSS的Endpoint分内网和公网如果你的脚本运行在ECS上建议用内网Endpoint比如oss-cn-hangzhou-internal.aliyuncs.com速度快且省流量本地开发就用公网Endpoint。填的时候别加https://前缀SDK会自动根据is_secure参数决定协议。3.2 下载PDF到指定文件夹的完整代码与参数说明下面这段代码实现了“从OSS下载一个PDF到本地指定文件夹并保持原始文件名”import os import oss2 # 配置OSS信息 ENDPOINT oss-cn-hangzhou.aliyuncs.com ACCESS_KEY_ID your-access-key-id ACCESS_KEY_SECRET your-access-key-secret BUCKET_NAME your-bucket-name # 本地目标文件夹不存在会自动创建 LOCAL_DIR rD:\pdf_downloads\contracts # 初始化Bucket对象 auth oss2.Auth(ACCESS_KEY_ID, ACCESS_KEY_SECRET) bucket oss2.Bucket(auth, ENDPOINT, BUCKET_NAME) def download_pdf_to_folder(object_key: str, local_dir: str) - str: 将OSS中的PDF对象下载到指定本地文件夹。 object_key 示例: pdf/contracts/2024/服务合同.pdf # 从Object Key中提取文件名 filename os.path.basename(object_key) # 组合本地完整路径 local_path os.path.join(local_dir, filename) # 如果目标目录不存在自动创建支持多级目录 os.makedirs(local_dir, exist_okTrue) # 执行下载第二个参数是本地保存路径 try: result bucket.get_object_to_file(object_key, local_path) if result.status 200: print(f下载成功: {object_key} - {local_path}) return local_path else: print(f下载失败状态码: {result.status}) return except oss2.exceptions.NoSuchKey: print(f文件不存在: {object_key}) return except oss2.exceptions.AccessDenied: print(f无权限访问: {object_key}) return if __name__ __main__: # 示例下载一个PDF到指定目录 key pdf/contracts/2024/服务合同.pdf download_pdf_to_folder(key, LOCAL_DIR)这里几个参数值得展开说明get_object_to_file(object_key, local_path)是OpenAPI中下载到文件的常用方法内部实现了流式写入不会把整个文件一次性加载到内存。尤其下载几十MB的PDF时用get_object再手动write也不是不行但流式处理对内存友好得多。os.makedirs(local_dir, exist_okTrue)这行很关键。很多脚本第一次运行时因为目录不存在直接报FileNotFoundError加上这个就能自动创建多级目录省掉每次手动建文件夹的动作。bucket.get_object_to_file返回的result.status 200表示成功。偶尔会有网络中断等异常需要配合try...except做降级处理比如重试一次或记录日志。3.3 批量下载多个PDF并保持目录结构单个文件下载太基础了实际工作中更多是批量下载一批PDF。比如上游系统在OSS的pdf/contracts/2024/目录下放了上百份合同你要全部下载到本地并保持2024这个子目录。这里的思想是遍历OSS对象按Key前缀过滤再逐一下载本地目录结构根据Key动态创建。代码如下import os import oss2 def download_batch_by_prefix(bucket, prefix: str, local_root: str): 按前缀批量下载PDF到本地保持OSS目录结构。 prefix 示例: pdf/contracts/ local_root 示例: rD:\pdf_downloads count 0 # oss2.ObjectIterator 用于分页遍历这里只取前1000个可自行加翻页逻辑 for obj in oss2.ObjectIterator(bucket, prefixprefix): if not obj.key.endswith(.pdf): continue # 只处理PDF文件 # 相对路径 去掉前缀后的部分 relative_path obj.key[len(prefix):] local_path os.path.join(local_root, relative_path) # 自动创建目标子目录 local_subdir os.path.dirname(local_path) os.makedirs(local_subdir, exist_okTrue) # 下载 bucket.get_object_to_file(obj.key, local_path) count 1 print(f已下载: {obj.key} - {local_path}) print(f批量下载完成共 {count} 个PDF文件) if __name__ __main__: auth oss2.Auth(ACCESS_KEY_ID, ACCESS_KEY_SECRET) bucket oss2.Bucket(auth, ENDPOINT, BUCKET_NAME) download_batch_by_prefix(bucket, pdf/contracts/, rD:\pdf_downloads)批量下载时容易踩两个坑遍历不全。ObjectIterator默认一次最多返回1000个对象如果你目录下的PDF超过1000个要处理分页。可以使用oss2.ObjectIterator的max_keys参数调大或者配合continuation_token手动翻页。路径穿越风险。如果Object Key里包含..这样的路径符号拼接到本地路径时可能产生目录穿越。正常业务生成的文件名可控但在处理用户上传的原始Key时要小心最好用os.path.basename或白名单校验过滤防止被恶意覆盖本地文件。4. 实操Web应用与服务端下载到指定目录的实现Python脚本适合运维和自动化但如果你在做Web应用用户点击页面上的一个按钮期望PDF被保存到服务器上某个共享目录或者用户本地那就得走Web后端的路子。这里我给你拆解几种主流写法。4.1 后端下载后保存到服务器指定目录Java/Node示例**Java生态Spring Boot**下用阿里云官方SDKaliyun-oss实现下载到文件核心代码如下import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import java.io.File; public class OssDownloader { public static void main(String[] args) { String endpoint oss-cn-hangzhou.aliyuncs.com; String accessKeyId your-ak-id; String accessKeySecret your-ak-secret; String bucketName your-bucket; String objectKey pdf/contracts/2024/服务合同.pdf; String localPath D:/pdf_downloads/contracts/服务合同.pdf; // 确保本地目录存在 File parentDir new File(localPath).getParentFile(); if (parentDir ! null !parentDir.exists()) { parentDir.mkdirs(); } OSS ossClient new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { ossClient.getObject(new GetObjectRequest(bucketName, objectKey), new File(localPath)); System.out.println(下载完成: localPath); } catch (Exception e) { e.printStackTrace(); } finally { ossClient.shutdown(); } } }注意一点Java SDK的getObject会直接把远程对象的内容写进本地File底层也是流式处理适合大文件。用完后记得shutdown()释放连接不然后台资源泄漏长时间运行会OOM。Node.js生态可以用ali-oss包const OSS require(ali-oss); const client new OSS({ region: oss-cn-hangzhou, accessKeyId: your-ak-id, accessKeySecret: your-ak-secret, bucket: your-bucket, }); async function downloadPDF(objectKey, localPath) { try { // 本地目录不存在时创建 const fs require(fs); const path require(path); const dir path.dirname(localPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // 下载到本地文件 await client.get(objectKey, localPath); console.log(下载成功: ${objectKey} - ${localPath}); } catch (err) { console.error(下载失败:, err); } } downloadPDF(pdf/contracts/2024/服务合同.pdf, D:/pdf_downloads/contracts/服务合同.pdf);Node版本的client.get(objectKey, localPath)写法非常简洁但要注意这个包是老牌维护新项目也可以考虑aws-sdk/client-s3风格的兼容APIOSS支持S3协议不过直接上ali-oss是最省事的。4.2 前端下载到本地文件夹的限制与可行替代如果你是前端程序员想在浏览器里把OSS的PDF下载到用户本地的特定文件夹必须清醒认识到一个硬限制浏览器出于安全沙箱不允许网页指定用户磁盘的任意目录。这不是阿里云OSS的特殊限制而是所有Web应用都必须遵守的规则。可行的替代方案有这么几种默认“下载”目录方案。最常规的做法是后端返回一个OSS签名URL前端用a hrefurl download触发下载。浏览器会下载到用户设置的默认下载目录用户可以在浏览器设置里改成“每次询问保存位置”从而实现手动指定文件夹。a hrefhttps://bucket.oss-cn-hangzhou.aliyuncs.com/pdf/xxx.pdf?Expires...Signature... download服务合同.pdf下载PDF/a后端中转流方案。前端发请求到后端后端从OSS拉流并设置Content-Disposition: attachment; filename...浏览器同样只能下载到默认目录但好处是URL不会暴露OSS地址方便做权限控制。// Spring Boot 示例后端从OSS转流给前端 GetMapping(/download/pdf) public void downloadPdf(HttpServletResponse response) throws IOException { OSSObject ossObject ossClient.getObject(bucketName, objectKey); response.setContentType(application/pdf); response.setHeader(Content-Disposition, attachment;filenameservice-contract.pdf); IOUtils.copy(ossObject.getObjectContent(), response.getOutputStream()); response.getOutputStream().flush(); }客户端安装软件方案。如果业务确实需要精确控制下载目录那就得开发一个小型桌面客户端或用Java Web Start这种老技术。现代Web里也可以用WebSocket/RPC让本机服务处理但复杂度高一般业务用不上。我的建议是不要试图突破浏览器的目录限制这是安全底线。如果产品经理非要“一键保存到指定文件夹”先沟通清楚是保存到服务器目录还是本地目录多数情况下用户真正想要的就是“下载到默认目录后打开”而已。4.3 FastAdmin等PHP框架中OSS PDF下载的集成方式用FastAdmin做过项目的朋友应该知道FastAdmin自带的上传附件默认可以配置存储驱动其中就支持阿里云OSS。插件市面上很多一般用think-aliyun-oss或者OSS官方SDK。FastAdmin里处理PDF下载到指定目录我的习惯是写一个公共方法放在公共类中调用?php namespace app\common\library; use OSS\OssClient; use OSS\Core\OssException; class OssService { protected $ossClient; protected $bucket; public function __construct() { $config config(site.oss); // 从FastAdmin后台配置读取 $this-ossClient new OssClient( $config[accessKeyId], $config[accessKeySecret], $config[endpoint] ); $this-bucket $config[bucket]; } /** * 下载PDF到服务器指定目录 * param string $objectKey OSS对象Key * param string $localPath 本地完整路径 * return bool */ public function downloadPdf(string $objectKey, string $localPath): bool { $dir dirname($localPath); if (!is_dir($dir)) { mkdir($dir, 0755, true); } try { $this-ossClient-getObject($this-bucket, $objectKey, [ OssClient::OSS_FILE_DOWNLOAD $localPath ]); return true; } catch (OssException $e) { \think\facade\Log::error(OSS PDF 下载失败: . $e-getMessage()); return false; } } }调用时只需要$oss new OssService(); $oss-downloadPdf(pdf/contracts/2024/服务合同.pdf, /www/wwwroot/data/pdf/服务合同.pdf);用FastAdmin集成时有个很实用的小技巧不要把AccessKey硬编码在业务代码里而是利用FastAdmin后台的“配置”功能把OSS配置存到config/site.php对应的表里这样换账号、换Bucket不用改代码业务同事也能自己维护。这也是FastAdmin这类低代码后台典型的运维思维。5. 常见问题与排查技巧实录这部分我把自己实际处理过的OSS PDF下载相关报错和坑整理成速查表每个问题都是真实发生的场景排查思路和解决方法可以直接抄。5.1 下载时提示“AccessDenied”或“You have no right to access this object”现象SDK下载PDF时抛出oss2.exceptions.AccessDenied或者用URL在浏览器打开时报ErrorCodeAccessDenied/Code。排查步骤先确认Bucket是公有读还是私有读。如果Bucket是私有任何不带签名的URL请求必然被拒这是正常保护逻辑。如果Bucket是私有检查SDK里用的AccessKey是不是被授权了oss:GetObject。RAM控制台里看策略至少要有{ Effect: Allow, Action: [oss:GetObject], Resource: [acs:oss:*:*:your-bucket/pdf/*] }如果用了STS临时凭证确认Policy里Resource指定的前缀包含你要下载的Object。我之前排查过一个案例前端拿到的临时凭证只能上传不能下载就是Policy的Action里漏了GetObject。实操心得出于安全考虑Bucket长期建议设为私有所有对外访问统一走签名URL或STS。但如果项目内网环境、对数据安全要求不高临时改成公有读排查问题是最快的只是别忘了排查完改回来。5.2 Chrome提示“文件可能已被篡改”或阻止下载现象在浏览器直接访问OSS的PDF链接时Chrome弹出安全提示说“由于网站未使用安全连接且文件可能已被篡改因此Chrome阻止了此次下载”。原因这个警告通常是因为OSS的域名没有启用HTTPS证书或者CDN回源链路上证书不完整。Chrome会对非HTTPS环境下的文件下载做额外的安全校验尤其内容类型是PDF等敏感格式时拦截概率更高。解决办法最彻底的方式为OSS绑定自定义域名并开启HTTPS。在OSS控制台“传输管理”→“域名管理”中绑定一个已备案的域名再申请免费SSL证书阿里云有免费的DV证书配置上去。临时方案生成签名URL时强制指定protocolhttps。Python SDK里可以用bucket.sign_url(GET, object_key, 3600, slash_bypassTrue, protocolhttps)Java SDK里构建URLBuilder时设置setProtocol(Protocol.HTTPS)。这样虽然OSS默认域名没有证书但通过CDN或自定义域名的HTTPS转发也可以消除警告。如果只是内部测试可以在Chrome设置里关闭“安全浏览”的某些选项但我不推荐长期这么做等于关掉了浏览器一层保障。补充说明还有个变体问题是“下载的文件格式不是PDF而是HTML内容”。打开下载文件发现里面是一段XML或HTML代码通常是签名URL被拼接错了或者请求被OSS拒绝后返回了错误页。检查URL里的Expires是否过期以及Bucket、Object名是否拼写正确。5.3 批量下载时本地目录结构错乱现象批量下载后本地目录出现一堆文件堆在一起没有保持OSS上的层级或者一些文件互相覆盖。原因最主要是代码里没有正确解析Object Key的相对路径。比如我把所有文件都按同一个local_dir存储却忽略了Key中的子目录部分。正确逻辑本地路径应该是local_root / 相对路径而这个“相对路径”是Object Key去掉你想保留的前缀之后的部分。比如Object Key是pdf/contracts/2024/文件A.pdf你希望本地保留contracts/2024/的层级那么相对路径是contracts/2024/文件A.pdf拼上本地根目录即可。实操心得这里有一个性能优化的思路。很多人在循环内反复调用os.makedirs虽然exist_okTrue能忽略重复创建的错误但大量调用还是有略微性能开销。可以做一个简单的缓存只对未出现过的子目录执行一次makedirscreated_dirs set() for obj in ObjectIterator(bucket, prefixprefix): # ... subdir os.path.dirname(local_path) if subdir not in created_dirs: os.makedirs(subdir, exist_okTrue) created_dirs.add(subdir)5.4 下载大PDF时卡住或内存暴涨现象下载一个几百MB的PDF时程序内存飙到1GB甚至直接OOM或者下载过程中网络闪断程序抛异常退出。原因早期很多人用bucket.get_object(object_key).read()这种方式一次性把整个文件读进内存。这个API适合小文件大文件就等着爆内存吧。正确做法始终用get_object_to_file或者get_object流式读取后边读边写。# 正确姿势流式写入 result bucket.get_object(object_key) with open(local_path, wb) as f: for chunk in result: f.write(chunk)断点续传场景如果下载的是超大PDF且网络不稳定建议用OSS的断点续传接口。Python SDK里有resumable_download方法Java里是ossClient.resumableDownload。它会在本地生成一个记录文件中断后从断点继续不用重来。虽然PDF一般不会大到极端但电子书PDF几十上百MB我确实遇到过下载到一半断网重试的尴尬。5.5 下载的PDF打开乱码或提示文件损坏现象PDF下载下来能打开但内容乱码或者提示“文件已损坏”。排查思路先确认是源文件本身的问题还是传输过程的问题。用OSS控制台直接预览能正常显示就说明源文件没问题。检查下载过程中有没有用文本模式打开文件。在Windows上如果你用了open(object_key, r)而不是wb二进制的写文件模式Windows会偷偷做换行符转换导致PDF损坏。务必用二进制模式。如果文件是通过服务端中转流下载的检查后端有没有对Content-Type做处理有些框架默认给输出流设置text/html导致响应被浏览器当作页面渲染。设置Content-Type: application/pdf和Content-Disposition: attachment; filenamexxx.pdf很关键。5.6 文件名中文乱码现象PDF文件名中包含中文下载到本地后变成一串乱码字符。原因这通常是HTTP响应头中Content-Disposition里的filename未做URL编码。现代浏览器对非ASCII文件名有严格要求。正确设置方式String fileName URLEncoder.encode(服务合同.pdf, UTF-8); response.setHeader(Content-Disposition, attachment; filename\ fileName \; filename*UTF-8 fileName);Python Flask后端类似from urllib.parse import quote filename 服务合同.pdf response.headers[Content-Disposition] fattachment; filename*UTF-8{quote(filename)}如果直接用OSS签名URL下载文件名会默认取Object Key的尾部本身是UTF-8存储的一般不会乱码。如果觉得文件名太长或包含业务编号不友好可以在下载前先重命名为你想要的名称再保存到目标文件夹。6. 可能被忽略的OSS下载安全问题与合规建议这部分聊聊容易被忽略的东西。OSS是承载PDF存储的公共平台PDF又经常是合同、报告这类敏感文件下载操作中的安全和合规绝对不能少。6.1 私有Bucket里不要把签名URL的有效期设得过长有些同事图方便把签名URL的有效期设置成一整天甚至一个月。这在内部工具里可能没感觉但一旦URL泄漏到外网相当于把文件脱光了扔在公开场合。我的经验是最终用户下载场景签名URL有效期尽量控制在5到15分钟内如果是给内部系统调用可以放宽到1小时但必须配合IP白名单或Referer白名单。6.2 下载操作要有日志与审计所谓“开源软件合规排查”的背景下很多公司对文件访问记录越来越重视。对OSS下载操作建议在代码层增加访问日志import logging logging.basicConfig(levellogging.INFO) def download_pdf_with_log(object_key, local_path, user_id): logging.info(f[OSS下载] user{user_id}, object{object_key}, time{datetime.now()}) # 实际下载逻辑生产环境里我习惯把这类日志统一送入ELK或云日志服务方便事后追溯谁在什么时间下载了哪份PDF。尤其处理客户合同、财务报告时这份日志是合规审计的依据。6.3 定期用Black Duck类的SCA工具扫描依赖热词里提到了“用black duck扫描了”这是软件成分分析SCA工具的典型用途。OSS的SDK虽然由云厂商维护但也会引入第三方HTTP库、JSON库等这些依赖自身的漏洞报告需要持续跟踪。建议团队在CI流水线中引入SCA扫描每次升级SDK版本后自动跑一遍看到提示就评估处置不要拖到上线前才补救。有些PDF本身也可能是扫描出来的产物——比如第三方安全扫描工具把某个开源组件的License文档导出成PDF后放进OSS那这类文件就属于公司软件资产的一部分同样需要用合规流程管理下载权限并不是“在OSS上就万事大吉”。7. 一个能直接用的完整自动化脚本模板前面拆了各种场景这里放一个我自己平时批量同步OSS PDF到本地目录的完整模板把权限判断、目录创建、日志、断点续传都串在一起。你可以按自己公司OSS配置稍作修改就能用。import os import sys import logging import configparser from datetime import datetime import oss2 # ---------- 日志配置 ---------- logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(oss_download.log, encodingutf-8), logging.StreamHandler(sys.stdout), ], ) logger logging.getLogger(__name__) # ---------- 配置读取ini文件 ---------- config configparser.ConfigParser() config.read(oss_config.ini) ENDPOINT config.get(oss, endpoint) ACCESS_KEY_ID config.get(oss, access_key_id) ACCESS_KEY_SECRET config.get(oss, access_key_secret) BUCKET_NAME config.get(oss, bucket_name) auth oss2.Auth(ACCESS_KEY_ID, ACCESS_KEY_SECRET) bucket oss2.Bucket(auth, ENDPOINT, BUCKET_NAME) def ensure_dir(path): 确保目录存在不存在则创建。 if not os.path.isdir(path): os.makedirs(path, exist_okTrue) return path def sync_pdf_from_oss(prefix: str, local_root: str) - None: 同步OSS指定前缀下所有PDF到本地目录。 prefix: OSS对象前缀如 pdf/contracts/ local_root: 本地存放根目录如 D:/oss_sync/ if not prefix.endswith(/): prefix / local_root ensure_dir(local_root.rstrip(/\\) os.sep) created_dirs set() download_count 0 failed_count 0 logger.info(f开始同步: prefix{prefix}, local_root{local_root}) start_time datetime.now() try: # 遍历OSS对象 for obj in oss2.ObjectIterator(bucket, prefixprefix): if not obj.key.lower().endswith(.pdf): continue relative_path obj.key[len(prefix):] local_path os.path.join(local_root, relative_path) subdir os.path.dirname(local_path) if subdir not in created_dirs: ensure_dir(subdir) created_dirs.add(subdir) try: # 如果本地文件已存在且大小一致跳过 if os.path.exists(local_path): local_size os.path.getsize(local_path) if local_size obj.size: logger.info(f已存在跳过: {local_path} ({local_size} bytes)) continue bucket.get_object_to_file(obj.key, local_path) logger.info(f下载成功: {obj.key} - {local_path} ({obj.size} bytes)) download_count 1 except oss2.exceptions.AccessDenied: logger.error(f无权限下载: {obj.key}) failed_count 1 except oss2.exceptions.NoSuchKey: logger.error(f对象已被删除: {obj.key}) failed_count 1 except Exception as e: logger.error(f下载异常: {obj.key}, 错误: {e}) failed_count 1 except Exception as e: logger.error(f列出对象失败: {e}) raise elapsed datetime.now() - start_time logger.info( f同步结束: 成功{download_count}, 失败{failed_count}, 耗时{elapsed.total_seconds():.2f}s ) if __name__ __main__: # 直接调用例如同步 all/contracts 下的PDF到 D:/pdf_sync sync_pdf_from_oss(pdf/contracts/, D:/pdf_sync)这个脚本有几个设计细节值得说明跳过逻辑本地已存在同名且大小一致的PDF就跳过避免重复下载浪费流量。如果你希望做强制覆盖把那段判断删掉即可。日志双输出控制台和文件同步写日志方便定时任务跑完后翻日志。配置外置用ini文件存AccessKey避免把密文提交到代码仓库。如果公司有更严格的密钥管理比如KMS建议进一步对接密钥托管服务。放在Windows计划任务或Linux crontab里就能每天/每小时自动把OSS上的PDF同步到本地归档夹。这个脚本我在项目里跑了近一年稳定可靠。最后再分享一个我在实际使用中的小习惯下载任务执行前先打印一条开始日志执行完再打印一条统计信息。哪怕你的代码再简单、再临时也值得加上这两行日志。否则等文件少了或发现丢了的时候排查起来非常痛苦。远程服务器上我见过太多“静默失败”的同步脚本文件悄悄没下载全业务方过了好几周才发现。整个运维链路里“下载到指定文件夹”看起来只是简单一步但把它做成可观测、可重试、可追溯的稳定流程才是真正成熟的工程做法。