1490 lines
53 KiB
Python
1490 lines
53 KiB
Python
"""
|
||
SEO诊断服务 - 5维度检测系统
|
||
|
||
诊断维度(总分100):
|
||
- 技术SEO (Technical SEO): 25分 - 索引、爬取、Core Web Vitals等
|
||
- 页面SEO (On-Page SEO): 20分 - Title/Meta、H标签、关键词等
|
||
- 内容质量 (Content Quality): 20分 - 可读性、E-E-A-T、新鲜度等
|
||
- 外链分析 (Backlink Analysis): 15分 - 反向链接质量、毒性信号等
|
||
- 用户体验 (User Experience): 20分 - 移动适配、页面速度、转化路径等
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ============================================================
|
||
# 枚举定义
|
||
# ============================================================
|
||
|
||
class DiagnosisStatus(str, Enum):
|
||
"""诊断状态"""
|
||
PASS = "pass"
|
||
WARNING = "warning"
|
||
FAIL = "fail"
|
||
|
||
|
||
class DimensionName(str, Enum):
|
||
"""诊断维度名称"""
|
||
TECHNICAL_SEO = "技术SEO"
|
||
ON_PAGE_SEO = "页面SEO"
|
||
CONTENT_QUALITY = "内容质量"
|
||
BACKLINK_ANALYSIS = "外链分析"
|
||
USER_EXPERIENCE = "用户体验"
|
||
|
||
|
||
# ============================================================
|
||
# 数据结构
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class DiagnosisItem:
|
||
"""单个诊断项"""
|
||
name: str # 诊断项名称
|
||
status: DiagnosisStatus # 诊断状态
|
||
description: str # 诊断说明
|
||
suggestion: str # 优化建议
|
||
score: float = 0.0 # 该项得分 (0-1)
|
||
details: dict[str, Any] = field(default_factory=dict) # 额外详情
|
||
|
||
|
||
@dataclass
|
||
class SEODimensionScore:
|
||
"""单个维度的诊断结果"""
|
||
name: str # 维度名称
|
||
score: float # 维度得分 (0-max_score)
|
||
max_score: float # 维度满分
|
||
items: list[DiagnosisItem] # 诊断项列表
|
||
status: DiagnosisStatus # 整体状态
|
||
|
||
@property
|
||
def percentage(self) -> float:
|
||
"""得分率 (0-100)"""
|
||
if self.max_score <= 0:
|
||
return 0.0
|
||
return round((self.score / self.max_score) * 100, 2)
|
||
|
||
def __post_init__(self):
|
||
"""计算整体状态"""
|
||
if not self.items:
|
||
self.status = DiagnosisStatus.WARNING
|
||
return
|
||
|
||
fail_count = sum(1 for item in self.items if item.status == DiagnosisStatus.FAIL)
|
||
warning_count = sum(1 for item in self.items if item.status == DiagnosisStatus.WARNING)
|
||
total = len(self.items)
|
||
|
||
if fail_count > total * 0.3:
|
||
self.status = DiagnosisStatus.FAIL
|
||
elif warning_count > total * 0.3 or fail_count > 0:
|
||
self.status = DiagnosisStatus.WARNING
|
||
else:
|
||
self.status = DiagnosisStatus.PASS
|
||
|
||
|
||
@dataclass
|
||
class SEORecommendation:
|
||
"""优化建议"""
|
||
priority: str # high/medium/low
|
||
dimension: str # 所属维度
|
||
item_name: str # 诊断项名称
|
||
description: str # 建议描述
|
||
impact: str # 预期影响
|
||
effort: str # 实施难度 easy/medium/hard
|
||
|
||
|
||
@dataclass
|
||
class SEODiagnosisResult:
|
||
"""SEO诊断结果"""
|
||
overall_score: float # 综合评分 0-100
|
||
dimensions: list[SEODimensionScore] # 各维度得分
|
||
recommendations: list[SEORecommendation] # 优化建议
|
||
health_level: str = "danger" # 健康等级
|
||
|
||
def __post_init__(self):
|
||
"""计算健康等级"""
|
||
self.overall_score = round(min(100.0, max(0.0, self.overall_score)), 2)
|
||
|
||
if self.overall_score >= 80:
|
||
self.health_level = "excellent"
|
||
elif self.overall_score >= 60:
|
||
self.health_level = "good"
|
||
elif self.overall_score >= 40:
|
||
self.health_level = "pass"
|
||
else:
|
||
self.health_level = "danger"
|
||
|
||
def to_dict(self) -> dict:
|
||
"""转换为字典格式"""
|
||
return {
|
||
"overall_score": self.overall_score,
|
||
"health_level": self.health_level,
|
||
"health_level_label": self._get_health_label(),
|
||
"dimensions": [
|
||
{
|
||
"name": dim.name,
|
||
"score": round(dim.score, 2),
|
||
"max_score": dim.max_score,
|
||
"percentage": dim.percentage,
|
||
"status": dim.status.value,
|
||
"items": [
|
||
{
|
||
"name": item.name,
|
||
"status": item.status.value,
|
||
"description": item.description,
|
||
"suggestion": item.suggestion,
|
||
"score": round(item.score, 2),
|
||
"details": item.details,
|
||
}
|
||
for item in dim.items
|
||
],
|
||
}
|
||
for dim in self.dimensions
|
||
],
|
||
"recommendations": [
|
||
{
|
||
"priority": rec.priority,
|
||
"dimension": rec.dimension,
|
||
"item_name": rec.item_name,
|
||
"description": rec.description,
|
||
"impact": rec.impact,
|
||
"effort": rec.effort,
|
||
}
|
||
for rec in self.recommendations
|
||
],
|
||
}
|
||
|
||
def _get_health_label(self) -> str:
|
||
"""获取健康等级中文标签"""
|
||
labels = {
|
||
"excellent": "优秀",
|
||
"good": "良好",
|
||
"pass": "及格",
|
||
"danger": "危险",
|
||
}
|
||
return labels.get(self.health_level, "未知")
|
||
|
||
|
||
# ============================================================
|
||
# 诊断数据输入
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class TechnicalSEOData:
|
||
"""技术SEO检测数据"""
|
||
is_indexed: bool = True # 是否被索引
|
||
crawl_errors: int = 0 # 爬取错误数
|
||
redirect_chains: int = 0 # 重定向链数
|
||
lcp_seconds: float = 2.0 # Largest Contentful Paint (秒)
|
||
fid_ms: float = 50.0 # First Input Delay (毫秒)
|
||
cls_score: float = 0.05 # Cumulative Layout Shift
|
||
has_robots_txt: bool = True # 是否有robots.txt
|
||
robots_txt_blocks_important: bool = False # robots.txt是否阻止重要页面
|
||
has_sitemap: bool = True # 是否有sitemap
|
||
sitemap_valid: bool = True # sitemap是否有效
|
||
url_structure_normalized: bool = True # URL结构是否规范
|
||
|
||
|
||
@dataclass
|
||
class OnPageSEOData:
|
||
"""页面SEO检测数据"""
|
||
has_title: bool = True # 是否有Title标签
|
||
title_length: int = 50 # Title长度
|
||
title_keyword_stuffing: bool = False # Title是否关键词堆砌
|
||
has_meta_description: bool = True # 是否有Meta Description
|
||
meta_description_length: int = 140 # Meta Description长度
|
||
h1_count: int = 1 # H1标签数量
|
||
h_structure_valid: bool = True # H标签结构是否合理
|
||
keyword_density: float = 2.0 # 关键词密度 (%)
|
||
internal_links: int = 10 # 内链数量
|
||
broken_internal_links: int = 0 # 死链数量
|
||
images_without_alt: int = 0 # 缺少Alt文本的图片数
|
||
total_images: int = 5 # 总图片数
|
||
|
||
|
||
@dataclass
|
||
class ContentQualityData:
|
||
"""内容质量检测数据"""
|
||
readability_score: float = 70.0 # 可读性评分 (0-100)
|
||
word_count: int = 1500 # 字数
|
||
topic_coverage: float = 0.8 # 主题覆盖率 (0-1)
|
||
has_author_info: bool = True # 是否有作者信息
|
||
has_publication_date: bool = True # 是否有发布日期
|
||
last_updated_days: int = 30 # 最后更新天数
|
||
has_citations: bool = True # 是否有引用/参考
|
||
citation_authority: float = 0.8 # 引用权威性 (0-1)
|
||
duplicate_content_ratio: float = 0.05 # 重复内容比例 (0-1)
|
||
has_expert_review: bool = False # 是否有专家审核
|
||
|
||
|
||
@dataclass
|
||
class BacklinkData:
|
||
"""外链检测数据"""
|
||
total_backlinks: int = 100 # 总反向链接数
|
||
referring_domains: int = 20 # 引用域名数
|
||
high_authority_links: int = 10 # 高权威链接数
|
||
toxic_links: int = 2 # 毒性链接数
|
||
nofollow_ratio: float = 0.3 # Nofollow比例
|
||
anchor_text_diversity: float = 0.8 # 锚文本多样性 (0-1)
|
||
exact_match_anchor_ratio: float = 0.2 # 精确匹配锚文本比例
|
||
|
||
|
||
@dataclass
|
||
class UserExperienceData:
|
||
"""用户体验检测数据"""
|
||
is_mobile_friendly: bool = True # 是否移动友好
|
||
mobile_viewport_set: bool = True # 是否设置viewport
|
||
page_load_time: float = 2.5 # 页面加载时间 (秒)
|
||
has_https: bool = True # 是否使用HTTPS
|
||
has_breadcrumbs: bool = True # 是否有面包屑导航
|
||
conversion_path_clear: bool = True # 转化路径是否清晰
|
||
has_cta: bool = True # 是否有明确的CTA
|
||
form_usability: float = 0.9 # 表单可用性 (0-1)
|
||
has_search: bool = True # 是否有站内搜索
|
||
|
||
|
||
# ============================================================
|
||
# 维度诊断函数
|
||
# ============================================================
|
||
|
||
def diagnose_technical_seo(data: TechnicalSEOData) -> SEODimensionScore:
|
||
"""
|
||
技术SEO诊断 (满分25分)
|
||
|
||
评分项:
|
||
- 索引状态 (4分)
|
||
- 爬取错误 (4分)
|
||
- Core Web Vitals (6分)
|
||
- URL结构 (3分)
|
||
- robots.txt (4分)
|
||
- sitemap (4分)
|
||
"""
|
||
max_score = 25.0
|
||
items: list[DiagnosisItem] = []
|
||
total_score = 0.0
|
||
|
||
# 1. 索引状态检查 (4分)
|
||
if data.is_indexed:
|
||
items.append(DiagnosisItem(
|
||
name="索引状态",
|
||
status=DiagnosisStatus.PASS,
|
||
description="网站已被搜索引擎正确索引",
|
||
suggestion="保持当前索引状态",
|
||
score=1.0,
|
||
))
|
||
total_score += 4.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="索引状态",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="网站未被搜索引擎索引",
|
||
suggestion="检查Search Console,提交sitemap,确保没有被noindex",
|
||
score=0.0,
|
||
))
|
||
|
||
# 2. 爬取错误检测 (4分)
|
||
if data.crawl_errors == 0:
|
||
items.append(DiagnosisItem(
|
||
name="爬取错误",
|
||
status=DiagnosisStatus.PASS,
|
||
description="未发现爬取错误",
|
||
suggestion="定期检查Search Console的爬取错误报告",
|
||
score=1.0,
|
||
))
|
||
total_score += 4.0
|
||
elif data.crawl_errors <= 5:
|
||
items.append(DiagnosisItem(
|
||
name="爬取错误",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"发现{data.crawl_errors}个爬取错误",
|
||
suggestion="修复404页面,检查5xx服务器错误,优化重定向链",
|
||
score=0.5,
|
||
details={"error_count": data.crawl_errors},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="爬取错误",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"发现{data.crawl_errors}个爬取错误,数量过多",
|
||
suggestion="立即修复所有爬取错误,特别是5xx服务器错误",
|
||
score=0.0,
|
||
details={"error_count": data.crawl_errors},
|
||
))
|
||
|
||
# 3. Core Web Vitals评估 (6分)
|
||
cwv_score = 0.0
|
||
cwv_items = []
|
||
|
||
# LCP评估 (2分)
|
||
if data.lcp_seconds <= 2.5:
|
||
cwv_score += 2.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="LCP",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"LCP为{data.lcp_seconds}s,符合<2.5s标准",
|
||
suggestion="保持当前性能水平",
|
||
score=1.0,
|
||
details={"value": data.lcp_seconds, "threshold": 2.5},
|
||
))
|
||
elif data.lcp_seconds <= 4.0:
|
||
cwv_score += 1.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="LCP",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"LCP为{data.lcp_seconds}s,超过2.5s标准",
|
||
suggestion="优化图片加载,使用CDN,减少服务器响应时间",
|
||
score=0.5,
|
||
details={"value": data.lcp_seconds, "threshold": 2.5},
|
||
))
|
||
else:
|
||
cwv_items.append(DiagnosisItem(
|
||
name="LCP",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"LCP为{data.lcp_seconds}s,严重超过标准",
|
||
suggestion="立即优化页面加载性能",
|
||
score=0.0,
|
||
details={"value": data.lcp_seconds, "threshold": 2.5},
|
||
))
|
||
|
||
# FID评估 (2分)
|
||
if data.fid_ms <= 100:
|
||
cwv_score += 2.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="FID",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"FID为{data.fid_ms}ms,符合<100ms标准",
|
||
suggestion="保持当前交互性能",
|
||
score=1.0,
|
||
details={"value": data.fid_ms, "threshold": 100},
|
||
))
|
||
elif data.fid_ms <= 300:
|
||
cwv_score += 1.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="FID",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"FID为{data.fid_ms}ms,超过100ms标准",
|
||
suggestion="减少JavaScript执行时间,优化主线程工作",
|
||
score=0.5,
|
||
details={"value": data.fid_ms, "threshold": 100},
|
||
))
|
||
else:
|
||
cwv_items.append(DiagnosisItem(
|
||
name="FID",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"FID为{data.fid_ms}ms,严重超过标准",
|
||
suggestion="立即优化JavaScript,减少主线程阻塞",
|
||
score=0.0,
|
||
details={"value": data.fid_ms, "threshold": 100},
|
||
))
|
||
|
||
# CLS评估 (2分)
|
||
if data.cls_score <= 0.1:
|
||
cwv_score += 2.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="CLS",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"CLS为{data.cls_score},符合<0.1标准",
|
||
suggestion="保持当前视觉稳定性",
|
||
score=1.0,
|
||
details={"value": data.cls_score, "threshold": 0.1},
|
||
))
|
||
elif data.cls_score <= 0.25:
|
||
cwv_score += 1.0
|
||
cwv_items.append(DiagnosisItem(
|
||
name="CLS",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"CLS为{data.cls_score},超过0.1标准",
|
||
suggestion="为图片和广告预留空间,避免动态插入内容",
|
||
score=0.5,
|
||
details={"value": data.cls_score, "threshold": 0.1},
|
||
))
|
||
else:
|
||
cwv_items.append(DiagnosisItem(
|
||
name="CLS",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"CLS为{data.cls_score},严重超过标准",
|
||
suggestion="立即修复布局偏移问题",
|
||
score=0.0,
|
||
details={"value": data.cls_score, "threshold": 0.1},
|
||
))
|
||
|
||
items.extend(cwv_items)
|
||
total_score += cwv_score
|
||
|
||
# 4. URL结构规范化 (3分)
|
||
if data.url_structure_normalized:
|
||
items.append(DiagnosisItem(
|
||
name="URL结构",
|
||
status=DiagnosisStatus.PASS,
|
||
description="URL结构规范,无重复URL问题",
|
||
suggestion="保持当前URL结构",
|
||
score=1.0,
|
||
))
|
||
total_score += 3.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="URL结构",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="URL结构存在问题,可能有重复URL",
|
||
suggestion="使用canonical标签,统一URL格式",
|
||
score=0.5,
|
||
))
|
||
total_score += 1.5
|
||
|
||
# 5. robots.txt配置检查 (4分)
|
||
if data.has_robots_txt and not data.robots_txt_blocks_important:
|
||
items.append(DiagnosisItem(
|
||
name="robots.txt",
|
||
status=DiagnosisStatus.PASS,
|
||
description="robots.txt配置正确,未阻止重要页面",
|
||
suggestion="定期检查robots.txt配置",
|
||
score=1.0,
|
||
))
|
||
total_score += 4.0
|
||
elif data.has_robots_txt:
|
||
items.append(DiagnosisItem(
|
||
name="robots.txt",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="robots.txt阻止了重要页面",
|
||
suggestion="检查并修改robots.txt,确保重要页面可被爬取",
|
||
score=0.0,
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="robots.txt",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="未找到robots.txt文件",
|
||
suggestion="创建robots.txt文件,明确指定允许和禁止爬取的路径",
|
||
score=0.5,
|
||
))
|
||
total_score += 2.0
|
||
|
||
# 6. sitemap完整性验证 (4分)
|
||
if data.has_sitemap and data.sitemap_valid:
|
||
items.append(DiagnosisItem(
|
||
name="sitemap",
|
||
status=DiagnosisStatus.PASS,
|
||
description="sitemap存在且有效",
|
||
suggestion="定期更新sitemap,确保包含所有重要页面",
|
||
score=1.0,
|
||
))
|
||
total_score += 4.0
|
||
elif data.has_sitemap:
|
||
items.append(DiagnosisItem(
|
||
name="sitemap",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="sitemap存在但可能无效",
|
||
suggestion="验证sitemap格式,确保所有URL可访问",
|
||
score=0.5,
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="sitemap",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="未找到sitemap",
|
||
suggestion="创建并提交sitemap到Search Console",
|
||
score=0.0,
|
||
))
|
||
|
||
return SEODimensionScore(
|
||
name=DimensionName.TECHNICAL_SEO,
|
||
score=total_score,
|
||
max_score=max_score,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS, # 会在__post_init__中重新计算
|
||
)
|
||
|
||
|
||
def diagnose_on_page_seo(data: OnPageSEOData) -> SEODimensionScore:
|
||
"""
|
||
页面SEO诊断 (满分20分)
|
||
|
||
评分项:
|
||
- Title/Meta标签 (5分)
|
||
- H标签结构 (4分)
|
||
- 关键词密度 (4分)
|
||
- 内链结构 (4分)
|
||
- 图片Alt文本 (3分)
|
||
"""
|
||
max_score = 20.0
|
||
items: list[DiagnosisItem] = []
|
||
total_score = 0.0
|
||
|
||
# 1. Title/Meta标签完整性 (5分)
|
||
title_score = 0.0
|
||
meta_score = 0.0
|
||
|
||
# Title检查 (2.5分)
|
||
if data.has_title:
|
||
if 30 <= data.title_length <= 60:
|
||
title_score += 2.5
|
||
if not data.title_keyword_stuffing:
|
||
items.append(DiagnosisItem(
|
||
name="Title标签",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"Title长度{data.title_length}字符,格式规范",
|
||
suggestion="保持当前Title优化",
|
||
score=1.0,
|
||
details={"length": data.title_length},
|
||
))
|
||
else:
|
||
title_score -= 1.0
|
||
items.append(DiagnosisItem(
|
||
name="Title标签",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="Title存在关键词堆砌问题",
|
||
suggestion="简化Title,自然使用关键词",
|
||
score=0.5,
|
||
))
|
||
else:
|
||
title_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="Title标签",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"Title长度{data.title_length}字符,建议30-60字符",
|
||
suggestion="调整Title长度到推荐范围",
|
||
score=0.5,
|
||
details={"length": data.title_length},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="Title标签",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="页面缺少Title标签",
|
||
suggestion="为每个页面添加唯一且描述性的Title",
|
||
score=0.0,
|
||
))
|
||
|
||
# Meta Description检查 (2.5分)
|
||
if data.has_meta_description:
|
||
if 120 <= data.meta_description_length <= 160:
|
||
meta_score += 2.5
|
||
items.append(DiagnosisItem(
|
||
name="Meta Description",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"Meta Description长度{data.meta_description_length}字符,格式规范",
|
||
suggestion="保持当前Meta Description优化",
|
||
score=1.0,
|
||
details={"length": data.meta_description_length},
|
||
))
|
||
else:
|
||
meta_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="Meta Description",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"Meta Description长度{data.meta_description_length}字符,建议120-160字符",
|
||
suggestion="调整Meta Description长度",
|
||
score=0.5,
|
||
details={"length": data.meta_description_length},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="Meta Description",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="页面缺少Meta Description",
|
||
suggestion="为每个页面添加描述性的Meta Description",
|
||
score=0.0,
|
||
))
|
||
|
||
total_score += title_score + meta_score
|
||
|
||
# 2. H标签结构层级 (4分)
|
||
if data.h1_count == 1 and data.h_structure_valid:
|
||
items.append(DiagnosisItem(
|
||
name="H标签结构",
|
||
status=DiagnosisStatus.PASS,
|
||
description="H标签结构清晰,有且仅有1个H1",
|
||
suggestion="保持当前H标签结构",
|
||
score=1.0,
|
||
details={"h1_count": data.h1_count},
|
||
))
|
||
total_score += 4.0
|
||
elif data.h1_count == 1:
|
||
items.append(DiagnosisItem(
|
||
name="H标签结构",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="有1个H1,但H标签层级可能不规范",
|
||
suggestion="确保H标签层级正确,不跳过级别",
|
||
score=0.5,
|
||
details={"h1_count": data.h1_count},
|
||
))
|
||
total_score += 2.0
|
||
elif data.h1_count == 0:
|
||
items.append(DiagnosisItem(
|
||
name="H标签结构",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="页面缺少H1标签",
|
||
suggestion="添加唯一的H1标签,包含主要关键词",
|
||
score=0.0,
|
||
details={"h1_count": data.h1_count},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="H标签结构",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"页面有{data.h1_count}个H1标签,建议只有1个",
|
||
suggestion="确保每个页面只有1个H1标签",
|
||
score=0.5,
|
||
details={"h1_count": data.h1_count},
|
||
))
|
||
total_score += 2.0
|
||
|
||
# 3. 关键词密度合理性 (4分)
|
||
if 1.0 <= data.keyword_density <= 3.0:
|
||
items.append(DiagnosisItem(
|
||
name="关键词密度",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"关键词密度{data.keyword_density}%,在合理范围内",
|
||
suggestion="保持当前关键词使用频率",
|
||
score=1.0,
|
||
details={"density": data.keyword_density},
|
||
))
|
||
total_score += 4.0
|
||
elif 0.5 <= data.keyword_density < 1.0 or 3.0 < data.keyword_density <= 5.0:
|
||
items.append(DiagnosisItem(
|
||
name="关键词密度",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"关键词密度{data.keyword_density}%,建议1-3%",
|
||
suggestion="调整关键词使用频率到推荐范围",
|
||
score=0.5,
|
||
details={"density": data.keyword_density},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
status = DiagnosisStatus.FAIL if data.keyword_density > 5.0 else DiagnosisStatus.WARNING
|
||
items.append(DiagnosisItem(
|
||
name="关键词密度",
|
||
status=status,
|
||
description=f"关键词密度{data.keyword_density}%,不合理",
|
||
suggestion="优化关键词使用,避免堆砌或过少",
|
||
score=0.0,
|
||
details={"density": data.keyword_density},
|
||
))
|
||
|
||
# 4. 内链结构 (4分)
|
||
if data.internal_links > 0:
|
||
if data.broken_internal_links == 0:
|
||
items.append(DiagnosisItem(
|
||
name="内链结构",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"内链结构良好,共{data.internal_links}个内链,无死链",
|
||
suggestion="保持内链更新,定期检查死链",
|
||
score=1.0,
|
||
details={"total": data.internal_links, "broken": 0},
|
||
))
|
||
total_score += 4.0
|
||
elif data.broken_internal_links <= 3:
|
||
items.append(DiagnosisItem(
|
||
name="内链结构",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"发现{data.broken_internal_links}个死链",
|
||
suggestion="修复所有死链,更新或移除无效链接",
|
||
score=0.5,
|
||
details={"total": data.internal_links, "broken": data.broken_internal_links},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="内链结构",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"发现{data.broken_internal_links}个死链,数量过多",
|
||
suggestion="立即修复所有死链",
|
||
score=0.0,
|
||
details={"total": data.internal_links, "broken": data.broken_internal_links},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="内链结构",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="页面没有内链",
|
||
suggestion="添加相关页面的内链,提升网站结构",
|
||
score=0.0,
|
||
))
|
||
|
||
# 5. 图片Alt文本 (3分)
|
||
if data.total_images == 0:
|
||
items.append(DiagnosisItem(
|
||
name="图片Alt文本",
|
||
status=DiagnosisStatus.PASS,
|
||
description="页面无图片",
|
||
suggestion="考虑添加相关图片并设置Alt文本",
|
||
score=1.0,
|
||
))
|
||
total_score += 3.0
|
||
elif data.images_without_alt == 0:
|
||
items.append(DiagnosisItem(
|
||
name="图片Alt文本",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"所有{data.total_images}张图片都有Alt文本",
|
||
suggestion="保持为所有图片添加描述性Alt文本",
|
||
score=1.0,
|
||
details={"total": data.total_images, "without_alt": 0},
|
||
))
|
||
total_score += 3.0
|
||
elif data.images_without_alt <= data.total_images * 0.3:
|
||
items.append(DiagnosisItem(
|
||
name="图片Alt文本",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"{data.images_without_alt}/{data.total_images}张图片缺少Alt文本",
|
||
suggestion="为所有图片添加描述性Alt文本",
|
||
score=0.5,
|
||
details={"total": data.total_images, "without_alt": data.images_without_alt},
|
||
))
|
||
total_score += 1.5
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="图片Alt文本",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"{data.images_without_alt}/{data.total_images}张图片缺少Alt文本",
|
||
suggestion="立即为所有图片添加Alt文本",
|
||
score=0.0,
|
||
details={"total": data.total_images, "without_alt": data.images_without_alt},
|
||
))
|
||
|
||
return SEODimensionScore(
|
||
name=DimensionName.ON_PAGE_SEO,
|
||
score=total_score,
|
||
max_score=max_score,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
|
||
|
||
def diagnose_content_quality(data: ContentQualityData) -> SEODimensionScore:
|
||
"""
|
||
内容质量诊断 (满分20分)
|
||
|
||
评分项:
|
||
- 可读性评分 (4分)
|
||
- 信息深度 (4分)
|
||
- E-E-A-T信号 (5分)
|
||
- 内容新鲜度 (4分)
|
||
- 重复内容检测 (3分)
|
||
"""
|
||
max_score = 20.0
|
||
items: list[DiagnosisItem] = []
|
||
total_score = 0.0
|
||
|
||
# 1. 可读性评分 (4分)
|
||
if data.readability_score >= 70:
|
||
items.append(DiagnosisItem(
|
||
name="可读性",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"可读性评分{data.readability_score},内容易于理解",
|
||
suggestion="保持当前内容质量",
|
||
score=1.0,
|
||
details={"score": data.readability_score},
|
||
))
|
||
total_score += 4.0
|
||
elif data.readability_score >= 50:
|
||
items.append(DiagnosisItem(
|
||
name="可读性",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"可读性评分{data.readability_score},内容较难理解",
|
||
suggestion="简化语言,使用短句,增加段落分隔",
|
||
score=0.5,
|
||
details={"score": data.readability_score},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="可读性",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"可读性评分{data.readability_score},内容难以理解",
|
||
suggestion="大幅简化内容,使用更通俗的语言",
|
||
score=0.0,
|
||
details={"score": data.readability_score},
|
||
))
|
||
|
||
# 2. 信息深度评估 (4分)
|
||
if data.word_count >= 1500 and data.topic_coverage >= 0.8:
|
||
items.append(DiagnosisItem(
|
||
name="信息深度",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"内容深度良好,{data.word_count}字,主题覆盖率{data.topic_coverage*100:.0f}%",
|
||
suggestion="保持当前内容深度",
|
||
score=1.0,
|
||
details={"word_count": data.word_count, "coverage": data.topic_coverage},
|
||
))
|
||
total_score += 4.0
|
||
elif data.word_count >= 800 and data.topic_coverage >= 0.6:
|
||
items.append(DiagnosisItem(
|
||
name="信息深度",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"内容深度一般,{data.word_count}字,主题覆盖率{data.topic_coverage*100:.0f}%",
|
||
suggestion="扩展内容深度,覆盖更多相关子话题",
|
||
score=0.5,
|
||
details={"word_count": data.word_count, "coverage": data.topic_coverage},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="信息深度",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"内容深度不足,{data.word_count}字,主题覆盖率{data.topic_coverage*100:.0f}%",
|
||
suggestion="大幅扩展内容,全面覆盖主题",
|
||
score=0.0,
|
||
details={"word_count": data.word_count, "coverage": data.topic_coverage},
|
||
))
|
||
|
||
# 3. E-E-A-T信号检测 (5分)
|
||
eeat_score = 0.0
|
||
|
||
# 作者信息 (1.5分)
|
||
if data.has_author_info:
|
||
eeat_score += 1.5
|
||
items.append(DiagnosisItem(
|
||
name="作者资质",
|
||
status=DiagnosisStatus.PASS,
|
||
description="内容包含作者信息",
|
||
suggestion="保持展示作者资质",
|
||
score=1.0,
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="作者资质",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="内容缺少作者信息",
|
||
suggestion="添加作者信息和专业背景",
|
||
score=0.0,
|
||
))
|
||
|
||
# 专业认证/专家审核 (1.5分)
|
||
if data.has_expert_review:
|
||
eeat_score += 1.5
|
||
items.append(DiagnosisItem(
|
||
name="专家审核",
|
||
status=DiagnosisStatus.PASS,
|
||
description="内容经过专家审核",
|
||
suggestion="保持专家审核流程",
|
||
score=1.0,
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="专家审核",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="内容未经专家审核",
|
||
suggestion="考虑邀请行业专家审核重要内容",
|
||
score=0.0,
|
||
))
|
||
|
||
# 数据来源权威性 (2分)
|
||
if data.has_citations and data.citation_authority >= 0.7:
|
||
eeat_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="数据来源",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"引用权威数据源,权威性评分{data.citation_authority:.2f}",
|
||
suggestion="保持引用高质量数据源",
|
||
score=1.0,
|
||
details={"authority": data.citation_authority},
|
||
))
|
||
elif data.has_citations:
|
||
eeat_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="数据来源",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"有引用但数据源权威性一般,评分{data.citation_authority:.2f}",
|
||
suggestion="引用更权威的数据源",
|
||
score=0.5,
|
||
details={"authority": data.citation_authority},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="数据来源",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="内容未引用任何数据源",
|
||
suggestion="引用权威数据支持内容观点",
|
||
score=0.0,
|
||
))
|
||
|
||
total_score += eeat_score
|
||
|
||
# 4. 内容新鲜度 (4分)
|
||
if data.has_publication_date and data.last_updated_days <= 30:
|
||
items.append(DiagnosisItem(
|
||
name="内容新鲜度",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"内容更新于{data.last_updated_days}天前",
|
||
suggestion="保持定期更新内容",
|
||
score=1.0,
|
||
details={"last_updated_days": data.last_updated_days},
|
||
))
|
||
total_score += 4.0
|
||
elif data.has_publication_date and data.last_updated_days <= 90:
|
||
items.append(DiagnosisItem(
|
||
name="内容新鲜度",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"内容更新于{data.last_updated_days}天前,建议30天内更新",
|
||
suggestion="定期更新内容,保持信息时效性",
|
||
score=0.5,
|
||
details={"last_updated_days": data.last_updated_days},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="内容新鲜度",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="内容长时间未更新或缺少发布日期",
|
||
suggestion="更新内容并显示发布/更新日期",
|
||
score=0.0,
|
||
details={"last_updated_days": data.last_updated_days},
|
||
))
|
||
|
||
# 5. 重复内容检测 (3分)
|
||
if data.duplicate_content_ratio <= 0.1:
|
||
items.append(DiagnosisItem(
|
||
name="重复内容",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"重复内容比例{data.duplicate_content_ratio*100:.0f}%,在可接受范围内",
|
||
suggestion="保持内容原创性",
|
||
score=1.0,
|
||
details={"duplicate_ratio": data.duplicate_content_ratio},
|
||
))
|
||
total_score += 3.0
|
||
elif data.duplicate_content_ratio <= 0.3:
|
||
items.append(DiagnosisItem(
|
||
name="重复内容",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"重复内容比例{data.duplicate_content_ratio*100:.0f}%",
|
||
suggestion="减少重复内容,使用canonical标签",
|
||
score=0.5,
|
||
details={"duplicate_ratio": data.duplicate_content_ratio},
|
||
))
|
||
total_score += 1.5
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="重复内容",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"重复内容比例{data.duplicate_content_ratio*100:.0f}%,过高",
|
||
suggestion="重写重复内容,确保每页独特价值",
|
||
score=0.0,
|
||
details={"duplicate_ratio": data.duplicate_content_ratio},
|
||
))
|
||
|
||
return SEODimensionScore(
|
||
name=DimensionName.CONTENT_QUALITY,
|
||
score=total_score,
|
||
max_score=max_score,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
|
||
|
||
def diagnose_backlinks(data: BacklinkData) -> SEODimensionScore:
|
||
"""
|
||
外链分析 (满分15分)
|
||
|
||
评分项:
|
||
- 反向链接质量 (6分)
|
||
- 毒性信号检测 (5分)
|
||
- 锚文本分布 (4分)
|
||
"""
|
||
max_score = 15.0
|
||
items: list[DiagnosisItem] = []
|
||
total_score = 0.0
|
||
|
||
# 1. 反向链接质量 (6分)
|
||
quality_score = 0.0
|
||
|
||
# 引用域名数 (2分)
|
||
if data.referring_domains >= 20:
|
||
quality_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="引用域名",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"有{data.referring_domains}个引用域名",
|
||
suggestion="继续增加高质量外链",
|
||
score=1.0,
|
||
details={"referring_domains": data.referring_domains},
|
||
))
|
||
elif data.referring_domains >= 10:
|
||
quality_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="引用域名",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"有{data.referring_domains}个引用域名,建议增加",
|
||
suggestion="通过内容营销获取更多外链",
|
||
score=0.5,
|
||
details={"referring_domains": data.referring_domains},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="引用域名",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"仅有{data.referring_domains}个引用域名",
|
||
suggestion="积极开展外链建设",
|
||
score=0.0,
|
||
details={"referring_domains": data.referring_domains},
|
||
))
|
||
|
||
# 高权威链接 (2分)
|
||
if data.high_authority_links >= 5:
|
||
quality_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="高权威链接",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"有{data.high_authority_links}个高权威外链",
|
||
suggestion="保持获取高质量外链",
|
||
score=1.0,
|
||
details={"high_authority_links": data.high_authority_links},
|
||
))
|
||
elif data.high_authority_links >= 2:
|
||
quality_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="高权威链接",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"有{data.high_authority_links}个高权威外链",
|
||
suggestion="争取更多权威网站的外链",
|
||
score=0.5,
|
||
details={"high_authority_links": data.high_authority_links},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="高权威链接",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"仅有{data.high_authority_links}个高权威外链",
|
||
suggestion="重点获取权威网站外链",
|
||
score=0.0,
|
||
details={"high_authority_links": data.high_authority_links},
|
||
))
|
||
|
||
# Nofollow比例 (2分)
|
||
if 0.2 <= data.nofollow_ratio <= 0.6:
|
||
quality_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="Nofollow比例",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"Nofollow比例{data.nofollow_ratio*100:.0f}%,自然合理",
|
||
suggestion="保持自然的外链结构",
|
||
score=1.0,
|
||
details={"nofollow_ratio": data.nofollow_ratio},
|
||
))
|
||
else:
|
||
quality_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="Nofollow比例",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"Nofollow比例{data.nofollow_ratio*100:.0f}%,可能不自然",
|
||
suggestion="确保外链结构自然多样",
|
||
score=0.5,
|
||
details={"nofollow_ratio": data.nofollow_ratio},
|
||
))
|
||
|
||
total_score += quality_score
|
||
|
||
# 2. 毒性信号检测 (5分)
|
||
toxic_ratio = data.toxic_links / data.total_backlinks if data.total_backlinks > 0 else 0
|
||
|
||
if data.toxic_links == 0:
|
||
items.append(DiagnosisItem(
|
||
name="毒性链接",
|
||
status=DiagnosisStatus.PASS,
|
||
description="未发现毒性外链",
|
||
suggestion="定期监控外链质量",
|
||
score=1.0,
|
||
details={"toxic_count": 0},
|
||
))
|
||
total_score += 5.0
|
||
elif toxic_ratio <= 0.05:
|
||
items.append(DiagnosisItem(
|
||
name="毒性链接",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"发现{data.toxic_links}个毒性外链",
|
||
suggestion="使用Disavow工具拒绝毒性外链",
|
||
score=0.5,
|
||
details={"toxic_count": data.toxic_links, "ratio": toxic_ratio},
|
||
))
|
||
total_score += 2.5
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="毒性链接",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"发现{data.toxic_links}个毒性外链,比例过高",
|
||
suggestion="立即使用Disavow工具拒绝所有毒性外链",
|
||
score=0.0,
|
||
details={"toxic_count": data.toxic_links, "ratio": toxic_ratio},
|
||
))
|
||
|
||
# 3. 锚文本分布 (4分)
|
||
if data.anchor_text_diversity >= 0.7 and data.exact_match_anchor_ratio <= 0.3:
|
||
items.append(DiagnosisItem(
|
||
name="锚文本分布",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"锚文本多样性{data.anchor_text_diversity:.2f},精确匹配比例{data.exact_match_anchor_ratio*100:.0f}%",
|
||
suggestion="保持自然的锚文本结构",
|
||
score=1.0,
|
||
details={
|
||
"diversity": data.anchor_text_diversity,
|
||
"exact_match_ratio": data.exact_match_anchor_ratio,
|
||
},
|
||
))
|
||
total_score += 4.0
|
||
elif data.anchor_text_diversity >= 0.5:
|
||
items.append(DiagnosisItem(
|
||
name="锚文本分布",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"锚文本多样性{data.anchor_text_diversity:.2f},建议增加多样性",
|
||
suggestion="使用更多样化的锚文本",
|
||
score=0.5,
|
||
details={
|
||
"diversity": data.anchor_text_diversity,
|
||
"exact_match_ratio": data.exact_match_anchor_ratio,
|
||
},
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="锚文本分布",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"锚文本多样性{data.anchor_text_diversity:.2f},过于单一",
|
||
suggestion="大幅增加锚文本多样性",
|
||
score=0.0,
|
||
details={
|
||
"diversity": data.anchor_text_diversity,
|
||
"exact_match_ratio": data.exact_match_anchor_ratio,
|
||
},
|
||
))
|
||
|
||
return SEODimensionScore(
|
||
name=DimensionName.BACKLINK_ANALYSIS,
|
||
score=total_score,
|
||
max_score=max_score,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
|
||
|
||
def diagnose_user_experience(data: UserExperienceData) -> SEODimensionScore:
|
||
"""
|
||
用户体验诊断 (满分20分)
|
||
|
||
评分项:
|
||
- 移动适配检查 (6分)
|
||
- 页面速度评估 (5分)
|
||
- 转化路径分析 (5分)
|
||
- 基础体验 (4分)
|
||
"""
|
||
max_score = 20.0
|
||
items: list[DiagnosisItem] = []
|
||
total_score = 0.0
|
||
|
||
# 1. 移动适配检查 (6分)
|
||
if data.is_mobile_friendly and data.mobile_viewport_set:
|
||
items.append(DiagnosisItem(
|
||
name="移动适配",
|
||
status=DiagnosisStatus.PASS,
|
||
description="页面移动适配良好",
|
||
suggestion="保持移动端优化",
|
||
score=1.0,
|
||
))
|
||
total_score += 6.0
|
||
elif data.is_mobile_friendly:
|
||
items.append(DiagnosisItem(
|
||
name="移动适配",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="页面基本适配移动端,但缺少viewport设置",
|
||
suggestion="添加viewport meta标签",
|
||
score=0.5,
|
||
))
|
||
total_score += 3.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="移动适配",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="页面未适配移动端",
|
||
suggestion="立即实现响应式设计或移动版本",
|
||
score=0.0,
|
||
))
|
||
|
||
# 2. 页面速度评估 (5分)
|
||
if data.page_load_time <= 2.0:
|
||
items.append(DiagnosisItem(
|
||
name="页面速度",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"页面加载时间{data.page_load_time}s,性能优秀",
|
||
suggestion="保持当前性能水平",
|
||
score=1.0,
|
||
details={"load_time": data.page_load_time},
|
||
))
|
||
total_score += 5.0
|
||
elif data.page_load_time <= 3.0:
|
||
items.append(DiagnosisItem(
|
||
name="页面速度",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"页面加载时间{data.page_load_time}s,建议优化到2s内",
|
||
suggestion="优化图片、启用缓存、使用CDN",
|
||
score=0.5,
|
||
details={"load_time": data.page_load_time},
|
||
))
|
||
total_score += 2.5
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="页面速度",
|
||
status=DiagnosisStatus.FAIL,
|
||
description=f"页面加载时间{data.page_load_time}s,严重超时",
|
||
suggestion="立即优化页面加载性能",
|
||
score=0.0,
|
||
details={"load_time": data.page_load_time},
|
||
))
|
||
|
||
# 3. 转化路径分析 (5分)
|
||
conversion_score = 0.0
|
||
|
||
# CTA检查 (2分)
|
||
if data.has_cta:
|
||
conversion_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="CTA",
|
||
status=DiagnosisStatus.PASS,
|
||
description="页面有明确的行动号召",
|
||
suggestion="保持清晰的CTA",
|
||
score=1.0,
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="CTA",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="页面缺少明确的行动号召",
|
||
suggestion="添加清晰的CTA按钮",
|
||
score=0.0,
|
||
))
|
||
|
||
# 转化路径清晰度 (2分)
|
||
if data.conversion_path_clear:
|
||
conversion_score += 2.0
|
||
items.append(DiagnosisItem(
|
||
name="转化路径",
|
||
status=DiagnosisStatus.PASS,
|
||
description="转化路径清晰",
|
||
suggestion="保持当前转化流程",
|
||
score=1.0,
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="转化路径",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="转化路径不够清晰",
|
||
suggestion="简化转化流程,减少步骤",
|
||
score=0.0,
|
||
))
|
||
|
||
# 表单可用性 (1分)
|
||
if data.form_usability >= 0.8:
|
||
conversion_score += 1.0
|
||
items.append(DiagnosisItem(
|
||
name="表单可用性",
|
||
status=DiagnosisStatus.PASS,
|
||
description=f"表单可用性{data.form_usability*100:.0f}%",
|
||
suggestion="保持表单体验",
|
||
score=1.0,
|
||
details={"usability": data.form_usability},
|
||
))
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="表单可用性",
|
||
status=DiagnosisStatus.WARNING,
|
||
description=f"表单可用性{data.form_usability*100:.0f}%,需要优化",
|
||
suggestion="简化表单,减少必填字段",
|
||
score=0.0,
|
||
details={"usability": data.form_usability},
|
||
))
|
||
|
||
total_score += conversion_score
|
||
|
||
# 4. 基础体验 (4分)
|
||
# HTTPS检查 (2分)
|
||
if data.has_https:
|
||
items.append(DiagnosisItem(
|
||
name="HTTPS",
|
||
status=DiagnosisStatus.PASS,
|
||
description="网站使用HTTPS",
|
||
suggestion="保持HTTPS配置",
|
||
score=1.0,
|
||
))
|
||
total_score += 2.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="HTTPS",
|
||
status=DiagnosisStatus.FAIL,
|
||
description="网站未使用HTTPS",
|
||
suggestion="立即启用HTTPS",
|
||
score=0.0,
|
||
))
|
||
|
||
# 面包屑导航 (1分)
|
||
if data.has_breadcrumbs:
|
||
items.append(DiagnosisItem(
|
||
name="面包屑导航",
|
||
status=DiagnosisStatus.PASS,
|
||
description="页面有面包屑导航",
|
||
suggestion="保持面包屑导航",
|
||
score=1.0,
|
||
))
|
||
total_score += 1.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="面包屑导航",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="页面缺少面包屑导航",
|
||
suggestion="添加面包屑导航提升用户体验",
|
||
score=0.0,
|
||
))
|
||
|
||
# 站内搜索 (1分)
|
||
if data.has_search:
|
||
items.append(DiagnosisItem(
|
||
name="站内搜索",
|
||
status=DiagnosisStatus.PASS,
|
||
description="网站有站内搜索功能",
|
||
suggestion="保持搜索功能优化",
|
||
score=1.0,
|
||
))
|
||
total_score += 1.0
|
||
else:
|
||
items.append(DiagnosisItem(
|
||
name="站内搜索",
|
||
status=DiagnosisStatus.WARNING,
|
||
description="网站缺少站内搜索",
|
||
suggestion="添加站内搜索功能",
|
||
score=0.0,
|
||
))
|
||
|
||
return SEODimensionScore(
|
||
name=DimensionName.USER_EXPERIENCE,
|
||
score=total_score,
|
||
max_score=max_score,
|
||
items=items,
|
||
status=DiagnosisStatus.PASS,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# 建议生成
|
||
# ============================================================
|
||
|
||
def generate_recommendations(result: SEODiagnosisResult) -> list[SEORecommendation]:
|
||
"""
|
||
根据诊断结果生成优化建议
|
||
|
||
优先级规则:
|
||
- FAIL状态 -> high priority
|
||
- WARNING状态 -> medium priority
|
||
- 影响大的建议 -> high priority
|
||
"""
|
||
recommendations: list[SEORecommendation] = []
|
||
|
||
for dimension in result.dimensions:
|
||
for item in dimension.items:
|
||
if item.status == DiagnosisStatus.FAIL:
|
||
recommendations.append(SEORecommendation(
|
||
priority="high",
|
||
dimension=dimension.name,
|
||
item_name=item.name,
|
||
description=item.suggestion,
|
||
impact="修复后可显著提升SEO表现",
|
||
effort="medium",
|
||
))
|
||
elif item.status == DiagnosisStatus.WARNING:
|
||
recommendations.append(SEORecommendation(
|
||
priority="medium",
|
||
dimension=dimension.name,
|
||
item_name=item.name,
|
||
description=item.suggestion,
|
||
impact="优化后可改善SEO表现",
|
||
effort="easy",
|
||
))
|
||
|
||
# 按优先级排序
|
||
priority_order = {"high": 0, "medium": 1, "low": 2}
|
||
recommendations.sort(key=lambda r: priority_order.get(r.priority, 3))
|
||
|
||
return recommendations
|
||
|
||
|
||
# ============================================================
|
||
# 主诊断服务
|
||
# ============================================================
|
||
|
||
class SEODiagnosisService:
|
||
"""SEO诊断服务"""
|
||
|
||
def diagnose(
|
||
self,
|
||
technical_data: TechnicalSEOData | None = None,
|
||
on_page_data: OnPageSEOData | None = None,
|
||
content_data: ContentQualityData | None = None,
|
||
backlink_data: BacklinkData | None = None,
|
||
ux_data: UserExperienceData | None = None,
|
||
) -> SEODiagnosisResult:
|
||
"""
|
||
执行完整SEO诊断
|
||
|
||
Args:
|
||
technical_data: 技术SEO检测数据
|
||
on_page_data: 页面SEO检测数据
|
||
content_data: 内容质量检测数据
|
||
backlink_data: 外链检测数据
|
||
ux_data: 用户体验检测数据
|
||
|
||
Returns:
|
||
SEODiagnosisResult: 诊断结果
|
||
"""
|
||
# 使用默认数据(模拟数据)
|
||
technical_data = technical_data or TechnicalSEOData()
|
||
on_page_data = on_page_data or OnPageSEOData()
|
||
content_data = content_data or ContentQualityData()
|
||
backlink_data = backlink_data or BacklinkData()
|
||
ux_data = ux_data or UserExperienceData()
|
||
|
||
# 执行5维度诊断
|
||
dimensions = [
|
||
diagnose_technical_seo(technical_data),
|
||
diagnose_on_page_seo(on_page_data),
|
||
diagnose_content_quality(content_data),
|
||
diagnose_backlinks(backlink_data),
|
||
diagnose_user_experience(ux_data),
|
||
]
|
||
|
||
# 计算综合评分
|
||
overall_score = sum(dim.score for dim in dimensions)
|
||
|
||
# 创建初步结果
|
||
result = SEODiagnosisResult(
|
||
overall_score=overall_score,
|
||
dimensions=dimensions,
|
||
recommendations=[],
|
||
)
|
||
|
||
# 生成优化建议
|
||
result.recommendations = generate_recommendations(result)
|
||
|
||
return result
|
||
|
||
def diagnose_technical_only(self, data: TechnicalSEOData | None = None) -> SEODimensionScore:
|
||
"""仅执行技术SEO诊断"""
|
||
return diagnose_technical_seo(data or TechnicalSEOData())
|
||
|
||
def diagnose_on_page_only(self, data: OnPageSEOData | None = None) -> SEODimensionScore:
|
||
"""仅执行页面SEO诊断"""
|
||
return diagnose_on_page_seo(data or OnPageSEOData())
|
||
|
||
def diagnose_content_only(self, data: ContentQualityData | None = None) -> SEODimensionScore:
|
||
"""仅执行内容质量诊断"""
|
||
return diagnose_content_quality(data or ContentQualityData())
|
||
|
||
def diagnose_backlinks_only(self, data: BacklinkData | None = None) -> SEODimensionScore:
|
||
"""仅执行外链分析"""
|
||
return diagnose_backlinks(data or BacklinkData())
|
||
|
||
def diagnose_ux_only(self, data: UserExperienceData | None = None) -> SEODimensionScore:
|
||
"""仅执行用户体验诊断"""
|
||
return diagnose_user_experience(data or UserExperienceData())
|