☰
文章封面批量生成:一条命令产出 1200×675 专属封面(PIL 实战)
2026/10/14 3:35:37 网站建设 项目流程

文章封面批量生成:一条命令产出 1200×675 专属封面(PIL 实战)

写技术博客的人大多经历过这一刻:文章发了,列表页却挂着一张灰蒙蒙的默认图。点率差一截,还以为是自己标题起得不好。

这篇文章做一件事:用一条命令,给每篇文章生成一张 1200×675 的专属封面,并且讲清楚封面到底有没有挂上——不是"我以为传上去了",而是用可验证的判据确认。

全部代码基于真实工作流,PIL 12.3.0 实测,无虚构。

一、先立判据:封面挂没挂,谁说了算

在动手之前必须先定义"成功"。我踩过的坑是:上传接口返回 200、编辑器里也显示了图,但文章详情页的分享缩略图还是默认图。

后来固定用两个判据,互相印证:

  1. 文章页og:image:抓详情页 HTML,读<meta property="og:image" content="...">。是平台默认图(形如.../202112131616291007.png)就是没挂上;是i-blog.csdnimg.cn/direct/*.png这种带direct路径的才是真封面。
  2. 列表页/详情页首图视觉确认:个别平台 og 更新有延迟,截图看一眼最稳妥。

经验:"接口成功"和"用户能看到"是两件事。凡是要交付到用户眼前的结果,都要有一个用户视角的验收动作。这条以后端开发也一样适用。

二、尺寸与规格:为什么是 1200×675

用途建议尺寸说明
CSDN 文章封面1200×67516:9,列表卡片不变形
通用 OG 图1200×630社交分享裁剪安全区
知乎/公众号头图900×383 / 1080×432更宽的横条

本文以 1200×675 为主。原则只有一条:按平台推荐尺寸的整数倍做,避免二次缩放发虚。

三、生成器设计:每篇文章一张"专属"图

"专属"的意思是:只看封面就知道是哪篇。做法是三要素——主色(按文章主题分配)+ 标题文字 + 关键词标签。

# gen_cover.py —— 一条命令生成一张封面# 用法: python gen_cover.py <输出.png> <标题> <标签1> <标签2> [主色]importsys,hashlibfromPILimportImage,ImageDraw,ImageFont W,H=1200,675# 主题色板:深底 + 高对比前景,暗色模式下也稳PALETTE=[((24,32,56),(96,165,250)),# 深蓝 / 亮蓝((28,40,38),(52,211,153)),# 深绿 / 薄荷((42,28,46),(244,114,182)),# 深紫 / 粉((46,34,24),(251,191,96)),# 深棕 / 琥珀((30,36,48),(148,163,184)),# 石板灰 / 银]defpick_palette(seed_text):"""同一篇文章每次生成同一配色——可复现,改内容才会改色"""idx=int(hashlib.md5(seed_text.encode("utf-8")).hexdigest(),16)%len(PALETTE)returnPALETTE[idx]defload_font(size,bold=False):candidates=["C:/Windows/Fonts/msyhbd.ttc"ifboldelse"C:/Windows/Fonts/msyh.ttc","C:/Windows/Fonts/simhei.ttf","C:/Windows/Fonts/Arial.ttf",]forpathincandidates:try:returnImageFont.truetype(path,size)exceptOSError:continuereturnImageFont.load_default()defwrap_text(draw,text,font,max_w):"""按像素宽度折行,中文无空格也能断"""lines,cur=[],""forchintext:trial=cur+chifdraw.textlength(trial,font=font)<=max_w:cur=trialelse:ifcur:lines.append(cur)cur=chifcur:lines.append(cur)returnlinesdefmain():out=sys.argv[1]title=sys.argv[2]tags=sys.argv[3:]bg,fg=pick_palette(title)img=Image.new("RGB",(W,H),bg)d=ImageDraw.Draw(img)# 左上角色带 + 右下角弱化圆,制造层次又不抢字d.rectangle([0,0,W,10],fill=fg)forr,alphain((150,26),(110,20),(70,14)):d.ellipse([W-r,H-r,W+r,H+r],fill=fg)# 标题:自动缩放 + 折行,最多三行forsizein(72,64,56,48):f=load_font(size,bold=True)lines=wrap_text(d,title,f,W-160)iflen(lines)<=3:breaky=150forlninlines[:3]:d.text((80,y),ln,font=f,fill=(245,247,250))y+=int(size*1.45)# 标签:小胶囊,排在标题下方x=80ft=load_font(30)fortgintags[:3]:tw=d.textlength(tg,font=ft)pad=18d.rounded_rectangle([x,y+30,x+tw+pad*2,y+30+52],radius=26,outline=fg,width=3)d.text((x+pad,y+41),tg,font=ft,fill=fg)x+=tw+pad*2+20# 右下角落款,弱化处理fs=load_font(24)d.text((W-260,H-56),"技术实战笔记",font=fs,fill=(150,158,172))img.save(out,"PNG")print(f"OK{out}{W}x{H}")if__name__=="__main__":main()

跑一条:

python gen_cover.py cs-cover-484.png\"Chrome 自动化「点了没反应」"\"Chrome""CDP""自动化"

几个设计取舍,都是被现实毒打出来的:

  • 配色用标题做哈希种子:同一篇文章每次生成同一张图,改一次标题才变色。批次重跑不会出现"昨天蓝今天绿",方便对照。
  • 标题自动缩放 + 按像素折行:中文没有空格,按字符数折行必然溢出或参差。textlength按实际字宽判断,长标题自动缩到 48px。
  • 暗色底 + 亮前景:CSDN 有深色模式,白底封面在深色列表里像一块膏药。
  • 圆角胶囊用outline不用填充:填充色一多就花,描边干净。

四、批量化:一次给 N 篇文章出封面

文章多了就该有个清单。用 JSON 驱动,一条命令全出:

# batch_cover.py —— 读 cover-list.json,批量生成 + 输出缺失报告importjson,subprocess,sys,os LIST="cover-list.json"# [{"id":"167589876","title":"...","tags":["a","b"],"file":"cs-cover-876.png"}]defmain():items=json.load(open(LIST,encoding="utf-8"))missing=[]foritinitems:ifos.path.exists(it["file"]):print(f"SKIP{it['file']}已存在")continuecmd=[sys.executable,"gen_cover.py",it["file"],it["title"],*it.get("tags",[])]subprocess.run(cmd,check=True)missing.append(it["file"])print(f"DONE 新生成{len(missing)}张,清单共{len(items)}篇")if__name__=="__main__":main()

再配一个核查脚本,把上面说的判据落成代码——这才是交付的闭环:

# check_cover.py —— 逐篇读 og:image,报出"没挂封面"的文章importre,sys,urllib.request DEFAULT_HINT="202112131616291007"# 平台默认图特征串defcheck(article_id):url=f"https://blog.csdn.net/qq_58833970/article/details/{article_id}"req=urllib.request.Request(url,headers={"User-Agent":"Mozilla/5.0"})html=urllib.request.urlopen(req,timeout=15).read().decode("utf-8","ignore")m=re.search(r'<meta property="og:image" content="([^"]+)"',html)og=m.group(1)ifmelse""status="MISSING"ifDEFAULT_HINTinogornotogelse"OK"print(f"{article_id}{status}{og[:80]}")if__name__=="__main__":foraidinsys.argv[1:]:try:check(aid)exceptExceptionase:print(f"{aid}ERROR{e}")

踩坑提醒:curl 直抓文章页经常被拦(空响应/验证页)。稳妥做法是走浏览器:CDP 连上已登录的 Chrome,Runtime.evaluate读document.querySelector('meta[property="og:image"]').content。需要登录态的页面,别用裸 HTTP 客户端赌运气。

五、上传:封面的"最后一公里"

图生成了,怎么塞进发布流程才是真门槛。以 CSDN 为例,要点三条:

  1. 封面 input 是隐藏的:input.el-upload__input,用DOM.setFileInputFiles直接塞文件路径,比模拟点击文件选择对话框可靠一万倍。
  2. 裁剪弹窗的确认按钮文案是「确认上传」,而且它不一定是button标签——用'*'全标签遍历按文本匹配,别限定button,div,span。
  3. 新建文章发布时塞文件会弹裁剪框;已发布文章回编辑页补封面时塞文件不弹框。两条路径都要留代码。
// 片段:塞文件 + 点「确认上传」(全标签遍历)awaitsend('DOM.setFileInputFiles',{nodeId:inputNodeId,files:[pngPath],});// 轮询等裁剪弹窗出现,再全标签找「确认上传」constr=awaitev(`(function(){ var els = document.querySelectorAll('*'); for (var i = 0; i < els.length; i++) { var t = (els[i].innerText || '').trim(); if (t === '确认上传') { els[i].click(); return 'clicked'; } } return 'not-yet'; })()`);

改已发布文章会触发平台风控(一天内第二次保存要微信扫码)。所以补封面这种活儿,能批量就批量,能提前就提前——每改一次就要扫一次码,码还有 10 分钟寿命,人等码、码等人,两头空耗。

六、一条命令的完整形态

最终固化下来的日常用法:

# 1) 有新增文章就往 cover-list.json 里加一条# 2) 一条命令:批量生成 + 上传 + 核查python batch_cover.py&&nodecs-cover-batch.js

cs-cover-batch.js负责:连 CDP → 逐篇打开发布页 → 塞图 → 确认 → 保存 →当场回读 og:image 复核,最后打印一张OK/MISSING对照表。哪篇 MISSING,一眼看到,不用逐篇翻网页。

七、这套做法的复用价值

回头看,封面这件事真正的产出不是 5 张 png,而是三个可迁移的习惯:

  1. 先定验收判据再动手:og:image是不是默认图,比"我觉得传上去了"可靠。
  2. 可复现生成:哈希定色 + JSON 清单,任何时候能重出一模一样的图。
  3. 批处理 + 自核查:生成、上传、验收串成一条命令,报告直接告诉你哪几篇没挂上。

下次平台改版、上传路径变了,要改的只有cs-cover-batch.js一个文件;清单和生成器原样复用。把"手工重复"关进脚本,是这类琐事唯一的解法。


本文方法来自真实的多平台发布工作流:PIL 12.3.0 / 1200×675 / 五篇 CSDN 文章封面全部核验挂载。所有代码可直接运行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询