2026年用Python检测网站薄内容与重复页:用向量相似度找出“搜索引擎眼中一样“的页面(附完整代码)

2026年用Python检测网站薄内容与重复页:用向量相似度找出"搜索引擎眼中一样"的页面(附完整代码)

发布时间: 2026-09-20

标签: SEO、重复内容、薄内容、向量相似度、内容质量、Python、实战工具

阅读时长: 约 16 分钟

难度: 中级


先说一个"看不见但致命"的排名杀手

你的网站可能正有两个问题,你自己完全不知道:

  1. 重复内容(duplicate content):两篇页面,文字换了个说法,意思几乎一样。Google 分不清该排名哪篇,于是两篇都不给好排名。
  2. 薄内容(thin content) :页面 300 字、一半是导航、实质信息为零。2026 年 E-E-A-T 成硬性门槛后,这种页基本被清洗

最要命的是------人眼能发现,但人眼查不完几百个页面。而且"意思一样但字面不同"的重复,靠文本匹配根本抓不到。

今天写一个 薄内容与重复页检测工具 :用向量相似度衡量"两页在 AI/搜索引擎眼里有多像",再叠加薄内容规则,把全站问题页一次性列出来。


一、工具思路

复制代码
站内页面集合
   ↓
提取正文(去导航/脚本/样式)
   ↓
向量化:BAAI/bge 中文模型编码成向量
   ↓
两两计算余弦相似度
   ├─ 相似度 > 0.85 → 判定"近重复",列成对
   └─ 正文 < 阈值 或 信息密度低 → 判定"薄内容"
   ↓
报告:重复页对 + 薄内容页 + CSV 导出

为什么用向量相似度?

因为"SEO 工具推荐"和"好用的优化软件合集"字面完全不同,但语义几乎一样。传统文本重合度(如 diff)算出来很低,向量相似度却能识别------这正是 Google/AI 判断重复内容的方式。

依赖:

bash 复制代码
pip install requests beautifulsoup4 sentence-transformers numpy

二、完整代码:content_dup_detector.py

python 复制代码
"""
content_dup_detector.py
薄内容与重复页检测:正文向量化 + 余弦相似度 + 薄内容规则
依赖:pip install requests beautifulsoup4 sentence-transformers numpy
"""
import csv
import re
import numpy as np
from collections import deque, defaultdict
from urllib.parse import urljoin, urlparse, urldefrag
import requests
from bs4 import BeautifulSoup
from sentence_transformers import SentenceTransformer


MODEL_NAME = "BAAI/bge-small-zh-v1.5"
DUP_THRESHOLD = 0.85      # 余弦相似度超过此值 → 判定近重复
THIN_CHARS = 300          # 正文字符数低于此值 → 疑似薄内容
STOPWORDS_RATIO = 0.6     # 停用词占比过高 → 信息密度低


# -------- 爬取并提取正文 --------
def extract_main_text(html: str) -> str:
    """粗略提取正文:去掉 script/style/nav/footer,取可见文本"""
    soup = BeautifulSoup(html, "html.parser")
    for tag in soup(["script", "style", "nav", "footer", "header", "noscript"]):
        tag.decompose()
    text = soup.get_text(separator=" ")
    # 压缩空白
    text = re.sub(r"\s+", " ", text).strip()
    return text


