Skip to content

实现 敏感词违禁词检测 工具

分类: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-inputel-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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
}

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 效果截图

敏感词违禁词检测 效果截图

工具访问地址:https://www.i91tools.com/tools/sensitive-words