本地丢图 · 自动压缩 · 云端识别 · 自动归档 · 全流程可直接复刻
环境:WSL2 Ubuntu + Docker + Python3.12
适用场景:电商图片打标、批量识别、OCR、内容审核等自动化流水线
前言
在电商图片处理、批量标注、自动化识别场景中,我们经常被这些痛点困扰:
-
本地环境依赖复杂,Python版本、第三方库冲突,换一台机器就无法运行;
-
图片体积过大,直接调用云端API易超时、失败,占用带宽;
-
批量处理后,待处理、已处理图片和识别结果混在一起,难以管理归档;
-
用Docker运行脚本,容器内文件是临时的,无法和本地互通,结果拿不出来、图片传不进去。
本文提供一套完整可直接发布的工程化解决方案,从Docker安装、项目编写、镜像构建,到目录挂载、跨设备文件问题修复、一键启动,全部闭环,复制粘贴就能落地使用,无需额外调试。
一、环境安装(Ubuntu / WSL2 通用,一键部署)
全程命令行操作,无需手动配置,新手也能快速上手。
1.1 安装Docker
执行以下命令,自动下载并安装Docker:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
1.2 免sudo使用Docker(可选,提升体验)
默认情况下,运行Docker需要sudo权限,配置后可直接使用docker命令:
sudo usermod -aG docker $USER
注意:配置完成后,必须退出当前终端,重新登录才能生效。
1.3 验证安装成功
执行以下命令,若出现Docker欢迎信息,说明安装成功:
docker --version
docker run hello-world
二、项目结构(标准工程化,清晰易维护)
统一项目目录结构,便于后续扩展和维护,目录说明如下:
coze-workflow/
├── input/ # 待处理图片(本地丢图到这个文件夹)
├── processed/ # 已处理图片归档(自动移动,避免重复处理)
├── output/ # 云端API识别结果(JSON格式,自动回写到本地)
├── your_script.py # 主程序(图片压缩、API调用、任务调度)
├── requirements.txt# Python依赖清单(明确版本,避免冲突)
└── Dockerfile # Docker镜像构建文件(一键构建,环境统一)
一键创建项目目录
无需手动创建文件夹,执行以下命令,自动生成完整目录:
cd ~
mkdir -p coze-workflow/{input,processed,output}
cd coze-workflow
执行完成后,当前目录就切换到了项目根目录(coze-workflow),后续所有操作都在此目录下进行。
三、编写项目文件(直接复制,无需修改)
以下所有文件内容,直接复制到对应文件中,无需额外调整(仅需后续替换API Token即可)。
3.1 requirements.txt(Python依赖清单)
明确依赖版本,避免因版本冲突导致程序报错:
pillow>=10.0.0
requests>=2.31.0
说明:pillow用于图片压缩、格式转换;requests用于调用云端API。
3.2 your_script.py(主程序,完整可运行)
包含图片压缩、Base64转换、API调用、任务调度、文件归档、日志记录等全部功能,已修复Docker跨设备文件移动问题:
import os
import time
import base64
import json
import threading
import shutil
from PIL import Image
from queue import Queue
from io import BytesIO
import requests
from datetime import datetime
# ===================== 核心配置区(仅需修改这里) =====================
INPUT_FOLDER = "./input" # 待处理图片目录
PROCESSED_FOLDER = "./processed" # 已处理图片归档目录
OUTPUT_FOLDER = "./output" # 识别结果JSON目录
COMPRESS_QUALITY = 80 # 图片压缩质量(0-100,越高越清晰)
MAX_WORKERS = 1 # 线程数(单线程足够,避免API限流)
MONITOR_INTERVAL = 2 # 文件夹监测间隔(秒)
API_TIMEOUT = 60 # API调用超时时间(秒)
API_RETRY_TIMES = 2 # API调用失败重试次数
API_RETRY_DELAY = 2 # 重试间隔(秒)
MAX_IMAGE_SIZE = 1000 # 图片最大边长(像素),超过自动缩放
# 云端API配置(从配置文件读取,不再明文暴露)
def load_config():
try:
with open("config.json", "r", encoding="utf-8") as f:
return json.load(f)
except:
print("❌ 请先创建 config.json 文件,并填入你的 API 信息!")
exit(1)
config = load_config()
API_URL = config.get("api_url")
API_TOKEN = config.get("api_token")
API_HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_TOKEN}"
}
# ======================================================================
# 自动创建目录(防止目录不存在导致报错)
for folder in [INPUT_FOLDER, PROCESSED_FOLDER, OUTPUT_FOLDER]:
os.makedirs(folder, exist_ok=True)
# 任务队列、已处理/处理中文件集合(避免重复处理)
task_queue = Queue()
processed_files = set()
processing_files = set()
file_mtime = {} # 记录文件修改时间,避免重复读取
def log_to_file(content):
"""日志记录到文件,方便排查问题"""
log_path = "./process_log.txt"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_path, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] {content}\n")
def compress_image(image_path, quality=COMPRESS_QUALITY, max_size=MAX_IMAGE_SIZE):
"""图片压缩+缩放,减少API调用带宽和超时概率"""
try:
log_to_file(f"开始压缩图片: {image_path}")
with Image.open(image_path) as img:
width, height = img.size
# 图片缩放(超过最大边长自动缩小)
if width > max_size or height > max_size:
ratio = min(max_size / width, max_size / height)
img = img.resize((int(width * ratio), int(height * ratio)), Image.Resampling.LANCZOS)
# PNG图片转RGB(避免透明通道导致Base64过大)
if img.format == "PNG" and img.mode == "RGBA":
img = img.convert("RGB")
# 压缩并保存到内存
buf = BytesIO()
img.save(buf, format=img.format or "JPEG", quality=quality)
buf.seek(0)
log_to_file(f"图片压缩成功,体积:{len(buf.getvalue()) / 1024:.2f}KB")
return buf
except Exception as e:
err_msg = f"图片压缩失败 {image_path}: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
return None
def image_to_base64(buf):
"""将压缩后的图片转为Base64字符串(API调用要求)"""
try:
b64_str = base64.b64encode(buf.getvalue()).decode("utf-8")
log_to_file(f"Base64转换成功,长度:{len(b64_str)} 字符")
return b64_str
except Exception as e:
err_msg = f"Base64转换失败: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
return None
def call_coze_api(b64_str):
"""调用云端API进行图片打标,包含重试机制"""
payload = {"image_data": b64_str}
log_to_file(f"开始调用API: {API_URL}")
for i in range(API_RETRY_TIMES + 1):
try:
response = requests.post(
API_URL,
headers=API_HEADERS,
json=payload,
timeout=API_TIMEOUT,
verify=False
)
log_to_file(f"API响应状态码: {response.status_code}")
response.raise_for_status()
result = response.json()
log_to_file(f"API调用成功,响应预览: {json.dumps(result)[:200]}...")
return result
except requests.exceptions.Timeout:
if i < API_RETRY_TIMES:
warn_msg = f"API调用超时,{API_RETRY_DELAY}秒后重试(剩余{API_RETRY_TIMES - i}次)"
print(f"⚠️ {warn_msg}")
log_to_file(warn_msg)
time.sleep(API_RETRY_DELAY)
continue
err_msg = f"API调用超时({API_TIMEOUT}秒,已重试{API_RETRY_TIMES}次)"
except requests.exceptions.SSLError:
err_msg = "API调用失败:SSL证书验证错误"
except requests.exceptions.ConnectionError:
err_msg = "API调用失败:无法连接到服务器(地址错误/网络不通)"
except requests.exceptions.RequestException as e:
err_msg = f"API调用失败: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
return None
def save_json_result(filename, data):
"""将API识别结果保存为JSON文件,回写到本地output目录"""
try:
name, _ = os.path.splitext(filename)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
json_file_name = f"{name}_{timestamp}.json"
json_save_path = os.path.join(OUTPUT_FOLDER, json_file_name)
with open(json_save_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
log_to_file(f"JSON结果保存成功: {json_save_path}")
return json_save_path
except Exception as e:
err_msg = f"JSON保存失败: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
return None
def move_to_processed(image_path):
"""跨设备文件移动(复制+删除)"""
try:
file_name = os.path.basename(image_path)
processed_path = os.path.join(PROCESSED_FOLDER, file_name)
if os.path.exists(processed_path):
name, ext = os.path.splitext(file_name)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
processed_path = os.path.join(PROCESSED_FOLDER, f"{name}_{timestamp}{ext}")
shutil.copy2(image_path, processed_path)
os.remove(image_path)
log_to_file(f"图片归档成功: {processed_path}")
return processed_path
except Exception as e:
err_msg = f"图片归档失败 {image_path}: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
return None
def worker():
"""工作线程:从队列中获取任务,执行图片处理全流程"""
while True:
image_path = task_queue.get()
if image_path is None:
break
file_name = os.path.basename(image_path)
log_to_file(f"开始处理图片: {file_name}")
print(f"\n📌 开始处理: {file_name}")
compressed_img = compress_image(image_path)
if not compressed_img:
processing_files.discard(file_name)
task_queue.task_done()
continue
base64_str = image_to_base64(compressed_img)
if not base64_str:
processing_files.discard(file_name)
task_queue.task_done()
continue
api_result = call_coze_api(base64_str)
if not api_result:
processing_files.discard(file_name)
task_queue.task_done()
continue
json_save_path = save_json_result(file_name, api_result)
if not json_save_path:
processing_files.discard(file_name)
task_queue.task_done()
continue
processed_path = move_to_processed(image_path)
if processed_path:
success_msg = f"处理完成: 原图片归档={processed_path} | JSON保存={json_save_path}"
print(f"✅ {success_msg}")
log_to_file(success_msg)
processed_files.add(file_name)
else:
warn_msg = "JSON保存成功,但图片归档失败"
print(f"⚠️ {warn_msg}")
log_to_file(warn_msg)
processing_files.discard(file_name)
task_queue.task_done()
def monitor_folder():
"""监测文件夹:实时检测input目录,发现新图片自动加入任务队列"""
print(f"\n🔍 开始实时监测文件夹: {os.path.abspath(INPUT_FOLDER)}")
print(f"📁 已处理图片归档到: {os.path.abspath(PROCESSED_FOLDER)}")
print(f"📄 API结果保存到: {os.path.abspath(OUTPUT_FOLDER)}")
print("💡 按 Ctrl+C 停止脚本\n")
log_to_file("脚本启动,开始监测文件夹")
while True:
try:
for file_name in os.listdir(INPUT_FOLDER):
image_path = os.path.join(INPUT_FOLDER, file_name)
if not os.path.isfile(image_path):
continue
if (file_name.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp")) and
file_name not in processed_files and
file_name not in processing_files):
current_mtime = os.path.getmtime(image_path)
if file_name not in file_mtime or current_mtime != file_mtime[file_name]:
file_mtime[file_name] = current_mtime
print(f"🆕 发现新图片: {file_name}")
log_to_file(f"发现新图片: {file_name}")
processing_files.add(file_name)
task_queue.put(image_path)
time.sleep(MONITOR_INTERVAL)
except Exception as e:
err_msg = f"文件夹监测异常: {str(e)}"
print(f"❌ {err_msg}")
log_to_file(err_msg)
time.sleep(MONITOR_INTERVAL)
def main():
"""主函数:启动工作线程和文件夹监测线程"""
threads = []
for i in range(MAX_WORKERS):
t = threading.Thread(target=worker, daemon=True)
t.start()
threads.append(t)
print(f"🚀 工作线程 {i + 1} 已启动")
monitor_thread = threading.Thread(target=monitor_folder, daemon=True)
monitor_thread.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n🛑 收到停止信号,正在退出...")
log_to_file("脚本收到停止信号,开始退出")
for _ in range(MAX_WORKERS):
task_queue.put(None)
for t in threads:
t.join()
print("✅ 所有线程已停止,脚本退出")
log_to_file("脚本已退出")
if __name__ == "__main__":
try:
import PIL
import requests
except ImportError as e:
print(f"❌ 缺少依赖包,请先执行安装命令:")
print(f" pip install pillow requests")
exit(1)
main()
3.3 Dockerfile(镜像构建文件)
基于轻量级Alpine镜像构建,体积小、启动快,解决pillow安装依赖问题:
FROM python:3.12-alpine
# 设置工作目录(容器内的项目根目录)
WORKDIR /app
# 安装编译依赖(解决pillow安装时的编译错误)
RUN apk add --no-cache gcc musl-dev libffi-dev python3-dev
# 复制依赖清单并安装(国内源加速,避免超时)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制主程序到容器内
COPY your_script.py .
# 启动脚本(容器启动时自动执行)
CMD ["python3", "your_script.py"]
说明:Alpine镜像体积小(仅几十MB),比Ubuntu镜像更轻量;国内PyPI源(清华源)加速依赖安装,避免因网络问题超时。
3.4 config.json(API密钥配置,必加)
在项目根目录创建 config.json 文件,填入你的云端API信息:
{
"api_url": "你的云端API地址",
"api_token": "你的API Token(仅填Bearer后面的字符串)"
}
同时修改Docker启动命令,把config.json也挂载到容器内(否则容器内找不到这个文件)
docker run --rm -it \
-v "$(pwd)/input:/app/input" \
-v "$(pwd)/processed:/app/processed" \
-v "$(pwd)/output:/app/output" \
-v "$(pwd)/config.json:/app/config.json" # 新增这一行
ec-labeler:v1.0
四、构建Docker镜像(一键构建,环境统一)
在项目根目录(coze-workflow)下,执行以下命令构建镜像,镜像名称为ec-labeler:v1.0(可自定义):
# 关闭BuildKit(避免缓存和层问题,新手更友好)
export DOCKER_BUILDKIT=0
# 构建镜像
docker build -t ec-labeler:v1.0 .
构建过程中,会自动下载基础镜像、安装依赖、复制项目文件,等待几分钟(首次构建较慢,后续构建会复用缓存)。
构建成功后,执行以下命令查看镜像:
docker images
若出现ec-labeler:v1.0的镜像信息,说明构建成功。
五、核心操作:挂载本地文件夹,实现双向同步
这是最关键的一步!通过Docker的-v参数,将本地文件夹挂载到容器内,实现「本地丢图、Docker处理、结果回写」的无感体验。
5.1 一键启动命令(复制直接运行)
cd ~/coze-workflow
docker run --rm -it \
-v "$(pwd)/input:/app/input" \
-v "$(pwd)/processed:/app/processed" \
-v "$(pwd)/output:/app/output" \
ec-labeler:v1.0
命令解读(关键必看)
-
--rm:容器停止后,自动删除容器,避免残留垃圾; -
-it:交互式运行,实时查看脚本输出(便于调试和观察处理进度); -
-v "$(pwd)/input:/app/input":本地input文件夹 ↔ 容器内/app/input文件夹,本地丢图,Docker直接读取; -
-v "$(pwd)/processed:/app/processed":容器内归档的图片,自动同步到本地processed文件夹; -
-v "$(pwd)/output:/app/output":容器内生成的JSON结果,自动回写到本地output文件夹; -
ec-labeler:v1.0:使用我们构建的镜像运行。
5.2 后台运行(可选,不占用终端)
若不想占用终端,可将-it替换为-d,让脚本在后台运行:
cd ~/coze-workflow
docker run -d --name ec-labeler -v "$(pwd)/input:/app/input" -v "$(pwd)/processed:/app/processed" -v "$(pwd)/output:/app/output" ec-labeler:v1.0
相关操作命令:
-
查看后台运行日志:
docker logs ec-labeler -
停止后台脚本:
docker stop ec-labeler
六、关键问题与优化
实战中遇到的问题及解决方案,也是这套方案的核心价值,新手可直接规避踩坑。
6.1 问题1:Docker跨设备文件移动失败(Cross-device link)
❌ 报错信息:Errno 18: Cross-device link
✅ 原因:Docker挂载的本地目录和容器内目录属于不同设备,Python的os.rename()方法无法跨设备移动文件。
✅ 解决方案:用shutil.copy2()复制文件到归档目录,再用os.remove()删除原文件,替代os.rename()。
核心代码(已集成到脚本中):
shutil.copy2(源文件路径, 目标路径)
os.remove(源文件路径)
6.2 问题2:容器内文件是临时的,停止容器后文件丢失
通过Docker目录挂载(-v参数),将容器内的文件同步到本地,数据永久保存,即使容器停止,本地文件也不会丢失。
6.3 优化1:国内PyPI源加速,避免依赖安装超时
在Dockerfile中添加清华源,安装依赖速度提升10倍:
pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
6.4 优化2:Alpine镜像补齐编译依赖,解决pillow安装失败
Alpine镜像默认缺少编译依赖,直接安装pillow会报错,添加以下命令补齐依赖:
RUN apk add --no-cache gcc musl-dev libffi-dev python3-dev
6.5 优化3:图片压缩+缩放,提升API调用成功率
将图片压缩到合适体积、缩放到固定最大边长,避免因图片过大导致API超时、失败,同时减少带宽占用。
七、使用流程
-
把需要打标的图片,丢进本地
~/coze-workflow/input文件夹(Windows路径:\\wsl$\Ubuntu\home\zc\coze-workflow\input); -
执行第五步的一键启动命令,启动Docker脚本;
-
脚本自动执行:监测图片 → 压缩 → Base64转换 → 云端API识别 → JSON结果保存 → 图片归档;
-
查看结果:
-
打标结果:
~/coze-workflow/output文件夹(JSON格式); -
已处理图片:
~/coze-workflow/processed文件夹; -
运行日志:
~/coze-workflow/process_log.txt(排查问题用)。
-
-
停止脚本:按
Ctrl+C,脚本优雅退出,不丢失文件。
八、总结
本文完整实现了一套「工程化、可复用、可部署」的全自动图片打标系统,核心优势如下:
-
环境统一:Docker封装所有依赖,一次构建,到处运行,换机器也无需重新配置;
-
无感使用:本地丢图即可自动处理,结果实时回写,无需手动操作Docker;
-
稳定可靠:包含图片压缩、API重试、日志记录、重名处理等优化,生产级可用;
-
可扩展性强:支持多线程、云端部署,可适配电商打标、OCR识别、内容审核等多种场景。
这套方案不仅解决了图片批量打标的痛点,更提供了Docker环境下「本地文件与容器互通」的通用思路,可直接复用到底其他自动化项目中。
所有代码可直接复制复刻,新手也能快速上手,赶紧试试吧!