接到一个很典型的需求:手上有一张线下活动海报,上面写着“Sonya Saranphat Hong Kong FC - 20260808 SWEET HONEYMOON PARTY FAN MEETING”,需要把这行信息快速拆成日期、地点、活动类型,然后生成日历提醒和倒计时页面。手动整理一条没问题,但如果是几十张海报、频繁更新,就必须用程序处理。
这次就来跑通一条“海报文字识别 → 字段解析 → 结构化输出 → 日历/网页生成”的完整流程。文章会给出 Python 代码、批量处理思路和 FastAPI 接口示例,全程可以本地部署,OCR 部分用普通 CPU 也能跑,显卡只是可选项。整体不依赖云端,适合活动运营、粉丝组织、线下展会信息整理,以及任何需要从图片中抽取事件的场景。
1. 核心能力速览与适用边界
| 能力项 | 说明 |
|---|---|
| 主要目标 | 把活动海报图片转换为结构化 JSON、ICS 日历文件和 HTML 倒计时页面 |
| 输入素材 | 活动海报 JPG/PNG/PDF 扫描件、截图 |
| 输出结果 | OCR 原始文本、结构化字段 JSON、.ics 日历文件、本地 HTML 页面 |
| OCR 引擎 | PaddleOCR / Tesseract / 云 OCR(可替换),本文以 PaddleOCR 为例给出本地实现 |
| 硬件要求 | 普通 CPU 可运行;OCR 批处理时启用 GPU 可缩短耗时 |
| 支持平台 | Windows / Linux / macOS |
| 启动方式 | Python 脚本运行;FastAPI 服务方式 |
| 接口能力 | 可通过 HTTP 上传图片并返回 JSON 字段 |
| 批量任务 | 支持目录批量扫描,每次输出独立 JSON 和 ICS |
| 关键依赖 | Python 3.9+、PaddleOCR、OpenCV、python-dateutil、ics、Jinja2、FastAPI |
| 适合场景 | 粉丝见面会、音乐会、展会、球赛等活动的日程整理与站点发布 |
| 不适合场景 | 未授权的商业图片采集、涉及非公开个人信息的批量处理 |
先明确使用边界。OCR 只负责“读取图片里的文字”,不负责判断信息的真实性和授权状态。如果海报中包含人物肖像、主办方 Logo、非公开联系方式,本地识别用于个人日程没有问题;但要把识别结果、裁剪图片或重建页面发布到公网,必须确认获得了主办方或权属方的授权。涉及真实艺人和线下活动时,尤其要避免伪造、篡改、误导性宣传。建议只处理你自己有权处理的海报素材,批量抓取他人页面图片再转存,可能违反平台条款或版权规定。
2. 环境准备与项目初始化
2.1 创建虚拟环境
推荐使用虚拟环境隔离依赖,避免污染系统 Python。可以新建一个目录:
mkdir activity_parser cd activity_parser python -m venv venvWindows 激活方式:
venv\Scripts\activateLinux / macOS 激活方式:
source venv/bin/activate2.2 安装依赖
PaddleOCR 依赖 PaddlePaddle 深度学习框架。PaddlePaddle 不同版本对应不同 CUDA 环境,建议先到 PaddlePaddle 官方安装页选择符合自己系统的安装命令,再安装 PaddleOCR。
CPU 环境的基础安装指令如下,实际版本号以官方为准:
pip install paddlepaddle pip install paddleocr pip install opencv-python pip install python-dateutil pip install ics pip install jinja2 pip install fastapi pip install uvicorn如果网络条件允许,同时安装pytesseract作为备用 OCR 引擎:
pip install pytesseractTesseract 还需要系统级安装,不是只装 Python 包就能运行。Windows 需要到 UB Mannheim 的 Tesseract 安装包页面安装;Linux 可以用apt install tesseract-ocr tesseract-ocr-chi-sim。文章中不会把 Tesseract 作为主流程,因为 PaddleOCR 对中文海报文字识别开箱即用效果更稳,但你要知道存在这个备选。
2.3 准备目录结构
activity_parser/ ├── inputs/ # 海报原图 ├── ocr_output/ # OCR 原始结果 ├── json_output/ # 结构化 JSON ├── ics_output/ # 日历文件 ├── page_output/ # HTML 倒计时页面 ├── templates/ # Jinja2 模板 ├── ocr_engine.py ├── parser.py ├── batch_extract.py └── server.py提前创建目录,避免运行时找不到路径:
mkdir inputs ocr_output json_output ics_output page_output templates这一步没有特殊门槛。PaddleOCR 第一次运行时需要下载检测、识别模型,需要保持网络畅通;下载完模型会缓存在本地,后续离线也能跑。
3. 单张海报文字识别(OCR)
3.1 OCR 引擎封装
写一个ocr_engine.py,把 PaddleOCR 初始化成单例。这样在批量任务中不会重复加载模型,降低内存占用。
import os from pathlib import Path try: from paddleocr import PaddleOCR except ImportError: PaddleOCR = None OCR_ENGINE = None OUTPUT_DIR = Path("ocr_output") OUTPUT_DIR.mkdir(exist_ok=True) def get_ocr_engine(): global OCR_ENGINE if OCR_ENGINE is None: if PaddleOCR is None: raise RuntimeError("PaddleOCR 未安装,请先安装 paddleocr") OCR_ENGINE = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) return OCR_ENGINE def ocr_image(image_path: str) -> list[str]: engine = get_ocr_engine() result = engine.ocr(image_path, cls=True) lines = [] # 不同版本 PaddleOCR 返回结构略有差异,这里做兼容处理 for page in result: if not page: continue for line in page: try: # 常见版本: [[box], (文本, 置信度)] text = line[1][0] confidence = float(line[1][1]) lines.append({"text": text, "confidence": confidence}) except Exception: continue # 把 OCR 原始文本落盘,方便排查 stem = Path(image_path).stem with open(OUTPUT_DIR / f"{stem}_ocr.json", "w", encoding="utf-8") as fp: json.dump(lines, fp, ensure_ascii=False, indent=2) return linesOCR 识别完成后,原始文本按行保存到ocr_output/目录。这个步骤的意义是保留中间结果。后续字段解析如果出错,可以回到这一步检查是 OCR 没读出来,还是正则规则没覆盖到。
3.2 海报样例
假设一张海报上的核心文案是:
Sonya Saranphat Hong Kong FC 20260808 SWEET HONEYMOON PARTY FAN MEETING实际情况中,海报还会带主办方、场馆地址、开票时间、嘉宾信息,OCR 会把这堆文字混在一起输出,顺序不一定和视觉一致。因此要做字段解析,而不是直接对一行字符串做切片。
3.3 验证 OCR 是否成功
运行以下命令测试单张图片:
python - <<'PY' from ocr_engine import ocr_image lines = ocr_image("inputs/fan_meeting.png") for item in lines: print(item["text"], round(item["confidence"], 4)) PY判断标准:
- 八位日期
20260808能原样输出; - 英文地名、活动名基本完整;
- 中文文字没有大面积乱码。
如果 OCR 结果出现大量空白,先检查图片亮度和对比度;如果文字扭曲,需要做透视矫正后再识别。
4. 字段解析与结构化输出
OCR 只是把图片变成文本。接下来要把“散装文本”变成带语义的字段:活动名称、地点、日期、活动类型、组织方。
4.1 用正则提取字段
写一个parser.py,先定义关键词表,再用正则匹配日期规律。这种方案不完美,但对于命名规则相对统一的活动海报,够用。
import json import re from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo OUTPUT_DIR = Path("json_output") OUTPUT_DIR.mkdir(exist_ok=True) def parse_event_from_ocr_text(text: str) -> dict: upper_text = text.upper() # 1. 八位日期:20260808 date_match = re.search(r"(20\d{2})[^\d]?(\d{2})[^\d]?(\d{2})", text) event_date = None if date_match: year = int(date_match.group(1)) month = int(date_match.group(2)) day = int(date_match.group(3)) event_date = datetime(year, month, day).date().isoformat() # 2. 活动地点:规则匹配 "Hong Kong" 等常见城市 location = None for keyword in ["Hong Kong", "HONG KONG", "Bangkok", "Taipei", "Singapore"]: if keyword in upper_text: location = keyword.title() break # 3. 活动类型:关键词匹配 event_type = None if "FAN MEET" in upper_text or "FANMEET" in upper_text: event_type = "Fan Meeting" elif "CONCERT" in upper_text: event_type = "Concert" elif "EXHIBITION" in upper_text: event_type = "Exhibition" # 4. 活动名称:取一个尽量可读的文本 event_name_match = re.search(r"SWEET[ A-Z0-9]*", upper_text) event_name = None if event_name_match: start = event_name_match.start() # 尽量还原原始大小写 raw_name = text[start:start + len(event_name_match.group(0))] event_name = raw_name.strip() # 5. 主要人物 / 组织名:这里只做最小实现 # 实际项目会维护关键词表 entity_keywords = ["Sonya Saranphat"] mentions = [] for entity in entity_keywords: if entity.lower() in text.lower(): mentions.append(entity) return { "event_name": event_name, "event_date": event_date, "location": location, "event_type": event_type, "mentioned_entities": mentions, "raw_text": text.strip(), } def parse_ocr_result(lines: list[dict]) -> dict: text = "\n".join([item["text"] for item in lines if item.get("text")]) return parse_event_from_ocr_text(text) def save_result(result: dict, input_name: str): stem = Path(input_name).stem output_file = OUTPUT_DIR / f"{stem}.json" with open(output_file, "w", encoding="utf-8") as fp: json.dump(result, fp, ensure_ascii=False, indent=2) return output_file这段代码针对“海报核心文案是标准英文字段”的场景。如果活动名称完全是小写或包含中文,正则规则需要扩展。最稳妥的做法不是用一条超长正则匹配所有情况,而是做规则组合:
- 先找特殊日期编码,比如
20260808、2026-08-08、2026/08/08、08 AUG 2026; - 再找地名关键词表;
- 然后用事件类型关键词表分类。
结构上把这三张表抽出来,后续新海报只扩展表,不重写解析逻辑。
4.2 单张海报主流程
把 OCR 和解析串起来,执行下面命令:
python - <<'PY' from ocr_engine import ocr_image from parser import parse_ocr_result, save_result image = "inputs/fan_meeting.png" lines = ocr_image(image) result = parse_ocr_result(lines) print(result) print(save_result(result, image)) PY预期 JSON 输出:
{ "event_name": "SWEET HONEYMOON PARTY", "event_date": "2026-08-08", "location": "Hong Kong", "event_type": "Fan Meeting", "mentioned_entities": ["Sonya Saranphat"], "raw_text": "Sonya Saranphat Hong Kong FC\n20260808\nSWEET HONEYMOON PARTY FAN MEETING" }这就是结构化输出。只要 OCR 文本里有这些关键字,解析器就能建立可查询的事件库。
5. 生成日历与倒计时页面
结构化 JSON 还只是一个中间产物。日常使用中,用户需要两种落地形式:能被手机日历直接导入的 ICS 文件,以及能在浏览器里打开的倒计时页面。
5.1 生成 ICS 日历事件
使用ics库创建日历事件。关键点是要指定时区。20260808这样的纯日期是否表示当地时间,需要依赖活动举办地。在此以 Hong Kong 所在的Asia/Hong_Kong为例。
from pathlib import Path from ics import Calendar, Event from zoneinfo import ZoneInfo from datetime import datetime ICS_DIR = Path("ics_output") ICS_DIR.mkdir(exist_ok=True) def generate_ics(event_result: dict, input_stem: str = "event"): cal = Calendar() ev = Event() event_name = event_result.get("event_name") or "Event" ev.name = event_name event_date = event_result.get("event_date") if event_date: # 默认按活动当地时区解释日期,缺少开始时间时按 00:00 占位 tz = ZoneInfo("Asia/Hong_Kong") local_dt = datetime.fromisoformat(event_date + "T00:00:00").replace(tzinfo=tz) ev.begin = local_dt # 这里不设置 duration,让日历自动判断为全天事件的样式 # 如果后续能识别到开始时间,再补充 ev.begin 的时分秒 location = event_result.get("location") if location: ev.location = location event_type = event_result.get("event_type") if event_type: ev.description = f"Type: {event_type}" if event_result.get("mentioned_entities"): ev.description += " | Entity: " + ", ".join(event_result["mentioned_entities"]) cal.events.add(ev) output_file = ICS_DIR / f"{input_stem}.ics" with open(output_file, "w", encoding="utf-8") as fp: fp.write(str(cal)) return output_file生成的 ICS 文件可以导入 Google Calendar、Apple Calendar、Outlook 和大多数手机日历应用。这里把开始时间设为活动当天零点,严格来说只是占位。如果你的海报里有明确的开始时间,比如Door Open 18:00,需要额外解析时分,再把ev.begin改成16:00:00真正的时间。
5.2 生成倒计时 HTML 页面
用 Jinja2 写一个模板,输出活动名称、日期、地点和倒计时脚本。倒计时脚本在浏览器本地运行,不依赖后端。
先创建模板文件templates/event_page.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ event_name }}</title> <style> body { font-family: Arial, sans-serif; max-width: 640px; margin: 40px auto; padding: 16px; } .info { margin: 12px 0; font-size: 18px; } #countdown { font-size: 28px; font-weight: bold; color: #b45a23; } </style> </head> <body> <h1>{{ event_name }}</h1> <div class="info">日期:{{ event_date }}</div> <div class="info">地点:{{ location }}</div> <div class="info">类型:{{ event_type }}</div> <h2>倒计时</h2> <div id="countdown">--</div> <p><a href="{{ ics_filename }}">下载日历文件</a></p> <script> const eventDate = new Date("{{ event_date }}T00:00:00+08:00").getTime(); function updateCountdown() { const now = Date.now(); const diff = eventDate - now; if (diff <= 0) { document.getElementById("countdown").textContent = "活动进行中,祝顺利!"; return; } const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff / (1000 * 60 * 60)) % 24); const minutes = Math.floor((diff / (1000 * 60)) % 60); const seconds = Math.floor((diff / 1000) % 60); document.getElementById("countdown").textContent = days + " 天 " + hours + " 小时 " + minutes + " 分 " + seconds + " 秒"; } updateCountdown(); setInterval(updateCountdown, 1000); </script> </body> </html>用 Jinja2 渲染。注意时区:如果使用东八区时间,模板里写+08:00。真实活动需要根据举办地选择时区,否则手机会提醒错时间。
from pathlib import Path from zoneinfo import ZoneInfo from datetime import datetime from jinja2 import Environment, FileSystemLoader PAGE_DIR = Path("page_output") PAGE_DIR.mkdir(exist_ok=True) env = Environment(loader=FileSystemLoader("templates")) template = env.get_template("event_page.html") def generate_page(event_result: dict, ics_filename: str, input_stem: str = "event"): event_name = event_result.get("event_name") or "Event" # 方便浏览器读取的 ISO 日期字符串,示例只处理日期,不带具体时间 event_date = event_result.get("event_date") local_tz = ZoneInfo("Asia/Hong_Kong") display_time = None if event_date: dt = datetime.fromisoformat(event_date).replace(tzinfo=local_tz) display_time = dt.isoformat() html = template.render( event_name=event_name, event_date=event_result.get("event_date"), location=event_result.get("location") or "待定", event_type=event_result.get("event_type") or "未分类", ics_filename=ics_filename, ) page_file = PAGE_DIR / f"{input_stem}.html" page_file.write_text(html, encoding="utf-8") return page_file最后,用一个脚本串联完整流程。
python - <<'PY' from ocr_engine import ocr_image from parser import parse_ocr_result, save_result from ics_writer import generate_ics from page_writer import generate_page image = "inputs/fan_meeting.png" stem = image.replace("/", "_").replace(".png", "").replace(".jpg", "") lines = ocr_image(image) result = parse_ocr_result(lines) save_result(result, image) ics_file = generate_ics(result, stem) page_file = generate_page(result, ics_file.name, stem) print("JSON:", result) print("ICS:", ics_file) print("HTML:", page_file) PY打开生成的 HTML 页面,看到活动名称和倒计时数字,说明整条流程已经跑通。
6. 批量任务与 API 接口
6.1 批量处理目录下所有海报
批量任务要考虑几点:输入文件格式、循环顺序、失败隔离、重复处理。最省事的方式是用Path.rglob遍历inputs目录下所有图片。每一张图片独立处理,某一张失败不能中断整个目录。
from pathlib import Path import traceback from ocr_engine import ocr_image from parser import parse_ocr_result, save_result from ics_writer import generate_ics from page_writer import generate_page IMAGE_SUFFIX = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} def run_batch(input_dir: str = "inputs"): input_dir = Path(input_dir) success = [] failed = [] for image_path in sorted(input_dir.rglob("*")): if image_path.suffix.lower() not in IMAGE_SUFFIX: continue stem = image_path.stem print("开始处理:", image_path) try: lines = ocr_image(str(image_path)) result = parse_ocr_result(lines) save_result(result, str(image_path)) ics_file = generate_ics(result, stem) page_file = generate_page(result, ics_file.name, stem) success.append({ "file": str(image_path), "json": str(save_result(result, str(image_path))), "ics": str(ics_file), "html": str(page_file), }) except Exception: failed.append(str(image_path)) traceback.print_exc() print("成功:", len(success), "失败:", len(failed)) return {"success": success, "failed": failed} if __name__ == "__main__": run_batch()判断批量任务是否成功的标准:json_output、ics_output、page_output三个目录中有对应文件生成;失败列表为空,或失败原因都是同一类问题。
6.2 FastAPI 上传接口
把批处理服务化,可以让其他人员通过网页上传一张海报,也可以让内部工具批量提交。下面是一个最小 FastAPI 服务,上传文件后立刻执行“识别 + 解析 + 生成 json”。
import tempfile from pathlib import Path from fastapi import FastAPI, File, UploadFile, HTTPException from ocr_engine import ocr_image from parser import parse_ocr_result app = FastAPI() @app.post("/api/extract") async def extract_activity(file: UploadFile = File(...)): suffix = Path(file.filename).suffix.lower() if suffix not in {".jpg", ".jpeg", ".png", ".bmp", ".webp"}: raise HTTPException(status_code=400, detail="不支持的图片格式") content = await file.read() if not content: raise HTTPException(status_code=400, detail="上传内容为空") with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: tmp.write(content) tmp_path = tmp.name try: lines = ocr_image(tmp_path) result = parse_ocr_result(lines) return result finally: Path(tmp_path).unlink(missing_ok=True)启动接口服务:
uvicorn server:app --host 127.0.0.1 --port 8000调用测试:
curl -X POST http://127.0.0.1:8000/api/extract \ -F "file=@inputs/fan_meeting.png"返回示例:
{ "event_name": "SWEET HONEYMOON PARTY", "event_date": "2026-08-08", "location": "Hong Kong", "event_type": "Fan Meeting", "mentioned_entities": ["Sonya Saranphat"], "raw_text": "Sonya Saranphat Hong Kong FC\n20260808\nSWEET HONEYMOON PARTY FAN MEETING" }接口跑通后,就可以把它接到后台管理系统、桌面小工具,或者其他需要“传图 → 得到结构化事件”的业务中。生产环境使用接口服务时,建议加访问控制。最简单的方式是设置 API Token 或只允许内网访问,不要把服务直接暴露到公网。
7. 资源占用与性能观察
这篇流程不需要大显存显卡。PaddleOCR 的模型较小,CPU 模式就能完成识别。资源占用主要取决于图片分辨率和批量并发数。
实际观察的维度:
- 启动阶段:PaddleOCR 第一次初始化会加载检测模型和识别模型,此时内存占用升高,但只发生一次。
- 单张识别:长边 2000 像素的普通海报,CPU 识别通常在秒级到十几秒之间,具体视 CPU 和图片复杂度而定。
- 批量任务:如果串行处理,内存稳定但耗时长;如果多用线程池并发处理,需要控制
ThreadPoolExecutor(max_workers),因为 OCR 引擎和 PaddleOCR 内部并不是绝对的线程安全。 - GPU 加速:PaddlePaddle 的 GPU 版本可以缩短识别时间,但需要额外安装 CUDA 环境。粉丝海报这种偏文字图,CPU 已经能跑,没必要一上来就配置 GPU。
减少资源占用的常用手段是预处理图片。识别前先用 OpenCV 转灰度、增强对比度,可以降低无效像素数量:
import cv2 def preprocess_image(image_path: str): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) resized = cv2.resize(gray, None, fx=1.5, fy=1.5, interpolation=cv2.INTER_CUBIC) _, thresh = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return thresh预处理不一定能提升所有图片的准确度。文字底色复杂、带强烈艺术字的海报,反而可能因为二值化丢失信息。建议保留原图和预处理两套流程,对比后选择更稳的方案。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| PaddleOCR 初始化时报错 | PaddlePaddle 版本不匹配 | 查看错误堆栈,检查 paddle 版本 | 按 PaddlePaddle 官方安装页重装对应版本 |
| 第一次运行需要下载模型却卡住 | 网络不稳定 | 检查模型缓存目录,稍后重试 | 配置镜像或手动放置模型文件到缓存目录 |
| 识别结果为空 | 图片亮度低、对比度低、文字过小 | 用 OpenCV 预览预处理后的图片 | 增强对比度、放大图片、去倾斜后再识别 |
| OCR 输出中文乱码 | 图片有非常规字体或字符集识别受限 | 检查 OCR 日志中是否加载了中文识别模型 | 确保lang="ch",必要时补充字体样本 |
| 日期解析失败 | 海报日期不是20260808格式 | 打印 OCR 文本,确认日期实际写法 | 增加正则分支,支持/和-分隔 |
| 生成的 ICS 导入日历后时间差一天 | 时区没有设置,或 UTC 转换错误 | 打开 ICS 文件查看 VTIMEZONE 和 DTSTART | 确保使用ZoneInfo并保持本地时区语义 |
| 批量任务只处理了第一张图 | 遍历逻辑只匹配了固定文件名 | 检查目录循环后缀过滤 | 使用rglob和完整后缀集合 |
| API 上传后返回 413 | 请求体超过服务器限制 | 查看 uvicorn 日志 | 在调用端限制文件大小,或在服务前增加反向代理限制 |
| 接口调用超时 | OCR 本身耗时较长 | 单张图片测试响应时间 | 增大 HTTP 超时,或改为异步任务队列 |
| 输出结果不稳定 | 不同海报规则差异大 | 对多条海报回放 OCR 文本 | 建立“字段关键词表 + 正则”双通道,维持回归样例 |
其中最容易忽略的是时区问题。解析出20260808并不等于可以在日历里直接盲写2026-08-08 UTC。活动日期应该绑定到举办地时区。建议程序内部统一保存“原始本地时间字符串 + 时区名”,生成 ICS 时做一次正确的replace(tzinfo=...),生成页面时再用浏览器本地时间计算倒计时。不要把两种语义混在一个字段里。
9. 最佳实践与总结
这套流程最适合作为“活动信息本地整理工具”的第一步。开发时不建议一开始就追求识别所有海报,先准备一套包含 10 张不同风格海报的测试集,每张都要输出 JSON 字段,然后把失败的案例逐步补进正则或关键词表。
几个实用建议:
- 保留 OCR 原始文本。不要只存最终 JSON,否则字段解析规则迭代时需要重新跑全部图片。
- 输入文件与输出文件分目录管理。
inputs只放原始图片,json_output、ics_output、page_output单独建目录,方便一键清理。 - 批量任务加日志。每张图片处理时间、成功状态、输出路径都要写入日志文件,出现中断时可以断点续跑。
- 批量任务加失败重试机制。OCR 偶尔会因为下载模型、临时文件读写错误失败,重试两次可以明显降低失败率。
- API 服务要限制访问范围。如果只是本地内部用,监听
127.0.0.1就够了;如果需要局域网访问,再加 Token 鉴权。 - 涉及真实人物、品牌、主办方的海报,发布前必须确认授权。本地解析不构成侵权,但把海报剪裁、转存、上传到公网平台需要另行判断。
如果你第一次跑通,优先验证三件事:单张海报能否稳定识别出20260808这八位日期;能否把英文活动名完整归入event_name;ICS 文件导入手机日历后,活动日期是否和海报一致。这三步通过后,再往下加批量任务和 API 服务。
继续扩展的方向也很明确:把结构化 JSON 存入 SQLite 或 PostgreSQL,做一个简易的活动日历管理后端;增加门票状态字段,处理后端通知;接入手机日历订阅链接,让粉丝看到更新后能一键订阅。核心逻辑就是这里“图片文本 → 结构化事件 → 日历/页面”这条管线,后续只是换入口和出口。
如果只是偶尔整理几张活动海报,直接用脚本就行,不需要写 API;如果要把能力开放给团队或重复使用的工具,再上 FastAPI。建议先把这条流程的代码和测试样例保存下来,后续遇到新的活动海报,直接丢进inputs目录跑一次批量任务,数据就整理好了。