用Flask把Python自动化脚本变成可收费的Web服务 1. 项目概述一个周末写出来的脚本怎么变成每月多赚三千的副业“From Weekend Hack to Side Income: Python Automation with Flask”——这个标题我第一次看到时手边正泡着第三杯咖啡盯着自己上个月刚上线的订单自动归档工具发呆。它确实诞生于一个周六下午客户发来27个Excel表格要求合并、去重、按SKU分类、生成PDF报告、邮件发送全程手动操作要3小时。我花了4小时写完Flask后端前端表单邮件模板测试通过后发给客户他当天就转了800元。后来这工具被隔壁三家公司复用我抽成维护费现在每月稳定进账2800–3500元而核心代码至今没超过320行。这不是什么高深的AI模型或SaaS平台就是一个带网页界面的Python自动化流水线。关键词很明确Python自动化、Flask、副业变现、周末项目、轻量级Web服务。它解决的不是“技术能不能实现”而是“普通人如何用最低门槛把重复劳动打包成可交付、可收费的服务”。适合三类人行政/运营/财务岗想摆脱机械操作的打工人刚学完基础Python、苦于没有实战项目的初学者以及自由职业者想快速积累可演示案例的接单人。它不依赖云服务器、不强求Docker、不碰数据库集群——你用一台旧MacBook Air、甚至树莓派4B都能跑起来只要能装Python 3.9就能把“我帮你点鼠标”升级成“我给你一个网址你填完就走”。很多人误以为副业必须做APP、搞流量、写公众号。但现实是企业里真正愿意为“省时间”付费的永远是那些每天被Excel、邮件、截图、复制粘贴压得喘不过气的中基层员工。他们不在乎你用Django还是FastAPI只关心“我上传文件→点提交→5秒后收邮件→打开PDF就是我要的结果”。Flask在这里的价值恰恰在于它不做多余的事没有ORM强制你建表没有Admin后台逼你配权限没有WSGI中间件链让你调试到怀疑人生。它就干一件事——把Python函数挂到URL上让非技术人员也能调用。我把这个思路叫“自动化接口化”把原来藏在.py文件里的逻辑变成一个带输入框、上传按钮、进度提示的网页普通人点三下就能完成过去半小时的工作。下面我会拆解整个路径从第一个周末写出来的原始脚本到客户愿意付钱的稳定服务再到可复制的副业模式——每一步都附真实参数、踩坑记录和成本测算。2. 整体设计思路为什么选Flask而不是其他方案2.1 不选Django省掉80%的配置换来100%的交付速度去年帮一家外贸公司做报关单自动生成时我第一反应是Django。毕竟有现成的Admin、用户系统、数据库迁移。但当我花2天搭好环境、建好User模型、配好PostgreSQL客户发来需求变更“老板说不用存历史记录只要每次生成新PDF就行越快越好。”那一刻我删掉了整个models.py。Django的强约束在快速验证阶段反而是枷锁你要定义字段类型就要想清楚未来要不要加索引你要用Admin就得处理权限分组你要部署就得配gunicornnginxstaticfiles收集。而Flask的哲学是“你先跑起来再决定要不要加功能。”实测对比一组数据同一Excel处理逻辑读取→清洗→生成PDF用Django从零搭建可访问页面平均耗时14.5小时含环境配置、路由注册、模板渲染、静态文件处理用Flask实现同等功能平均耗时3.2小时核心代码127行含HTML模板和错误提示。关键差异在初始化成本。Djangostartproject生成12个文件夹47个文件Flask只需一个app.py和一个templates/目录。我统计过自己近18个副业项目其中15个首版交付都在4小时内完成靠的就是Flask的“最小可行界面”能力——它不阻止你后期加功能但绝不强迫你一开始就想全。提示如果你的自动化任务需要长期存储结果比如每日销售报表归档Django的ORM确实更稳妥但若任务本质是“一次性的转换服务”如合同PDF生成、发票OCR校对、邮件群发Flask的无状态设计反而更干净避免数据库成为单点故障源。2.2 不选FastAPI绕开异步陷阱专注业务逻辑本身FastAPI文档里写着“最高性能”但它的异步机制对新手是个隐形坑。我试过用FastAPI写一个文件上传PDF生成服务本地测试一切正常一上阿里云轻量服务器就报错“RuntimeError: asyncio.run() cannot be called from a running event loop”。查了3小时才发现PDF生成库reportlab内部用了同步IO和FastAPI的async route冲突。最后改成run_in_executor包装代码复杂度翻倍而Flask默认同步执行完全不用操心事件循环。更重要的是绝大多数办公自动化场景根本不需要异步。你处理一个10MB Excel文件瓶颈在CPU计算pandas数据清洗和磁盘IOPDF写入不是网络等待。Flask单进程每秒处理3–5个请求已足够应付中小企业的日常需求我们客户日均请求量峰值是67次。FastAPI的async优势在百万级并发API网关才有意义而你的副业项目前六个月连1000次请求都不到。我列了个决策表帮你判断何时该坚持Flask场景Flask是否适用原因客户只要一个URL上传文件→收邮件→完事✅ 强烈推荐单文件app.py即可承载全部逻辑需要登录账号、查看历史记录、导出CSV⚠️ 可扩展但建议加Flask-LoginSQLite比Django轻量比纯文件存储可靠要对接微信公众号回调、实时推送消息❌ 改用FastAPI或Tornado需要长连接和异步HTTP客户端处理视频转码、大文件分片上传❌ 上CeleryRedis队列Flask主线程阻塞会导致超时2.3 为什么必须带Web界面——破解“技术价值”到“商业价值”的最后一公里很多程序员卡在副业变现前夜写了完美的爬虫却没人愿意为“自动下载豆瓣电影TOP250”付费做了精准的Excel公式但老板觉得“宏命令也能干”。问题不在技术而在交付形态。Flask的核心价值是把Python脚本的“命令行交互”升级为“业务人员交互”。举个真实案例我帮HR部门做的“入职材料自动归档工具”。原始需求是“把扫描件PDF按姓名日期重命名存到指定文件夹”。如果只给.py脚本行政同事要① 打开终端 ② cd到目录 ③ python main.py --input /path/to/pdf ④ 等待输出提示。而Flask版本她只需要① 打开Chrome ② 输入http://192.168.1.100:5000 ③ 拖入PDF文件 ④ 点“提交” ⑤ 查收邮箱。后者她愿意付钱前者她觉得“我自己点几下鼠标就行”。这个转变背后是三个关键设计原则输入极简不让她填路径、不选格式、不设参数默认值覆盖90%场景反馈即时上传进度条、成功弹窗、失败红字提示消除“卡住”焦虑结果可追溯每份处理记录生成唯一ID邮件里带链接可重新下载避免“我弄丢了”。Flask用不到50行代码就能实现这三点而命令行脚本做不到。这才是周末项目能变成副业的根本原因——你卖的不是代码是“确定性”。3. 核心细节解析从脚本到服务的5个关键改造点3.1 文件上传别碰request.files用secure_filename保命所有自动化服务的第一道门是文件上传。新手常犯的致命错误直接用request.files[file].save(filename)。这等于给黑客开了后门——如果用户上传文件名是../../etc/passwd你的save()会把它写到系统关键目录。Flask官方文档强调secure_filename()但很多人忽略它的局限性它只过滤路径分隔符不处理空字节、Unicode欺骗等高级攻击。我的实操方案是三层防护前置校验用mimetypes.guess_type()检查文件真实类型拒绝text/html伪装成image/png名称净化secure_filename()后再用正则re.sub(r[^a-zA-Z0-9._-], _, filename)替换所有非法字符存储隔离所有上传文件存到独立目录/uploads/且该目录禁止执行权限Linux下chmod 755 uploadsWindows下取消“执行”属性。具体代码片段app.py中from werkzeug.utils import secure_filename import re import mimetypes def safe_upload(file): # 步骤1检查MIME类型 mime_type, _ mimetypes.guess_type(file.filename) if mime_type not in [application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/pdf, text/csv]: raise ValueError(不支持的文件类型请上传Excel、PDF或CSV) # 步骤2安全重命名 filename secure_filename(file.filename) filename re.sub(r[^a-zA-Z0-9._-], _, filename) # 替换中文、空格等 if not filename: raise ValueError(文件名为空请检查文件名) # 步骤3添加时间戳防重名 timestamp int(time.time()) name, ext os.path.splitext(filename) safe_name f{name}_{timestamp}{ext} # 存储到绝对路径避免相对路径风险 upload_path os.path.join(os.path.dirname(__file__), uploads, safe_name) file.save(upload_path) return upload_path注意os.path.join()必须用绝对路径我见过太多人用./uploads/导致在不同工作目录下路径错乱。os.path.dirname(__file__)确保始终以当前py文件为基准。3.2 任务执行用subprocess.run()替代os.system()掌握超时与错误捕获自动化脚本的核心是执行外部命令如pandoc转PDF、ffmpeg剪辑视频。新手爱用os.system()但它无法捕获输出、无法设置超时、错误码处理粗糙。subprocess.run()才是生产环境标配。以Excel转PDF为例用libreoffice --headlessimport subprocess import time def convert_excel_to_pdf(excel_path): pdf_path excel_path.replace(.xlsx, .pdf) try: # 关键参数timeout60超时1分钟capture_outputTrue捕获stdout/stderr result subprocess.run( [libreoffice, --headless, --convert-to, pdf, --outdir, os.path.dirname(excel_path), excel_path], timeout60, capture_outputTrue, textTrue, checkTrue # 自动抛异常 ) if result.returncode 0: return pdf_path else: raise RuntimeError(fLibreOffice转换失败{result.stderr}) except subprocess.TimeoutExpired: raise TimeoutError(文件转换超时请检查文件大小或服务器负载) except FileNotFoundError: raise EnvironmentError(未安装LibreOffice请运行sudo apt install libreoffice)这个函数解决了三个实际痛点超时控制大Excel50MB转换可能卡死timeout60强制终止并返回友好提示错误溯源result.stderr包含LibreOffice原生报错比如“内存不足”或“字体缺失”比os.system()的模糊返回码有用十倍环境检测FileNotFoundError明确告诉用户缺什么依赖而不是让程序静默崩溃。3.3 邮件发送避开SMTP认证雷区用App Password最稳自动化服务的终点往往是邮件通知。但SMTP配置是新手最大绊脚石Gmail默认禁用“不够安全的应用”Outlook需要开启两步验证后生成App Password。我踩过的坑用客户公司邮箱发信结果被当成钓鱼邮件进垃圾箱用QQ邮箱一天发超50封触发风控。解决方案统一用Gmail App Password 专用邮箱。注册一个yourname.automationgmail.com开启两步验证生成16位App Password不是账户密码。代码里硬编码这个密码比动态读取环境变量更可靠避免部署时漏配.env。邮件发送函数精简版import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart def send_email(to_email, subject, body, attachment_pathNone): msg MIMEMultipart() msg[From] yourname.automationgmail.com msg[To] to_email msg[Subject] subject msg.attach(MIMEText(body, plain)) if attachment_path and os.path.exists(attachment_path): with open(attachment_path, rb) as f: part MIMEApplication(f.read(), _subtypepdf) part.add_header(Content-Disposition, fattachment; filename{os.path.basename(attachment_path)}) msg.attach(part) try: server smtplib.SMTP(smtp.gmail.com, 587) server.starttls() # 必须启用TLS server.login(yourname.automationgmail.com, xxxx xxxx xxxx xxxx) # 16位App Password server.send_message(msg) server.quit() return True except Exception as e: print(f邮件发送失败{e}) return False实操心得Gmail App Password有效期无限但一旦泄露必须立即重置。我建议在代码里用# TODO: 生产环境改用密钥管理服务注释提醒自己后续升级但初期就用明文——副业项目首要目标是跑通不是架构完美。3.4 错误处理用try/except包裹每个环节但错误信息绝不暴露给用户自动化服务最怕“白屏”或“500 Internal Server Error”。用户看到这个第一反应是“这玩意坏了”而不是“我传错了文件”。Flask默认错误页对开发者友好对客户灾难性。必须用app.errorhandler全局捕获并返回业务语义明确的提示。我的错误处理策略分三级用户级错误如文件格式不对、必填字段为空前端JS验证后端二次校验返回flash(请上传Excel文件)系统级错误如磁盘满、内存不足app.errorhandler(500)捕获记录日志返回“服务暂时繁忙请稍后再试”开发级错误如代码语法错误仅在DEBUGTrue时显示详细traceback生产环境关闭。关键代码app.py底部app.errorhandler(400) def bad_request(e): return render_template(error.html, message请求参数错误请检查上传文件), 400 app.errorhandler(500) def internal_error(e): # 记录完整错误到日志文件 with open(error.log, a) as f: f.write(f[{time.ctime()}] {str(e)}\n) return render_template(error.html, message服务暂时繁忙请稍后再试), 500 # 全局异常捕获捕获未被route处理的Exception app.errorhandler(Exception) def unhandled_exception(e): app.logger.error(Unhandled Exception: %s, (e)) return render_template(error.html, message系统遇到未知错误请联系技术支持), 500配套的templates/error.html就一行div classalert alert-danger{{ message }}/div。简单粗暴但用户体验远超默认500页。3.5 部署上线用gunicornnginx但先学会nohup保命很多新手卡在最后一步本地能跑一上服务器就404。根本原因是没理解Flask开发服务器app.run()和生产服务器的区别。开发服务器单线程、无超时、不处理静态文件只适合调试生产必须用gunicorn这类WSGI服务器。但gunicorn配置有学习成本。我的过渡方案是先用nohup守护进程跑通再逐步升级。步骤安装gunicornpip install gunicorn创建启动脚本start.sh#!/bin/bash cd /home/user/my_automation nohup gunicorn -w 2 -b 0.0.0.0:5000 --timeout 120 --max-requests 1000 app:app gunicorn.log 21 echo $! gunicorn.pid运行chmod x start.sh ./start.sh服务即以后台进程运行。参数解释-w 22个工作进程平衡性能与内存1GB内存服务器够用-b 0.0.0.0:5000绑定所有IP的5000端口--timeout 120请求超时2分钟防止大文件卡死--max-requests 1000每处理1000次请求重启进程避免内存泄漏。注意nohup是Linux基础技能比学systemd服务文件简单十倍。我所有副业项目前3个月都用nohup稳定运行超2000小时无中断。等客户多了、需要域名、HTTPS时再加nginx反向代理——但那是第二阶段的事。4. 实操全流程从零开始搭建“发票PDF自动归档”服务4.1 需求分析与功能定义用一张表锁定最小可行范围客户原始需求“我们每月收200张电子发票要按开票日期建文件夹PDF重命名为‘公司名_金额_开票日期.pdf’存到NAS指定路径”。听起来简单但隐含陷阱发票PDF里文字是图片OCR识别难还是文本层可提取开票日期在PDF第几页位置是否固定公司名和金额是否在同一行有没有特殊符号干扰我用半天时间抽样分析了37张发票得出结论92%的发票是文本型PDFAdobe Acrobat可复制文字开票日期固定在右上角第一页公司名在左上角金额在右下角。于是定义MVP功能上传PDF发票自动提取开票日期、销售方名称、价税合计按规则重命名并移动到/nas/invoices/2024/06/目录邮件通知处理结果。放弃的功能V2再做图片型PDF的OCR识别需Tesseract准确率仅78%先人工审核多发票批量上传单次限1个避免超时历史记录查询用邮件ID代替。这个取舍让我首版交付从预估5天压缩到8小时。4.2 环境准备与依赖安装3条命令搞定全部依赖在Ubuntu 22.04服务器上执行以下命令CentOS用yumMac用brew# 1. 创建虚拟环境隔离依赖避免污染系统Python python3 -m venv venv source venv/bin/activate # 2. 安装核心包注意pypdf2已弃用用PyPDF2 pip install Flask PyPDF2 python-dotenv python-magic # 3. 安装系统级依赖PDF文本提取必需 sudo apt update sudo apt install poppler-utils # 提供pdfinfo、pdftotext命令关键点说明poppler-utils比pypdf2提取文本更准尤其对扫描件混合PDFpython-magic用于文件类型检测比mimetypes更可靠能识别PDF魔数虚拟环境venv必须用否则不同项目依赖冲突会让你崩溃。4.3 核心代码实现217行完成全部逻辑app.py完整代码已脱敏可直接运行from flask import Flask, request, render_template, flash, redirect, url_for import os import time import re import subprocess from PyPDF2 import PdfReader import magic from datetime import datetime import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart app Flask(__name__) app.secret_key your-secret-key-change-in-production # 配置路径 UPLOAD_FOLDER uploads OUTPUT_FOLDER /nas/invoices ALLOWED_EXTENSIONS {pdf} os.makedirs(UPLOAD_FOLDER, exist_okTrue) os.makedirs(OUTPUT_FOLDER, exist_okTrue) def allowed_file(filename): return . in filename and \ filename.rsplit(., 1)[1].lower() in ALLOWED_EXTENSIONS def extract_invoice_info(pdf_path): 提取PDF中的关键信息 try: # 方法1用pdftotextpoppler-utils提供精度高 result subprocess.run([pdftotext, -layout, pdf_path, -], capture_outputTrue, textTrue, timeout30) if result.returncode ! 0: raise ValueError(pdftotext提取失败) text result.stdout # 提取开票日期匹配开票日期2024年06月15日 date_match re.search(r开票日期[:]\s*(\d{4}年\d{1,2}月\d{1,2}日), text) if not date_match: raise ValueError(未找到开票日期) invoice_date date_match.group(1) # 提取销售方名称通常在左上角取前3行 lines text.split(\n)[:3] seller for line in lines: if 销售方 in line or 名称 in line: seller line.replace(销售方, ).replace(名称, ).strip() break if not seller: seller 未知公司 # 提取价税合计匹配价税合计后数字 amount_match re.search(r价税合计.*?([0-9,.\s])元, text) if not amount_match: amount_match re.search(r金额.*?([0-9,.\s]), text) amount amount_match.group(1).replace(,, ).replace( , ) if amount_match else 0.00 return { date: invoice_date, seller: seller[:20], # 截断过长公司名 amount: amount } except Exception as e: raise ValueError(fPDF解析失败{str(e)}) def rename_and_move(pdf_path, info): 重命名并移动文件 try: # 解析日期生成路径/nas/invoices/2024/06/ year_month re.search(r(\d{4})年(\d{1,2})月, info[date]) if not year_month: raise ValueError(日期格式错误) year, month year_month.groups() target_dir os.path.join(OUTPUT_FOLDER, year, month.zfill(2)) os.makedirs(target_dir, exist_okTrue) # 生成新文件名公司名_金额_开票日期.pdf clean_seller re.sub(r[\\/*?:|], _, info[seller]) new_name f{clean_seller}_{info[amount]}_{info[date].replace(年,).replace(月,).replace(日,)}.pdf new_path os.path.join(target_dir, new_name) # 移动文件 os.rename(pdf_path, new_path) return new_path except Exception as e: raise ValueError(f文件移动失败{str(e)}) def send_notification(email, original_name, new_path, errorNone): 发送通知邮件 subject 发票归档完成 if not error else 发票归档失败 if not error: body f您的发票 {original_name} 已成功归档。\n新文件路径{new_path}\n处理时间{datetime.now().strftime(%Y-%m-%d %H:%M:%S)} else: body f发票 {original_name} 归档失败。\n错误信息{error}\n请检查PDF格式或联系技术支持。 msg MIMEMultipart() msg[From] yourname.automationgmail.com msg[To] email msg[Subject] subject msg.attach(MIMEText(body, plain)) try: server smtplib.SMTP(smtp.gmail.com, 587) server.starttls() server.login(yourname.automationgmail.com, your-app-password) server.send_message(msg) server.quit() return True except Exception as e: print(f邮件发送失败{e}) return False app.route(/) def index(): return render_template(index.html) app.route(/upload, methods[POST]) def upload_file(): if file not in request.files: flash(未选择文件) return redirect(request.url) file request.files[file] email request.form.get(email, ) if file.filename : flash(未选择文件) return redirect(request.url) if not allowed_file(file.filename): flash(仅支持PDF文件) return redirect(request.url) try: # 保存上传文件 filename f{int(time.time())}_{file.filename} filepath os.path.join(UPLOAD_FOLDER, filename) file.save(filepath) # 提取信息 info extract_invoice_info(filepath) # 重命名移动 new_path rename_and_move(filepath, info) # 发送邮件 success send_notification(email, file.filename, new_path) if success: flash(f处理成功文件已存至{new_path}) else: flash(邮件发送失败但文件已归档) except ValueError as e: flash(f处理失败{str(e)}) # 清理临时文件 if os.path.exists(filepath): os.remove(filepath) except Exception as e: flash(f系统错误{str(e)}) if os.path.exists(filepath): os.remove(filepath) return redirect(url_for(index)) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境必须debugFalse配套templates/index.html精简版!DOCTYPE html html headtitle发票自动归档/title/head body h1发票PDF自动归档服务/h1 form methodpost enctypemultipart/form-data action/upload label上传发票PDFinput typefile namefile accept.pdf required/labelbrbr label接收通知邮箱input typeemail nameemail placeholderyourcompany.com required/labelbrbr input typesubmit value提交处理 /form {% with messages get_flashed_messages() %} {% if messages %} {% for message in messages %} div classalert{{ message }}/div {% endfor %} {% endif %} {% endwith %} /body /html4.4 本地测试与调试用真实发票样本验证每一步不要等部署才测试。我准备了4类样本标准文本PDFAdobe Acrobat生成验证pdftotext提取扫描图片PDF手机拍照转PDF验证poppler能否识别实际不能需人工介入加密PDF带密码验证PdfReader报错是否友好超大PDF100MB验证timeout30是否生效。测试命令# 本地启动DEBUGTrue便于看错误 FLASK_APPapp.py FLASK_ENVdevelopment flask run --host0.0.0.0 --port5000 # 用curl模拟上传比浏览器更快 curl -F fileinvoice_sample.pdf -F emailtestexample.com http://localhost:5000/upload关键观察点gunicorn.log里是否有TimeoutExpireduploads/目录下临时文件是否被清理邮件是否收到附件能否打开error.log里是否有未捕获异常我坚持“每个函数单独单元测试”比如extract_invoice_info()单独写测试脚本传入不同PDF断言返回值。这比等整个流程跑通再调试高效十倍。4.5 生产部署与监控3个命令让服务7x24运行服务器上执行# 1. 克隆代码假设用Git git clone https://github.com/you/invoice-archiver.git cd invoice-archiver # 2. 安装依赖 source venv/bin/activate pip install -r requirements.txt # 3. 启动服务用前面写的start.sh chmod x start.sh ./start.sh监控命令随时检查# 查看进程是否存活 ps aux | grep gunicorn # 查看最近10行日志 tail -10 gunicorn.log # 检查端口占用 netstat -tuln | grep :5000实操心得我给自己定铁律——任何副业服务上线前必须亲自用客户身份走一遍全流程上传→等邮件→查NAS→打开PDF。这比写100行测试代码更能发现体验断点。有一次我发现邮件里路径是/nas/invoices/2024/06/但客户NAS挂载点是/mnt/nas/invoices差了一个符号导致打不开——这种问题只有真用户才能暴露。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 文件上传失败413 Request Entity Too Large现象上传大于1MB的PDF时Nginx返回413错误但Flask日志里完全没记录。原因Nginx默认client_max_body_size是1MB请求在到达Flask前就被拦截。解决修改/etc/nginx/sites-available/default在server块内加client_max_body_size 100M;然后sudo nginx -t sudo systemctl reload nginx。注意这个值要和Flask代码里的request.files[file].stream.read()超时配合。我设100M是因为客户最大发票包是87MB含扫描件留13MB余量。5.2 PDF文本提取为空pdftotext返回空字符串现象subprocess.run()执行成功但result.stdout是空导致正则匹配全失败。排查步骤手动运行pdftotext -layout sample.pdf -看终端输出如果输出为空用pdfinfo sample.pdf检查PDF版本1.7可能不兼容用pdfimages -list sample.pdf确认是否为图片型PDF。终极方案当pdftotext失败时降级用PyPDF2读取文本层try: reader PdfReader(pdf_path) text for page in reader.pages: text page.extract_text() or except Exception as e: text # 降级失败保持空文本触发错误提示5.3 邮件被拒收Gmail标记为“不安全应用”现象邮件发送函数返回True但收件箱无邮件Gmail安全中心提示“不安全应用尝试登录”。原因App Password未正确生成或账户未开启两步验证。验证方法登录myaccount.google.com/security确认“两步验证”已开启在“应用专用密码”下生成新密码必须选“Mail”应用设备选“Other”复制16位密码粘贴到代码中注意空格。提示Gmail每天最多发500封但副业项目前半年日均10封完全够用。超量时改用SendGrid免费层100封/天。5.4 中文文件名乱码secure_filename()失效现象上传发票_202406.pdf正常但发票-测试.pdf变成-test.pdf中文全丢失。原因secure_filename()只保留ASCII字符中文被过滤。修复用urllib.parse.quote()编码中文再unquote()解码from urllib.parse import quote, unquote def safe_filename(filename): # 保留中文用URL编码 name, ext os.path.splitext(filename) encoded_name quote(name, safe) return encoded_name ext # 使用时 safe_name safe