news 2026/10/3 3:07:04

电商文案自动抽取:规则与序列标注结合的Python工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
电商文案自动抽取:规则与序列标注结合的Python工程实践

简介:一套基于Python实现的电商营销文案自动生成完整项目,源自京东NLP高阶实战训练营二期,面向自然语言处理学习者、电商数据分析师以及计算机相关专业毕业生。项目利用商品标题、属性标签与OCR信息,基于seq2seq、attention及pointer networks等模型,实现从商品数据到营销文案的端到端生成,既有源码又有说明文档,适合课设、毕设或项目初期演示。资源包共包含63个文件,以27个Python脚本为核心,辅以7个文本说明、6张架构图示、5个XML配置、3个Markdown文档和预训练数据等,整体压缩包约21.78MB,目录结构清晰,便于按模块研究。目前已有157人学习或下载,适合具备一定Python基础、希望深入NLP实战的读者。通过这份资源,可学习数据处理与特征构建、模型训练和评估的完整流程,也能对照README与项目过程问题记录,快速复现营销文案生成实验,并在此基础扩展其他生成任务。

1. 电商文案自动抽取到底在解决什么问题:一次上新就能看到的效率差

帮朋友跑电商商品文案时,一千多个商品池,每次上新要写一轮卖点,光靠人从详情页里翻,一天最多处理三四十个。“京东NLP高阶实战训练营二期”这类把电商数据自动抽取当目标的实战项目,解决的正是这个问题:用 Python 把标题、详情、评价里的卖点自动抽出来,再组装成营销文案草稿。它不是套提示词让模型瞎编,而是抽取式、可解释、能质检的工程方案。做这个方向的人,可以从一条规则跑通,再一步步把 NLP 模型接进去。

2. 先拆 NLP 抽取链路:规则管句子,模型管属性

文案抽取看起来是一个功能,拆开其实是两件事:句子级抽取是判断“这段话值不值得写进营销文案”,片段级抽取是拿到“成分、规格、功效、适用人群”这些属性值。两件事混在一起做,后面改参数会非常痛苦。

我建议先把链路拆成五段:清洗、分句、粗过滤、属性抽取、文案组装。前两段是纯文本工程,中间是规则和模型的分工点,最后一段决定文案是“拼出来的”还是“编出来的”。

2.1 先把输入输出说清楚:手里有什么文本,要抽成什么结构

电商场景里能拿到的原始文本大概有四类:

数据来源特点适合抽什么
商品标题短、关键词密集品牌、品类、核心功效
详情描述长、HTML 标签多、促销话术混杂规格、成分、使用场景
用户评价口语化、正负混杂真实体验、适用人群、优缺点
导购话术/直播口播接近营销语言,量少整句卖点,可直接复用

输出不要只给“一段文案”,要分成两个层级。句子级输出返回“适合换季敏感肌”这类营销句;片段级输出返回“功效=保湿、规格=30ml、成分=烟酰胺”这样的槽位。我一般把两者都存下来,句子给运营做初稿,槽位给后续的详情页结构化填充用。

2.2 为什么先上规则和词典,而不是一上来就训练模型

很多第一次接触这个方向的同学,第一反应是“用大模型生成文案”。但在电商文案这个场景里,生成式输出不可控,抽句式方案至少能保证每句话都来自真实商品数据。

规则的落地成本很低:一个词典、一组正则、几十行 Python,就能覆盖价格、规格、含量、活动信息这类明确卖点。模型要标签数据,而电商商品类目多、说法五花八门,一份标注两三百条只是起点。所以我通常把落地顺序定成:先用规则做到“能出结果”,再针对模型抽不准的槽位补序列标注。

2.3 模型管属性,规则管句子,这个分工要提前定

规则擅长判断“这句话是不是卖点”,但不擅长从长句里抠出“5% 烟酰胺”这样的片段;序列标注模型擅长抠片段,但判断整句话能否作为营销文案时,需要大量标签数据。

因此分工是这样的:规则负责句子级筛选,模型负责属性级抽取。具体流程是清洗 → 分句 → 规则粗滤 → 句子评分排序 → 用模型补全属性槽位 → 模板组装。这样任何一个环节出问题,都能单独修,不用推翻整条链路。

