前端上传图片时经常把文件转成 Base64 字符串传给后端。问题来了Base64 只是编码不保留文件头Magic Bytes后端拿到一串字符怎么知道它原本是 JPG、PNG 还是 WebP这篇文章把所有可行方案讲透给出可直接用的代码。一、先搞清楚文件头到底是什么每种图片格式的二进制文件开头都有固定的几个字节叫做Magic Bytes魔数格式Magic Bytes十六进制对应 Base64 前缀PNG89 50 4E 47 0D 0A 1A 0AiVBORw0KGgoJPG/JPEGFF D8 FF E0或FF D8 FF E1/9j/4GIF47 49 46 38R0lGWebP52 49 46 46 ... 57 45 42 50UklGR...WEBPBMP42 4DQkICO00 00 01 00AAAA关键点Base64 编码不会改变原始二进制内容只是换了一种表示方式。所以即便没有文件扩展名解码后的前几个字节依然包含 Magic Bytes。二、五种判断方案从最推荐到最不推荐方案一解码后检查 Magic Bytes✅ 最推荐最准确、最快零依赖。importbase64defdetect_image_format(base64_string): 通过 Magic Bytes 判断图片格式 支持格式PNG, JPG, GIF, WebP, BMP, ICO # 去掉可能存在的 data URL 前缀if,inbase64_string:base64_stringbase64_string.split(,)[1]try:# 解码前 12 个字节足够判断所有常见格式headerbase64.b64decode(base64_string)[:12]exceptException:returnNone,解码失败# PNG: 89 50 4E 47 0D 0A 1A 0Aifheader.startswith(b\x89PNG\r\n\x1a\n):returnpng,PNG# JPG: FF D8 FFifheader.startswith(b\xff\xd8\xff):returnjpg,JPEG# GIF: GIF8ifheader.startswith(bGIF8):returngif,GIF# WebP: RIFF....WEBPifheader.startswith(bRIFF)andbWEBPinheader:returnwebp,WebP# BMP: BMifheader.startswith(bBM):returnbmp,BMP# ICO: 00 00 01 00ifheader.startswith(b\x00\x00\x01\x00):returnico,ICOreturnNone,无法识别# 使用b64_striVBORw0KGgoAAAANSUhEUgAAAAUA...# 一段 PNG 的 base64fmt,namedetect_image_format(b64_str)print(f格式:{name})# 输出: PNG这个方案准确率接近 100%因为 Magic Bytes 是格式的身份证不会骗人。方案二用 Pillow 尝试打开✅ 简单粗暴Pillow 会自动识别格式不需要你手动判断。fromPILimportImageimportbase64importiodefdetect_by_pillow(base64_string):if,inbase64_string:base64_stringbase64_string.split(,)[1]try:img_database64.b64decode(base64_string)imgImage.open(io.BytesIO(img_data))# Pillow 内部已经识别了格式returnimg.format.lower()# PNG, JPEG, GIF, WEBP...exceptExceptionase:returnf识别失败:{e}# 使用fmtdetect_by_pillow(b64_str)print(f格式:{fmt})# 输出: PNG优点缺点代码极简一行搞定需要装 Pillow依赖重能识别 Pillow 支持的所有格式损坏的图可能误判自动处理透明通道等细节速度比方案一慢方案三从 Base64 字符串本身推断⚠️ 有限适用有些 Base64 字符串带有 Data URL 前缀格式信息藏在里面data:image/png;base64,iVBORw0KGgo... data:image/jpeg;base64,/9j/4AAQ...直接解析 MIME typedefdetect_from_data_url(data_url):if;indata_url:mimedata_url.split(;)[0].split(/)[-1]# image/png → pngreturnmimereturnNone局限很多场景下前端只传纯 Base64不带data:image/xxx;base64,前缀此方案失效。方案四后端让前端额外传格式字段✅ 工程上最可靠不猜了直接让前端告诉你{image:iVBORw0KGgoAAAANSUhEUgAAAAUA...,format:png}前端从File.type获取constfileinput.files[0];console.log(file.type);// image/png优点缺点100% 准确零计算开销依赖前端配合多传一个字段不怕数据损坏或异常如果前端没传还是得兜底实际工程中推荐方案四 方案一组合使用优先信前端传的信不过就自己判断。方案五暴力尝试所有格式❌ 不推荐把 Base64 解码后依次用 Pillow 尝试以每种格式打开# 不推荐效率低容易误判forfmtin[png,jpg,gif,webp,bmp]:try:Image.open(io.BytesIO(data)).verify()returnfmtexcept:continue浪费计算资源且损坏的图片可能被错误识别成某种格式。三、实战完整的后端处理函数把上面最优的方案组合起来importbase64fromPILimportImageimportiodefget_image_info(base64_string): 综合判断图片格式返回 (format_name, extension, pillow_image) # 1. 先去掉 data URL 前缀if,inbase64_string:base64_stringbase64_string.split(,,1)[1]# 2. Magic Bytes 判断最快fmt_map{b\x89PNG\r\n\x1a\n:(png,PNG),b\xff\xd8\xff:(jpg,JPEG),bGIF8:(gif,GIF),bRIFF:(webp,WEBP),# 需进一步确认含 WEBPbBM:(bmp,BMP),b\x00\x00\x01\x00:(ico,ICO),}try:headerbase64.b64decode(base64_string)[:12]formagic,(ext,name)infmt_map.items():ifheader.startswith(magic):ifextwebpandbWEBPnotinheader:continue# 解码后用 Pillow 验证imgImage.open(io.BytesIO(base64.b64decode(base64_string)))img.verify()# 验证文件完整性returnext,name,imgexceptException:pass# 3. 兜底让 Pillow 尝试try:img_database64.b64decode(base64_string)imgImage.open(io.BytesIO(img_data))img.verify()returnimg.format.lower(),img.format,imgexceptExceptionase:returnNone,None,f无效图片:{e}# 使用ext,name,imgget_image_info(b64_str)print(f格式:{name}, 扩展名: .{ext})四、方案对比总结方案准确率速度依赖推荐场景Magic Bytes⭐⭐⭐⭐⭐极快无首选所有场景通用Pillow 尝试⭐⭐⭐⭐快Pillow已有 Pillow 依赖时Data URL 前缀⭐⭐⭐⭐⭐极快无前端传了 data URL 时前端传格式⭐⭐⭐⭐⭐极快无工程首选配合兜底暴力枚举⭐⭐慢Pillow❌ 别用核心结论Base64 不会破坏原始二进制数据Magic Bytes 依然在解码后检查前 12 个字节就能判断格式。PNG 最好认iVBORw0KGgo开头几乎不会误判。JPG 次之/9j/4开头注意 JPEG 也可能是/9j/2JFIF 格式。工程上最稳的做法前端传file.type 后端 Magic Bytes 兜底双重保险。别再靠猜了几行代码就能解决。