全面架构重构:建立分层架构与高度可扩展的插件系统
后端重构: - 新增分层架构:API Routes -> Services -> Repositories -> Infrastructure - 彻底移除全局单例,全面采用 FastAPI 依赖注入 - 新增 api/ 目录拆分路由(proxies, plugins, scheduler, settings, stats) - 新增 services/ 业务逻辑层:ProxyService, PluginService, SchedulerService, ValidatorService, SettingsService - 新增 repositories/ 数据访问层:ProxyRepository, SettingsRepository, PluginSettingsRepository - 新增 models/ 层:Pydantic Schemas + Domain Models - 重写 core/config.py:采用 Pydantic Settings 管理配置 - 新增 core/db.py:基于 asynccontextmanager 的连接管理,支持数据库迁移 - 新增 core/exceptions.py:统一业务异常体系 插件系统重构(核心): - 新增 core/plugin_system/:BaseCrawlerPlugin + PluginRegistry - 采用显式注册模式(装饰器 + plugins/__init__.py),类型安全、测试友好 - 新增 plugins/base.py:BaseHTTPPlugin 通用 HTTP 爬虫基类 - 迁移全部 7 个插件到新架构(fate0, proxylist_download, ip3366, ip89, kuaidaili, speedx, yundaili) - 插件状态持久化到 plugin_settings 表 任务调度重构: - 新增 core/tasks/queue.py:ValidationQueue + WorkerPool - 解耦爬取与验证:爬虫只负责爬取,代理提交队列后由 Worker 异步验证 - 调度器定时从数据库拉取存量代理并分批投入验证队列 前端调整: - 新增 frontend/src/services/ 层拆分 API 调用逻辑 - 调整 stores/ 和 views/ 使用 Service 层 - 保持 API 兼容性,页面无需大幅修改 其他: - 新增 main.py 作为新入口 - 新增 DESIGN.md 架构设计文档 - 更新 requirements.txt 增加 pydantic-settings
This commit is contained in:
@@ -1,79 +1,49 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.crawler import BasePlugin
|
||||
from core.log import logger
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import asyncio
|
||||
from typing import List
|
||||
from bs4 import BeautifulSoup
|
||||
from core.plugin_system import ProxyRaw
|
||||
from plugins.base import BaseHTTPPlugin
|
||||
from core.log import logger
|
||||
|
||||
VALID_PROTOCOLS = ['http', 'https', 'socks4', 'socks5']
|
||||
VALID_PROTOCOLS = ("http", "https", "socks4", "socks5")
|
||||
|
||||
|
||||
class KuaiDaiLiPlugin(BaseHTTPPlugin):
|
||||
name = "kuaidaili"
|
||||
display_name = "快代理"
|
||||
description = "从快代理网站爬取免费代理"
|
||||
|
||||
class KuaiDaiLiPlugin(BasePlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "快代理"
|
||||
# 抓取国内高匿和国内普通代理的前 10 页
|
||||
self.urls = [
|
||||
f"https://www.kuaidaili.com/free/inha/{i}/" for i in range(1, 11)
|
||||
] + [
|
||||
f"https://www.kuaidaili.com/free/intr/{i}/" for i in range(1, 11)
|
||||
]
|
||||
|
||||
async def parse(self, html):
|
||||
"""
|
||||
解析快代理页面
|
||||
"""
|
||||
if not html:
|
||||
return
|
||||
|
||||
soup = BeautifulSoup(html, 'lxml')
|
||||
# 快代理的表格在 tbody 中
|
||||
table = soup.find('table')
|
||||
if not table:
|
||||
# 尝试通过正则表达式匹配可能被加密或特殊处理的数据
|
||||
logger.warning(f"{self.name} 未能找到表格,可能是触发了反爬或结构变化")
|
||||
return
|
||||
async def crawl(self) -> List[ProxyRaw]:
|
||||
results = []
|
||||
for url in self.urls:
|
||||
html = await self.fetch(url, timeout=15)
|
||||
if not html:
|
||||
continue
|
||||
soup = BeautifulSoup(html, "lxml")
|
||||
table = soup.find("table")
|
||||
if not table:
|
||||
logger.warning(f"{self.display_name} 未能找到表格,可能是触发了反爬")
|
||||
continue
|
||||
|
||||
rows = table.find_all('tr')
|
||||
count = 0
|
||||
for row in rows:
|
||||
tds = row.find_all('td')
|
||||
if len(tds) >= 5:
|
||||
ip = tds[0].get_text(strip=True)
|
||||
port = tds[1].get_text(strip=True)
|
||||
protocol = tds[4].get_text(strip=True).lower() if len(tds) > 4 else 'http'
|
||||
|
||||
if protocol not in VALID_PROTOCOLS:
|
||||
protocol = 'http'
|
||||
|
||||
# 简单校验格式
|
||||
if re.match(r'^\d+\.\d+\.\d+\.\d+$', ip) and port.isdigit():
|
||||
yield ip, int(port), protocol
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
logger.info(f"{self.name} 解析完成,获得 {count} 个潜在代理")
|
||||
for row in table.find_all("tr"):
|
||||
tds = row.find_all("td")
|
||||
if len(tds) >= 5:
|
||||
ip = tds[0].get_text(strip=True)
|
||||
port = tds[1].get_text(strip=True)
|
||||
protocol = tds[4].get_text(strip=True).lower() if len(tds) > 4 else "http"
|
||||
if protocol not in VALID_PROTOCOLS:
|
||||
protocol = "http"
|
||||
if re.match(r"^\d+\.\d+\.\d+\.\d+$", ip) and port.isdigit():
|
||||
results.append(ProxyRaw(ip, int(port), protocol))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def test_plugin():
|
||||
plugin = KuaiDaiLiPlugin()
|
||||
print(f"========== 测试 {plugin.name} ==========")
|
||||
print(f"目标URL数量: {len(plugin.urls)}")
|
||||
print(f"开始抓取...\n")
|
||||
|
||||
proxies = await plugin.run()
|
||||
|
||||
print(f"\n========== 抓取结果 ==========")
|
||||
print(f"总计获取 {len(proxies)} 个代理:")
|
||||
print("-" * 60)
|
||||
|
||||
for idx, (ip, port, protocol) in enumerate(proxies, 1):
|
||||
print(f"{idx:3d}. {ip:15s} : {str(port):5s} | {protocol}")
|
||||
|
||||
print("-" * 60)
|
||||
print(f"完成!共 {len(proxies)} 个代理~")
|
||||
|
||||
asyncio.run(test_plugin())
|
||||
if results:
|
||||
logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理")
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user