class ContentScanner:
    def __init__(self, start_url: str, max_pages: int = 200, delay: float = 0.3):
        self.start_url = start_url.rstrip("/")
        self.domain = urlparse(start_url).netloc
        self.max_pages = max_pages
        self.delay = delay
        self.headers = {"User-Agent": "Mozilla/5.0 (compatible; ContentBot/1.0)"}
        self.texts = {}   # url -> 正文

    def _norm(self, url):
        url, _ = urldefrag(url)
        return url.rstrip("/") or url

    def _is_internal(self, url):
        return urlparse(url).netloc == self.domain

    def crawl(self):
        queue = deque([self.start_url])
        seen = set()
        while queue and len(self.texts) < self.max_pages:
            url = self._norm(queue.popleft())
            if url in seen:
                continue
            seen.add(url)
            try:
                r = requests.get(url, headers=self.headers, timeout=10)
            except Exception:
                continue
            if r.status_code >= 400 or "text/html" not in r.headers.get("Content-Type", ""):
                continue
            self.texts[url] = extract_main_text(r.text)
            soup = BeautifulSoup(r.text, "html.parser")
            for a in soup.find_all("a", href=True):
                t = self._norm(urljoin(url, a["href"]))
                if self._is_internal(t) and t not in seen \
                        and len(self.texts) < self.max_pages:
                    queue.append(t)
        return self

    # -------- 向量化 --------
    def embed(self):
        model = SentenceTransformer(MODEL_NAME)
        urls = list(self.texts.keys())
        embs = model.encode([self.texts[u] for u in urls],
                            normalize_embeddings=True, show_progress_bar=True)
        return urls, np.array(embs)

    # -------- 检测 --------
    def detect(self, urls, embs, dup_threshold=0.85, thin_chars=300):
        n = len(urls)
        duplicates = []   # (url_a, url_b, sim)
        thin = []         # (url, char_count, info_density)

        # 1. 薄内容规则(单页)
        for u, t in self.texts.items():
            chars = len(t)
            words = re.findall(r"[\u4e00-\u9fff]|[a-zA-Z0-9]+", t)
            # 粗略信息密度:实词占比(这里用长度近似,真实场景可去停用词)
            density = round(len(words) / chars, 3) if chars else 0
            if chars < thin_chars:
                thin.append((u, chars, density))

        # 2. 两两余弦相似度(O(n^2),几百页无压力)
        print("🔍 计算页面两两相似度...")
        for i in range(n):
            for j in range(i + 1, n):
                sim = float(np.dot(embs[i], embs[j]))
                if sim >= dup_threshold:
                    duplicates.append((urls[i], urls[j], round(sim, 3)))
        # 按相似度降序
        duplicates.sort(key=lambda x: -x[2])
        return duplicates, thin

    def report(self, duplicates, thin):
        print("=" * 62)
        print(f"📄 内容质量检测:{self.domain}({len(self.texts)} 页)")
        print("=" * 62)
        print(f"\n🔁 近重复页面(语义相似度 ≥ {DUP_THRESHOLD}):{len(duplicates)} 对")
        for a, b, s in duplicates[:30]:
            print(f"   {s}  {a}")
            print(f"        ↕  {b}")

        print(f"\n📉 薄内容页面(正文 < {THIN_CHARS} 字):{len(thin)} 个")
        for u, c, d in sorted(thin, key=lambda x: x[1])[:20]:
            print(f"   {c} 字 (密度{d})  {u}")

    def export(self, duplicates, thin, path: str = "content_issues.csv"):
        with open(path, "w", newline="", encoding="utf-8-sig") as f:
            w = csv.writer(f)
            w.writerow(["type", "url_a", "url_b", "similarity", "char_count"])
            for a, b, s in duplicates:
                w.writerow(["duplicate", a, b, s, ""])
            for u, c, d in thin:
                w.writerow(["thin", u, "", "", c])
        print(f"✅ 问题清单已导出:{path}")


if __name__ == "__main__":
    import sys
    start = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/"
    scanner = ContentScanner(start, max_pages=200).crawl()
    urls, embs = scanner.embed()
    dups, thin = scanner.detect(urls, embs)
    scanner.report(dups, thin)
    scanner.export(dups, thin)

三、运行 & 输出示例

bash 复制代码
python content_dup_detector.py https://your-site.com/

输出(节选):

复制代码
==============================================================
📄 内容质量检测:your-site.com(186 页)
==============================================================

🔁 近重复页面(语义相似度 ≥ 0.85):4 对
   0.93  https://your-site.com/blog/seo-tools-2025
        ↕  https://your-site.com/blog/best-seo-software
   0.88  https://your-site.com/p/blue-widget
        ↕  https://your-site.com/p/blue-widget-pro

📉 薄内容页面(正文 < 300 字):11 个
   86 字 (密度0.21)  https://your-site.com/tag/apple
   142 字 (密度0.31) https://your-site.com/author/john

一眼能看出:

  • "seo-tools-2025" 和 "best-seo-software" 语义重复(0.93),Google 只会挑一篇排名 → 合并或 canonical
  • 11 个 tag/author 归档页正文不足 100 字 → 用 noindex 或充实内容

