Skip to content

实现 文本集合运算 工具

分类:developer | 标签:集合、交集、差集 两段文本求交集/差集/并集/对称差与计数

2.1 功能说明

两段文本求交集/差集/并集/对称差与计数

2.1.1 使用指南

功能说明

把两段文本当作两个集合来做比对运算,常用于核对名单、找出新增或缺失的数据、排查重复项。所有计算都在浏览器本地完成,不会上传任何内容。

六种运算

  • 交集:A 与 B 中都存在的元素,用于找出两份名单的重合部分。
  • 差集 (A - B):只在 A 中出现、B 中没有的元素,用于找出「缺失」或「新增」。
  • 并集:A 与 B 合并后去重的全部元素。
  • 对称差:只在其中一边出现的元素,即 (A - B) 加上 (B - A),用于快速定位两份数据的所有差异。
  • 提取 A 中重复项:列出在 A 内部出现两次及以上的元素。
  • 统计出现次数:输出制表符分隔的四列(元素、A 中次数、B 中次数、合计),按合计倒序排列,可直接粘贴进 Excel。

选项含义

  • 分割方式:决定如何把文本拆成元素。按行最常用;按逗号会同时兼容中文逗号;按空白会用连续空格或换行拆分。
  • 忽略大小写:勾选后 Appleapple 视为同一元素,输出时保留首次出现的原始写法。
  • 忽略首尾空白:去掉每个元素两端的空格与制表符,避免因为多敲了空格而误判为不同元素。
  • 忽略空项:丢弃拆分后得到的空字符串,避免空行参与运算。

使用提示

运算结果保留元素在原文中首次出现的书写形式与顺序(统计功能除外,它按出现总次数倒序)。若两份数据来自不同系统,建议同时勾选「忽略大小写」和「忽略首尾空白」,可以显著减少误差。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • splitText()
  • keyOf()
  • uniqueMap()
  • countMap()
  • run()
  • swapAB()
  • loadDemo()
  • clearAll()
  • copyResult()
  • downloadResult()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'

const textA = ref('')
const textB = ref('')
const splitMode = ref('line')
const ignoreCase = ref(false)
const trimItem = ref(true)
const dropEmpty = ref(true)

const result = ref('')
const summary = ref('')
const currentOp = ref('')

const operations = [
  { key: 'intersect', label: '交集' },
  { key: 'diff', label: '差集 (A - B)' },
  { key: 'union', label: '并集' },
  { key: 'symmetric', label: '对称差' },
  { key: 'dupA', label: '提取 A 中重复项' },
  { key: 'count', label: '统计出现次数' }
]

function splitText(text) {
  if (!text) return []
  let parts
  if (splitMode.value === 'line') parts = text.split(/\r?\n/)
  else if (splitMode.value === 'comma') parts = text.split(/[,,]/)
  else parts = text.split(/\s+/)
  if (trimItem.value) parts = parts.map(s => s.trim())
  if (dropEmpty.value) parts = parts.filter(s => s.length > 0)
  return parts
}

const listA = computed(() => splitText(textA.value))
const listB = computed(() => splitText(textB.value))

function keyOf(s) {
  return ignoreCase.value ? s.toLowerCase() : s
}

// 键 -> 首次出现的原始文本,保证输出保留原始书写形式
function uniqueMap(list) {
  const map = new Map()
  list.forEach(v => {
    const k = keyOf(v)
    if (!map.has(k)) map.set(k, v)
  })
  return map
}

function countMap(list) {
  const map = new Map()
  list.forEach(v => {
    const k = keyOf(v)
    map.set(k, (map.get(k) || 0) + 1)
  })
  return map
}

const uniqCountA = computed(() => uniqueMap(listA.value).size)
const uniqCountB = computed(() => uniqueMap(listB.value).size)

function run(op) {
  currentOp.value = op
  const a = listA.value
  const b = listB.value
  if (!a.length && !b.length) {
    result.value = ''
    summary.value = ''
    ElMessage.warning('请先输入文本 A 或文本 B')
    return
  }

  const uniqA = uniqueMap(a)
  const uniqB = uniqueMap(b)
  let lines = []

  if (op === 'intersect') {
    lines = [...uniqA.entries()].filter(([k]) => uniqB.has(k)).map(([, v]) => v)
    summary.value = `交集:${lines.length} 项(A 去重 ${uniqA.size} 项,B 去重 ${uniqB.size} 项)`
  } else if (op === 'diff') {
    lines = [...uniqA.entries()].filter(([k]) => !uniqB.has(k)).map(([, v]) => v)
    summary.value = `差集 A - B:${lines.length} 项(A 去重 ${uniqA.size} 项)`
  } else if (op === 'union') {
    const merged = new Map(uniqA)
    uniqB.forEach((v, k) => {
      if (!merged.has(k)) merged.set(k, v)
    })
    lines = [...merged.values()]
    summary.value = `并集:${lines.length} 项`
  } else if (op === 'symmetric') {
    const onlyA = [...uniqA.entries()].filter(([k]) => !uniqB.has(k)).map(([, v]) => v)
    const onlyB = [...uniqB.entries()].filter(([k]) => !uniqA.has(k)).map(([, v]) => v)
    lines = [...onlyA, ...onlyB]
    summary.value = `对称差:${lines.length} 项(仅 A 有 ${onlyA.length} 项,仅 B 有 ${onlyB.length} 项)`
  } else if (op === 'dupA') {
    const counts = countMap(a)
    lines = [...uniqA.entries()].filter(([k]) => counts.get(k) > 1).map(([, v]) => v)
    summary.value = `A 中重复元素:${lines.length} 项(A 共 ${a.length} 项,去重后 ${uniqA.size} 项)`
  } else if (op === 'count') {
    const ca = countMap(a)
    const cb = countMap(b)
    const all = new Map(uniqA)
    uniqB.forEach((v, k) => {
      if (!all.has(k)) all.set(k, v)
    })
    const rows = [...all.entries()].map(([k, v]) => {
      const na = ca.get(k) || 0
      const nb = cb.get(k) || 0
      return { text: v, na, nb, total: na + nb }
    })
    rows.sort((x, y) => y.total - x.total)
    lines = ['元素\tA中次数\tB中次数\t合计', ...rows.map(r => `${r.text}\t${r.na}\t${r.nb}\t${r.total}`)]
    summary.value = `统计完成:共 ${rows.length} 个不同元素(制表符分隔,可直接粘贴到表格软件)`
  }

  result.value = lines.join('\n')
}

function swapAB() {
  const tmp = textA.value
  textA.value = textB.value
  textB.value = tmp
  if (currentOp.value) run(currentOp.value)
}

function loadDemo() {
  textA.value = ['apple', 'banana', 'cherry', 'apple', 'durian', 'Elderberry'].join('\n')
  textB.value = ['banana', 'cherry', 'fig', 'grape', 'elderberry'].join('\n')
  run('symmetric')
}

function clearAll() {
  textA.value = ''
  textB.value = ''
  result.value = ''
  summary.value = ''
  currentOp.value = ''
}

async function copyResult() {
  if (!result.value) return
  try {
    await navigator.clipboard.writeText(result.value)
    ElMessage.success('已复制到剪贴板')
  } catch {
    ElMessage.warning('复制失败,请手动选择文本复制')
  }
}

function downloadResult() {
  if (!result.value) return
  const blob = new Blob([result.value], { type: 'text/plain;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `text-set-${currentOp.value || 'result'}.txt`
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
  ElMessage.success('已开始下载')
}

2.3 效果截图

文本集合运算 效果截图

工具访问地址:https://www.i91tools.com/tools/text-set-ops