Files

332 lines
14 KiB
Python

import json
import os
from datetime import datetime
from utils.logger import get_logger
from utils.blog_manager import BlogManager
import re
logger = get_logger('article_manager')
class ArticleManager:
def __init__(self, analyzer, blog_manager=None):
self.analyzer = analyzer
self.blog_manager = blog_manager or BlogManager()
self.categories = {
"漏洞分析": "🔍",
"安全研究": "🔬",
"威胁情报": "🎯",
"安全工具": "🛠️",
"最佳实践": "📚",
"吃瓜新闻": "🍉",
"其他": "📌"
}
# 分类别名映射
self.category_aliases = {
" 漏洞分析": "漏洞分析", # 处理前导空格
"漏洞利用": "漏洞分析", # 将漏洞利用归类到漏洞分析
}
# 创建必要的目录
self.data_dir = "data/articles"
self.results_dir = "results/daily_articles"
os.makedirs(self.data_dir, exist_ok=True)
os.makedirs(self.results_dir, exist_ok=True)
# 加载已处理的URL和分类文章
self.processed_urls_file = os.path.join(self.data_dir, "processed_urls.json")
self.categorized_articles_file = os.path.join(self.data_dir, "categorized_articles.json")
self.processed_urls = self._load_processed_urls()
self.categorized_articles = self._load_categorized_articles()
def _load_processed_urls(self):
"""加载已处理的URL列表"""
if os.path.exists(self.processed_urls_file):
try:
with open(self.processed_urls_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"加载已处理URL文件失败: {str(e)}")
return []
return []
def _load_categorized_articles(self):
"""加载已分类的文章"""
if os.path.exists(self.categorized_articles_file):
try:
with open(self.categorized_articles_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"加载分类文章文件失败: {str(e)}")
return {category: [] for category in self.categories.keys()}
return {category: [] for category in self.categories.keys()}
def _save_processed_urls(self):
"""保存已处理的URL列表"""
try:
with open(self.processed_urls_file, 'w', encoding='utf-8') as f:
json.dump(self.processed_urls, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存已处理URL文件失败: {str(e)}")
def _save_categorized_articles(self):
"""保存分类文章"""
try:
with open(self.categorized_articles_file, 'w', encoding='utf-8') as f:
json.dump(self.categorized_articles, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存分类文章文件失败: {str(e)}")
def filter_new_articles(self, articles):
"""过滤出新文章"""
new_articles = []
for title, url in articles:
if url not in self.processed_urls:
new_articles.append((title, url))
return new_articles
def process_articles(self, articles):
"""处理新文章"""
if not articles:
return None
# 分析文章
analysis_result = self.analyzer.analyze_article_titles(articles)
if not analysis_result or not analysis_result['filtered_articles']:
return None
# 更新已处理的URL和分类文章
today = datetime.now().strftime('%Y-%m-%d')
try:
# 首先加载现有的分类文章数据
today_articles = {category: [] for category in self.categories.keys()}
# 保留今天已有的文章
for category, articles_list in self.categorized_articles.items():
today_articles[category] = [
article for article in articles_list
if isinstance(article, dict) and article.get('date') == today
]
# 创建URL集合用于去重(包含所有已存在的URL)
existing_urls = set(self.processed_urls)
for category_articles in today_articles.values():
for article in category_articles:
if isinstance(article, dict) and 'url' in article:
existing_urls.add(article['url'])
# 处理新文章
new_articles_added = False
for article in analysis_result['filtered_articles']:
if 'url' not in article:
logger.warning(f"跳过缺少URL的文章: {article.get('title', 'Unknown')}")
continue
# 检查URL是否已经存在
if article['url'] in existing_urls:
logger.info(f"跳过已存在的文章: {article.get('title', article['url'])}")
continue
# 添加到已处理URL集合
existing_urls.add(article['url'])
if article['url'] not in self.processed_urls:
self.processed_urls.append(article['url'])
# 处理分类
category = article.get('category', '其他')
category = self.category_aliases.get(category, category)
if category not in self.categories:
logger.warning(f"未知分类: {category},将归类为'其他'")
category = '其他'
# 创建文章数据
article_data = {
'title': article.get('title', 'Unknown'),
'url': article['url'],
'date': today
}
# 添加新文章到对应分类
today_articles[category].append(article_data)
logger.info(f"添加新文章到{category}分类: {article_data['title']}")
new_articles_added = True
# 更新分类文章数据
# 保留非今天的文章,并添加今天的文章
for category in self.categories.keys():
old_articles = [
article for article in self.categorized_articles.get(category, [])
if isinstance(article, dict) and article.get('date') != today
]
self.categorized_articles[category] = old_articles + today_articles[category]
# 只有在添加了新文章时才保存数据
if new_articles_added:
self._save_processed_urls()
self._save_categorized_articles()
logger.info("保存更新后的文章数据")
else:
logger.info("没有新文章需要添加,跳过保存")
return analysis_result
except Exception as e:
logger.error(f"处理文章时发生错误: {str(e)}", exc_info=True)
return None
def generate_daily_report(self, analysis_result, daily_report_path):
"""生成每日安全资讯报告并发布到博客
Args:
analysis_result (dict): 包含过滤后文章信息的字典
daily_report_path (str): 报告文件的完整路径
"""
# 检查 analysis_result 是否有效
if not analysis_result or not analysis_result.get('filtered_articles'):
logger.warning("没有有效的分析结果,无法生成每日报告")
return
today = datetime.now()
date_str = today.strftime('%Y-%m-%d')
# 使用传入的路径,不再自行构造
# filename = os.path.join(self.results_dir, f"{date_str}.md")
filename = daily_report_path # 使用传入的路径
logger.info(f"准备生成每日报告: {filename}")
# 用于去重的集合
processed_urls = set()
processed_titles = set()
# 读取已有的文章(如果存在)
existing_categorized_articles = {cat: [] for cat in self.categories.keys()}
if os.path.exists(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()
# 解析已有内容中的文章
for category in self.categories.keys():
pattern = f"### {self.categories[category]} {category}\n\n([^#]*?)(?=\n###|\n## |$)"
matches = re.findall(pattern, content, re.DOTALL)
if matches:
articles_text = matches[0]
article_matches = re.findall(r'\* \[(.*?)\]\((.*?)\)', articles_text)
for title, url in article_matches:
processed_urls.add(url)
processed_titles.add(title)
existing_categorized_articles[category].append({
'title': title,
'url': url,
'date': date_str
})
except Exception as e:
logger.error(f"读取已有文件失败: {str(e)}")
# 按分类整理新文章
for article in analysis_result['filtered_articles']:
# 获取文章信息
title = article.get('title', '无标题')
url = article.get('url', '#')
# 检查URL和标题是否已处理过
if url in processed_urls or title in processed_titles:
logger.info(f"跳过重复文章: {title}")
continue
# 添加到已处理集合
processed_urls.add(url)
processed_titles.add(title)
# 处理分类
category = article.get('category', '其他')
# 处理分类别名
category = self.category_aliases.get(category, category)
# 如果分类不在预定义分类中,归类到"其他"
if category not in self.categories:
logger.warning(f"未知分类: {category},将归类为'其他'")
category = '其他'
# 添加文章到对应分类
existing_categorized_articles[category].append({
'title': title,
'url': url,
'date': date_str
})
# 生成markdown内容
content = f"""
# 安全资讯日报 {date_str}
> 本文由AI自动生成,基于对安全相关仓库、CVE和最新安全研究成果的自动化分析。
>
> 更新时间:{today.strftime('%Y-%m-%d %H:%M:%S')}
<!-- more -->
## 今日资讯
"""
# 添加分类文章
for category, emoji in self.categories.items():
articles = existing_categorized_articles[category]
if articles:
content += f"\n### {emoji} {category}\n\n"
# 按日期倒序排序,处理空值情况
articles.sort(
key=lambda x: x.get('date') or '0000-00-00', # 使用默认值处理空值
reverse=True
)
# 添加文章
for article in articles:
title = article.get('title', '无标题').strip() # 去除标题前后空格
url = article.get('url', '#')
content += f"* [{title}]({url})\n"
# 添加AI分析结果
content += "\n## 安全分析\n"
# 从AI分析目录读取今日的分析结果
ai_analysis_file = os.path.join("results/ai_analysis", f"Ai_{date_str.replace('-', '_')}.md")
if os.path.exists(ai_analysis_file):
try:
with open(ai_analysis_file, 'r', encoding='utf-8') as f:
ai_content = f.read()
# 移除原始文件的标题(如果有)
ai_content = ai_content.replace("# AI 安全分析日报", "").strip()
content += ai_content
except Exception as e:
logger.error(f"读取AI分析文件失败: {str(e)}")
content += "\n> 暂无AI分析结果\n"
else:
content += "\n> 暂无AI分析结果\n"
# 添加页脚
content += """
## 免责声明
本文内容由 AI 自动生成,仅供参考和学习交流。文章中的观点和建议不代表作者立场,使用本文信息需自行承担风险和责任。
"""
# 保存文件
try:
# 确保目录存在
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"已生成每日安全资讯报告: {filename}")
# 发布到博客
if self.blog_manager.publish_daily_report(date_str, content):
logger.info(f"已成功发布到博客: {date_str}")
else:
logger.error(f"发布到博客失败: {date_str}")
except Exception as e:
logger.error(f"保存每日报告失败: {str(e)}")
def set_blog_token(self, token):
"""设置博客认证token"""
self.blog_manager.set_token(token)