Skip to content

实现 robots.txt 与 sitemap 工具

分类:general | 标签:robots、sitemap、SEO 生成 robots.txt 与 sitemap.xml,解析 sitemap

2.1 功能说明

生成 robots.txt 与 sitemap.xml,解析 sitemap

核心能力

  • 生成 robots.txt
  • Allow
  • Disallow
  • 生成 sitemap.xml
  • 不输出
  • 解析 sitemap.xml

2.1.1 使用指南

功能说明

围绕站点收录做的三件事:按规则生成 robots.txt、把一批网址打包成标准 sitemap.xml、以及把别人的 sitemap 拆回纯网址列表。三个标签页互相独立,全部在浏览器本地完成,不会读取或访问任何线上站点。

一、生成 robots.txt

robots.txt 用来告诉搜索引擎哪些路径可以抓、哪些不要抓。按 User-agent 分组配置,每组下可添加任意条 Allow 或 Disallow 规则:

  • User-agent:填 * 表示对所有爬虫生效,也可以针对特定爬虫单独建一组,写更严格或更宽松的规则。
  • Disallow:禁止抓取的路径,以斜杠开头。填 / 表示整站禁止,留空表示不限制。
  • Allow:在被禁止的目录中开一个例外,通常配合 Disallow 使用。
  • Crawl-delay:建议的抓取间隔秒数,用于减轻服务器压力,设为 0 时不输出该行。注意部分主流搜索引擎会忽略这一指令。
  • Sitemap:站点地图的完整地址,建议填写绝对路径,可以写多行。

三个预设按钮可以快速铺底:全站开放、全站禁止、以及屏蔽常见后台与接口目录。

二、生成 sitemap.xml

每行填一个网址,也可以在网址后面用逗号或制表符附加两个可选字段,顺序为「最后修改日期, 优先级」,例如 https://example.com/about,2026-08-01,0.8。没有单独指定的条目会使用下方设置的默认值。

  • lastmod:最后修改日期,推荐 YYYY-MM-DD 格式,可一键填入今天。
  • priority:相对优先级,取值 0 到 1,只表示站内页面之间的相对重要性。
  • changefreq:预计更新频率,可选择不输出。
  • 不以 http 或 https 开头的行会被忽略,界面会提示被忽略的行数;网址中的特殊字符会做 XML 转义。

三、解析 sitemap.xml

粘贴任意 sitemap.xml 内容,工具会提取全部 loc 标签中的网址,同时读取对应的 lastmod 与 priority。支持去重和按字母排序,结果可复制、下载为 TXT,或导出成包含三列的 CSV 便于进一步整理。即使粘贴的是不完整的 XML 片段,也能尽量提取出其中的网址。

使用建议

单个 sitemap 文件建议不超过 5 万条网址且未压缩时小于 50MB,超出时应拆分成多个文件并用 sitemap 索引文件组织。robots.txt 与 sitemap.xml 都要放在站点根目录才能被正确读取。

2.2 代码实现

2.2.1 组件结构

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

2.2.2 核心逻辑概览

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

  • addGroup()
  • removeGroup()
  • addRule()
  • removeRule()
  • applyPreset()
  • today()
  • escapeXml()
  • loadSitemapSample()
  • unescapeXml()
  • downloadParsedCsv()
  • copyText()
  • download()

2.2.3 关键实现代码

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

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

const activeTab = ref('robots')

/* ---------- 一、robots.txt ---------- */

const groups = reactive([
  {
    ua: '*',
    delay: 0,
    rules: [{ type: 'Disallow', path: '/admin/' }]
  }
])
const sitemapUrls = ref('')
const robotsComment = ref('')

function addGroup() {
  groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '/' }] })
}

function removeGroup(index) {
  if (groups.length === 1) return
  groups.splice(index, 1)
}

function addRule(gi) {
  groups[gi].rules.push({ type: 'Disallow', path: '/' })
}

function removeRule(gi, ri) {
  groups[gi].rules.splice(ri, 1)
}

function applyPreset(kind) {
  groups.splice(0, groups.length)
  if (kind === 'open') {
    groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '' }] })
  } else if (kind === 'block') {
    groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '/' }] })
  } else {
    groups.push({
      ua: '*',
      delay: 0,
      rules: [
        { type: 'Disallow', path: '/admin/' },
        { type: 'Disallow', path: '/api/' },
        { type: 'Disallow', path: '/login' },
        { type: 'Disallow', path: '/*?s=' },
        { type: 'Allow', path: '/api/public/' }
      ]
    })
  }
  ElMessage.success('预设已应用')
}

const robotsText = computed(() => {
  const lines = []
  if (robotsComment.value.trim()) lines.push('# ' + robotsComment.value.trim())
  groups.forEach((g, index) => {
    if (index > 0 || lines.length) lines.push('')
    lines.push(`User-agent: ${g.ua.trim() || '*'}`)
    g.rules.forEach(r => {
      lines.push(`${r.type}: ${r.path.trim()}`)
    })
    if (g.delay > 0) lines.push(`Crawl-delay: ${g.delay}`)
  })
  const maps = sitemapUrls.value
    .split(/\r?\n/)
    .map(s => s.trim())
    .filter(Boolean)
  if (maps.length) {
    lines.push('')
    maps.forEach(m => lines.push(`Sitemap: ${m}`))
  }
  return lines.join('\n') + '\n'
})

/* ---------- 二、生成 sitemap.xml ---------- */

const freqOptions = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']
const sitemapInput = ref('')
const defaultFreq = ref('')
const defaultPriority = ref(0.5)
const usePriority = ref(true)
const useLastmod = ref(true)
const defaultLastmod = ref('')

