Initial AlphaPulse implementation: news crawler and Binance klines
- Backend (FastAPI, SQLAlchemy 2, Alembic): single-admin cookie auth, news/crawler/market APIs with SSE and WebSocket streaming - Crawler (Scrapy + Scrapy-Redis): wscn_news, wscn_live, cls_depth, cls_telegraph spiders, idempotent upserts with revisions, raw response archiving, per-source subprocess supervision, restart and backoff - Market worker: full-history Binance spot/USD-M kline backfill, realtime WS stream, hourly and on-reconnect catch-up - Frontend (Vue 3, Element Plus, UnoCSS, KLineChart): news feed, crawler console with live logs, Binance-style chart page - Deploy: docker compose, nginx site, migrate and daily backup scripts
This commit is contained in:
commit
aabc81ba5b
88 files changed
+12603
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
import gzip
|
||||
import hashlib
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.http import Request, Response
|
||||
from sqlalchemy import insert
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
from alphapulse.core.db import SessionLocal
|
||||
from alphapulse.core.redis import KEY_PREFIX, redis
|
||||
from alphapulse.models import RawBody, RawResponse
|
||||
|
||||
|
||||
class RedisRateLimitMiddleware:
|
||||
"""按域名的跨进程限速:同一域名两次请求至少间隔 interval_ms"""
|
||||
|
||||
interval_ms = 1000
|
||||
|
||||
async def process_request(self, request: Request) -> None:
|
||||
key = f"{KEY_PREFIX}rl:{urlparse(request.url).hostname}"
|
||||
while not await redis.set(key, "1", px=self.interval_ms, nx=True):
|
||||
ttl = await redis.pttl(key)
|
||||
await asyncio.sleep(max(ttl, 50) / 1000)
|
||||
|
||||
|
||||
class RawResponseMiddleware:
|
||||
"""每次响应都写 raw_responses;响应体按 sha256 去重存 raw_bodies"""
|
||||
|
||||
def __init__(self, crawler: Crawler) -> None:
|
||||
self.crawler = crawler
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler: Crawler) -> "RawResponseMiddleware":
|
||||
return cls(crawler)
|
||||
|
||||
async def process_response(self, request: Request, response: Response) -> Response:
|
||||
sha256 = hashlib.sha256(response.body).hexdigest()
|
||||
headers = {k.decode(): b", ".join(v).decode("latin-1") for k, v in response.headers.items()}
|
||||
async with SessionLocal() as session:
|
||||
await session.execute(
|
||||
pg_insert(RawBody).values(sha256=sha256, body=gzip.compress(response.body)).on_conflict_do_nothing()
|
||||
)
|
||||
raw_id = await session.scalar(
|
||||
insert(RawResponse).values(
|
||||
run_id=request.meta.get("run_id"),
|
||||
source_code=self.crawler.spider.name,
|
||||
url=response.url,
|
||||
method=request.method,
|
||||
status=response.status,
|
||||
headers=headers,
|
||||
body_sha256=sha256,
|
||||
).returning(RawResponse.id)
|
||||
)
|
||||
await session.commit()
|
||||
request.meta["raw_response_id"] = raw_id
|
||||
return response
|
||||
Reference in new issue
Block a user