Files
yu-zhi-ran/scripts/rank_tracker.py
T
2026-06-09 17:18:09 +08:00

217 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
搜索排名追踪模块
功能:
1. 对已发布的文章关键词,查搜索排名(Bing/Baidu)
2. 检测文章是否被 AI 搜索引用(通过特定查询判断)
3. 结果写入 SearchRanking 表
4. 可作为定时任务每天运行
"""
import json, logging, sys, re, datetime
from pathlib import Path
from typing import List, Dict, Optional
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
from web_search import search_api
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
DOMAIN = "yu-zhi-ran.com"
def get_published_articles() -> List[Dict]:
"""从 DB 获取所有已发布的文章"""
try:
from app.database import SessionLocal
from app.models import Article, Topic
db = SessionLocal()
try:
results = db.query(Article, Topic).join(Topic, Article.topic_id == Topic.id).all()
articles = []
for article, topic in results:
articles.append({
"id": article.id,
"topic_id": article.topic_id,
"platform": article.platform,
"title": article.title or topic.title,
"status": article.status,
"topic_title": topic.title,
"field": topic.field or "",
})
return articles
finally:
db.close()
except Exception as e:
logger.warning(f"无法读取文章列表: {e}")
return []
def _build_search_queries(article: Dict) -> List[str]:
"""为文章生成需要追踪的关键词"""
queries = []
title = article.get("title", "") or article.get("topic_title", "")
field = article.get("field", "")
if title:
queries.append(title[:30])
# 核心关键词:标题短句
parts = re.split(r'[:,。.!?]', title)
for p in parts[:2]:
p = p.strip()
if 4 <= len(p) <= 25:
queries.append(p)
if field:
queries.append(field[:20])
return list(set(q for q in queries if len(q) >= 4))[:5]
def _generate_keywords_for_article(article: Dict) -> List[str]:
"""通过 LLM 生成更多 SEO 关键词(可选)"""
try:
from app.core.nvidia_client import call_llm
from prompt_loader import get_prompt
except ImportError:
return []
title = article.get("title") or article.get("topic_title", "")
field = article.get("field", "")
try:
prompt = get_prompt("tags_generation",
platform="搜索引擎",
title=title,
field=field,
core=article.get("topic_id", ""),
)
resp = call_llm(prompt, temperature=0.3, max_tokens=500)
if resp:
from content_cleaner import strip_thinking
resp = strip_thinking(resp)
keywords = [t.strip("# ") for t in resp.split() if len(t.strip("# ")) >= 3]
return keywords[:5]
except Exception as e:
logger.warning(f"关键词生成失败: {e}")
return []
def check_rankings(article: Dict, keywords: List[str], engine: str = "bing") -> List[Dict]:
"""检查文章关键词在搜索引擎的排名"""
title = article.get("title") or article.get("topic_title", "")
article_id = article.get("id", "")
topic_id = article.get("topic_id", "")
platform = article.get("platform", "")
results = []
for keyword in keywords:
try:
search_results = search_api(keyword, max_results=10)
position = None
url_found = None
for i, sr in enumerate(search_results):
url = sr.get("url", "")
if DOMAIN in url or any(part in url for part in title.split() if len(part) >= 4):
position = i + 1
url_found = url[:200]
break
results.append({
"article_id": article_id,
"topic_id": topic_id,
"keyword": keyword,
"platform": platform,
"search_engine": engine,
"position": position,
"url_found": url_found,
"ai_cited": False,
"ai_source": None,
})
logger.info(f" [{engine}] '{keyword}'{'#' + str(position) if position else '未上榜'}"
f"{' ' + url_found[:60] if url_found else ''}")
except Exception as e:
logger.warning(f"检查关键词 '{keyword}' 失败: {e}")
return results
def save_rankings(rankings: List[Dict]):
"""将排名结果写入数据库"""
if not rankings:
return
try:
from app.database import SessionLocal
from app.models import SearchRanking
db = SessionLocal()
try:
for r in rankings:
record = SearchRanking(
article_id=r.get("article_id"),
topic_id=r.get("topic_id"),
keyword=r.get("keyword"),
platform=r.get("platform"),
search_engine=r.get("search_engine", "bing"),
position=r.get("position"),
url_found=r.get("url_found"),
ai_cited=r.get("ai_cited", False),
ai_source=r.get("ai_source"),
)
db.add(record)
db.commit()
logger.info(f"已保存 {len(rankings)} 条排名记录")
finally:
db.close()
except Exception as e:
logger.warning(f"保存排名记录失败: {e}")
def run_all(engine: str = "bing") -> Dict:
"""对所有已发布文章执行排名追踪"""
articles = get_published_articles()
if not articles:
logger.warning("没有已发布的文章可追踪")
return {"ok": True, "tracked": 0, "articles": 0}
all_rankings = []
for article in articles:
keywords = _build_search_queries(article)
if not keywords:
continue
logger.info(f"追踪 [{article['id']}] {article.get('title','')[:30]} keywords: {keywords}")
rankings = check_rankings(article, keywords, engine)
all_rankings.extend(rankings)
save_rankings(all_rankings)
on_page = sum(1 for r in all_rankings if r.get("position") is not None)
logger.info(f"排名追踪完成: {len(articles)} 篇文章, "
f"{len(all_rankings)} 条关键词检查, "
f"{on_page} 条有排名")
return {
"ok": True,
"articles_checked": len(articles),
"keywords_checked": len(all_rankings),
"on_page": on_page,
}
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--engine', default='bing', help='搜索引擎 (bing/baidu/google)')
args = parser.parse_args()
result = run_all(args.engine)
print(json.dumps(result, ensure_ascii=False))
sys.exit(0 if result['ok'] else 1)
if __name__ == "__main__":
main()