#!/usr/bin/env python3
"""
国内技术网站抓取器
支持掘金、OSChina、知乎热榜
"""
import os
import sys
import json
import sqlite3
import datetime
import time
import random
import logging
import re
import html
import urllib.request
from pathlib import Path
from typing import List, Dict, Optional
import requests
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class JuejinCrawler:
"""掘金抓取器 - 使用官方API"""
API_URL = "https://api.juejin.cn/recommend_api/v1/article/recommend_all_feed"
def fetch_hot_articles(self, limit: int = 10) -> List[Dict]:
"""获取热门文章"""
articles = []
try:
headers = {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
payload = {
"cursor": "0",
"id_type": 2,
"limit": limit,
"sort_type": 200 # 200=热门
}
response = requests.post(
self.API_URL,
headers=headers,
json=payload,
timeout=15
)
response.raise_for_status()
data = response.json()
if data.get('data'):
for item in data['data']:
article_info = item.get('item_info', {}).get('article_info', {})
author_info = item.get('item_info', {}).get('author_user_info', {})
title = article_info.get('title', '')
article_id = article_info.get('article_id', '')
brief = article_info.get('brief_content', '')
if title and article_id:
articles.append({
'title': title,
'url': f"https://juejin.cn/post/{article_id}",
'description': brief[:200] if brief else '',
'source': '掘金',
'category': 'tech',
'author': author_info.get('user_name', ''),
'word_count': article_info.get('word_count', 0)
})
logger.info(f"掘金API获取到 {len(articles)} 篇文章")
else:
logger.warning(f"掘金API返回空数据: {data.get('err_msg', '未知错误')}")
except Exception as e:
logger.error(f"掘金API请求失败: {e}")
return articles
class OSChinaCrawler:
"""OSChina抓取器 - 使用AJAX API"""
API_URL = "https://www.oschina.net/action/ajax/get_more_recommend_blog"
def fetch_hot_articles(self, limit: int = 10) -> List[Dict]:
"""获取热门博客文章"""
articles = []
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Referer': 'https://www.oschina.net/blog'
}
url = f"{self.API_URL}?classification=0&p=1"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as response:
content = response.read().decode('utf-8')
# 解析HTML内容
titles = re.findall(r'
]*>(.*?)
', content, re.DOTALL)
links = re.findall(r'href="(https://my\.oschina\.net/[^"]+/blog/\d+)"', content)
briefs = re.findall(r'', content, re.DOTALL)
for i, (title_html, url) in enumerate(zip(titles[:limit], links[:limit])):
title = html.unescape(re.sub(r'<[^>]+>', '', title_html)).strip()
brief = ''
if i < len(briefs):
brief = html.unescape(re.sub(r'<[^>]+>', '', briefs[i])).strip()[:200]
if title and url:
articles.append({
'title': title,
'url': url,
'description': brief,
'source': 'OSChina',
'category': 'tech',
'author': '',
'word_count': 0
})
logger.info(f"OSChina获取到 {len(articles)} 篇文章")
except Exception as e:
logger.error(f"OSChina请求失败: {e}")
return articles
class ZhihuHotCrawler:
"""知乎热榜抓取器"""
def __init__(self, cookie_file: str = "/home/ubuntu/zhihu_cookies.txt"):
self.cookie_file = cookie_file
def fetch_hot_list(self, limit: int = 10) -> List[Dict]:
"""获取知乎热榜"""
articles = []
try:
# 读取cookies(支持JSON格式)
cookies = {}
if os.path.exists(self.cookie_file):
with open(self.cookie_file, 'r') as f:
cookie_content = f.read().strip()
# 尝试解析为JSON(浏览器导出格式)
try:
cookie_list = json.loads(cookie_content)
if isinstance(cookie_list, list):
for cookie in cookie_list:
if 'name' in cookie and 'value' in cookie:
cookies[cookie['name']] = cookie['value']
elif isinstance(cookie_list, dict):
cookies = cookie_list
except json.JSONDecodeError:
# 回退到简单的key=value格式
for item in cookie_content.split(';'):
if '=' in item:
key, value = item.strip().split('=', 1)
cookies[key] = value
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://www.zhihu.com/',
}
# 知乎热榜API
url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total"
response = requests.get(url, headers=headers, cookies=cookies, timeout=15)
response.raise_for_status()
data = response.json()
if data.get('data'):
for item in data['data'][:limit]:
target = item.get('target', {})
title = target.get('title', '')
question_id = target.get('id', '')
excerpt = target.get('excerpt', '')
# 只保留技术相关问题(简单过滤)
tech_keywords = ['编程', '代码', '开发', 'AI', '人工智能', '算法',
'Python', 'Java', '前端', '后端', '数据库', '云',
'技术', '软件', '系统', '架构', '工程师', '程序员',
'ChatGPT', '大模型', '机器学习', '深度学习', '区块链']
is_tech = any(kw in title for kw in tech_keywords)
if title and question_id:
articles.append({
'title': title,
'url': f"https://www.zhihu.com/question/{question_id}",
'description': excerpt[:200] if excerpt else '',
'source': '知乎热榜',
'category': 'tech' if is_tech else 'general',
'author': '',
'word_count': 0,
'is_tech': is_tech
})
logger.info(f"知乎热榜获取到 {len(articles)} 个问题")
except Exception as e:
logger.error(f"知乎热榜请求失败: {e}")
return articles
class ChinaTechCrawler:
"""国内技术网站统一抓取器"""
def __init__(self, config_path: str = None):
if config_path is None:
config_path = "/home/ubuntu/nanocode/it_automation_enhanced/config/config.json"
self.config_path = config_path
self.load_config()
# 初始化各平台抓取器
self.juejin = JuejinCrawler()
self.oschina = OSChinaCrawler()
self.zhihu = ZhihuHotCrawler()
def load_config(self):
"""加载配置"""
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
except Exception as e:
logger.error(f"配置加载失败: {e}")
self.config = {}
def init_database(self) -> bool:
"""初始化数据库"""
db_path = self.config.get('paths', {}).get('database_path')
if not db_path:
db_path = "/home/ubuntu/nanocode/it_automation_enhanced/data/articles.db"
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT UNIQUE NOT NULL,
source TEXT NOT NULL,
category TEXT,
publish_date TEXT,
word_count INTEGER DEFAULT 0,
content TEXT,
processed INTEGER DEFAULT 0,
translation_status INTEGER DEFAULT 0,
humanizer_status INTEGER DEFAULT 0,
xhs_status INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_url ON articles(url)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_processed ON articles(processed)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_source ON articles(source)')
conn.commit()
conn.close()
logger.info(f"数据库初始化完成: {db_path}")
return True
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
return False
def save_article(self, article: Dict) -> tuple:
"""保存文章到数据库"""
db_path = self.config.get('paths', {}).get('database_path')
if not db_path:
db_path = "/home/ubuntu/nanocode/it_automation_enhanced/data/articles.db"
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 检查是否已存在
cursor.execute('SELECT id FROM articles WHERE url = ?', (article['url'],))
existing = cursor.fetchone()
if existing:
logger.debug(f"文章已存在,跳过: {article['title'][:50]}...")
conn.close()
return True, False
# 插入新文章
cursor.execute('''
INSERT INTO articles
(title, url, source, category, publish_date, word_count, content)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
article['title'],
article['url'],
article.get('source', 'unknown'),
article.get('category', 'tech'),
datetime.datetime.now().strftime('%Y-%m-%d'),
article.get('word_count', 0),
article.get('description', '')
))
conn.commit()
conn.close()
logger.info(f"新文章保存成功: [{article.get('source')}] {article['title'][:50]}...")
return True, True
except Exception as e:
logger.error(f"文章保存失败: {e}")
return False, False
def fetch_all_sources(self, limit_per_source: int = 5) -> List[Dict]:
"""从所有源抓取文章"""
all_articles = []
# 获取配置中启用的源
websites = self.config.get('websites', [])
for site in websites:
if not site.get('enabled', True):
continue
source_name = site.get('name', '')
articles = []
if source_name == '掘金':
articles = self.juejin.fetch_hot_articles(limit_per_source)
elif source_name == 'OSChina':
articles = self.oschina.fetch_hot_articles(limit_per_source)
elif source_name == '知乎热榜':
articles = self.zhihu.fetch_hot_list(limit_per_source)
# 知乎只保留技术相关问题
articles = [a for a in articles if a.get('is_tech', False)]
all_articles.extend(articles)
# 避免请求过快
time.sleep(1)
return all_articles
def run(self) -> int:
"""运行抓取器"""
logger.info("=" * 60)
logger.info("🇨🇳 开始国内技术网站抓取")
logger.info("=" * 60)
# 初始化数据库
if not self.init_database():
logger.error("数据库初始化失败,退出")
return 0
# 抓取所有源
articles = self.fetch_all_sources(limit_per_source=5)
if not articles:
logger.warning("未获取到任何文章")
return 0
logger.info(f"共获取 {len(articles)} 篇文章,开始保存...")
# 保存文章
saved_count = 0
new_count = 0
for article in articles:
success, is_new = self.save_article(article)
if success:
saved_count += 1
if is_new:
new_count += 1
logger.info("=" * 60)
logger.info(f"📊 抓取完成: 处理 {saved_count} 篇,新增 {new_count} 篇")
return new_count
def main():
"""主函数"""
crawler = ChinaTechCrawler()
try:
saved_count = crawler.run()
if saved_count > 0:
print(f"\n🎉 抓取成功,新增 {saved_count} 篇文章")
return 0
else:
print("\n⚠️ 抓取完成,但未新增文章")
return 1
except KeyboardInterrupt:
print("\n⏹️ 用户中断")
return 130
except Exception as e:
logger.error(f"抓取过程中出现错误: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit(main())