四、拿到结果怎么处理

问题 动作 参考
近重复页对 合并内容;或保留一篇、其余加 canonical 指向主篇 重复内容政策
薄内容页 充实实质信息(数据/案例/步骤);归档类页加 noindex 创建有用内容
标签/作者归档 无独特价值的加 noindex,避免稀释 低质量页面处理

关键判断: 相似度 0.85 是经验阈值。≥0.92 基本是同一篇换皮,必须合并;0.85~0.92 是"高度相关",可保留但要用 canonical / 差异化角度区分。


五、关键参考链接

主题 链接
重复内容政策 https://developers.google.com/search/docs/essentials/spam-policies
创建有帮助的内容 https://developers.google.com/search/docs/fundamentals/creating-helpful-content
sentence-transformers https://www.sbert.net/
BGE 中文向量模型 https://huggingface.co/BAAI/bge-small-zh-v1.5
Google 低质量内容说明 https://developers.google.com/search/docs/essentials/spam-policies

六、进阶

6.1 调阈值按站点规模

python 复制代码
# 大站(上千页)提阈值,减少误报
dups, thin = scanner.detect(urls, embs, dup_threshold=0.90)

# 小站(几十页)可降阈值,更严格
dups, thin = scanner.detect(urls, embs, dup_threshold=0.80)

6.2 更准的信息密度

示例用"字符数 + 粗略密度"近似薄内容。更严谨可加载中文停用词表,计算实词占比作为信息密度指标,误报更低。

6.3 接 GEO:薄内容 = 不被 AI 引用

2026 年 AI 引用内容时,会优先选事实密集、信息完整 的页面。薄内容不仅 SEO 吃亏,GEO 也基本不会被引用。所以这个工具同时也是"GEO 内容体检"的预检------修好重复与薄内容,传统排名和 AI 引用一起受益。

6.4 定期跑,监控内容膨胀

把脚本挂定时任务,每月跑一次,重复对数突然增多往往意味着:有人批量发了相似产品页/地区页 → 及时 canonical,避免被算法清洗。


七、总结

重复内容和薄内容,是最隐蔽、也最普遍的排名杀手。

这个工具的价值:

  • 向量相似度识别"字面不同但语义重复"的页面(传统方法做不到)
  • 叠加薄内容规则,把"正文不足/信息密度低"的页挑出来
  • 输出 CSV:重复页对 + 薄内容页,直接给开发排期
  • 一处修复,SEO 与 GEO 双受益

百度/Google 不缺页面,缺的是"互相不同的好页面"。先清掉重复和薄内容,再谈增长。


跑一遍你的站,重复页有几对?评论区贴数字,我帮你看是合并还是 canonical。


📮 想要本文完整代码包 + 更多 SEO/GEO 实战工具合集?评论区留言或私信我即可,看到都会回。同名公众号「全域SEO增长」有同步更新,搜名字就能找到。

相关推荐
老王爱玩车1 小时前
第6讲:数组和函数实践-----控制台扫雷
c语言·开发语言·学习
IT_陈寒1 小时前
Vite这坑我踩了,说说静态资源加载那些事儿
前端·人工智能·后端
m0_734571761 小时前
深入理解C++ 多态<三>(Polymorphism)模板方法模式
开发语言·c++
君顾11 小时前
24小时自助健身房系统开发实战:从0到1完整技术架构指南
java·开发语言·健身房
梦曦i1 小时前
uni-app x 0.7.0重磅升级:插件实例化,原生编译彻底打通
前端·uni-app x
开开心心就好2 小时前
C盘空间不够用?一键扫描清理系统垃圾
前端·javascript·安全·支持向量机·语音识别·etcd·模拟退火算法
爱喝水的小周2 小时前
实验八 Vue‑Router 路由插件实践
前端·javascript
江屿风2 小时前
【Linux系统】【Linux 进程程序替换机制解析及自定义 Shell 核心逻辑实现 】流食般投喂
linux·运维·服务器·开发语言·笔记
ℋᙚᵐⁱᒻᵉ鲸落2 小时前
ETypeError: nameRef.value?.focus is not a functio. 设置el-input自动聚焦时报错
前端·javascript·vue.js·elementui