2026年用Python检测网站薄内容与重复页:用向量相似度找出"搜索引擎眼中一样"的页面(附完整代码)
发布时间: 2026-09-20
标签: SEO、重复内容、薄内容、向量相似度、内容质量、Python、实战工具
阅读时长: 约 16 分钟
难度: 中级
先说一个"看不见但致命"的排名杀手
你的网站可能正有两个问题,你自己完全不知道:
- 重复内容(duplicate content):两篇页面,文字换了个说法,意思几乎一样。Google 分不清该排名哪篇,于是两篇都不给好排名。
- 薄内容(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增长」有同步更新,搜名字就能找到。