3. 用 Python 实现候选文案抽取:清洗、分句、过滤与兜底正则

下面这套代码是基于 Python 实现的最小可用版本,只依赖标准库和 jieba,核心是把商品数据读进来,清洗后切成句子,再用词典给句子打分,把高分句子作为营销文案候选。

3.1 加载商品数据与清洗函数

商品数据我习惯用 JSONL 格式输入,每个商品一行,字段包括 title、detail、reviews、price。不直接接数据库,目的是让脚本能在本地小批量跑,调参不依赖线上环境。

# -*- coding: utf-8 -*- import re import json def clean_text(raw: str) -> str: if not raw: return "" raw = re.sub(r"<[^>]+>", "", raw) # 去 HTML 标签 raw = re.sub(r"\s+", " ", raw) # 压缩空白 raw = raw.replace("&nbsp;", " ").replace("&amp;", "&") return raw.strip() def load_products(path: str): products = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue obj = json.loads(line) products.append({ "id": obj.get("id"), "title": clean_text(obj.get("title", "")), "detail": clean_text(obj.get("detail", "")), "reviews": [clean_text(r) for r in obj.get("reviews", [])], "price": obj.get("price", "") }) return products

clean_text 里的正则顺序有讲究:先去标签,再压空白,最后替换 HTML 实体。先替换实体再压空白的话,&nbsp;会被压成空格,但标签残留依然存在。加载后用products[:20]调试,避免一上来处理全量数据拖慢速度。

3.2 中文分句与噪音过滤

中文分句不能直接按split("。")处理,因为详情页里经常出现“买一送一!限时抢购!立即下单!”这种连续感叹句,拆出来全是噪音。我一般按标点切完再叠加长度和内容过滤。

def split_sentences(text: str) -> list: text = re.sub(r"([。!?!?;;])", r"\1\n", text) lines = [ln.strip() for ln in text.split("\n") if ln.strip()] result = [] for ln in lines: if len(ln) > 80: ln = re.sub(r"[,,]", "\n", ln) result.extend([s.strip() for s in ln.split("\n") if s.strip()]) else: result.append(ln) return result STOP_WORDS = { "亲", "您好", "下单", "发货", "物流", "客服", "售后", "点击", "关注", "收藏", "优惠券", "活动" } def is_useless(sentence: str) -> bool: if len(sentence) < 6: return True if any(w in sentence for w in STOP_WORDS): return True if sentence.count("!") + sentence.count("!") >= 2: return True if re.fullmatch(r"[\s\W_]+", sentence): return True return False

80 字符阈值是经验值:小于 80 的句子保留整体语义,大于 80 的句子按逗号拆,避免一长句里混多个卖点。注意“按逗号二次拆分”有副作用,比如“含 5% 烟酰胺,能提亮肤色,改善暗沉”会拆成三句,但后面用槽位召回可以弥补。

3.3 候选句评分:词典匹配、词频统计与正则兜底

句子过滤完,要给每句话打分。我用一个按类目组织的词典,命中不同类目累计得分,再正则化兜底“数字+单位”的规格表达。

from collections import Counter SELLING_DICT = { "功效": ["保湿", "提亮", "舒缓", "控油", "抗皱", "修护", "淡纹", "紧致"], "成分": ["烟酰胺", "玻尿酸", "视黄醇", "胜肽", "角鲨烷", "水杨酸", "神经酰胺"], "场景": ["换季", "熬夜", "夏天", "晒后", "敏感"], "人群": ["敏感肌", "油皮", "干皮", "熟龄肌", "学生党"], "规格": ["ml", "g", "片", "支", "盒"] } def score_sentence(sentence: str) -> dict: hits = {} for cat, words in SELLING_DICT.items(): n = sum(1 for w in words if w in sentence) if n: hits[cat] = n score = sum(hits.values()) if re.search(r"\d+(\.\d+)?\s*(ml|g|片|支|盒)", sentence, re.I): hits.setdefault("规格", 1) score += 1 counter = Counter(re.findall(r"[\u4e00-\u9fa5]{2,}", sentence)) dup = sum(v for v in counter.values() if v > 2) score -= dup return {"score": score, "hits": hits, "text": sentence} def extract_candidates(products, min_score=2): candidates = [] for p in products[:20]: sentences = split_sentences(p["detail"]) + p["reviews"] for s in sentences: if is_useless(s): continue result = score_sentence(s) if result["score"] >= min_score: candidates.append({**p, **result}) return candidates