function today() {
  const d = new Date()
  const pad = n => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}

const sitemapParsed = computed(() => {
  const entries = []
  let invalid = 0
  sitemapInput.value
    .split(/\r?\n/)
    .map(l => l.trim())
    .filter(Boolean)
    .forEach(line => {
      const parts = line
        .split(/[,\t]/)
        .map(p => p.trim())
        .filter(p => p !== '')
      const loc = parts[0]
      if (!/^https?:\/\//i.test(loc)) {
        invalid++
        return
      }
      let lastmod = ''
      let priority = ''
      parts.slice(1).forEach(p => {
        if (/^\d{4}-\d{2}-\d{2}/.test(p)) lastmod = p
        else if (/^0?\.\d+$|^[01](\.0+)?$/.test(p)) priority = p
      })
      entries.push({ loc, lastmod, priority })
    })
  return { entries, invalid }
})

const sitemapEntries = computed(() => sitemapParsed.value.entries)
const sitemapInvalid = computed(() => sitemapParsed.value.invalid)

function escapeXml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

const sitemapXml = computed(() => {
  const entries = sitemapEntries.value
  if (!entries.length) return ''
  const out = []
  out.push('<?xml version="1.0" encoding="UTF-8"?>')
  out.push('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
  entries.forEach(e => {
    out.push('  <url>')
    out.push(`    <loc>${escapeXml(e.loc)}</loc>`)
    if (useLastmod.value) {
      const lm = e.lastmod || defaultLastmod.value.trim()
      if (lm) out.push(`    <lastmod>${escapeXml(lm)}</lastmod>`)
    }
    if (defaultFreq.value) out.push(`    <changefreq>${defaultFreq.value}</changefreq>`)
    if (usePriority.value) {
      const pr = e.priority || Number(defaultPriority.value).toFixed(1)
      out.push(`    <priority>${escapeXml(pr)}</priority>`)
    }
    out.push('  </url>')
  })
  out.push('</urlset>')
  return out.join('\n') + '\n'
})

function loadSitemapSample() {
  sitemapInput.value = [
    'https://example.com/',
    'https://example.com/about,2026-08-01,0.8',
    'https://example.com/products,2026-07-20,0.9',
    'https://example.com/blog/hello-world,2026-06-15'
  ].join('\n')
  defaultLastmod.value = today()
}

/* ---------- 三、解析 sitemap.xml ---------- */

const parseInput = ref('')
const parseDedupe = ref(true)
const parseSort = ref(false)

function unescapeXml(str) {
  return String(str)
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&#(\d+);/g, (m, code) => String.fromCodePoint(Number(code)))
    .replace(/&amp;/g, '&')
}

const parsedUrls = computed(() => {
  const text = parseInput.value
  if (!text.trim()) return []

  // 逐个 url 块解析,保证 loc 与同块内的 lastmod、priority 对应
  const blocks = text.match(/<url\b[\s\S]*?<\/url>/gi)
  let list = []
  if (blocks && blocks.length) {
    blocks.forEach(block => {
      const locMatch = block.match(/<loc[^>]*>([\s\S]*?)<\/loc>/i)
      if (!locMatch) return
      const lastmod = block.match(/<lastmod[^>]*>([\s\S]*?)<\/lastmod>/i)
      const priority = block.match(/<priority[^>]*>([\s\S]*?)<\/priority>/i)
      list.push({
        loc: unescapeXml(locMatch[1].trim()),
        lastmod: lastmod ? unescapeXml(lastmod[1].trim()) : '',
        priority: priority ? unescapeXml(priority[1].trim()) : ''
      })
    })
  } else {
    // 兼容 sitemap 索引文件或不完整片段,直接抓取所有 loc
    const locs = text.match(/<loc[^>]*>([\s\S]*?)<\/loc>/gi) || []
    list = locs.map(item => ({
      loc: unescapeXml(item.replace(/<[^>]+>/g, '').trim()),
      lastmod: '',
      priority: ''
    }))
  }

  list = list.filter(item => item.loc)

  if (parseDedupe.value) {
    const seen = new Set()
    list = list.filter(item => {
      if (seen.has(item.loc)) return false
      seen.add(item.loc)
      return true
    })
  }
  if (parseSort.value) list = [...list].sort((a, b) => a.loc.localeCompare(b.loc))
  return list
})

const parsedText = computed(() => parsedUrls.value.map(item => item.loc).join('\n'))

const parseSummary = computed(() => {
  const n = parsedUrls.value.length
  if (!n) return '未提取到任何 loc 标签,请确认粘贴的内容是 sitemap.xml'
  const withDate = parsedUrls.value.filter(item => item.lastmod).length
  return `提取到 ${n} 条 URL,其中 ${withDate} 条带有 lastmod`
})

function downloadParsedCsv() {
  if (!parsedUrls.value.length) return
  const escape = v => {
    const s = String(v || '')
    return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s
  }
  const lines = ['loc,lastmod,priority']
  parsedUrls.value.forEach(item => {
    lines.push([escape(item.loc), escape(item.lastmod), escape(item.priority)].join(','))
  })
  download('\ufeff' + lines.join('\r\n'), 'sitemap-urls.csv', 'text/csv;charset=utf-8')
}

/* ---------- 公共方法 ---------- */

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

function download(content, filename, mime) {
  if (!content) return
  const blob = new Blob([content], { type: mime })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
  ElMessage.success('已开始下载')
}

2.3 效果截图

robots.txt 与 sitemap 工具 效果截图

工具访问地址:https://www.i91tools.com/tools/robots-sitemap