first commit

This commit is contained in:
BF
2026-04-14 22:53:52 +08:00
commit b37cba8735
156 changed files with 773977 additions and 0 deletions

4
app/ui/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
# -*- coding: UTF-8 -*-
'''
UI 模块
'''

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1335
app/ui/domain_filter.py Normal file

File diff suppressed because it is too large Load Diff

510
app/ui/domain_import.py Normal file
View File

@@ -0,0 +1,510 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :domain_import.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:47
@explain : 域名导入界面
'''
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFileDialog, QProgressBar
from PySide6.QtCore import Qt, QThread, Signal
from loguru import logger
from app.core.domain_collector import DomainCollector
class ImportThread(QThread):
"""
导入线程
"""
progress_updated = Signal(int)
finished = Signal(bool, str)
def __init__(self, domain_list, source_type):
"""
初始化导入线程
:param domain_list: 域名列表
:param source_type: 来源类型
"""
super().__init__()
self.domain_list = domain_list
self.source_type = source_type
def run(self):
"""
运行导入线程
"""
try:
collector = DomainCollector()
total = len(self.domain_list)
# 实时更新进度:开始
self.progress_updated.emit(0)
# 标准化域名和检查是否存在占30%进度)
normalized_domains = []
for i, domain in enumerate(self.domain_list):
from app.utils.domain_utils import normalize_domain
import tldextract
normalized = normalize_domain(domain)
if normalized:
# 提取顶级域名
ext = tldextract.extract(normalized)
tld = ext.suffix
normalized_domains.append((normalized, tld))
# 更新进度
progress = int((i + 1) / total * 30)
self.progress_updated.emit(progress)
# 批量检查域名是否存在
batch_data = []
existing_domains = []
if normalized_domains:
all_domains = [domain for domain, tld in normalized_domains]
existing_domains = collector.db.check_domains_exist(all_domains)
existing_set = set(existing_domains)
# 准备批量添加数据
for domain, tld in normalized_domains:
if domain not in existing_set:
batch_data.append((domain, tld, self.source_type))
# 分批次添加域名占70%进度)
batch_size = 1000
total_batches = len(batch_data)
for i in range(0, len(batch_data), batch_size):
batch = batch_data[i:i+batch_size]
collector.db.add_domains_batch(batch)
# 更新进度
processed = min(i + len(batch), total_batches)
progress = 30 + int(processed / total_batches * 70)
self.progress_updated.emit(progress)
# 完成导入
self.progress_updated.emit(100)
# 计算统计信息
stats = {
'total': total,
'valid': len(normalized_domains),
'added': len(batch_data),
'exists': len(existing_domains),
'invalid': total - len(normalized_domains),
'failed': 0
}
# 根据统计信息生成消息
if stats['added'] > 0:
message = f"导入完成: 总域名数 {stats['total']}, 有效域名数 {stats['valid']}, 新增域名数 {stats['added']}, 已存在域名数 {stats['exists']}, 无效域名数 {stats['invalid']}"
else:
message = f"导入完成: 所有域名已存在,未添加新域名"
self.finished.emit(True, message)
except Exception as e:
logger.error(f"导入失败: {e}")
self.finished.emit(False, f"导入失败: {str(e)}")
class ImportFileThread(QThread):
"""
文件导入线程,用于处理大文件
"""
progress_updated = Signal(int)
finished = Signal(bool, str)
def __init__(self, file_path, source_type):
"""
初始化文件导入线程
:param file_path: 文件路径
:param source_type: 来源类型
"""
super().__init__()
self.file_path = file_path
self.source_type = source_type
def run(self):
"""
运行文件导入线程
"""
try:
collector = DomainCollector()
# 首先计算文件中的域名数量
total = 0
encodings = ['utf-8', 'utf-8-sig', 'gbk', 'gb2312', 'cp936', 'latin-1', 'ascii']
encoding = 'utf-8' # 默认编码
# 尝试不同的编码格式计算域名数量
for enc in encodings:
try:
with open(self.file_path, 'r', encoding=enc) as f:
total = sum(1 for line in f if line.strip())
encoding = enc
break
except UnicodeDecodeError:
continue
if total == 0:
# 尝试使用二进制模式读取
try:
import chardet
with open(self.file_path, 'rb') as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result['encoding']
if encoding:
total = sum(1 for line in raw_data.decode(encoding).split('\n') if line.strip())
else:
# 最后尝试使用 replace 模式读取
with open(self.file_path, 'r', encoding='utf-8', errors='replace') as f:
total = sum(1 for line in f if line.strip())
encoding = 'utf-8'
except Exception:
# 最后尝试使用 replace 模式读取
with open(self.file_path, 'r', encoding='utf-8', errors='replace') as f:
total = sum(1 for line in f if line.strip())
encoding = 'utf-8'
# 实时更新进度:开始
self.progress_updated.emit(0)
# 逐行读取文件并处理域名
normalized_domains = []
processed = 0
with open(self.file_path, 'r', encoding=encoding, errors='replace') as f:
for line in f:
domain = line.strip()
if domain:
from app.utils.domain_utils import normalize_domain
import tldextract
normalized = normalize_domain(domain)
if normalized:
# 提取顶级域名
ext = tldextract.extract(normalized)
tld = ext.suffix
normalized_domains.append((normalized, tld))
processed += 1
# 更新进度占30%
progress = int(processed / total * 30)
self.progress_updated.emit(progress)
# 批量检查域名是否存在
batch_data = []
existing_domains = []
if normalized_domains:
all_domains = [domain for domain, tld in normalized_domains]
existing_domains = collector.db.check_domains_exist(all_domains)
existing_set = set(existing_domains)
# 准备批量添加数据
for domain, tld in normalized_domains:
if domain not in existing_set:
batch_data.append((domain, tld, self.source_type))
# 分批次添加域名占70%进度)
batch_size = 1000
total_batches = len(batch_data)
for i in range(0, len(batch_data), batch_size):
batch = batch_data[i:i+batch_size]
collector.db.add_domains_batch(batch)
# 更新进度
processed_batches = min(i + len(batch), total_batches)
progress = 30 + int(processed_batches / total_batches * 70)
self.progress_updated.emit(progress)
# 完成导入
self.progress_updated.emit(100)
# 计算统计信息
stats = {
'total': total,
'valid': len(normalized_domains),
'added': len(batch_data),
'exists': len(existing_domains),
'invalid': total - len(normalized_domains),
'failed': 0
}
# 根据统计信息生成消息
if stats['added'] > 0:
message = f"导入完成: 总域名数 {stats['total']}, 有效域名数 {stats['valid']}, 新增域名数 {stats['added']}, 已存在域名数 {stats['exists']}, 无效域名数 {stats['invalid']}"
else:
message = f"导入完成: 所有域名已存在,未添加新域名"
self.finished.emit(True, message)
except Exception as e:
logger.error(f"导入失败: {e}")
self.finished.emit(False, f"导入失败: {str(e)}")
class DomainImportWidget(QWidget):
"""
域名导入界面
"""
def __init__(self):
"""
初始化域名导入界面
"""
super().__init__()
# 创建布局
layout = QVBoxLayout(self)
# 创建文本编辑框
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText("请输入域名,一行一个")
self.text_edit.setStyleSheet("""
QTextEdit {
font-size: 14px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
min-height: 300px;
}
""")
layout.addWidget(self.text_edit)
# 创建按钮布局
button_layout = QHBoxLayout()
# 导入文件按钮
self.import_file_btn = QPushButton("导入文件")
self.import_file_btn.clicked.connect(self.import_file)
self.import_file_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #0b7dda;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
button_layout.addWidget(self.import_file_btn)
# 开始导入按钮
self.start_import_btn = QPushButton("开始导入")
self.start_import_btn.clicked.connect(self.start_import)
self.start_import_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
button_layout.addWidget(self.start_import_btn)
button_layout.setContentsMargins(0, 15, 0, 15)
layout.addLayout(button_layout)
# 创建进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
height: 20px;
border: 1px solid #ddd;
border-radius: 10px;
background-color: #f0f0f0;
margin-bottom: 10px;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 10px;
}
""")
layout.addWidget(self.progress_bar)
# 创建状态标签
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("font-size: 14px; color: #333; padding: 10px; background-color: #f0f8ff; border-radius: 4px;")
layout.addWidget(self.status_label)
logger.info("域名导入界面创建完成")
def import_file(self):
"""
导入文件
"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "文本文件 (*.txt)")
if file_path:
try:
# 尝试不同的编码格式
encodings = ['utf-8', 'utf-8-sig', 'gbk', 'gb2312', 'cp936', 'latin-1', 'ascii']
domain_count = 0
# 尝试使用不同编码读取并计数
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
domain_count = sum(1 for line in f if line.strip())
logger.info(f"使用编码 {encoding} 成功读取文件")
break
except UnicodeDecodeError:
continue
# 如果仍然失败,尝试使用二进制模式读取并猜测编码
if domain_count == 0:
try:
import chardet
with open(file_path, 'rb') as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result['encoding']
if encoding:
domain_count = sum(1 for line in raw_data.decode(encoding).split('\n') if line.strip())
logger.info(f"使用 chardet 检测到编码 {encoding} 并成功读取文件")
else:
raise Exception("无法识别文件编码")
except Exception as e:
logger.warning(f"chardet 检测失败: {e}")
# 最后尝试使用 replace 模式读取
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
domain_count = sum(1 for line in f if line.strip())
logger.info("使用 utf-8 replace 模式读取文件")
# 对于大文件,不显示所有域名,只显示文件路径和域名数量
if domain_count > 1000:
self.text_edit.setText(f"文件路径: {file_path}\n域名数量: {domain_count}\n\n提示: 由于文件较大,仅显示文件信息,不显示具体域名。")
# 保存文件路径,用于后续导入
self.imported_file_path = file_path
else:
# 对于小文件,显示所有域名
domains = []
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
domains = f.readlines()
break
except UnicodeDecodeError:
continue
if not domains:
# 尝试使用二进制模式读取
try:
import chardet
with open(file_path, 'rb') as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result['encoding']
if encoding:
domains = raw_data.decode(encoding).split('\n')
else:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
domains = f.readlines()
except Exception:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
domains = f.readlines()
domains = [domain.strip() for domain in domains if domain.strip()]
self.text_edit.setText('\n'.join(domains))
# 清除文件路径,使用文本框中的域名
self.imported_file_path = None
self.status_label.setText(f"成功读取 {domain_count} 个域名")
logger.info(f"成功读取文件: {file_path}, 共 {domain_count} 个域名")
except Exception as e:
self.status_label.setText(f"读取文件失败: {str(e)}")
logger.error(f"读取文件失败: {e}")
def start_import(self):
"""
开始导入
"""
# 检查是否有导入的文件路径
if hasattr(self, 'imported_file_path') and self.imported_file_path:
# 大文件导入,使用文件路径
file_path = self.imported_file_path
# 显示进度条
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText("正在导入...")
# 禁用按钮
self.import_file_btn.setEnabled(False)
self.start_import_btn.setEnabled(False)
# 创建并启动导入线程
self.import_thread = ImportFileThread(file_path, 7) # 7 表示 TXT 导入
self.import_thread.progress_updated.connect(self.update_progress)
self.import_thread.finished.connect(self.import_finished)
self.import_thread.start()
logger.info(f"开始从文件导入: {file_path}")
else:
# 小文件或手动输入的域名
domains = self.text_edit.toPlainText().split('\n')
domains = [domain.strip() for domain in domains if domain.strip()]
if not domains:
self.status_label.setText("请输入域名")
return
# 显示进度条
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText("正在导入...")
# 禁用按钮
self.import_file_btn.setEnabled(False)
self.start_import_btn.setEnabled(False)
# 创建并启动导入线程
self.import_thread = ImportThread(domains, 7) # 7 表示 TXT 导入
self.import_thread.progress_updated.connect(self.update_progress)
self.import_thread.finished.connect(self.import_finished)
self.import_thread.start()
logger.info(f"开始导入 {len(domains)} 个域名")
def update_progress(self, progress):
"""
更新进度
:param progress: 进度值
"""
self.progress_bar.setValue(progress)
def import_finished(self, success, message):
"""
导入完成
:param success: 是否成功
:param message: 消息
"""
self.status_label.setText(message)
self.progress_bar.setVisible(False)
# 启用按钮
self.import_file_btn.setEnabled(True)
self.start_import_btn.setEnabled(True)
logger.info(f"导入完成: {message}")

577
app/ui/juming_crawler.py Normal file
View File

@@ -0,0 +1,577 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :juming_crawler.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 15:00
@explain : 聚名网爬取页面
'''
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QProgressBar, QLineEdit, QComboBox, QDateEdit, QCheckBox
from PySide6.QtCore import Qt, QThread, Signal, QDate
from PySide6.QtGui import QIntValidator
from loguru import logger
import re
import datetime
import time
from app.core.domain_collector import DomainCollector
from detect.juming import JM
class JumingCrawlThread(QThread):
"""
聚名爬取线程
"""
progress_updated = Signal(int)
status_updated = Signal(str)
finished = Signal(bool, str)
def __init__(self, crawl_type, page_start=1, page_size=50, crawl_date=None, auto_date=True):
"""
初始化聚名爬取线程
:param crawl_type: 爬取类型 (1: 一口价, 2: 删除列表)
:param page_start: 起始页码
:param page_size: 每页数量
:param crawl_date: 爬取日期(删除列表用)
:param auto_date: 是否自动新增日期
"""
super().__init__()
self.crawl_type = crawl_type
self.page_start = page_start
self.page_size = page_size
self.crawl_date = crawl_date
self.auto_date = auto_date
self.is_paused = False
self.is_stopped = False
self.current_page = 0
self.total_count = 0
def run(self):
"""
运行聚名爬取线程
"""
try:
# 初始化聚名客户端
jm = JM()
# 加载 Cookie
jm.load_cookies()
logger.info("已加载 Cookie")
self.status_updated.emit("已加载 Cookie")
# 直接开始爬取,不需要登录,因为 Cookie 已经在系统设置页面加载了
self.progress_updated.emit(10)
if self.crawl_type == 1: # 一口价
self.progress_updated.emit(30)
logger.info("开始获取一口价域名")
self.status_updated.emit("开始获取一口价域名")
# 自动爬取多页
page = self.page_start
while not self.is_stopped:
if self.is_paused:
time.sleep(0.1)
continue
self.current_page = page
self.status_updated.emit(f"正在爬取第 {page}")
logger.info(f"正在爬取第 {page}")
# 获取当前页
success, html = jm.ykj_get_list(page=page, page_size=self.page_size)
if success:
pattern_ym = r"<a class='yda1 ydz' ym='([^']*)'"
results = re.findall(pattern_ym, html)
domains = [domain.strip() for domain in results if domain.strip()]
domain_count = len(domains)
self.total_count += domain_count
self.status_updated.emit(f"{page} 页找到 {domain_count} 个域名,累计 {self.total_count}")
logger.info(f"{page} 页找到 {domain_count} 个域名,累计 {self.total_count}")
# 自动入库
if domains:
collector = DomainCollector()
stats = collector.add_domains_batch(domains, 1) # 1 表示一口价
logger.info(f"自动入库完成: {stats}")
self.status_updated.emit(f"自动入库完成: 成功添加 {stats['added']} 个域名")
# 如果返回的数量小于指定的数量,停止爬取
if domain_count < self.page_size:
logger.info(f"返回数量小于指定数量,停止爬取")
self.status_updated.emit("返回数量小于指定数量,停止爬取")
break
# 增加页码
page += 1
# 模拟网络延迟
time.sleep(1)
else:
logger.error(f"获取一口价域名失败: {html}")
self.status_updated.emit(f"获取一口价域名失败: {html}")
break
elif self.crawl_type == 2: # 删除列表
self.progress_updated.emit(30)
logger.info("开始获取删除域名列表")
self.status_updated.emit("开始获取删除域名列表")
# 使用传入的日期或默认今天
start_date_str = self.crawl_date if self.crawl_date else datetime.date.today().strftime("%Y-%m-%d")
start_date = datetime.datetime.strptime(start_date_str, "%Y-%m-%d").date()
# 计算结束日期:今天 + 4天
end_date = datetime.date.today() + datetime.timedelta(days=4)
if self.auto_date:
# 自动新增日期,从起始日期到今天+4天
current_date = start_date
while current_date <= end_date and not self.is_stopped:
if self.is_paused:
time.sleep(0.1)
continue
crawl_date = current_date.strftime("%Y-%m-%d")
self.status_updated.emit(f"正在爬取 {crawl_date} 的删除域名")
logger.info(f"正在爬取 {crawl_date} 的删除域名")
deleted_domains = jm.new_cha_del(crawl_date)
domains = [domain.strip() for domain in deleted_domains if domain.strip()]
domain_count = len(domains)
self.total_count += domain_count
self.status_updated.emit(f"{crawl_date} 找到 {domain_count} 个删除域名,累计 {self.total_count}")
logger.info(f"{crawl_date} 找到 {domain_count} 个删除域名,累计 {self.total_count}")
# 自动入库
if domains:
collector = DomainCollector()
stats = collector.add_domains_batch(domains, 2) # 2 表示删除列表
logger.info(f"{crawl_date} 自动入库完成: {stats}")
self.status_updated.emit(f"{crawl_date} 自动入库完成: 成功添加 {stats['added']} 个域名")
# 增加日期
current_date = current_date + datetime.timedelta(days=1)
# 模拟网络延迟
time.sleep(1)
else:
# 只爬取指定日期
crawl_date = start_date_str
self.status_updated.emit(f"正在爬取 {crawl_date} 的删除域名")
logger.info(f"正在爬取 {crawl_date} 的删除域名")
deleted_domains = jm.new_cha_del(crawl_date)
domains = [domain.strip() for domain in deleted_domains if domain.strip()]
domain_count = len(domains)
self.total_count = domain_count
self.status_updated.emit(f"找到 {domain_count} 个删除域名")
logger.info(f"找到 {domain_count} 个删除域名")
# 自动入库
if domains:
collector = DomainCollector()
stats = collector.add_domains_batch(domains, 2) # 2 表示删除列表
logger.info(f"自动入库完成: {stats}")
self.status_updated.emit(f"自动入库完成: 成功添加 {stats['added']} 个域名")
self.progress_updated.emit(90)
self.progress_updated.emit(100)
self.finished.emit(True, f"成功获取 {self.total_count} 个域名")
except Exception as e:
logger.error(f"从聚名网爬取失败: {e}")
self.status_updated.emit(f"爬取失败: {str(e)}")
self.finished.emit(False, f"爬取失败: {str(e)}")
def pause(self):
"""
暂停爬取
"""
self.is_paused = True
logger.info("爬取已暂停")
self.status_updated.emit("爬取已暂停")
def resume(self):
"""
恢复爬取
"""
self.is_paused = False
logger.info("爬取已恢复")
self.status_updated.emit("爬取已恢复")
def stop(self):
"""
停止爬取
"""
self.is_stopped = True
logger.info("爬取已停止")
self.status_updated.emit("爬取已停止")
class JumingCrawlerWidget(QWidget):
"""
聚名网爬取页面
"""
def __init__(self):
"""
初始化聚名网爬取页面
"""
super().__init__()
# 创建布局
layout = QVBoxLayout(self)
# 爬取类型选择
type_layout = QHBoxLayout()
type_label = QLabel("爬取类型:")
type_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px;")
self.type_combo = QComboBox()
self.type_combo.addItem("一口价域名", 1)
self.type_combo.addItem("删除列表域名", 2)
self.type_combo.setStyleSheet("font-size: 14px; padding: 5px; border: 1px solid #ddd; border-radius: 4px;")
# 监听类型变化
self.type_combo.currentIndexChanged.connect(self.on_type_changed)
type_layout.addWidget(type_label)
type_layout.addWidget(self.type_combo)
type_layout.setContentsMargins(0, 0, 0, 15)
layout.addLayout(type_layout)
# 页码和每页数量设置
page_layout = QHBoxLayout()
# 起始页码
page_start_label = QLabel("起始页码:")
page_start_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px;")
self.page_start_edit = QLineEdit("1")
# 移除所有限制,允许输入任意正整数
self.page_start_edit.setStyleSheet("font-size: 14px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; width: 120px;")
page_layout.addWidget(page_start_label)
page_layout.addWidget(self.page_start_edit)
# 每页数量
page_size_label = QLabel("每页数量:")
page_size_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px; margin-left: 20px;")
self.page_size_edit = QLineEdit("500")
self.page_size_edit.setValidator(QIntValidator(1, 1000))
self.page_size_edit.setStyleSheet("font-size: 14px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; width: 80px;")
page_layout.addWidget(page_size_label)
page_layout.addWidget(self.page_size_edit)
page_layout.setContentsMargins(0, 0, 0, 20)
layout.addLayout(page_layout)
# 日期设置(删除列表用)
date_container = QWidget()
date_layout = QHBoxLayout(date_container)
# 起始日期
date_label = QLabel("起始日期:")
date_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px;")
self.date_edit = QDateEdit()
self.date_edit.setDate(QDate.currentDate())
self.date_edit.setCalendarPopup(True)
self.date_edit.setStyleSheet("font-size: 14px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; width: 150px;")
# 设置最大日期为今天+4天最小日期为今天的前4天
max_date = QDate.currentDate().addDays(4)
min_date = QDate.currentDate().addDays(-4)
self.date_edit.setMinimumDate(min_date)
self.date_edit.setMaximumDate(max_date)
date_layout.addWidget(date_label)
date_layout.addWidget(self.date_edit)
# 自动新增日期选项
auto_date_checkbox = QCheckBox("自动新增日期")
auto_date_checkbox.setChecked(True)
auto_date_checkbox.setStyleSheet("font-size: 14px; margin-left: 20px;")
self.auto_date_checkbox = auto_date_checkbox
date_layout.addWidget(auto_date_checkbox)
date_layout.setContentsMargins(0, 0, 0, 20)
self.date_container = date_container
layout.addWidget(date_container)
# 默认隐藏日期输入框
self.date_container.setVisible(False)
# 按钮布局
button_layout = QHBoxLayout()
# 创建开始按钮
self.start_btn = QPushButton("开始爬取")
self.start_btn.clicked.connect(self.start_crawl)
self.start_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
button_layout.addWidget(self.start_btn)
# 创建暂停按钮
self.pause_btn = QPushButton("暂停爬取")
self.pause_btn.clicked.connect(self.pause_crawl)
self.pause_btn.setEnabled(False)
self.pause_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #ff9800;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #f57c00;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
button_layout.addWidget(self.pause_btn)
# 创建停止按钮
self.stop_btn = QPushButton("停止爬取")
self.stop_btn.clicked.connect(self.stop_crawl)
self.stop_btn.setEnabled(False)
self.stop_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #d32f2f;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
button_layout.addWidget(self.stop_btn)
button_layout.setContentsMargins(0, 0, 0, 20)
layout.addLayout(button_layout)
# 创建日志显示区域
self.log_edit = QTextEdit()
self.log_edit.setPlaceholderText("爬取日志将显示在这里")
self.log_edit.setReadOnly(True)
self.log_edit.setStyleSheet("""
QTextEdit {
font-size: 13px;
font-family: Consolas, Monaco, monospace;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
""")
layout.addWidget(self.log_edit)
# 创建进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
height: 20px;
border: 1px solid #ddd;
border-radius: 10px;
background-color: #f0f0f0;
margin-top: 10px;
margin-bottom: 10px;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 10px;
}
""")
layout.addWidget(self.progress_bar)
# 创建状态标签
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("font-size: 14px; color: #333; margin-top: 10px; padding: 8px; background-color: #f0f8ff; border-radius: 4px;")
layout.addWidget(self.status_label)
# 存储爬取线程
self.crawl_thread = None
logger.info("聚名网爬取页面创建完成")
def start_crawl(self):
"""
开始爬取
"""
crawl_type = self.type_combo.currentData()
# 获取起始页码和每页数量
try:
page_start = int(self.page_start_edit.text())
page_size = int(self.page_size_edit.text())
if page_start < 1:
self.status_label.setText("起始页码必须大于0")
return
if page_size < 1:
self.status_label.setText("每页数量必须大于0")
return
except ValueError:
self.status_label.setText("请输入有效的页码和每页数量")
return
# 显示进度条
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.status_label.setText("正在爬取域名...")
# 清空日志
self.log_edit.clear()
# 启用/禁用按钮
self.start_btn.setEnabled(False)
self.pause_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
# 获取爬取日期和自动新增日期选项(删除列表用)
crawl_date = None
auto_date = False
if crawl_type == 2: # 删除列表
crawl_date = self.date_edit.date().toString("yyyy-MM-dd")
auto_date = self.auto_date_checkbox.isChecked()
# 创建并启动爬取线程
self.crawl_thread = JumingCrawlThread(crawl_type, page_start, page_size, crawl_date, auto_date)
self.crawl_thread.progress_updated.connect(self.update_progress)
self.crawl_thread.status_updated.connect(self.update_status)
self.crawl_thread.finished.connect(self.crawl_finished)
self.crawl_thread.start()
if crawl_type == 2: # 删除列表
auto_date_str = "" if auto_date else ""
logger.info(f"开始爬取聚名网域名, 类型: {crawl_type}, 爬取日期: {crawl_date}, 自动新增日期: {auto_date_str}")
self.log_edit.append(f"开始爬取聚名网域名, 类型: {crawl_type}, 爬取日期: {crawl_date}, 自动新增日期: {auto_date_str}")
else: # 一口价
logger.info(f"开始爬取聚名网域名, 类型: {crawl_type}, 起始页码: {page_start}, 每页数量: {page_size}")
self.log_edit.append(f"开始爬取聚名网域名, 类型: {crawl_type}, 起始页码: {page_start}, 每页数量: {page_size}")
def pause_crawl(self):
"""
暂停爬取
"""
if self.crawl_thread:
if self.crawl_thread.is_paused:
self.crawl_thread.resume()
self.pause_btn.setText("暂停爬取")
else:
self.crawl_thread.pause()
self.pause_btn.setText("恢复爬取")
def stop_crawl(self):
"""
停止爬取
"""
if self.crawl_thread:
self.crawl_thread.stop()
def update_progress(self, progress):
"""
更新进度
:param progress: 进度值
"""
self.progress_bar.setValue(progress)
def update_status(self, status):
"""
更新状态
:param status: 状态消息
"""
self.status_label.setText(status)
self.log_edit.append(status)
def crawl_finished(self, success, message):
"""
爬取完成
:param success: 是否成功
:param message: 消息
"""
self.status_label.setText(message)
self.log_edit.append(message)
# 启用/禁用按钮
self.start_btn.setEnabled(True)
self.pause_btn.setEnabled(False)
self.pause_btn.setText("暂停爬取")
self.stop_btn.setEnabled(False)
self.progress_bar.setVisible(False)
logger.info(f"聚名网爬取完成: {message}")
def on_type_changed(self, index):
"""
爬取类型变化时的处理
:param index: 选择的索引
"""
crawl_type = self.type_combo.currentData()
if crawl_type == 2: # 删除列表
self.date_container.setVisible(True)
else: # 一口价
self.date_container.setVisible(False)
class ImportThread(QThread):
"""
导入线程
"""
progress_updated = Signal(int)
finished = Signal(bool, str)
def __init__(self, domain_list, source_type):
"""
初始化导入线程
:param domain_list: 域名列表
:param source_type: 来源类型
"""
super().__init__()
self.domain_list = domain_list
self.source_type = source_type
def run(self):
"""
运行导入线程
"""
try:
collector = DomainCollector()
total = len(self.domain_list)
for i, domain in enumerate(self.domain_list):
collector.add_domain(domain, self.source_type)
progress = int((i + 1) / total * 100)
self.progress_updated.emit(progress)
self.finished.emit(True, f"成功导入 {total} 个域名")
except Exception as e:
logger.error(f"导入失败: {e}")
self.finished.emit(False, f"导入失败: {str(e)}")

117
app/ui/main_window.py Normal file
View File

@@ -0,0 +1,117 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :main_window.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:46
@explain : 主窗口
'''
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget, QLabel
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
import os
from loguru import logger
from app.ui.domain_import import DomainImportWidget
from app.ui.domain_filter import DomainFilterWidget
from app.ui.sensitive_words import SensitiveWordsWidget
from app.ui.juming_crawler import JumingCrawlerWidget
from app.ui.system_settings import SystemSettingsWidget
class MainWindow(QMainWindow):
"""
主窗口
"""
def __init__(self):
"""
初始化主窗口
"""
super().__init__()
# 设置窗口标题和大小
self.setWindowTitle("域名工具")
self.setGeometry(100, 100, 2000, 800)
# 设置窗口图标
icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "favicon.ico")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
logger.info(f"设置窗口图标成功: {icon_path}")
else:
logger.warning(f"窗口图标文件不存在: {icon_path}")
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout(central_widget)
# 创建标签页
self.tab_widget = QTabWidget()
self.tab_widget.setStyleSheet("""
QTabWidget {
font-size: 14px;
}
QTabBar::tab {
padding: 10px 20px;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
margin-right: 2px;
}
QTabBar::tab:hover {
background-color: #e0e0e0;
}
QTabBar::tab:selected {
background-color: white;
color: #4CAF50;
font-weight: bold;
border-color: #4CAF50;
}
QTabWidget::pane {
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
padding: 10px;
}
""")
main_layout.addWidget(self.tab_widget)
# 创建标签页内容
self.create_tabs()
# 记录日志
logger.info("主窗口创建完成")
def create_tabs(self):
"""
创建标签页
"""
# 聚名爬取标签页
juming_widget = JumingCrawlerWidget()
self.tab_widget.addTab(juming_widget, "聚名爬取")
# 域名筛选标签页
filter_widget = DomainFilterWidget()
self.tab_widget.addTab(filter_widget, "域名筛选")
# 域名导入标签页
import_widget = DomainImportWidget()
self.tab_widget.addTab(import_widget, "域名导入")
# 敏感词配置标签页
sensitive_widget = SensitiveWordsWidget()
self.tab_widget.addTab(sensitive_widget, "敏感词配置")
# 系统设置标签页
settings_widget = SystemSettingsWidget()
self.tab_widget.addTab(settings_widget, "系统设置")
logger.info("标签页创建完成")

284
app/ui/sensitive_words.py Normal file
View File

@@ -0,0 +1,284 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :sensitive_words.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:49
@explain : 敏感词配置界面
'''
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFileDialog
from PySide6.QtCore import Qt, QThread, Signal
from loguru import logger
from app.utils.database import Database
class SaveWordsThread(QThread):
"""
保存敏感词线程
"""
finished = Signal(bool, str, int)
def __init__(self, words):
super().__init__()
self.words = words
def run(self):
try:
db = Database()
# 先清空现有敏感词
db.execute("DELETE FROM sensitive_words")
# 批量添加敏感词
word_tuples = [(word, 'default', 1) for word in self.words]
if word_tuples:
db.batch_add_sensitive_words(word_tuples)
db.close()
self.finished.emit(True, "成功保存敏感词", len(self.words))
except Exception as e:
self.finished.emit(False, str(e), 0)
class LoadWordsThread(QThread):
"""
加载敏感词线程
"""
finished = Signal(bool, list, str)
def run(self):
try:
db = Database()
sensitive_words = db.get_sensitive_words()
words = [word['word'] for word in sensitive_words]
db.close()
self.finished.emit(True, words, f"成功加载 {len(words)} 个敏感词")
except Exception as e:
self.finished.emit(False, [], str(e))
class SensitiveWordsWidget(QWidget):
"""
敏感词配置界面
"""
def __init__(self):
"""
初始化敏感词配置界面
"""
super().__init__()
# 创建布局
layout = QVBoxLayout(self)
# 创建文本编辑框
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText("请输入敏感词,一行一个")
self.text_edit.setStyleSheet("""
QTextEdit {
font-size: 14px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
min-height: 300px;
}
""")
layout.addWidget(self.text_edit)
# 创建按钮布局
button_layout = QHBoxLayout()
# 导入按钮
self.import_btn = QPushButton("导入")
self.import_btn.clicked.connect(self.import_words)
self.import_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #0b7dda;
}
""")
button_layout.addWidget(self.import_btn)
# 导出按钮
self.export_btn = QPushButton("导出")
self.export_btn.clicked.connect(self.export_words)
self.export_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #ff9800;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #f57c00;
}
""")
button_layout.addWidget(self.export_btn)
# 保存按钮
self.save_btn = QPushButton("保存")
self.save_btn.clicked.connect(self.save_words)
self.save_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
button_layout.addWidget(self.save_btn)
# 加载按钮
self.load_btn = QPushButton("加载")
self.load_btn.clicked.connect(self.load_words)
self.load_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #9c27b0;
color: white;
border: none;
border-radius: 4px;
margin-left: 10px;
}
QPushButton:hover {
background-color: #7b1fa2;
}
""")
button_layout.addWidget(self.load_btn)
button_layout.setContentsMargins(0, 15, 0, 15)
layout.addLayout(button_layout)
# 创建状态标签
self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("font-size: 14px; color: #333; padding: 10px; background-color: #f0f8ff; border-radius: 4px;")
layout.addWidget(self.status_label)
# 初始化线程
self.save_thread = None
self.load_thread = None
# 加载敏感词
self.load_words()
logger.info("敏感词配置界面创建完成")
def import_words(self):
"""
导入敏感词
"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "文本文件 (*.txt)")
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
words = f.readlines()
words = [word.strip() for word in words if word.strip()]
self.text_edit.setText('\n'.join(words))
self.status_label.setText(f"成功导入 {len(words)} 个敏感词")
logger.info(f"成功导入敏感词文件: {file_path}, 共 {len(words)} 个敏感词")
except Exception as e:
self.status_label.setText(f"导入失败: {str(e)}")
logger.error(f"导入敏感词失败: {e}")
def export_words(self):
"""
导出敏感词
"""
words = self.text_edit.toPlainText().split('\n')
words = [word.strip() for word in words if word.strip()]
if not words:
self.status_label.setText("没有敏感词可导出")
return
file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)")
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
for word in words:
f.write(word + '\n')
self.status_label.setText(f"成功导出 {len(words)} 个敏感词")
logger.info(f"成功导出 {len(words)} 个敏感词到 {file_path}")
except Exception as e:
self.status_label.setText(f"导出失败: {str(e)}")
logger.error(f"导出敏感词失败: {e}")
def save_words(self):
"""
保存敏感词
"""
words = self.text_edit.toPlainText().split('\n')
words = [word.strip() for word in words if word.strip()]
# 禁用按钮,防止重复点击
self.save_btn.setEnabled(False)
self.status_label.setText("正在保存敏感词...")
# 创建并启动保存线程
self.save_thread = SaveWordsThread(words)
self.save_thread.finished.connect(self.on_save_finished)
self.save_thread.start()
def on_save_finished(self, success, message, count):
"""
保存完成的回调函数
"""
if success:
self.status_label.setText(f"成功保存 {count} 个敏感词")
logger.info(f"成功保存 {count} 个敏感词到数据库")
else:
self.status_label.setText(f"保存失败: {message}")
logger.error(f"保存敏感词失败: {message}")
# 重新启用按钮
self.save_btn.setEnabled(True)
def load_words(self):
"""
加载敏感词
"""
# 禁用按钮,防止重复点击
self.load_btn.setEnabled(False)
self.status_label.setText("正在加载敏感词...")
# 创建并启动加载线程
self.load_thread = LoadWordsThread()
self.load_thread.finished.connect(self.on_load_finished)
self.load_thread.start()
def on_load_finished(self, success, words, message):
"""
加载完成的回调函数
"""
if success:
self.text_edit.setText('\n'.join(words))
self.status_label.setText(message)
logger.info(message)
else:
self.status_label.setText(f"加载失败: {message}")
logger.error(f"加载敏感词失败: {message}")
# 重新启用按钮
self.load_btn.setEnabled(True)

1072
app/ui/system_settings.py Normal file

File diff suppressed because it is too large Load Diff