min_score=2 表示一句话至少要命中两个类目才够格,比如“保湿+敏感肌”可以,“保湿”单独出现先不进候选。词频统计这里做的是扣分惩罚:一句话里同一个词出现超过 2 次,多半是凑字,减去重复次数。

3.4 这几个参数值得反复调

参数默认值影响
min_score2候选数量与精度的平衡点,越大越严
分句长度阈值80长句是否二次拆分
is_useless 最短长度6过滤“好用”“不错”这类短评
STOP_WORDS内置集合过滤售前售后句式
正则规格模板数字+单位兜底抽取规格槽位

调参顺序我建议是:先调 min_score 看候选量,再调 STOP_WORDS 处理本类目特有的噪音,最后才动分句长度。一上来动分句阈值,会把整个候选结构打乱,后面不好定位问题。

4. NLP 增强:用序列标注补上“属性槽位”抽取

规则能解决句子筛选,但“5% 烟酰胺”“30ml”“适合熟龄肌”这些片段还是需要模型来抽。这里说的模型不是大模型,而是轻量的序列标注模型,训练快、推理快、好排查。

4.1 半自动标注:从规则结果生成第一批训练数据

给序列标注准备数据时,不要从零开始标。跑一遍上一章的规则,把命中的句子导出成文本文件,标注员只需要在句子里把“成分”“功效”“规格”划出来。这样做有两个好处:第一,候选句本身已经过滤掉大量无效文本;第二,标注结果可以直接回填到规则管道里做校验。

标签集合我常用 BIO 格式:

标签含义
B-EFFECT / I-EFFECT功效词首字 / 功效词剩余部分
B-INGREDIENT / I-INGREDIENT成分词首字 / 剩余部分
B-SPEC / I-SPEC规格词首字 / 剩余部分
B-SCENE / I-SCENE场景词首字 / 剩余部分
O非槽位

标注时注意“不闷”“不卡粉”这类词,负向表达很容易被标成功效。我的习惯是负向否定词不单独建标签,直接交给规则层处理,模型只抽正向片段。

4.2 用 sklearn-crfsuite 训练属性抽取模型

特征工程是 CRF 的核心。我用 jieba 分词和词性,再叠加前后词、词长、是否数字这些基础特征,足够覆盖电商商品文本的常见模式。

# -*- coding: utf-8 -*- import jieba.posseg as pseg import sklearn_crfsuite def word2features(sent, i): word = sent[i][0] pos = sent[i][1] features = { "w": word, "pos": pos, "w.lower": word.lower(), "w.isdigit": word.isdigit(), "w.length": len(word), "prefix1": word[:1], "suffix1": word[-1:], "pos_prefix": pos[:2], } if i > 0: wprev, pprev = sent[i-1][0], sent[i-1][1] features.update({ "w.prev": wprev, "pos.prev": pprev, }) if i < len(sent) - 1: wnext, pnext = sent[i+1][0], sent[i+1][1] features.update({ "w.next": wnext, "pos.next": pnext, }) return features def sent2features(sent): return [word2features(sent, i) for i in range(len(sent))] def sent2labels(sent): return [label for _, _, label in sent] # train_sents 是形如 [(word, pos, label), ...] 的标注数据 X_train = [sent2features(s) for s in train_sents] y_train = [sent2labels(s) for s in train_sents] crf = sklearn_crfsuite.CRF( algorithm="lbfgs", c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True, ) crf.fit(X_train, y_train)

c1 控制 L1 正则,c2 控制 L2 正则。电商文本特征稀疏,c1 设 0.1 到 0.3 能压掉不少无关特征;c2 太大会让模型变得保守,抽出的槽位变少。max_iterations=100 对几千条训练样本足够,如果 loss 还在下降,可以加到 200。

提示:sklearn-crfsuite 依赖 python-crfsuite,安装时不要分开装两个版本,否则会出现模型可以训练但 predict 报错的情况。

