Appearance
实现 敏感词违禁词检测 工具
分类:developer | 标签:敏感词、违禁词、检测 检测文案中的广告违禁词并高亮,支持自定义词库
2.1 功能说明
检测文案中的广告违禁词并高亮,支持自定义词库
2.1.1 使用指南
功能说明
把文案粘贴进来,一键找出其中的广告法极限词、医疗夸大用语、金融违规承诺等风险词汇, 按类别用不同颜色标注出位置,并给出命中统计和替换后的安全文案。词库内置在页面中,检测过程不联网、不上传。
内置词库
共 {{ BUILTIN_WORDS.length }} 个词,覆盖 {{ CATEGORIES.length }} 个类别:
{{ c.label }}:{{ c.desc }}
使用方法
1. 在左侧输入或粘贴文案,检测结果实时更新;
2. 右上方查看命中总数与分类分布,点击类别标签可以只看某一类;
3. 在「替换预览」中调整替换字符与替换方式,复制处理后的安全文案;
4. 如需检测行业特有词汇,在「自定义词」中按行或用逗号分隔追加,仅在当前页面生效,刷新即失效。
关于广告法极限词
《广告法》第九条禁止使用"国家级""最高级""最佳"等表示最高等级、最佳效果的用语。实际执法中,能提供权威第三方评测依据、或明确限定时间与范围的表述(例如"2025 年双十一本店销量第一")风险较低,需结合语境判断。
结果仅供参考
本工具基于关键词匹配,无法理解上下文语义,可能出现误报(例如"最后一天"中的"最后"、"绝对值"中的"绝对")与漏报。正式对外发布前请结合人工复核,或接入专业的内容合规审核服务。
涉政类词汇未内置,此类内容请以主管部门与平台方提供的官方词库为准。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
escapeHtml()copyReplaced()downloadReplaced()loadSample()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import {computed, ref} from 'vue'
import {ElMessage} from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'
/* ------------------------- 内置词库 ------------------------- */
const CATEGORIES = [
{key: 'ad', label: '广告法极限词', color: '#E6511F', desc: '表示最高等级、绝对化效果的用语,《广告法》第九条明令禁止。'},
{key: 'medical', label: '医疗保健违禁', color: '#B34AE0', desc: '普通商品或食品宣称疗效、承诺治愈的表述,涉嫌违反《广告法》第十七条。'},
{key: 'finance', label: '金融理财违规', color: '#D48806', desc: '承诺收益、保本保息、诱导投资类表述,属于金融营销宣传的禁区。'},
{key: 'illegal', label: '违法违规内容', color: '#C41D3A', desc: '赌博、假证、票据、管制物品等明确违法的内容。'},
{key: 'vulgar', label: '低俗不良信息', color: '#7B61FF', desc: '低俗、软色情导流类词汇,多数平台会直接限流或下架。'},
{key: 'feudal', label: '封建迷信', color: '#0F8A6B', desc: '算命、转运、开光等宣扬迷信的表述。'},
{key: 'custom', label: '自定义词', color: '#2F6FED', desc: '你在页面中临时追加的词汇,刷新后失效。'}
]
const CATEGORY_MAP = Object.fromEntries(CATEGORIES.map(c => [c.key, c]))
const BUILTIN_WORDS = [
// ---- 广告法极限词 ----
{w: '国家级', c: 'ad'}, {w: '世界级', c: 'ad'}, {w: '最高级', c: 'ad'}, {w: '最佳', c: 'ad'},
{w: '最大', c: 'ad'}, {w: '最好', c: 'ad'}, {w: '最优', c: 'ad'}, {w: '最低价', c: 'ad'},
{w: '最便宜', c: 'ad'}, {w: '最先进', c: 'ad'}, {w: '最强', c: 'ad'}, {w: '最新技术', c: 'ad'},
{w: '第一品牌', c: 'ad'}, {w: '销量第一', c: 'ad'}, {w: '销量冠军', c: 'ad'}, {w: '全国第一', c: 'ad'},
{w: '全网最低', c: 'ad'}, {w: '史上最低', c: 'ad'}, {w: '独一无二', c: 'ad'}, {w: '绝无仅有', c: 'ad'},
{w: '空前绝后', c: 'ad'}, {w: '前无古人', c: 'ad'}, {w: '史无前例', c: 'ad'}, {w: '首选', c: 'ad'},
{w: '唯一', c: 'ad'}, {w: '独家', c: 'ad'}, {w: '首家', c: 'ad'}, {w: '首创', c: 'ad'},
{w: '领导品牌', c: 'ad'}, {w: '领先品牌', c: 'ad'}, {w: '王牌', c: 'ad'}, {w: '巨星', c: 'ad'},
{w: '顶级', c: 'ad'}, {w: '极品', c: 'ad'}, {w: '极致', c: 'ad'}, {w: '绝对', c: 'ad'},
{w: '完美', c: 'ad'}, {w: '万能', c: 'ad'}, {w: '无敌', c: 'ad'}, {w: '永久', c: 'ad'},
{w: '终身', c: 'ad'}, {w: '百分百', c: 'ad'}, {w: '100%', c: 'ad'}, {w: '零风险', c: 'ad'},
{w: '免费送', c: 'ad'}, {w: '点击领奖', c: 'ad'}, {w: '恭喜获奖', c: 'ad'}, {w: '秒杀', c: 'ad'},
{w: '抢购', c: 'ad'}, {w: '大甩卖', c: 'ad'}, {w: '清仓', c: 'ad'}, {w: '限时优惠', c: 'ad'},
{w: '最后一次', c: 'ad'}, {w: '仅此一次', c: 'ad'}, {w: '国家免检', c: 'ad'}, {w: '驰名商标', c: 'ad'},
{w: '老字号', c: 'ad'}, {w: '权威推荐', c: 'ad'}, {w: '专家推荐', c: 'ad'}, {w: '央视上榜', c: 'ad'},
// ---- 医疗保健违禁 ----
{w: '治愈', c: 'medical'}, {w: '根治', c: 'medical'}, {w: '痊愈', c: 'medical'}, {w: '疗效', c: 'medical'},
{w: '药到病除', c: 'medical'}, {w: '包治百病', c: 'medical'}, {w: '祖传秘方', c: 'medical'},
{w: '特效药', c: 'medical'}, {w: '无副作用', c: 'medical'}, {w: '无毒副作用', c: 'medical'},
{w: '安全无害', c: 'medical'}, {w: '消炎', c: 'medical'}, {w: '抗菌', c: 'medical'}, {w: '杀菌', c: 'medical'},
{w: '防癌', c: 'medical'}, {w: '抗癌', c: 'medical'}, {w: '壮阳', c: 'medical'}, {w: '丰胸', c: 'medical'},
{w: '排毒', c: 'medical'}, {w: '解毒', c: 'medical'}, {w: '减肥瘦身', c: 'medical'}, {w: '快速瘦', c: 'medical'},
{w: '不反弹', c: 'medical'}, {w: '生发', c: 'medical'}, {w: '去疤', c: 'medical'}, {w: '美白祛斑', c: 'medical'},
{w: '延年益寿', c: 'medical'}, {w: '返老还童', c: 'medical'}, {w: '增强免疫力', c: 'medical'},
// ---- 金融理财违规 ----
{w: '稳赚不赔', c: 'finance'}, {w: '稳赚', c: 'finance'}, {w: '保本保息', c: 'finance'},
{w: '保本', c: 'finance'}, {w: '保收益', c: 'finance'}, {w: '高回报', c: 'finance'},
{w: '躺赚', c: 'finance'}, {w: '暴利', c: 'finance'}, {w: '一夜暴富', c: 'finance'},
{w: '内幕消息', c: 'finance'}, {w: '原始股', c: 'finance'}, {w: '资金盘', c: 'finance'},
{w: '日结返利', c: 'finance'}, {w: '拉人头', c: 'finance'}, {w: '高额返现', c: 'finance'},
{w: '无抵押贷款', c: 'finance'}, {w: '当天放款', c: 'finance'}, {w: '黑户可贷', c: 'finance'},
// ---- 违法违规内容 ----
{w: '赌博', c: 'illegal'}, {w: '博彩', c: 'illegal'}, {w: '六合彩', c: 'illegal'}, {w: '私彩', c: 'illegal'},
{w: '代开发票', c: 'illegal'}, {w: '代办证件', c: 'illegal'}, {w: '办假证', c: 'illegal'},
{w: '刻假章', c: 'illegal'}, {w: '枪支', c: 'illegal'}, {w: '弹药', c: 'illegal'}, {w: '管制刀具', c: 'illegal'},
{w: '毒品', c: 'illegal'}, {w: '冰毒', c: 'illegal'}, {w: '大麻', c: 'illegal'}, {w: '洗钱', c: 'illegal'},
{w: '传销', c: 'illegal'}, {w: '高利贷', c: 'illegal'}, {w: '套现', c: 'illegal'}, {w: '走私', c: 'illegal'},
{w: '偷税漏税', c: 'illegal'}, {w: '木马程序', c: 'illegal'}, {w: '游戏外挂', c: 'illegal'},
{w: '刷单', c: 'illegal'}, {w: '刷好评', c: 'illegal'}, {w: '代考', c: 'illegal'}, {w: '答案泄露', c: 'illegal'},
// ---- 低俗不良信息 ----
{w: '色情', c: 'vulgar'}, {w: '裸聊', c: 'vulgar'}, {w: '一夜情', c: 'vulgar'}, {w: '约炮', c: 'vulgar'},
{w: '成人用品', c: 'vulgar'}, {w: '情色', c: 'vulgar'}, {w: '男公关', c: 'vulgar'}, {w: '陪聊', c: 'vulgar'},
// ---- 封建迷信 ----
{w: '开光', c: 'feudal'}, {w: '转运', c: 'feudal'}, {w: '辟邪', c: 'feudal'}, {w: '算命', c: 'feudal'},
{w: '看风水', c: 'feudal'}, {w: '改命', c: 'feudal'}, {w: '消灾', c: 'feudal'}, {w: '招财符', c: 'feudal'},
{w: '有求必应', c: 'feudal'}
]
const SAMPLE = `【全网最低价】本店独家研发的祖传秘方养生茶,采用世界级顶尖工艺,效果绝对完美!
连续三年销量第一,是行业内公认的领导品牌,被誉为国家级免检产品。
坚持饮用可以排毒养颜、增强免疫力,对慢性咽炎有明显疗效,无副作用,一个月见效,不反弹!
现在下单立享限时优惠,前 100 名免费送开光转运手链一条,稳赚不赔的养生投资,错过就是最后一次。
点击领奖即可参与秒杀,日结返利高达 30%,零风险高回报,欢迎拉人头组队抢购!`
/* ------------------------- 状态 ------------------------- */
const text = ref('')
const customWordsRaw = ref('')
const ignoreCase = ref(true)
const activeCategory = ref('')
const replaceChar = ref('*')
const replaceMode = ref('each')
const customWords = computed(() =>
Array.from(new Set(
customWordsRaw.value
.split(/[\n,,、;;]+/)
.map(s => s.trim())
.filter(Boolean)
))
)
const allWords = computed(() => {
const list = BUILTIN_WORDS.map(item => ({word: item.w, category: item.c}))
const exists = new Set(list.map(i => i.word))
for (const w of customWords.value) {
if (!exists.has(w)) list.push({word: w, category: 'custom'})
}
// 长词优先匹配,避免"最低价"被"最低"抢先命中
return list.sort((a, b) => b.word.length - a.word.length)
})
/* ------------------------- 检测 ------------------------- */
// 返回互不重叠的命中列表,按出现位置排序
const matches = computed(() => {
const source = text.value
if (!source) return []
const haystack = ignoreCase.value ? source.toLowerCase() : source
const taken = new Uint8Array(source.length)
const found = []
for (const {word, category} of allWords.value) {
const needle = ignoreCase.value ? word.toLowerCase() : word
if (!needle) continue
let from = 0
while (from <= haystack.length - needle.length) {
const idx = haystack.indexOf(needle, from)
if (idx === -1) break
// 与已命中的更长词重叠时跳过
let overlap = false
for (let i = idx; i < idx + needle.length; i++) {
if (taken[i]) {
overlap = true
break
}
}
if (!overlap) {
for (let i = idx; i < idx + needle.length; i++) taken[i] = 1
found.push({word, category, start: idx, end: idx + needle.length})
}
from = idx + needle.length
}
}
return found.sort((a, b) => a.start - b.start)
})
const filteredMatches = computed(() =>
activeCategory.value ? matches.value.filter(m => m.category === activeCategory.value) : matches.value
)
const hitWordCount = computed(() => new Set(matches.value.map(m => m.word)).size)
const categoryStats = computed(() => {
const counter = {}
for (const m of matches.value) counter[m.category] = (counter[m.category] || 0) + 1
return CATEGORIES
.filter(c => counter[c.key])
.map(c => ({...c, count: counter[c.key]}))
})
const wordStats = computed(() => {
const map = new Map()
for (const m of filteredMatches.value) {
if (!map.has(m.word)) {
map.set(m.word, {
word: m.word,
label: CATEGORY_MAP[m.category].label,
color: CATEGORY_MAP[m.category].color,
count: 0,
positions: []
})
}
const item = map.get(m.word)
item.count++
if (item.positions.length < 6) item.positions.push(m.start + 1)
}
return Array.from(map.values()).sort((a, b) => b.count - a.count)
})
/* ------------------------- 标注渲染 ------------------------- */
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
const highlightedHtml = computed(() => {
const source = text.value
if (!source) return '<span class="placeholder">输入文案后,命中的风险词会在这里按类别高亮显示</span>'
const list = filteredMatches.value
if (!list.length) return escapeHtml(source).replace(/\n/g, '<br/>')
let html = ''
let cursor = 0
for (const m of list) {
if (m.start > cursor) html += escapeHtml(source.slice(cursor, m.start))
const cat = CATEGORY_MAP[m.category]
html += `<mark class="hit" style="background:${cat.color}1f;color:${cat.color};border-color:${cat.color}" title="${escapeHtml(cat.label)}">${escapeHtml(source.slice(m.start, m.end))}</mark>`
cursor = m.end
}
if (cursor < source.length) html += escapeHtml(source.slice(cursor))
return html.replace(/\n/g, '<br/>')
})
/* ------------------------- 替换 ------------------------- */
const replacedText = computed(() => {
const source = text.value
if (!source) return ''
const list = matches.value
if (!list.length) return source
const mask = replaceChar.value || '*'
let out = ''
let cursor = 0
for (const m of list) {
out += source.slice(cursor, m.start)
const raw = source.slice(m.start, m.end)
if (replaceMode.value === 'whole') {
out += mask
} else if (replaceMode.value === 'keepFirst') {
out += raw[0] + mask.repeat(Math.max(0, raw.length - 1))
} else {
out += mask.repeat(raw.length)
}
cursor = m.end
}
out += source.slice(cursor)
return out
})
async function copyReplaced() {
try {
await navigator.clipboard.writeText(replacedText.value)
ElMessage.success('已复制处理后的文案')
} catch {
ElMessage.warning('复制失败,请手动选择内容复制')
}
}
function downloadReplaced() {
const blob = new Blob([replacedText.value], {type: 'text/plain;charset=utf-8'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '安全文案.txt'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
setTimeout(() => URL.revokeObjectURL(url), 1000)
}
function loadSample() {
text.value = SAMPLE
activeCategory.value = ''
}2.3 效果截图
