Files

416 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""
实时动态页面监控器
监控不断更新的页面(如实时行情、公告列表等)
"""
import json
import time
import hashlib
from pathlib import Path
from typing import Dict, Any, List, Optional, Callable
from datetime import datetime, timedelta
from dataclasses import dataclass, field
try:
from scrapling.fetchers import StealthyFetcher, DynamicFetcher
from scrapling.parser import Selector
SCRAPLING_AVAILABLE = True
except ImportError:
SCRAPLING_AVAILABLE = False
try:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = False
@dataclass
class PageSnapshot:
"""页面快照"""
url: str
timestamp: str
content_hash: str # 内容哈希,用于检测变化
content: str # 原始内容
items: List[Dict] = field(default_factory=list) # 提取的条目
metadata: Dict = field(default_factory=dict)
@dataclass
class ChangeEvent:
"""变化事件"""
event_type: str # new, updated, removed
item: Dict
timestamp: str
previous_state: Optional[Dict] = None
class RealtimePageMonitor:
"""实时页面监控器"""
def __init__(self, cache_dir: str = None):
self.cache_dir = Path(cache_dir) if cache_dir else Path(__file__).parent.parent / "data" / "page_cache"
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.fetcher = None
if SCRAPLING_AVAILABLE:
self.fetcher = StealthyFetcher()
self.session = None
if REQUESTS_AVAILABLE:
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
self.last_snapshot: Optional[PageSnapshot] = None
self.snapshots: List[PageSnapshot] = []
def fetch_page(self, url: str, use_dynamic: bool = False) -> Optional[PageSnapshot]:
"""
获取页面快照
Args:
url: 页面URL
use_dynamic: 是否使用动态渲染
Returns:
PageSnapshot对象
"""
try:
if use_dynamic and SCRAPLING_AVAILABLE:
page = DynamicFetcher.fetch(url, headless=True, network_idle=True)
if hasattr(page, 'html_content'):
page.html = page.html_content
elif hasattr(page, 'body') and isinstance(page.body, str):
page.html = page.body
content = page.html
elif self.fetcher:
page = self.fetcher.fetch(url, headless=True)
if hasattr(page, 'html_content'):
page.html = page.html_content
elif hasattr(page, 'body') and isinstance(page.body, str):
page.html = page.body
content = page.html
elif self.session:
resp = self.session.get(url, timeout=30)
content = resp.text
else:
return None
# 计算内容哈希
content_hash = hashlib.md5(content[:10000].encode()).hexdigest()
snapshot = PageSnapshot(
url=url,
timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
content_hash=content_hash,
content=content[:50000] # 限制长度
)
self.last_snapshot = snapshot
return snapshot
except Exception as e:
print(f"[错误] 获取页面失败: {e}")
return None
def extract_items(self, snapshot: PageSnapshot,
selectors: List[str] = None,
item_pattern: str = None) -> List[Dict]:
"""
从快照中提取条目
Args:
snapshot: 页面快照
selectors: CSS选择器列表
item_pattern: 条目匹配正则
Returns:
提取的条目列表
"""
items = []
if not snapshot.content:
return items
# 使用选择器
if selectors and self.fetcher:
try:
page = Selector(snapshot.content)
for sel in selectors:
elements = page.css(sel)
for el in elements:
text = el.text().strip()
href = el.get_attribute("href") or ""
if text:
items.append({
"text": text,
"href": href,
"timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
if items:
break
except Exception as e:
print(f"[错误] 选择器提取失败: {e}")
# 使用正则模式
elif item_pattern:
try:
import re
matches = re.findall(item_pattern, snapshot.content)
for match in matches:
if isinstance(match, tuple):
items.append({"match": match, "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
else:
items.append({"text": match, "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
except Exception as e:
print(f"[错误] 正则提取失败: {e}")
snapshot.items = items
return items
def detect_changes(self, new_snapshot: PageSnapshot) -> List[ChangeEvent]:
"""
检测页面变化
Args:
new_snapshot: 新快照
Returns:
变化事件列表
"""
events = []
if not self.last_snapshot:
self.last_snapshot = new_snapshot
return events
# 内容哈希变化
if new_snapshot.content_hash != self.last_snapshot.content_hash:
print(f"[监控] 检测到页面变化: {new_snapshot.url}")
# 提取新条目
new_items = new_snapshot.items or self.extract_items(new_snapshot)
old_items = self.last_snapshot.items or []
# 对比
old_texts = {item.get('text', '') for item in old_items}
old_hrefs = {item.get('href', '') for item in old_items}
for item in new_items:
text = item.get('text', '')
href = item.get('href', '')
if text and text not in old_texts:
events.append(ChangeEvent(
event_type="new",
item=item,
timestamp=new_snapshot.timestamp
))
elif href and href not in old_hrefs and href.startswith('http'):
events.append(ChangeEvent(
event_type="new",
item=item,
timestamp=new_snapshot.timestamp
))
self.last_snapshot = new_snapshot
return events
def monitor_loop(self, url: str,
interval: int = 60,
duration: int = 3600,
on_change: Callable[[ChangeEvent], None] = None,
selectors: List[str] = None) -> List[ChangeEvent]:
"""
监控循环
Args:
url: 监控URL
interval: 检查间隔(秒)
duration: 监控时长(秒)
on_change: 变化回调函数
selectors: 条目选择器
Returns:
所有变化事件
"""
all_events = []
start_time = time.time()
print(f"[监控] 开始监控: {url}")
print(f"[监控] 间隔: {interval}秒, 时长: {duration}秒")
while time.time() - start_time < duration:
snapshot = self.fetch_page(url)
if snapshot:
if selectors:
self.extract_items(snapshot, selectors)
changes = self.detect_changes(snapshot)
for change in changes:
all_events.append(change)
if on_change:
on_change(change)
print(f"[变化] {change.event_type}: {change.item.get('text', '')[:50]}")
time.sleep(interval)
print(f"[监控] 结束,共检测到{len(all_events)}个变化")
return all_events
def save_snapshot(self, snapshot: PageSnapshot):
"""保存快照到缓存"""
cache_file = self.cache_dir / f"{hashlib.md5(snapshot.url.encode()).hexdigest()}_{snapshot.timestamp.replace(':', '-')}.json"
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump({
'url': snapshot.url,
'timestamp': snapshot.timestamp,
'content_hash': snapshot.content_hash,
'items': snapshot.items,
'metadata': snapshot.metadata
}, f, ensure_ascii=False, indent=2)
def load_latest_snapshot(self, url: str) -> Optional[PageSnapshot]:
"""加载最新的缓存快照"""
url_hash = hashlib.md5(url.encode()).hexdigest()
cache_files = list(self.cache_dir.glob(f"{url_hash}_*.json"))
if not cache_files:
return None
latest = max(cache_files, key=lambda p: p.stat().st_mtime)
with open(latest, 'r', encoding='utf-8') as f:
data = json.load(f)
return PageSnapshot(
url=data['url'],
timestamp=data['timestamp'],
content_hash=data['content_hash'],
content='',
items=data.get('items', []),
metadata=data.get('metadata', {})
)
class AnnouncementMonitor(RealtimePageMonitor):
"""公告监控器 - 专门用于监控新公告"""
def __init__(self):
super().__init__()
self.announcement_selectors = [
'.news-list li a',
'.notice-list li a',
'[class*="notice"] li a',
'.announcement tr a'
]
def check_new_announcements(self, fund_code: str) -> List[ChangeEvent]:
"""
检查新公告
Args:
fund_code: 基金代码
Returns:
新公告列表
"""
url = f"https://fund.eastmoney.com/Notice/{fund_code}.html"
snapshot = self.fetch_page(url)
if not snapshot:
return []
self.extract_items(snapshot, self.announcement_selectors)
changes = self.detect_changes(snapshot)
# 只返回新的公告
return [c for c in changes if c.event_type == 'new']
def monitor_fund_announcements(self, fund_code: str,
interval: int = 300,
duration: int = 7200) -> List[ChangeEvent]:
"""
监控基金公告
Args:
fund_code: 基金代码
interval: 检查间隔(秒)
duration: 监控时长(秒)
Returns:
新公告列表
"""
url = f"https://fund.eastmoney.com/Notice/{fund_code}.html"
def on_change(event: ChangeEvent):
print(f"[新公告] {event.item.get('text', '')[:60]}")
return self.monitor_loop(url, interval, duration, on_change,
self.announcement_selectors)
class MarketNewsMonitor(RealtimePageMonitor):
"""市场新闻监控器"""
def __init__(self):
super().__init__()
self.news_selectors = [
'.news-list li a',
'[class*="news"] li a',
'.headline-list li a'
]
def check_market_news(self) -> List[ChangeEvent]:
"""检查市场新闻"""
url = "https://fund.eastmoney.com/news.html"
snapshot = self.fetch_page(url)
if not snapshot:
return []
self.extract_items(snapshot, self.news_selectors)
return self.detect_changes(snapshot)
# CLI入口
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(" python realtime_monitor.py check <URL> # 检查页面")
print(" python realtime_monitor.py monitor <URL> # 持续监控")
print(" python realtime_monitor.py announcements <代码> # 监控公告")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "check":
url = sys.argv[2] if len(sys.argv) > 2 else "https://fund.eastmoney.com/000001.html"
monitor = RealtimePageMonitor()
snapshot = monitor.fetch_page(url)
if snapshot:
print(f"获取成功: {snapshot.url}")
print(f"时间戳: {snapshot.timestamp}")
print(f"内容哈希: {snapshot.content_hash}")
elif cmd == "monitor":
url = sys.argv[2] if len(sys.argv) > 2 else "https://fund.eastmoney.com/news.html"
monitor = RealtimePageMonitor()
events = monitor.monitor_loop(url, interval=60, duration=300)
print(f"\n共检测到{len(events)}个变化")
elif cmd == "announcements":
fund_code = sys.argv[2] if len(sys.argv) > 2 else "000001"
monitor = AnnouncementMonitor()
events = monitor.monitor_fund_announcements(fund_code, interval=60, duration=300)
print(f"\n共检测到{len(events)}个新公告")
else:
print(f"未知命令: {cmd}")