4.3 把模型输出接回规则管道:槽位补全与文案组装

模型输出的是标签序列,要转回槽位字典,再和规则结果合并。合并规则是:同属性槽位模型优先,模型没抽到的用规则正则兜底。

def slots_from_crf(sent, y_pred): slots = {"effect": [], "ingredient": [], "spec": [], "scene": []} cur_type = None cur_value = [] for word, label in zip(sent, y_pred): if label.startswith("B-"): if cur_type and cur_value: slots[cur_type].append("".join(cur_value)) _, cur_type = label.split("-") cur_value = [word] elif label.startswith("I-"): cur_value.append(word) else: if cur_type and cur_value: slots[cur_type].append("".join(cur_value)) cur_type, cur_value = None, [] if cur_type and cur_value: slots[cur_type].append("".join(cur_value)) return slots def assemble_copy(title, slots): effect = "、".join(slots.get("effect", [])) spec = "/".join(slots.get("spec", [])) scene = slots.get("scene", []) parts = [title] if effect: parts.append(f"主打{effect}") if spec: parts.append(spec) if scene: parts.append(f"适合{scene[0]}") return " ".join(parts)

组装只用模板,不用生成模型。好处是每段文案都能讲清楚来自哪个字段,运营要改也能直接改模板。最后实际项目里,模板会变成配置项,不同类目用不同模板,而不是一套写死。

5. 电商文案抽取避坑指南:5 个高频翻车点与排查方法

这个方向踩坑太常见了。下面几条是我在调参和跑数据集时反复遇到的,按现象、原因、解决三段式写。

5.1 同一句话被抽出五个重复卖点

现象:抽取结果里“保湿”出现了 5 次,第一条和第五条的句子完全一样,只是措辞略有不同。

原因:分句后没有做相似句去重。详情页经常把同一卖点换几种说法重复写,比如“深层保湿”“补水保湿”“水润保湿”。

解决:先在句子层面做粗去重,用字面相似度或 SimHash 都行。我一般用最简单的方式:把句子里的数字和单位去掉后做 MD5,再计算连续两个字符的 Jaccard 相似度,相似度高于 0.7 就只保留得分高的一句。

5.2 “不闷”“不卡粉”被当成正向卖点

现象:用户评价里写“不闷痘”“不卡粉”,规则把“不闷”拆出来,当成正向功效词进了候选。

原因:词典匹配只看单字词,没处理否定前缀。模型训练时也没给否定词单独建标签。

解决:维护一个否定词表,包含“不”“没”“无”“非”等前缀,命中这些前缀时,后面紧跟的词不进正向卖点槽位。更简单的方式是先在词典层加例外:把“不闷”“不卡粉”“不搓泥”作为整体加入黑名单,整句过滤。

5.3 规格被拆成“3盒”和“2件”,组句时回不来

现象:商品文案里写“买 3 盒送 2 件旅行装”,规则抽出来变成了“3盒”“2件”两个独立规格,组装时文案变成“3盒2件”,用户不知道哪个是正装哪个是赠品。

原因:正则优先抽“数字+单位”,没有做上下文组合。

解决:在分句前先做规格句的模板合并。常见做法是维护一组活动模板,如“买 A 送 B”“A 件 B 盒”,先匹配完整模板,再让候选句直接引用模板结果。“买 3 盒送 2 件旅行装”应该抽成“规格=3盒,赠品=2件旅行装”,而不是两个并列规格。

5.4 详情页跳转长句和活动口号混进结果

现象:抽取结果里出现“点击下方链接了解更多详情”“立即抢购”这类句子,甚至还有“关注店铺有惊喜”。

原因:STOP_WORDS 覆盖不够,本类目特有的运营话术没进词表。

解决:每换一个类目,先跑一遍全量抽取,把结果里明显不像卖点的句子按文本聚类,然后批量加进 STOP_WORDS。不要想着一次性写好词表,抽取工程本来就是滚动的。

5.5 模型标签不一致,同属性出现多个值

现象:同一个商品抽出的“功效”有“保湿”“锁水”“水润”,其实都是同一属性的近义词。

原因:序列标注模型只负责切片段,不负责归一化。

解决:在规则层加一个同义词归并表,把“锁水”“水润”归一到“保湿”。这个表可以很小,覆盖 80% 的重复情况就够。归一化放在模型输出之后、文案组装之前,这样模板拿到的数据是干净的。

6. 验证与进阶:用最小标注集把抽取效果固定下来

抽取系统的效果评估,不能靠肉眼每次看十几条。我的做法是固定一组 30 到 50 个商品作为回归集,每次改词典、改参数、换模型后,全量跑一遍,记录三个指标:能出候选的商品比例、候选句准确率、槽位命中率。

回归集不追求大,但要有代表性:包含标点混乱的详情页、短标题商品、纯促销型商品、含负向评价的商品。每次跑完把结果导出成表格,记录“问题类型”,下次改完再看同一个 case 有没有变好。

日期商品 id输出片段问题类型修改项
03-1810021主打保湿、提亮无词典新增“水润”
03-1810022主打美白、适合熟龄肌场景误抽场景词表去重

进阶可以做两件事:把模板配置化,不同类目用不同模板;把同义词表升级成可维护的词库文件。不要试图在代码里硬改词表,词库应该独立维护,每次改动走 review。

我的习惯是每次调参前先跑回归集,调完再跑一遍,对比差异。这比对着网页一条条看靠谱得多,也能避免“调好 A 类目毁了 B 类目”的玄学。电商文案抽取这个方向,真正难的不是单个模型,而是把句子、槽位、模板、词表串成一条可维护的链路。源码和项目说明文档分开写,文档里记录数据格式、参数表、回归集改动,三个月后回头看自己代码,能少掉不少头发。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:06:08

NUAA PL0编译器实战解析:词法语法分析到栈式代码生成

简介&#xff1a;本资源是南京航空航天大学编译原理课程设计的完整实践包&#xff0c;面向计算机专业本科生及编译技术初学者&#xff0c;聚焦PL0语言编译器从理论到落地的全流程实现。包内共7个文件&#xff0c;含1个C源码文件&#xff08;实现词法分析、语法分析与代码生成核…

作者头像 李华
网站建设 2026/10/3 3:05:57

Nano Banana实测:用对话式Prompt替代复杂提示词工程

1. 先说清楚 Nano Banana 是谁&#xff0c;为什么它最近总被提起最近好几个圈子的人都在问配图的事——自媒体做头图的、电商做详情页的、程序员要画个示意图的、设计师找灵感的&#xff0c;最后都绕到同一个东西上&#xff1a;Nano Banana。Nano Banana 不是某个咖啡店的限定甜…

作者头像 李华
网站建设 2026/10/3 3:04:27

用Commitizen和commitlint建立可追溯的git提交规范

你接手过别人的项目&#xff0c;打开git log --oneline一看&#xff0c;满屏都是fix、update、bug fix&#xff0c;甚至还有asdf、111这种随手敲的提交。你根本不知道哪个提交对应哪个需求&#xff0c;也不知道哪个改动引入了回归。我经历过太多次这种"提交考古"现场…

作者头像 李华
网站建设 2026/10/3 3:04:01

ONNX垃圾分类系统部署实战:从模型导入到Web服务避坑指南

简介&#xff1a;这份资源面向深度学习入门者与计算机视觉方向的开发者&#xff0c;提供一套基于Python实现的垃圾分类识别项目&#xff0c;重点解决图像自动分类的落地问题&#xff0c;并采用ONNX格式导入模型以提升跨平台部署的灵活性。压缩包共6个文件&#xff0c;包含3个cs…

作者头像 李华
网站建设 2026/10/3 3:03:45

Java企业人事管理系统毕设:从设计到答辩的完整实战指南

每年到了毕设季和课程设计季&#xff0c;Java方向出镜率最高的项目类型里&#xff0c;“企业人事管理系统”绝对能排进前三。这个名字听起来不复杂&#xff0c;但真拿到手你会发现&#xff1a;涉及的角色多、业务流程长、要交付的东西也不只是代码——文档、PPT、答辩演示一样都…

作者头像 李华
网站建设 2026/10/3 3:02:44

大连理工openGauss上机作业全流程:从建库到查询的实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华