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

12
.env Normal file
View File

@@ -0,0 +1,12 @@
# 数据库配置
DB_HOST=120.78.93.250
DB_PORT=5432
DB_DATABASE=domainscan
DB_USER=postgres
DB_PASSWORD=y5BCDhPfHSZSBRr8
# Redis配置
REDIS_HOST=120.78.93.250
REDIS_PORT=6379
REDIS_PASSWORD=Abc123456
REDIS_DB=0

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=domain_scan_db
DB_USER=postgres
DB_PASSWORD=postgres
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# 忽略整个目录
/node_modules/
/build/
/dist/
# 忽略所有压缩文件
*.spec

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/MarsCodeWorkspaceAppSettings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="chatAppRouterInfo" value="builder/69d513b791dad3be35b89ef1" />
<option name="progress" value="1.0" />
</component>
</project>

10
.idea/domainScanDemo.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (domainScanDemo)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,22 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N803" />
<option value="N801" />
<option value="N802" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnreachableCodeInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="object.*" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (yiqianbao)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (domainScanDemo)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/domainScanDemo.iml" filepath="$PROJECT_DIR$/.idea/domainScanDemo.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

221
DEPLOYMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,221 @@
# 域名工具项目部署与使用文档
## 1. 项目概述
域名工具是一个用于域名筛选、检测和管理的综合性工具,主要功能包括:
- 域名批量导入
- 域名筛选与查询
- 域名状态检测(注册状态、使用状态、检测状态等)
- 域名详细信息检测(备案历史、快照年份、百度收录等)
- 域名数据导出Excel、CSV
- 批量更新域名状态
## 2. 环境要求
### 2.1 系统要求
- Windows 10/11 64位操作系统
- 至少4GB内存
- 至少500MB磁盘空间
### 2.2 软件依赖
- Python 3.9+
- PostgreSQL 12+
- Redis用于缓存+布隆过滤器(用于域名去重)
## 3. 安装步骤
### 3.1 克隆项目
```bash
git clone <项目仓库地址>
cd domainScanDemo
```
### 3.2 创建虚拟环境
```bash
python -m venv .venv
```
### 3.3 激活虚拟环境
```bash
# Windows
.venv\Scripts\activate
# Linux/Mac
source .venv/bin/activate
```
### 3.4 安装依赖
```bash
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
```
### 3.5 安装额外依赖用于Excel导出
```bash
pip install openpyxl
```
## 4. 配置说明
### 4.1 环境变量配置
复制 `.env.example` 文件为 `.env`,并根据实际情况修改配置:
```env
# 数据库连接信息
DATABASE_URL=postgresql://username:password@localhost:5432/domain_db
# Redis连接信息可选
REDIS_URL=redis://localhost:6379/0
# 日志配置
LOG_LEVEL=INFO
LOG_FILE=logs/app.log
# 线程配置
MAX_WORKERS=10
# 检测配置
DETECT_TIMEOUT=30
```
### 4.2 其他配置文件
- `detect_options.json` - 检测选项配置
- `proxy_config.json` - 代理配置
- `domain_suffixes.json` - 域名后缀配置
- `thread_count.json` - 线程数量配置
## 5. 数据库初始化
### 5.1 创建数据库
在PostgreSQL中创建数据库
```sql
CREATE DATABASE domain_db;
```
### 5.2 初始化表结构
运行数据库初始化脚本:
```bash
python init_database.py
```
此脚本会创建以下表:
- `domains` - 域名基本信息表
- `domain_detections` - 域名检测详情表
- `sensitive_words` - 敏感词表
### 5.3 添加敏感词(可选)
```bash
python add_sensitive_words_table.py
```
## 6. 运行项目
### 6.1 启动主应用
```bash
python app/main.py
```
### 6.2 启动检测端(可选,用于分布式检测)
```bash
python detect_worker.py
```
## 7. 打包指南
### 7.1 打包主应用
```bash
pyinstaller --onefile --windowed --add-data="app/sdk_leg.js;detect" --add-data="app/sdk_leg_env.js;detect" --add-data="app/chinaz.js;detect" --add-data="detect/module/crack_geetest2x.js;detect/module" --add-data="favicon.ico;." --add-data="new_logo.svg;." --add-data=".env;." --icon="favicon.ico" --name="域名工具" app/main.py
```
### 7.2 打包检测端
```bash
pyinstaller --onefile --windowed --add-data="detect/sdk_leg.js;detect" --add-data="detect/sdk_leg_env.js;detect" --add-data="detect/chinaz.js;detect" --add-data="detect/module/crack_geetest2x.js;detect/module" --add-data="favicon2.ico;." --add-data="new_logo.svg;." --add-data=".env;." --icon="favicon2.ico" --name="域名检测端" detect_worker.py
```
### 7.3 打包结果
打包完成后,可执行文件会生成在 `dist` 目录中:
- `域名工具.exe` - 主应用
- `域名检测端.exe` - 检测端
## 8. 使用说明
### 8.1 域名导入
1. 点击"域名导入"标签页
2. 选择导入方式(文件导入或聚名网爬取)
3. 选择文件或输入聚名网URL
4. 点击"开始导入"按钮
5. 等待导入完成
### 8.2 域名筛选
1. 点击"域名筛选"标签页
2. 设置筛选条件(注册状态、使用状态、检测状态等)
3. 点击"查询"按钮
4. 查看查询结果
5. 可选择域名进行批量更新或导出
### 8.3 域名检测
1. 确保检测端已启动
2. 在主应用中选择需要检测的域名
3. 点击"开始检测"按钮
4. 等待检测完成
5. 查看检测结果
### 8.4 数据导出
1. 在域名筛选界面选择需要导出的域名
2. 点击"导出"按钮
3. 选择导出格式Excel或CSV
4. 选择保存路径
5. 等待导出完成
## 9. 常见问题
### 9.1 数据库连接失败
- 检查 `.env` 文件中的数据库连接信息是否正确
- 确保 PostgreSQL 服务已启动
- 确保数据库用户有足够的权限
### 9.2 Redis 连接失败
- 检查 `.env` 文件中的 Redis 连接信息是否正确
- 确保 Redis 服务已启动
- 如果不需要 Redis 缓存,可以忽略此警告
### 9.3 检测失败
- 检查网络连接是否正常
- 检查代理配置是否正确
- 检查检测端是否已启动
### 9.4 导出失败
- 确保已安装 `openpyxl`用于Excel导出
- 确保导出路径有写入权限
- 检查导出文件格式是否正确
## 10. 技术支持
如果遇到问题,请查看日志文件 `logs/app.log` 获取详细错误信息,或联系技术支持。
## 11. 版本信息
- 项目版本1.0.0
- 最后更新2026-04-14
---
**注意**:本文档仅供参考,具体配置和使用方法可能因环境不同而有所差异。

192
README_acw_sc__v2.md Normal file
View File

@@ -0,0 +1,192 @@
# acw_sc__v2 生成器
从混淆的HTML代码中提取的 `acw_sc__v2` cookie 生成函数。
## 文件说明
### JavaScript版本
- **acw_sc__v2_generator.js** - JavaScript版本的生成函数可直接在浏览器控制台中使用
### Python版本
- **acw_sc__v2_fixed.py** - 简化版Python生成器适合快速使用
- **acw_sc__v2_complete.py** - 完整版Python生成器包含更多功能和错误处理
- **acw_sc__v2_simple.py** - 最简化版本,直接使用硬编码密钥
## 使用方法
### JavaScript版本
在浏览器控制台中执行:
```javascript
// 加载acw_sc__v2_generator.js文件后
var arg1 = '1EEB3321F0286855D50CD9E96C1CDF3D4A913DB0';
var acwScV2 = generateAcwScV2(arg1);
console.log('acw_sc__v2的值:', acwScV2);
// 设置cookie可选
setAcwScV2Cookie(acwScV2);
```
### Python版本
#### 简化版acw_sc__v2_fixed.py
```python
from acw_sc__v2_fixed import generate_acw_sc_v2
arg1 = '1EEB3321F0286855D50CD9E96C1CDF3D4A913DB0'
acw_sc_v2 = generate_acw_sc_v2(arg1)
print(f'acw_sc__v2的值: {acw_sc_v2}')
```
#### 完整版acw_sc__v2_complete.py
```python
from acw_sc__v2_complete import AcwScV2Generator, generate_acw_sc_v2
# 方法1使用便捷函数
arg1 = '1EEB3321F0286855D50CD9E96C1CDF3D4A913DB0'
result = generate_acw_sc_v2(arg1)
print(f'acw_sc__v2的值: {result}')
# 方法2使用生成器类
generator = AcwScV2Generator()
result = generator.generate(arg1)
print(f'acw_sc__v2的值: {result}')
# 方法3批量生成
test_args = [
'1EEB3321F0286855D50CD9E96C1CDF3D4A913DB0',
'A13D8B4174A10C57667C1CD92E54D01F449CA76E',
'F0286855D50CD9E96C1CDF3D4A913DB01EEB3321'
]
batch_results = generator.generate_batch(test_args)
for arg, res in batch_results.items():
print(f'{arg} -> {res}')
# 方法4使用自定义密钥
custom_key = 'YXBwbHk=' # Base64编码的密钥
generator.set_key(custom_key)
result = generator.generate(arg1)
print(f'使用自定义密钥的acw_sc__v2值: {result}')
```
## 核心算法
### 1. 字符重排
根据索引映射数组 `m` 重新排列输入字符串 `arg1`
```python
m = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24]
```
### 2. 密钥解码
将Base64编码的密钥解码为十六进制字符串
```python
key_encoded = 'Aw5PDg9JAw4='
key_hex = base64.b64decode(key_encoded).hex()
# 结果: '030e4f0e0f49030e'
```
### 3. 异或运算
将重排后的字符串与解码后的密钥进行按位异或:
```python
for x in range(0, min(len(u), len(p)), 2):
u_sub = u[x:x+2]
p_sub = p[x:x+2]
a = format(int(u_sub, 16) ^ int(p_sub, 16), '02x')
v += a
```
## 输入输出示例
### 输入
```python
arg1 = '1EEB3321F0286855D50CD9E96C1CDF3D4A913DB0'
```
### 输出
```python
acw_sc__v2 = '5ad70a13004a0290'
```
## 注意事项
1. **输入长度**`arg1` 必须是40位的十六进制字符串
2. **输入格式**`arg1` 必须是有效的十六进制字符串
3. **密钥**:默认使用内置密钥,也可以自定义密钥
4. **输出格式**:输出为十六进制字符串
## 错误处理
完整版生成器包含以下错误处理:
- 输入长度验证
- 输入格式验证
- Base64解码错误处理
- 异常捕获和错误信息输出
## 应用场景
- 网站爬虫绕过阿里云CDN的反爬虫保护
- 安全测试:测试网站的反爬虫机制
- 学习研究了解混淆JavaScript代码的逆向工程
## 技术细节
### 混淆技术
原始HTML代码使用了多种混淆技术
1. **字符串编码**Base64编码 + URL编码
2. **变量名混淆**:使用无意义的变量名
3. **控制流混淆**:使用闭包和条件函数
4. **反调试**:通过数学计算和正则表达式检测调试器
### 逆向工程
通过分析原始代码,提取出核心算法:
1. 移除反调试逻辑
2. 简化字符串解码
3. 提取核心计算逻辑
4. 重构为可读代码
## 性能优化
- 使用缓存机制避免重复解码
- 批量生成功能提高效率
- 简化算法减少计算量
## 安全提示
本工具仅供学习和研究使用,请勿用于非法用途。使用本工具绕过网站的反爬虫保护可能违反网站的使用条款。
## 许可证
本代码仅供学习和研究使用。
## 联系方式
如有问题或建议,请通过以下方式联系:
- 提交Issue
- 发送Pull Request
- 联系作者
## 更新日志
### v1.0.0 (2024-01-01)
- 初始版本发布
- 支持JavaScript和Python版本
- 包含完整的使用示例
## 致谢
感谢所有为逆向工程和反爬虫技术研究做出贡献的开发者。

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :add_backlink_count_column.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/10 03:50
@explain : 为domains表添加backlink_count字段
'''
import psycopg2
from app.config import config
def add_backlink_count_column():
"""
为domains表添加backlink_count字段
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
print(f"成功连接到数据库: {config.DB_HOST}:{config.DB_PORT}/{config.DB_DATABASE}")
# 创建游标
cur = conn.cursor()
# 添加backlink_count字段
add_column_sql = """
ALTER TABLE domains ADD COLUMN IF NOT EXISTS backlink_count INTEGER DEFAULT 0
"""
cur.execute(add_column_sql)
print("添加backlink_count字段成功")
# 添加字段注释
add_comment_sql = """
COMMENT ON COLUMN domains.backlink_count IS '友情链接数量'
"""
cur.execute(add_comment_sql)
print("添加字段注释成功")
# 创建索引
create_index_sql = """
CREATE INDEX IF NOT EXISTS idx_domains_backlink_count ON domains(backlink_count)
"""
cur.execute(create_index_sql)
print("创建索引成功")
# 提交事务
conn.commit()
print("事务提交成功")
# 关闭游标和连接
cur.close()
conn.close()
print("连接关闭成功")
return True
except Exception as e:
print(f"添加backlink_count字段失败: {e}")
return False
if __name__ == "__main__":
print("开始为domains表添加backlink_count字段...")
success = add_backlink_count_column()
if success:
print("backlink_count字段添加成功")
else:
print("backlink_count字段添加失败")

View File

@@ -0,0 +1,8 @@
-- 为domains表添加backlink_count字段
ALTER TABLE domains ADD COLUMN IF NOT EXISTS backlink_count INTEGER DEFAULT 0;
-- 添加字段注释
COMMENT ON COLUMN domains.backlink_count IS '友情链接数量';
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_domains_backlink_count ON domains(backlink_count);

51
add_expire_date_field.py Normal file
View File

@@ -0,0 +1,51 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :add_expire_date_field.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/11 22:40
@explain : 为domains表添加expire_date字段
'''
import psycopg2
from loguru import logger
from app.config import config
def add_expire_date_field():
"""
为domains表添加expire_date字段
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
cur = conn.cursor()
logger.info("数据库连接成功")
# 添加expire_date字段
try:
cur.execute("ALTER TABLE domains ADD COLUMN IF NOT EXISTS expire_date DATE")
cur.execute("COMMENT ON COLUMN domains.expire_date IS '过期时间'")
logger.info("添加expire_date字段成功")
except Exception as e:
logger.error(f"添加expire_date字段失败: {e}")
# 提交事务
conn.commit()
logger.info("字段添加完成")
# 关闭连接
cur.close()
conn.close()
except Exception as e:
logger.error(f"执行失败: {e}")
if __name__ == "__main__":
add_expire_date_field()

59
add_fields_to_domains.py Normal file
View File

@@ -0,0 +1,59 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :add_fields_to_domains.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/11 22:40
@explain : 为domains表添加缺失的字段
'''
import psycopg2
from loguru import logger
from app.config import config
def add_fields_to_domains():
"""
为domains表添加缺失的字段
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
cur = conn.cursor()
logger.info("数据库连接成功")
# 添加company_type字段
try:
cur.execute("ALTER TABLE domains ADD COLUMN IF NOT EXISTS company_type VARCHAR(100)")
cur.execute("COMMENT ON COLUMN domains.company_type IS '单位性质'")
logger.info("添加company_type字段成功")
except Exception as e:
logger.error(f"添加company_type字段失败: {e}")
# 添加website_url字段
try:
cur.execute("ALTER TABLE domains ADD COLUMN IF NOT EXISTS website_url VARCHAR(255)")
cur.execute("COMMENT ON COLUMN domains.website_url IS '网站首页网址'")
logger.info("添加website_url字段成功")
except Exception as e:
logger.error(f"添加website_url字段失败: {e}")
# 提交事务
conn.commit()
logger.info("字段添加完成")
# 关闭连接
cur.close()
conn.close()
except Exception as e:
logger.error(f"执行失败: {e}")
if __name__ == "__main__":
add_fields_to_domains()

View File

@@ -0,0 +1,58 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :add_sensitive_words_table.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/12 00:03
@explain : 为数据库添加sensitive_words表
'''
import psycopg2
from loguru import logger
from app.config import config
def add_sensitive_words_table():
"""
为数据库添加sensitive_words表
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
cur = conn.cursor()
logger.info("数据库连接成功")
# 创建sensitive_words表
create_sensitive_words_table = """
CREATE TABLE IF NOT EXISTS sensitive_words (
id SERIAL PRIMARY KEY,
word VARCHAR(255) UNIQUE NOT NULL
)
"""
cur.execute(create_sensitive_words_table)
logger.info("创建sensitive_words表成功")
# 添加字段注释
cur.execute("COMMENT ON COLUMN sensitive_words.id IS '主键ID'")
cur.execute("COMMENT ON COLUMN sensitive_words.word IS '敏感词'")
logger.info("添加sensitive_words表字段注释成功")
# 提交事务
conn.commit()
logger.info("sensitive_words表添加完成")
# 关闭连接
cur.close()
conn.close()
except Exception as e:
logger.error(f"执行失败: {e}")
if __name__ == "__main__":
add_sensitive_words_table()

6
app/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding: UTF-8 -*-
'''
域名库系统
'''
__version__ = "1.0.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1257
app/chinaz.js Normal file

File diff suppressed because one or more lines are too long

196
app/config.py Normal file
View File

@@ -0,0 +1,196 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :config.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:08
@explain : 系统配置
'''
import os
import sys
from dotenv import load_dotenv
# 确定基础目录
if hasattr(sys, '_MEIPASS'):
# PyInstaller 打包后的临时目录
BASE_DIR = sys._MEIPASS
else:
# 开发环境目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 加载环境变量
env_path = os.path.join(BASE_DIR, '.env')
if os.path.exists(env_path):
load_dotenv(env_path)
print(f"成功加载环境变量文件: {env_path}")
else:
load_dotenv()
print(f"环境变量文件不存在: {env_path},使用默认环境变量")
class Config:
"""
系统配置
"""
# 数据库配置
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', 5432))
DB_DATABASE = os.getenv('DB_DATABASE', 'domain_scan_db')
DB_USER = os.getenv('DB_USER', 'postgres')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'postgres')
# 消息队列配置
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost')
RABBITMQ_PORT = int(os.getenv('RABBITMQ_PORT', 5672))
RABBITMQ_USER = os.getenv('RABBITMQ_USER', 'guest')
RABBITMQ_PASSWORD = os.getenv('RABBITMQ_PASSWORD', 'guest')
RABBITMQ_VHOST = os.getenv('RABBITMQ_VHOST', '/')
# Redis配置
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', '')
REDIS_DB = int(os.getenv('REDIS_DB', 0))
# 聚名网配置
JUMING_COOKIE = os.getenv('JUMING_COOKIE', '')
JUMING_REFERER = os.getenv('JUMING_REFERER', 'https://www.juming.com/')
JUMING_USER_AGENT = os.getenv('JUMING_USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36')
# 代理配置
PROXY_ENABLED = os.getenv('PROXY_ENABLED', 'false').lower() == 'true'
PROXY_URL = os.getenv('PROXY_URL', '')
# 检测配置
DETECT_TIMEOUT = int(os.getenv('DETECT_TIMEOUT', 30))
DETECT_RETRY_COUNT = int(os.getenv('DETECT_RETRY_COUNT', 3))
DETECT_CONCURRENCY = int(os.getenv('DETECT_CONCURRENCY', 10))
# 域名配置
DOMAIN_TLDS = ['com', 'net']
DOMAIN_BATCH_SIZE = int(os.getenv('DOMAIN_BATCH_SIZE', 1000))
# 日志配置
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
LOG_FILE = os.getenv('LOG_FILE', 'app.log')
# 任务配置
TASK_PRIORITY = {
'rdap': 10,
'wayback': 8,
'baidu': 6,
'qihu360': 5,
'google': 5,
'chinaz': 4,
'aizhan': 4,
'juziseo': 3,
'jucha': 3
}
# 敏感词配置 - 现在从数据库加载
SENSITIVE_WORDS = []
# 检测项配置
DETECT_ITEMS = {
'rdap': True,
'wayback': True,
'baidu': True,
'qihu360': True,
'google': True,
'chinaz': True,
'aizhan': True,
'juziseo': True,
'jucha': True
}
# 目录配置
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, 'data')
LOG_DIR = os.path.join(BASE_DIR, 'logs')
# 确保目录存在
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(LOG_DIR, exist_ok=True)
# 从数据库加载敏感词
def load_sensitive_words():
"""
从数据库加载敏感词
"""
try:
from app.utils.database import Database
db = Database()
sensitive_words = db.get_sensitive_words()
words = [word['word'] for word in sensitive_words]
db.close()
return words
except Exception as e:
print(f"加载敏感词失败: {e}")
return []
# 从文件加载检测选项
def load_detect_options():
"""
从文件加载检测选项
"""
try:
import json
import os
if os.path.exists('detect_options.json'):
with open('detect_options.json', 'r', encoding='utf-8') as f:
detect_options = json.load(f)
return detect_options
return {}
except Exception as e:
print(f"加载检测选项失败: {e}")
return {}
# 导出配置
config = Config()
# 加载检测选项
config.DETECT_OPTIONS = load_detect_options()
# 延迟加载敏感词,避免循环导入
def load_sensitive_words_lazy():
"""
延迟加载敏感词
"""
return load_sensitive_words()
# 设置敏感词属性为延迟加载
config.load_sensitive_words = load_sensitive_words_lazy
# 检查检测类型是否应该执行
def should_detect(detect_type):
"""
检查检测类型是否应该执行
:param detect_type: 检测类型
:return: bool - 是否应该执行
"""
# 检测类型映射
type_mapping = {
'register': 'detect_register',
'chinaz': 'detect_chinaz',
'aizhan': 'detect_aizhan',
'baidu_site': 'detect_baidu_site',
'360_site': 'detect_360_site',
'baidu_security': 'detect_baidu_security',
'whois': 'detect_whois',
'beian': 'detect_beian',
'intercept': 'detect_intercept',
'juziseo': 'detect_juziseo',
'juziseo_outlink': 'detect_juziseo_outlink'
}
# 获取对应的配置键
config_key = type_mapping.get(detect_type)
if not config_key:
return True # 默认执行
# 检查是否在配置中默认为True
return config.DETECT_OPTIONS.get(config_key, True)

View File

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

@@ -0,0 +1,4 @@
# -*- coding: UTF-8 -*-
'''
核心功能模块
'''

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

281
app/core/detect_engine.py Normal file
View File

@@ -0,0 +1,281 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :detect_engine.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:52
@explain : 检测引擎
'''
import time
from loguru import logger
from app.utils.database import Database
from app.detectors.rdap_detector import RDAPDetector
from app.detectors.wayback_detector import WaybackDetector
from app.detectors.baidu_detector import BaiduDetector
from app.detectors.qihu360_detector import Qihu360Detector
from app.detectors.google_detector import GoogleDetector
from app.detectors.chinaz_detector import ChinazDetector
from app.detectors.aizhan_detector import AizhanDetector
from app.detectors.juziseo_detector import JuziseoDetector
from app.detectors.jucha_detector import JuchaDetector
class DetectEngine:
"""
检测引擎
"""
def __init__(self):
"""
初始化检测引擎
"""
self.db = Database()
self.rdap_detector = RDAPDetector()
self.wayback_detector = WaybackDetector()
self.baidu_detector = BaiduDetector()
self.qihu360_detector = Qihu360Detector()
self.google_detector = GoogleDetector()
self.chinaz_detector = ChinazDetector()
self.aizhan_detector = AizhanDetector()
self.juziseo_detector = JuziseoDetector()
self.jucha_detector = JuchaDetector()
def detect_domain(self, domain_id):
"""
检测域名
:param domain_id: 域名ID
:return: bool - 是否检测成功
"""
try:
# 获取域名信息
domain_info = self.db.get_domain_by_id(domain_id)
if not domain_info:
logger.error(f"域名不存在: {domain_id}")
return False
domain = domain_info['domain']
logger.info(f"开始检测域名: {domain}")
# 更新检测状态为检测中
self.db.update_domain_detect_status(domain_id, 2) # 2 表示检测中
# 1. 基础检测
if not self._basic_detect(domain_id, domain):
logger.info(f"基础检测失败,停止后续检测: {domain}")
return False
# 2. 深度检测
if not self._deep_detect(domain_id, domain):
logger.info(f"深度检测失败: {domain}")
return False
# 更新检测状态为正常
self.db.update_domain_detect_status(domain_id, 3) # 3 表示正常
logger.info(f"域名检测完成: {domain}")
return True
except Exception as e:
logger.error(f"检测域名出错: {e}")
# 更新检测状态为检测失败
self.db.update_domain_detect_status(domain_id, 5) # 5 表示检测失败
return False
def _basic_detect(self, domain_id, domain):
"""
基础检测
:param domain_id: 域名ID
:param domain: 域名
:return: bool - 是否检测通过
"""
# 1. 检查是否为一口价域名
is_ykj = self.db.is_ykj_domain(domain_id)
# 2. 注册状态检测(一口价域名跳过)
if not is_ykj:
register_status = self.rdap_detector.check_register_status(domain)
self.db.update_domain_register_status(domain_id, register_status)
# 3. 黑名单缓存检查
if self.db.is_blacklisted(domain):
logger.info(f"域名在黑名单中: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
return False
# 4. 时光机快照年份采集
snapshot_years = self.wayback_detector.get_snapshot_years(domain)
if snapshot_years:
self.db.update_domain_snapshot_years(domain_id, ','.join(map(str, snapshot_years)))
# 5. 时光机正文抽样与敏感词匹配
if self.wayback_detector.has_sensitive_content(domain):
logger.info(f"域名包含敏感词: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "快照包含敏感词")
return False
return True
def _deep_detect(self, domain_id, domain):
"""
深度检测
:param domain_id: 域名ID
:param domain: 域名
:return: bool - 是否检测通过
"""
# 1. 百度历史/Site
baidu_history = self.baidu_detector.check_history(domain)
baidu_site = self.baidu_detector.check_site(domain)
# 2. 360 Site
qihu360_site = self.qihu360_detector.check_site(domain)
# 3. Google Site
google_site = self.google_detector.check_site(domain)
# 4. 站长之家
chinaz_info = self.chinaz_detector.check_domain(domain)
# 5. 爱站网
aizhan_info = self.aizhan_detector.check_domain(domain)
# 6. 桔子SEO
juziseo_info = self.juziseo_detector.check_domain(domain)
# 7. 聚查
jucha_info = self.jucha_detector.check_domain(domain)
# 检查是否有风险
if self._check_risk(domain_id, domain, baidu_history, baidu_site, qihu360_site, google_site, chinaz_info, aizhan_info, juziseo_info, jucha_info):
return False
# 保存检测结果
self.db.add_detection_result(domain_id, baidu_history, baidu_site, qihu360_site, google_site, chinaz_info, aizhan_info, juziseo_info, jucha_info)
return True
def _check_risk(self, domain_id, domain, baidu_history, baidu_site, qihu360_site, google_site, chinaz_info, aizhan_info, juziseo_info, jucha_info):
"""
检查风险
:param domain_id: 域名ID
:param domain: 域名
:param baidu_history: 百度历史
:param baidu_site: 百度site
:param qihu360_site: 360 site
:param google_site: Google site
:param chinaz_info: 站长之家信息
:param aizhan_info: 爱站网信息
:param juziseo_info: 桔子SEO信息
:param jucha_info: 聚查信息
:return: bool - 是否有风险
"""
# 检查百度历史过灰
if baidu_history and '' in str(baidu_history):
logger.info(f"百度历史过灰: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "百度历史过灰")
return True
# 检查标题敏感词
if chinaz_info and 'title' in chinaz_info:
if self._contains_sensitive_words(chinaz_info['title']):
logger.info(f"标题包含敏感词: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "标题包含敏感词")
return True
# 检查子域名
if baidu_site and 'subdomains' in baidu_site:
subdomains = baidu_site['subdomains']
# 排除 www, @, m
valid_subdomains = [sub for sub in subdomains if sub not in ['www', '@', 'm']]
if valid_subdomains:
logger.info(f"存在子域名: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "存在子域名")
return True
# 检查风险提示
if aizhan_info and 'risk' in aizhan_info:
if aizhan_info['risk'] in ['低风险', '疑似色情博彩风险', '严重影响权重']:
logger.info(f"风险提示: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, f"风险提示: {aizhan_info['risk']}")
return True
# 检查WHOIS状态
if jucha_info and 'whois' in jucha_info:
if jucha_info['whois'].get('status') in ['clientHold', 'serverHold']:
logger.info(f"WHOIS状态异常: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "WHOIS状态异常")
return True
# 检查拦截检测
if jucha_info and 'intercept' in jucha_info:
if not jucha_info['intercept'].get('normal', True):
logger.info(f"拦截检测异常: {domain}")
self.db.update_domain_detect_status(domain_id, 4) # 4 表示黑名单
self.db.add_to_blacklist(domain, "拦截检测异常")
return True
return False
def _contains_sensitive_words(self, text):
"""
检查文本是否包含敏感词
:param text: 文本
:return: bool - 是否包含敏感词
"""
# 这里可以添加敏感词检查逻辑
sensitive_words = ['色情', '博彩', '赌博', '毒品', '暴力']
for word in sensitive_words:
if word in text:
return True
return False
def process_task(self, task_id):
"""
处理检测任务
:param task_id: 任务ID
:return: bool - 是否处理成功
"""
try:
# 获取任务信息
task = self.db.get_task_by_id(task_id)
if not task:
logger.error(f"任务不存在: {task_id}")
return False
domain_id = task['domain_id']
# 更新任务状态为执行中
self.db.update_task_status(task_id, 1) # 1 表示执行中
# 执行检测
success = self.detect_domain(domain_id)
# 更新任务状态
if success:
self.db.update_task_status(task_id, 2) # 2 表示完成
else:
# 增加重试次数
retry_count = task.get('retry_count', 0) + 1
if retry_count < 3:
self.db.update_task_retry_count(task_id, retry_count)
self.db.update_task_status(task_id, 0) # 0 表示待执行
else:
self.db.update_task_status(task_id, 3) # 3 表示失败
return success
except Exception as e:
logger.error(f"处理任务出错: {e}")
# 更新任务状态为失败
self.db.update_task_status(task_id, 3) # 3 表示失败
return False

View File

@@ -0,0 +1,261 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :domain_collector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:50
@explain : 域名收集器
'''
import re
import tldextract
from loguru import logger
from app.utils.database import Database
from app.utils.domain_utils import normalize_domain
from app.config import config
class DomainCollector:
"""
域名收集器
"""
def __init__(self):
"""
初始化域名收集器
"""
self.db = Database(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
def add_domain(self, domain, source_type):
"""
添加域名
:param domain: 域名
:param source_type: 来源类型
:return: bool - 是否添加成功
"""
try:
# 标准化域名
normalized_domain = normalize_domain(domain)
if not normalized_domain:
logger.warning(f"无效域名: {domain}")
return False
# 提取顶级域名
ext = tldextract.extract(normalized_domain)
tld = ext.suffix
# 只保留 .com 和 .net
if tld not in ['com', 'net']:
logger.warning(f"不支持的顶级域名: {tld}")
return False
# 检查是否已存在
if self.db.domain_exists(normalized_domain):
logger.info(f"域名已存在: {normalized_domain}")
return False
# 添加域名
domain_id = self.db.add_domain(normalized_domain, tld, source_type)
if domain_id:
# 创建检测任务
self.db.create_detect_task(domain_id, 1) # 1 表示基础检测
logger.info(f"成功添加域名: {normalized_domain}")
return True
else:
logger.error(f"添加域名失败: {normalized_domain}")
return False
except Exception as e:
logger.error(f"添加域名出错: {e}")
return False
def add_domains_batch(self, domains, source_type, batch_size=1000, dry_run=False):
"""
批量添加域名
:param domains: 域名列表
:param source_type: 来源类型
:param batch_size: 批量大小
:param dry_run: 是否仅进行干运行(不实际添加域名)
:return: dict - 统计信息
"""
try:
# 统计信息
stats = {
'total': len(domains),
'valid': 0,
'added': 0,
'exists': 0,
'invalid': 0,
'failed': 0
}
# 处理大规模数据时,分批进行标准化和过滤
normalized_domains = []
batch_domains = []
for i, domain in enumerate(domains):
normalized = normalize_domain(domain)
if not normalized:
stats['invalid'] += 1
continue
# 提取顶级域名
ext = tldextract.extract(normalized)
tld = ext.suffix
# 只保留 .com 和 .net
if tld not in ['com', 'net']:
stats['invalid'] += 1
continue
normalized_domains.append((normalized, tld))
batch_domains.append(normalized)
stats['valid'] += 1
# 每1000个域名检查一次避免内存占用过高
if (i + 1) % 1000 == 0:
logger.info(f"已处理 {i + 1}/{len(domains)} 个域名")
logger.info(f"域名标准化完成,有效域名: {stats['valid']}")
# 提取所有域名
all_domains = [domain for domain, tld in normalized_domains]
# 批量检查域名是否存在
existing_domains = self.db.check_domains_exist(all_domains)
existing_set = set(existing_domains)
# 准备批量添加数据
batch_data = []
for domain, tld in normalized_domains:
if domain not in existing_set:
batch_data.append((domain, tld, source_type))
stats['exists'] = len(existing_domains)
stats['valid'] = len(normalized_domains)
# 干运行模式下直接返回统计信息
if dry_run:
stats['added'] = len(batch_data)
logger.info(f"干运行模式:准备添加 {len(batch_data)} 个新域名")
return stats
logger.info(f"准备添加 {len(batch_data)} 个新域名")
# 分批次添加
for i in range(0, len(batch_data), batch_size):
batch = batch_data[i:i+batch_size]
added_count = self.db.add_domains_batch(batch)
stats['added'] += added_count
stats['failed'] += len(batch) - added_count
# 每处理一批,记录一次进度
if (i + len(batch)) % (batch_size * 10) == 0:
logger.info(f"已添加 {i + len(batch)}/{len(batch_data)} 个域名")
logger.info(f"批量添加域名完成: {stats}")
return stats
except Exception as e:
logger.error(f"批量添加域名出错: {e}")
return {
'total': len(domains),
'valid': 0,
'added': 0,
'exists': 0,
'invalid': 0,
'failed': len(domains)
}
def import_from_file(self, file_path, source_type, batch_size=1000):
"""
从文件导入域名
:param file_path: 文件路径
:param source_type: 来源类型
:param batch_size: 批量大小
:return: dict - 导入统计信息
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
domains = f.readlines()
# 提取域名
domain_list = []
for domain in domains:
domain = domain.strip()
if domain:
domain_list.append(domain)
# 批量添加域名
stats = self.add_domains_batch(domain_list, source_type, batch_size)
logger.info(f"从文件导入完成: {stats}")
return stats
except Exception as e:
logger.error(f"从文件导入出错: {e}")
return {
'total': 0,
'valid': 0,
'added': 0,
'exists': 0,
'invalid': 0,
'failed': 0
}
def collect_from_juming(self, type_='一口价'):
"""
从聚名网收集域名
:param type_: 类型,一口价或过期删除
:return: int - 收集到的域名数量
"""
# 这里可以添加从聚名网收集域名的逻辑
logger.info(f"从聚名网收集 {type_} 域名")
# 模拟收集结果
return 0
def collect_from_search_engine(self, keyword, limit=100):
"""
从搜索引擎收集域名
:param keyword: 关键词
:param limit: 限制数量
:return: int - 收集到的域名数量
"""
# 这里可以添加从搜索引擎收集域名的逻辑
logger.info(f"从搜索引擎收集域名,关键词: {keyword}, 限制: {limit}")
# 模拟收集结果
return 0
def collect_from_enterprise_directory(self, url, limit=100):
"""
从企业目录收集域名
:param url: 企业目录URL
:param limit: 限制数量
:return: int - 收集到的域名数量
"""
# 这里可以添加从企业目录收集域名的逻辑
logger.info(f"从企业目录收集域名URL: {url}, 限制: {limit}")
# 模拟收集结果
return 0
def collect_from_zone_file(self, file_path):
"""
从Zone File收集域名
:param file_path: Zone File路径
:return: int - 收集到的域名数量
"""
# 这里可以添加从Zone File收集域名的逻辑
logger.info(f"从Zone File收集域名文件: {file_path}")
# 模拟收集结果
return 0

View File

@@ -0,0 +1,150 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :domain_processor.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:51
@explain : 域名处理器
'''
import re
import tldextract
from loguru import logger
from app.utils.database import Database
from app.utils.domain_utils import normalize_domain
class DomainProcessor:
"""
域名处理器
"""
def __init__(self):
"""
初始化域名处理器
"""
self.db = Database()
def process_domain(self, domain):
"""
处理域名
:param domain: 域名
:return: dict - 处理结果
"""
result = {
'original': domain,
'normalized': None,
'tld': None,
'valid': False,
'reason': ''
}
try:
# 标准化域名
normalized = normalize_domain(domain)
if not normalized:
result['reason'] = '无效域名格式'
return result
# 提取顶级域名
ext = tldextract.extract(normalized)
tld = ext.suffix
# 检查顶级域名
if tld not in ['com', 'net']:
result['reason'] = '不支持的顶级域名'
return result
result['normalized'] = normalized
result['tld'] = tld
result['valid'] = True
return result
except Exception as e:
logger.error(f"处理域名出错: {e}")
result['reason'] = f"处理出错: {str(e)}"
return result
def batch_process(self, domains):
"""
批量处理域名
:param domains: 域名列表
:return: list - 处理结果列表
"""
results = []
for domain in domains:
result = self.process_domain(domain)
results.append(result)
return results
def filter_valid_domains(self, domains):
"""
过滤有效的域名
:param domains: 域名列表
:return: list - 有效的域名列表
"""
valid_domains = []
for domain in domains:
result = self.process_domain(domain)
if result['valid']:
valid_domains.append(result['normalized'])
return valid_domains
def update_domain_status(self, domain_id, status_type, status_value):
"""
更新域名状态
:param domain_id: 域名ID
:param status_type: 状态类型
:param status_value: 状态值
:return: bool - 是否更新成功
"""
try:
if status_type == 'use_status':
return self.db.update_domain_use_status(domain_id, status_value)
elif status_type == 'detect_status':
return self.db.update_domain_detect_status(domain_id, status_value)
elif status_type == 'register_status':
return self.db.update_domain_register_status(domain_id, status_value)
else:
logger.error(f"未知的状态类型: {status_type}")
return False
except Exception as e:
logger.error(f"更新域名状态出错: {e}")
return False
def blacklist_domain(self, domain_id, reason):
"""
将域名加入黑名单
:param domain_id: 域名ID
:param reason: 黑名单原因
:return: bool - 是否操作成功
"""
try:
# 更新检测状态为黑名单
if self.db.update_domain_detect_status(domain_id, 4): # 4 表示黑名单
# 添加到黑名单表
domain = self.db.get_domain_by_id(domain_id)
if domain:
return self.db.add_to_blacklist(domain['domain'], reason)
return False
except Exception as e:
logger.error(f"将域名加入黑名单出错: {e}")
return False
def get_domain_statistics(self):
"""
获取域名统计信息
:return: dict - 统计信息
"""
try:
return self.db.get_domain_statistics()
except Exception as e:
logger.error(f"获取域名统计信息出错: {e}")
return {}

273
app/core/export_manager.py Normal file
View File

@@ -0,0 +1,273 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :export_manager.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:54
@explain : 导出管理器
'''
import os
from loguru import logger
from app.utils.database import Database
class ExportManager:
"""
导出管理器
"""
def __init__(self):
"""
初始化导出管理器
"""
self.db = Database()
def export_domains(self, filter_conditions, output_file):
"""
导出域名
:param filter_conditions: 筛选条件
:param output_file: 输出文件路径
:return: int - 导出的域名数量
"""
try:
# 查询符合条件的域名
domains = self.db.get_domains_by_conditions(filter_conditions)
if not domains:
logger.warning("没有符合条件的域名")
return 0
# 导出到文件
with open(output_file, 'w', encoding='utf-8') as f:
for domain in domains:
f.write(domain['domain'] + '\n')
logger.info(f"成功导出 {len(domains)} 个域名到 {output_file}")
return len(domains)
except Exception as e:
logger.error(f"导出域名出错: {e}")
return 0
def export_with_details(self, filter_conditions, output_file):
"""
导出域名及其详细信息
:param filter_conditions: 筛选条件
:param output_file: 输出文件路径
:return: int - 导出的域名数量
"""
try:
# 查询符合条件的域名及其详细信息
domains = self.db.get_domains_with_details(filter_conditions)
if not domains:
logger.warning("没有符合条件的域名")
return 0
# 导出到文件
with open(output_file, 'w', encoding='utf-8') as f:
# 写入表头
f.write('域名,注册状态,使用状态,检测状态,备案历史,备案年份,快照年份,友情链接数量\n')
# 写入数据
for domain in domains:
line = f"{domain['domain']},{domain['register_status']},{domain['use_status']},{domain['detect_status']},{domain['has_beian']},{domain['beian_year']},{domain['snapshot_years']},{domain['backlink_count']}\n"
f.write(line)
logger.info(f"成功导出 {len(domains)} 个域名及其详细信息到 {output_file}")
return len(domains)
except Exception as e:
logger.error(f"导出域名详细信息出错: {e}")
return 0
def batch_update_status(self, domain_ids, status_type, status_value):
"""
批量更新域名状态
:param domain_ids: 域名ID列表
:param status_type: 状态类型
:param status_value: 状态值
:return: int - 更新成功的域名数量
"""
try:
success_count = 0
for domain_id in domain_ids:
if self.db.update_domain_status(domain_id, status_type, status_value):
success_count += 1
logger.info(f"成功更新 {success_count} 个域名的状态")
return success_count
except Exception as e:
logger.error(f"批量更新域名状态出错: {e}")
return 0
def get_filtered_domains(self, filter_conditions, limit=1000):
"""
获取符合条件的域名
:param filter_conditions: 筛选条件
:param limit: 限制数量
:return: list - 域名列表
"""
try:
domains = self.db.get_domains_by_conditions(filter_conditions, limit)
logger.info(f"获取到 {len(domains)} 个符合条件的域名")
return domains
except Exception as e:
logger.error(f"获取符合条件的域名出错: {e}")
return []
def generate_report(self, output_file):
"""
生成统计报告
:param output_file: 输出文件路径
:return: bool - 是否生成成功
"""
try:
# 获取域名统计信息
domain_stats = self.db.get_domain_statistics()
# 获取任务统计信息
task_stats = self.db.get_task_statistics()
# 生成报告
with open(output_file, 'w', encoding='utf-8') as f:
f.write('域名库系统统计报告\n')
f.write('=' * 50 + '\n')
f.write('\n域名统计:\n')
f.write(f'总域名数: {domain_stats.get("total", 0)}\n')
f.write(f'可注册域名: {domain_stats.get("available", 0)}\n')
f.write(f'已注册域名: {domain_stats.get("registered", 0)}\n')
f.write(f'黑名单域名: {domain_stats.get("blacklisted", 0)}\n')
f.write('\n任务统计:\n')
f.write(f'总任务数: {task_stats.get("total", 0)}\n')
f.write(f'待执行任务: {task_stats.get("pending", 0)}\n')
f.write(f'执行中任务: {task_stats.get("running", 0)}\n')
f.write(f'已完成任务: {task_stats.get("completed", 0)}\n')
f.write(f'失败任务: {task_stats.get("failed", 0)}\n')
logger.info(f"成功生成统计报告到 {output_file}")
return True
except Exception as e:
logger.error(f"生成统计报告出错: {e}")
return False
def export_to_excel(self, domains, output_file):
"""
导出域名到Excel文件
:param domains: 域名列表
:param output_file: 输出文件路径
:return: int - 导出的域名数量
"""
try:
# 尝试导入openpyxl
try:
from openpyxl import Workbook
except ImportError:
# 如果没有安装openpyxl使用CSV格式作为替代
logger.warning("openpyxl库未安装将使用CSV格式导出")
return self.export_to_csv(domains, output_file.replace('.xlsx', '.csv'))
# 创建工作簿
wb = Workbook()
ws = wb.active
# 写入表头
headers = ['域名', '注册状态', '使用状态', '检测状态', '人工复核状态', '过期时间', '单位性质', '网站首页网址', '检测时间', '备案历史', '备案年份', '快照年份', '百度历史', '百度Site', '是否中文标题', '360 Site', 'Google Site', '友情链接数量']
ws.append(headers)
# 写入数据
for domain in domains:
row = [
domain.get('domain', ''),
domain.get('register_status', ''),
domain.get('use_status', ''),
domain.get('detect_status', ''),
domain.get('review_status', ''),
domain.get('expire_date', ''),
domain.get('company_type', ''),
domain.get('website_url', ''),
domain.get('detect_time', ''),
domain.get('has_beian', ''),
domain.get('beian_year', ''),
domain.get('snapshot_years', ''),
domain.get('baidu_history', ''),
domain.get('baidu_site', ''),
domain.get('is_chinese_title', ''),
domain.get('qihu360_site', ''),
domain.get('google_site', ''),
domain.get('backlink_count', '')
]
ws.append(row)
# 保存文件
wb.save(output_file)
logger.info(f"成功导出 {len(domains)} 个域名到Excel文件: {output_file}")
return len(domains)
except Exception as e:
logger.error(f"导出Excel文件出错: {e}")
# 尝试使用CSV格式作为替代
try:
csv_file = output_file.replace('.xlsx', '.csv')
logger.info(f"尝试使用CSV格式导出到: {csv_file}")
return self.export_to_csv(domains, csv_file)
except Exception as e2:
logger.error(f"导出CSV文件也失败: {e2}")
raise
def export_to_csv(self, domains, output_file):
"""
导出域名到CSV文件
:param domains: 域名列表
:param output_file: 输出文件路径
:return: int - 导出的域名数量
"""
try:
import csv
# 写入文件
with open(output_file, 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
# 写入表头
headers = ['域名', '注册状态', '使用状态', '检测状态', '人工复核状态', '过期时间', '单位性质', '网站首页网址', '检测时间', '备案历史', '备案年份', '快照年份', '百度历史', '百度Site', '是否中文标题', '360 Site', 'Google Site', '友情链接数量']
writer.writerow(headers)
# 写入数据
for domain in domains:
row = [
domain.get('domain', ''),
domain.get('register_status', ''),
domain.get('use_status', ''),
domain.get('detect_status', ''),
domain.get('review_status', ''),
domain.get('expire_date', ''),
domain.get('company_type', ''),
domain.get('website_url', ''),
domain.get('detect_time', ''),
domain.get('has_beian', ''),
domain.get('beian_year', ''),
domain.get('snapshot_years', ''),
domain.get('baidu_history', ''),
domain.get('baidu_site', ''),
domain.get('is_chinese_title', ''),
domain.get('qihu360_site', ''),
domain.get('google_site', ''),
domain.get('backlink_count', '')
]
writer.writerow(row)
logger.info(f"成功导出 {len(domains)} 个域名到CSV文件: {output_file}")
return len(domains)
except Exception as e:
logger.error(f"导出CSV文件出错: {e}")
raise

162
app/core/task_scheduler.py Normal file
View File

@@ -0,0 +1,162 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :task_scheduler.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:53
@explain : 任务调度器
'''
import time
import threading
from loguru import logger
from app.utils.database import Database
from app.core.detect_engine import DetectEngine
class TaskScheduler:
"""
任务调度器
"""
def __init__(self):
"""
初始化任务调度器
"""
self.db = Database()
self.detect_engine = DetectEngine()
self.running = False
self.threads = []
self.max_threads = 10
def start(self):
"""
启动任务调度器
"""
if self.running:
logger.info("任务调度器已经在运行中")
return
self.running = True
logger.info("启动任务调度器")
# 启动多个线程处理任务
for i in range(self.max_threads):
thread = threading.Thread(target=self._process_tasks, daemon=True)
thread.start()
self.threads.append(thread)
logger.info(f"启动任务处理线程 {i+1}")
def stop(self):
"""
停止任务调度器
"""
self.running = False
logger.info("停止任务调度器")
# 等待线程结束
for thread in self.threads:
thread.join(timeout=5)
self.threads.clear()
logger.info("任务调度器已停止")
def _process_tasks(self):
"""
处理任务
"""
while self.running:
try:
# 获取待执行的任务
task = self.db.get_pending_task()
if not task:
# 没有任务,休眠一段时间
time.sleep(1)
continue
task_id = task['id']
domain_id = task['domain_id']
logger.info(f"处理任务: {task_id}, 域名ID: {domain_id}")
# 执行任务
success = self.detect_engine.process_task(task_id)
if success:
logger.info(f"任务处理成功: {task_id}")
else:
logger.warning(f"任务处理失败: {task_id}")
# 短暂休眠,避免过于频繁的数据库操作
time.sleep(0.1)
except Exception as e:
logger.error(f"处理任务出错: {e}")
# 休眠一段时间,避免出错后无限循环
time.sleep(5)
def add_task(self, domain_id, task_type=1, priority=0):
"""
添加任务
:param domain_id: 域名ID
:param task_type: 任务类型1-基础检测2-深度检测
:param priority: 优先级0-低1-中2-高
:return: int - 任务ID
"""
try:
task_id = self.db.create_detect_task(domain_id, task_type, priority)
logger.info(f"添加任务成功: {task_id}, 域名ID: {domain_id}")
return task_id
except Exception as e:
logger.error(f"添加任务失败: {e}")
return None
def get_task_stats(self):
"""
获取任务统计信息
:return: dict - 任务统计信息
"""
try:
return self.db.get_task_statistics()
except Exception as e:
logger.error(f"获取任务统计信息出错: {e}")
return {}
def retry_failed_tasks(self):
"""
重试失败的任务
:return: int - 重试的任务数量
"""
try:
tasks = self.db.get_failed_tasks()
retry_count = 0
for task in tasks:
task_id = task['id']
self.db.update_task_status(task_id, 0) # 0 表示待执行
self.db.update_task_retry_count(task_id, 0) # 重置重试次数
retry_count += 1
logger.info(f"重试 {retry_count} 个失败的任务")
return retry_count
except Exception as e:
logger.error(f"重试失败任务出错: {e}")
return 0
def clear_completed_tasks(self, days=7):
"""
清理已完成的任务
:param days: 保留天数
:return: int - 清理的任务数量
"""
try:
count = self.db.clear_completed_tasks(days)
logger.info(f"清理 {count} 个已完成的任务")
return count
except Exception as e:
logger.error(f"清理已完成任务出错: {e}")
return 0

10
app/credentials.json Normal file
View File

@@ -0,0 +1,10 @@
{
"juming": {
"email": "chaofanai1998@gmail.com",
"password": "llzz123,./"
},
"juziseo": {
"email": "mamian",
"password": "Abc123456"
}
}

13
app/detect_options.json Normal file
View File

@@ -0,0 +1,13 @@
{
"detect_register": false,
"detect_chinaz": false,
"detect_aizhan": false,
"detect_baidu_site": false,
"detect_360_site": false,
"detect_baidu_security": false,
"detect_whois": false,
"detect_beian": true,
"detect_intercept": false,
"detect_juziseo": false,
"detect_juziseo_outlink": false
}

View File

@@ -0,0 +1,4 @@
# -*- coding: UTF-8 -*-
'''
检测插件模块
'''

View File

@@ -0,0 +1,130 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :aizhan_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:02
@explain : 爱站网检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class AizhanDetector(BaseDetector):
"""
爱站网检测器
"""
def __init__(self):
"""
初始化爱站网检测器
"""
super().__init__()
self.url = 'https://www.aizhan.com'
self.query_url = 'https://www.aizhan.com/cha/{domain}'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.query_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取标题
title = self._extract_title(content)
# 提取风险信息
risk = self._extract_risk(content)
# 检查是否包含敏感词
has_sensitive = self._check_sensitive(title, risk)
return {
'title': title,
'risk': risk,
'has_sensitive': has_sensitive
}
else:
self._log_warning(f"爱站网查询失败: {response.status_code}")
return {'title': '', 'risk': '', 'has_sensitive': False}
except Exception as e:
return self._handle_exception(e, domain)
def _extract_title(self, content):
"""
提取标题
:param content: 页面内容
:return: str - 标题
"""
try:
pattern = r'<title>(.*?)</title>'
match = re.search(pattern, content)
if match:
return match.group(1).strip()
return ''
except Exception as e:
self._handle_exception(e, 'extract_title')
return ''
def _extract_risk(self, content):
"""
提取风险信息
:param content: 页面内容
:return: str - 风险信息
"""
try:
# 这里需要根据实际页面结构调整正则表达式
pattern = r'百度网址检测:<span[^>]+>(.*?)</span>'
match = re.search(pattern, content)
if match:
return match.group(1).strip()
return ''
except Exception as e:
self._handle_exception(e, 'extract_risk')
return ''
def _check_sensitive(self, title, risk):
"""
检查是否包含敏感词
:param title: 标题
:param risk: 风险信息
:return: bool - 是否包含敏感词
"""
# 风险类型
sensitive_risks = ['低风险', '疑似色情博彩风险', '严重影响权重']
# 敏感词
sensitive_words = ['色情', '赌博', '博彩', '毒品', '暴力', '诈骗']
# 检查风险
for r in sensitive_risks:
if r in risk:
return True
# 检查标题
for word in sensitive_words:
if word in title:
return True
return False

View File

@@ -0,0 +1,151 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :baidu_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:58
@explain : 百度检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class BaiduDetector(BaseDetector):
"""
百度检测器
"""
def __init__(self):
"""
初始化百度检测器
"""
super().__init__()
self.site_url = 'https://www.baidu.com/s'
self.history_url = 'https://www.baidu.com/s'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 检查百度site
site_result = self.check_site(domain)
# 检查百度历史
history_result = self.check_history(domain)
return {
'site': site_result,
'history': history_result
}
except Exception as e:
return self._handle_exception(e, domain)
def check_site(self, domain):
"""
检查百度site收录
:param domain: 域名
:return: dict - 检测结果
"""
try:
params = {
'wd': f'site:{domain}',
'rn': '50'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(self.site_url, params=params, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取子域名
subdomains = self._extract_subdomains(content, domain)
# 检查是否有收录
has_收录 = '没有找到相关结果' not in content
return {
'has_收录': has_收录,
'subdomains': subdomains
}
else:
self._log_warning(f"百度site查询失败: {response.status_code}")
return {'has_收录': False, 'subdomains': []}
except Exception as e:
self._handle_exception(e, domain)
return {'has_收录': False, 'subdomains': []}
def check_history(self, domain):
"""
检查百度历史收录
:param domain: 域名
:return: dict - 检测结果
"""
try:
params = {
'wd': f'cache:{domain}',
'rn': '50'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(self.history_url, params=params, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 检查是否有历史收录
has_history = '百度快照' in content
# 检查是否有灰色内容
has_gray = '风险提示' in content or '安全警告' in content
return {
'has_history': has_history,
'has_gray': has_gray
}
else:
self._log_warning(f"百度历史查询失败: {response.status_code}")
return {'has_history': False, 'has_gray': False}
except Exception as e:
self._handle_exception(e, domain)
return {'has_history': False, 'has_gray': False}
def _extract_subdomains(self, content, domain):
"""
提取子域名
:param content: 搜索结果内容
:param domain: 主域名
:return: list - 子域名列表
"""
try:
# 提取所有包含域名的链接
pattern = r'https?://([a-zA-Z0-9-]+)\.' + re.escape(domain)
matches = re.findall(pattern, content)
# 去重并过滤空值
subdomains = list(set(matches))
subdomains = [sub for sub in subdomains if sub]
return subdomains
except Exception as e:
self._handle_exception(e, domain)
return []

70
app/detectors/base.py Normal file
View File

@@ -0,0 +1,70 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :base.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:55
@explain : 基础检测类
'''
from abc import ABC, abstractmethod
from loguru import logger
class BaseDetector(ABC):
"""
基础检测类
"""
def __init__(self):
"""
初始化检测类
"""
self.name = self.__class__.__name__
logger.info(f"初始化检测器: {self.name}")
@abstractmethod
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
pass
def _log_info(self, message):
"""
记录信息日志
:param message: 消息
"""
logger.info(f"[{self.name}] {message}")
def _log_warning(self, message):
"""
记录警告日志
:param message: 消息
"""
logger.warning(f"[{self.name}] {message}")
def _log_error(self, message):
"""
记录错误日志
:param message: 消息
"""
logger.error(f"[{self.name}] {message}")
def _handle_exception(self, e, domain):
"""
处理异常
:param e: 异常
:param domain: 域名
:return: dict - 错误结果
"""
self._log_error(f"检测域名 {domain} 出错: {e}")
return {'error': str(e)}

View File

@@ -0,0 +1,130 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :chinaz_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:01
@explain : 站长之家检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class ChinazDetector(BaseDetector):
"""
站长之家检测器
"""
def __init__(self):
"""
初始化站长之家检测器
"""
super().__init__()
self.url = 'https://seo.chinaz.com'
self.query_url = 'https://seo.chinaz.com/{domain}'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.query_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取标题
title = self._extract_title(content)
# 提取网站分类
category = self._extract_category(content)
# 检查是否包含敏感词
has_sensitive = self._check_sensitive(title, category)
return {
'title': title,
'category': category,
'has_sensitive': has_sensitive
}
else:
self._log_warning(f"站长之家查询失败: {response.status_code}")
return {'title': '', 'category': '', 'has_sensitive': False}
except Exception as e:
return self._handle_exception(e, domain)
def _extract_title(self, content):
"""
提取标题
:param content: 页面内容
:return: str - 标题
"""
try:
pattern = r'<title>(.*?)</title>'
match = re.search(pattern, content)
if match:
return match.group(1).strip()
return ''
except Exception as e:
self._handle_exception(e, 'extract_title')
return ''
def _extract_category(self, content):
"""
提取网站分类
:param content: 页面内容
:return: str - 分类
"""
try:
# 这里需要根据实际页面结构调整正则表达式
pattern = r'网站分类:<a[^>]+>(.*?)</a>'
match = re.search(pattern, content)
if match:
return match.group(1).strip()
return ''
except Exception as e:
self._handle_exception(e, 'extract_category')
return ''
def _check_sensitive(self, title, category):
"""
检查是否包含敏感词
:param title: 标题
:param category: 分类
:return: bool - 是否包含敏感词
"""
# 敏感分类
sensitive_categories = ['视频电影', '体育运动', '常用查询']
# 敏感词
sensitive_words = ['色情', '赌博', '博彩', '毒品', '暴力', '诈骗']
# 检查分类
for cat in sensitive_categories:
if cat in category:
return True
# 检查标题
for word in sensitive_words:
if word in title:
return True
return False

View File

@@ -0,0 +1,80 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :google_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:00
@explain : Google检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class GoogleDetector(BaseDetector):
"""
Google检测器
"""
def __init__(self):
"""
初始化Google检测器
"""
super().__init__()
self.site_url = 'https://www.google.com/search'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 检查Google site
site_result = self.check_site(domain)
return {
'site': site_result
}
except Exception as e:
return self._handle_exception(e, domain)
def check_site(self, domain):
"""
检查Google site收录
:param domain: 域名
:return: dict - 检测结果
"""
try:
params = {
'q': f'site:{domain}',
'num': '50'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(self.site_url, params=params, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 检查是否有收录
has_收录 = 'No results found for' not in content
return {
'has_收录': has_收录
}
else:
self._log_warning(f"Google site查询失败: {response.status_code}")
return {'has_收录': False}
except Exception as e:
self._handle_exception(e, domain)
return {'has_收录': False}

View File

@@ -0,0 +1,228 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :jucha_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:04
@explain : 聚查检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class JuchaDetector(BaseDetector):
"""
聚查检测器
"""
def __init__(self):
"""
初始化聚查检测器
"""
super().__init__()
self.url = 'https://www.jucha.com'
self.whois_url = 'https://www.jucha.com/whois/{domain}'
self.beian_url = 'https://www.jucha.com/beian/{domain}'
self.intercept_url = 'https://www.jucha.com/intercept/{domain}'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 检查WHOIS
whois_result = self.check_whois(domain)
# 检查备案
beian_result = self.check_beian(domain)
# 检查拦截
intercept_result = self.check_intercept(domain)
return {
'whois': whois_result,
'beian': beian_result,
'intercept': intercept_result
}
except Exception as e:
return self._handle_exception(e, domain)
def check_whois(self, domain):
"""
检查WHOIS
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.whois_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取WHOIS信息
whois_info = self._extract_whois_info(content)
return whois_info
else:
self._log_warning(f"聚查WHOIS查询失败: {response.status_code}")
return {'status': ''}
except Exception as e:
self._handle_exception(e, domain)
return {'status': ''}
def check_beian(self, domain):
"""
检查备案
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.beian_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取备案信息
beian_info = self._extract_beian_info(content)
return beian_info
else:
self._log_warning(f"聚查备案查询失败: {response.status_code}")
return {'has_beian': False, 'beian_year': '', 'is_enterprise': False, 'beian_match': False}
except Exception as e:
self._handle_exception(e, domain)
return {'has_beian': False, 'beian_year': '', 'is_enterprise': False, 'beian_match': False}
def check_intercept(self, domain):
"""
检查拦截
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.intercept_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 检查是否被拦截
is_normal = self._check_intercept_status(content)
return {
'normal': is_normal
}
else:
self._log_warning(f"聚查拦截查询失败: {response.status_code}")
return {'normal': False}
except Exception as e:
self._handle_exception(e, domain)
return {'normal': False}
def _extract_whois_info(self, content):
"""
提取WHOIS信息
:param content: 页面内容
:return: dict - WHOIS信息
"""
try:
# 提取状态信息
pattern = r'域名状态:<span[^>]+>(.*?)</span>'
match = re.search(pattern, content)
status = match.group(1).strip() if match else ''
return {
'status': status
}
except Exception as e:
self._handle_exception(e, 'extract_whois_info')
return {'status': ''}
def _extract_beian_info(self, content):
"""
提取备案信息
:param content: 页面内容
:return: dict - 备案信息
"""
try:
# 检查是否有备案
has_beian = '备案信息' in content
# 提取备案年份
beian_year = ''
pattern = r'审核时间:(\d{4})-\d{2}-\d{2}'
match = re.search(pattern, content)
if match:
beian_year = match.group(1)
# 提取单位性质
is_enterprise = '企业' in content
# 检查首网址和备案网址是否一致
beian_match = '网站首页网址' in content
return {
'has_beian': has_beian,
'beian_year': beian_year,
'is_enterprise': is_enterprise,
'beian_match': beian_match
}
except Exception as e:
self._handle_exception(e, 'extract_beian_info')
return {'has_beian': False, 'beian_year': '', 'is_enterprise': False, 'beian_match': False}
def _check_intercept_status(self, content):
"""
检查拦截状态
:param content: 页面内容
:return: bool - 是否正常
"""
try:
# 检查是否包含正常标识
if '正常' in content:
return True
# 检查是否包含拦截标识
if '拦截' in content:
return False
return False
except Exception as e:
self._handle_exception(e, 'check_intercept_status')
return False

View File

@@ -0,0 +1,214 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :juziseo_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:03
@explain : 桔子SEO检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class JuziseoDetector(BaseDetector):
"""
桔子SEO检测器
"""
def __init__(self):
"""
初始化桔子SEO检测器
"""
super().__init__()
self.url = 'https://seo.juziseo.com'
self.history_url = 'https://seo.juziseo.com/history/{domain}'
self.backlink_url = 'https://seo.juziseo.com/backlink/{domain}'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 检查历史信息
history_result = self.check_history(domain)
# 检查外链
backlink_result = self.check_backlink(domain)
return {
'history': history_result,
'backlink': backlink_result
}
except Exception as e:
return self._handle_exception(e, domain)
def check_history(self, domain):
"""
检查历史信息
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.history_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取历史信息
history_info = self._extract_history_info(content)
# 检查是否包含敏感词
has_sensitive = self._check_sensitive(history_info)
# 检查是否有百度历史收录
has_baidu_history = '百度历史收录' in content
# 检查是否有子域名
has_subdomains = '子域名' in content
# 检查是否为简体中文
is_simplified = self._check_simplified(content)
return {
'has_sensitive': has_sensitive,
'has_baidu_history': has_baidu_history,
'has_subdomains': has_subdomains,
'is_simplified': is_simplified
}
else:
self._log_warning(f"桔子SEO历史查询失败: {response.status_code}")
return {'has_sensitive': False, 'has_baidu_history': False, 'has_subdomains': False, 'is_simplified': True}
except Exception as e:
self._handle_exception(e, domain)
return {'has_sensitive': False, 'has_baidu_history': False, 'has_subdomains': False, 'is_simplified': True}
def check_backlink(self, domain):
"""
检查外链
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 构建查询URL
query_url = self.backlink_url.format(domain=domain)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(query_url, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 检查是否包含敏感词
has_sensitive = self._check_backlink_sensitive(content)
# 检查是否有子域名
has_subdomains = '子域名' in content
return {
'has_sensitive': has_sensitive,
'has_subdomains': has_subdomains
}
else:
self._log_warning(f"桔子SEO外链查询失败: {response.status_code}")
return {'has_sensitive': False, 'has_subdomains': False}
except Exception as e:
self._handle_exception(e, domain)
return {'has_sensitive': False, 'has_subdomains': False}
def _extract_history_info(self, content):
"""
提取历史信息
:param content: 页面内容
:return: str - 历史信息
"""
try:
# 这里需要根据实际页面结构调整正则表达式
pattern = r'<div class="history-info">(.*?)</div>'
match = re.search(pattern, content, re.DOTALL)
if match:
return match.group(1).strip()
return ''
except Exception as e:
self._handle_exception(e, 'extract_history_info')
return ''
def _check_sensitive(self, history_info):
"""
检查是否包含敏感词
:param history_info: 历史信息
:return: bool - 是否包含敏感词
"""
# 敏感词
sensitive_words = [
'色情', '赌博', '博彩', '毒品', '暴力', '诈骗',
'足球', '直播', '证券', '配资', '软件',
'体育', '商行', '下载', '影视', '网络',
'计算', 'app', 'HTML SiteMap', '模拟器', '传媒',
'二次元', '成人', '米乐', '小说', '凯发',
'人才', '华体', '娱乐', '开户'
]
for word in sensitive_words:
if word in history_info:
return True
return False
def _check_simplified(self, content):
"""
检查是否为简体中文
:param content: 页面内容
:return: bool - 是否为简体中文
"""
# 检查是否包含简体中文标识
if '简体中文' in content:
return True
# 检查是否包含繁体中文标识
if '繁体中文' in content:
return False
return True
def _check_backlink_sensitive(self, content):
"""
检查外链是否包含敏感词
:param content: 页面内容
:return: bool - 是否包含敏感词
"""
# 敏感词
sensitive_words = [
'内幕', '猛料', '精料', '高手', '绝杀',
'权威', '澳门', '色情', '赌博', '博彩'
]
for word in sensitive_words:
if word in content:
return True
return False

View File

@@ -0,0 +1,107 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :qihu360_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:59
@explain : 360检测器
'''
import requests
from curl_cffi import requests as curl_requests
import re
from app.detectors.base import BaseDetector
class Qihu360Detector(BaseDetector):
"""
360检测器
"""
def __init__(self):
"""
初始化360检测器
"""
super().__init__()
self.site_url = 'https://www.so.com/s'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 检查360 site
site_result = self.check_site(domain)
return {
'site': site_result
}
except Exception as e:
return self._handle_exception(e, domain)
def check_site(self, domain):
"""
检查360 site收录
:param domain: 域名
:return: dict - 检测结果
"""
try:
params = {
'q': f'site:{domain}',
'pn': '1',
'rn': '50'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
}
# 使用curl_cffi模拟浏览器
response = curl_requests.get(self.site_url, params=params, headers=headers, impersonate='chrome', timeout=10)
if response.status_code == 200:
content = response.text
# 提取子域名
subdomains = self._extract_subdomains(content, domain)
# 检查是否有收录
has_收录 = '没有找到相关结果' not in content
return {
'has_收录': has_收录,
'subdomains': subdomains
}
else:
self._log_warning(f"360 site查询失败: {response.status_code}")
return {'has_收录': False, 'subdomains': []}
except Exception as e:
self._handle_exception(e, domain)
return {'has_收录': False, 'subdomains': []}
def _extract_subdomains(self, content, domain):
"""
提取子域名
:param content: 搜索结果内容
:param domain: 主域名
:return: list - 子域名列表
"""
try:
# 提取所有包含域名的链接
pattern = r'https?://([a-zA-Z0-9-]+)\.' + re.escape(domain)
matches = re.findall(pattern, content)
# 去重并过滤空值
subdomains = list(set(matches))
subdomains = [sub for sub in subdomains if sub]
return subdomains
except Exception as e:
self._handle_exception(e, domain)
return []

View File

@@ -0,0 +1,128 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :rdap_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:56
@explain : RDAP检测器
'''
import requests
from app.detectors.base import BaseDetector
class RDAPDetector(BaseDetector):
"""
RDAP检测器
"""
def __init__(self):
"""
初始化RDAP检测器
"""
super().__init__()
self.rdap_urls = {
'com': 'https://rdap.verisign.com/com/v1/domain/',
'net': 'https://rdap.verisign.com/net/v1/domain/'
}
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 提取顶级域名
tld = domain.split('.')[-1]
if tld not in self.rdap_urls:
return {'error': '不支持的顶级域名'}
# 构建RDAP查询URL
url = self.rdap_urls[tld] + domain
# 发送请求
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
return self._parse_rdap_response(data)
elif response.status_code == 404:
return {'status': 'available'}
else:
return {'error': f'RDAP查询失败: {response.status_code}'}
except Exception as e:
return self._handle_exception(e, domain)
def check_register_status(self, domain):
"""
检查注册状态
:param domain: 域名
:return: int - 注册状态码
"""
try:
result = self.check_domain(domain)
if 'error' in result:
return 9 # 状态未知
if result.get('status') == 'available':
return 2 # 可注册
# 检查域名状态
statuses = result.get('status', [])
if 'clientHold' in statuses:
return 7 # clientHold
elif 'serverHold' in statuses:
return 8 # serverHold
elif 'autoRenewPeriod' in statuses:
return 4 # 宽限期
elif 'redemptionPeriod' in statuses:
return 5 # 赎回期
elif 'pendingDelete' in statuses:
return 6 # 删除期
else:
return 3 # 已注册
except Exception as e:
self._handle_exception(e, domain)
return 10 # 检测失败
def _parse_rdap_response(self, data):
"""
解析RDAP响应
:param data: RDAP响应数据
:return: dict - 解析结果
"""
result = {
'status': 'registered',
'domain': data.get('ldhName'),
'statuses': data.get('status', []),
'registrar': None,
'creation_date': None,
'expiration_date': None,
'last_update': None
}
# 解析注册商信息
for entity in data.get('entities', []):
if 'registrar' in entity.get('roles', []):
result['registrar'] = entity.get('vcardArray', [[], []])[1][1][3]
break
# 解析时间信息
for event in data.get('events', []):
event_action = event.get('eventAction')
event_date = event.get('eventDate')
if event_action == 'registration':
result['creation_date'] = event_date
elif event_action == 'expiration':
result['expiration_date'] = event_date
elif event_action == 'last update':
result['last_update'] = event_date
return result

View File

@@ -0,0 +1,193 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :wayback_detector.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/8 23:57
@explain : Wayback检测器
'''
import requests
from app.detectors.base import BaseDetector
class WaybackDetector(BaseDetector):
"""
Wayback检测器
"""
def __init__(self):
"""
初始化Wayback检测器
"""
super().__init__()
self.cdx_api_url = 'https://web.archive.org/cdx/search/cdx'
self.snapshot_url = 'https://web.archive.org/web/{timestamp}/{domain}'
def check_domain(self, domain):
"""
检测域名
:param domain: 域名
:return: dict - 检测结果
"""
try:
# 获取快照年份
years = self.get_snapshot_years(domain)
# 检查是否包含敏感内容
has_sensitive = self.has_sensitive_content(domain)
return {
'snapshot_years': years,
'has_sensitive_content': has_sensitive
}
except Exception as e:
return self._handle_exception(e, domain)
def get_snapshot_years(self, domain):
"""
获取快照年份
:param domain: 域名
:return: list - 快照年份列表
"""
try:
params = {
'url': domain,
'output': 'json',
'fl': 'timestamp',
'filter': 'statuscode:200'
}
response = requests.get(self.cdx_api_url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
years = set()
# 跳过表头
for item in data[1:]:
timestamp = item[0]
if len(timestamp) >= 4:
year = int(timestamp[:4])
years.add(year)
return sorted(years)
else:
self._log_warning(f"获取快照年份失败: {response.status_code}")
return []
except Exception as e:
self._handle_exception(e, domain)
return []
def has_sensitive_content(self, domain):
"""
检查是否包含敏感内容
:param domain: 域名
:return: bool - 是否包含敏感内容
"""
try:
# 获取最近的快照
params = {
'url': domain,
'output': 'json',
'fl': 'timestamp',
'filter': 'statuscode:200',
'limit': '1'
}
response = requests.get(self.cdx_api_url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if len(data) > 1:
timestamp = data[1][0]
snapshot_url = self.snapshot_url.format(timestamp=timestamp, domain=domain)
# 获取快照内容
snapshot_response = requests.get(snapshot_url, timeout=10)
if snapshot_response.status_code == 200:
content = snapshot_response.text
return self._check_sensitive_words(content)
return False
except Exception as e:
self._handle_exception(e, domain)
return False
def _check_sensitive_words(self, content):
"""
检查敏感词
:param content: 内容
:return: bool - 是否包含敏感词
"""
# 敏感词列表
sensitive_words = [
'色情', '赌博', '博彩', '毒品', '暴力', '诈骗',
'私服', '外挂', '破解', '盗版', '黄色', '反动'
]
for word in sensitive_words:
if word in content:
return True
return False
def get_backlink_count(self, domain):
"""
获取友情链接数量
:param domain: 域名
:return: int - 友情链接数量
"""
try:
# 获取最近的快照
params = {
'url': domain,
'output': 'json',
'fl': 'timestamp',
'filter': 'statuscode:200',
'limit': '1'
}
response = requests.get(self.cdx_api_url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if len(data) > 1:
timestamp = data[1][0]
snapshot_url = self.snapshot_url.format(timestamp=timestamp, domain=domain)
# 获取快照内容
snapshot_response = requests.get(snapshot_url, timeout=10)
if snapshot_response.status_code == 200:
content = snapshot_response.text
return self._count_backlinks(content)
return 0
except Exception as e:
self._handle_exception(e, domain)
return 0
def _count_backlinks(self, content):
"""
统计友情链接数量
:param content: 内容
:return: int - 友情链接数量
"""
# 简单的友情链接检测
import re
links = re.findall(r'<a\s+href=["\'](https?://[^"\']+)["\']', content)
# 过滤掉同一域名的链接
domain_links = set()
for link in links:
if 'http' in link:
domain_links.add(link)
return len(domain_links)

3
app/domain_suffixes.json Normal file
View File

@@ -0,0 +1,3 @@
{
"suffixes": ".com,.net"
}

BIN
app/jucha_cookies.pkl Normal file

Binary file not shown.

BIN
app/juming_cookies.pkl Normal file

Binary file not shown.

BIN
app/juziseo_cookies.pkl Normal file

Binary file not shown.

68
app/main.py Normal file
View File

@@ -0,0 +1,68 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :main.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:09
@explain : 系统主入口
'''
import sys
import os
# 添加项目根目录到 sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from loguru import logger
from PySide6.QtWidgets import QApplication
from app.ui.main_window import MainWindow
from app.utils.database import Database
from app.config import config
# 配置日志
logger.add(
os.path.join(config.LOG_DIR, config.LOG_FILE),
level=config.LOG_LEVEL,
rotation="10 MB",
compression="zip"
)
def main():
"""
主函数
"""
try:
# 初始化数据库连接
db = Database(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
# 测试数据库连接
db.execute("SELECT 1")
logger.info("数据库连接成功")
# 初始化应用程序
app = QApplication(sys.argv)
window = MainWindow()
window.show()
# 运行应用程序
sys.exit(app.exec())
except Exception as e:
logger.error(f"启动应用程序失败: {e}")
sys.exit(1)
finally:
# 关闭数据库连接
if 'db' in locals():
db.close()
if __name__ == "__main__":
main()

4
app/proxy_config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"proxy_enable": false,
"proxy_url": ""
}

15108
app/sdk_leg.js Normal file

File diff suppressed because one or more lines are too long

897
app/sdk_leg_env.js Normal file
View File

@@ -0,0 +1,897 @@
process_ = process;
require_ = require;
delete Buffer;
// delete process;
delete require;
delete global;
delete module;
delete exports;
delete __filename;
delete __dirname;
delete SharedArrayBuffer;
AsObj = {
// print: console.log,
print: function () { },
// print_:console.log,
}
no_print = ['Boolean','String','parseFloat','Array','Object','prepareStackTrace_'];
function watch(object, WatchName) {
const handler = {
get(target, property, receiver) {
if (
property !== 'isNaN' &&
property !== 'encodeURI' &&
property !== "Uint8Array" &&
property !== 'undefined' &&
property !== 'JSON' &&
property !== 'Number' &&
!no_print.includes(property) &&
property !== Symbol.for('nodejs.util.inspect.custom') &&
typeof property !== 'symbol'
) {
if (property === 'global') {
return undefined;
}
if (property === 'Buffer') {
return undefined;
}
if (property === 'process') {
return undefined;
}
if (WatchName === 'config_data') {
debugger
}
if (WatchName.indexOf('.prototype') != -1 && target[property] != undefined) {
return Reflect.get(target, property, receiver);
}
AsObj.print(
"方法:", "get",
"对象:", WatchName,
"属性:", property,
"属性类型:", typeof property,
"属性值:", typeof target[property] == 'object' ? "object" : target[property],
"属性值类型:", typeof target[property]
);
}
if (WatchName === 'top') {
return window;
}
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
if (WatchName.indexOf('.prototype') != -1 && value != undefined) {
return Reflect.set(target, property, value, receiver);
}
AsObj.print(
"方法:", "set",
"对象:", WatchName,
"属性:", property,
"属性类型:", typeof property,
"属性值:", typeof value == 'object' ? "object" : value,
"属性值类型:", typeof target[property]
);
return Reflect.set(target, property, value, receiver);
},
// in操作 检测
has(target, property) {
AsObj.print(
"代理对象:", WatchName,
"方法:", "has",
"检查属性:", property,
"结果:", typeof target[property] == 'object' ? "object" : target[property],
);
return Reflect.has(target, property);
},
// Object.key 检测
ownKeys(target) {
AsObj.print(
"方法:", "ownKeys",
"对象:", target+''
);
return Reflect.ownKeys(target);
}
};
return new Proxy(object, handler);
}
// function watch(object, WatchName) {
// return object
// }
// 保护函数toString检测
const safeFunction = function safeFunction(func) {
//处理安全函数
Function.prototype.$call = Function.prototype.call;
const $toString = Function.toString;
const myFunction_toString_symbol = Symbol('('.concat('', ')'));
const myToString = function myToString() {
return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this);
}
const set_native = function set_native(func, key, value) {
Object.defineProperty(func, key, {
"enumerable": false,
"configurable": true,
"writable": true,
"value": value
});
}
delete Function.prototype['toString'];
set_native(Function.prototype, "toString", myToString);
set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");
const safe_Function = function safe_Function(func) {
set_native(func, myFunction_toString_symbol, "function" + (func.name ? " " + func.name : "") + "() { [native code] }");
}
return safe_Function(func)
}
//创建函数,并代理上
const makeFunction = function makeFunction(name) {
v_log = AsObj.print;
// 使用 Function 保留函数名
func = new Function("v_log", `
return function ${name}() {
v_log('函数${name}传参-->', arguments);
};
`)(v_log); // 传递 v_log 到动态函数
safeFunction(func);
func = watch(func,`${name}`);
func.prototype = watch(func.prototype, `${name}.prototype`);
return func;
}
!(function () {
"use strict";
const $toString = Function.toString;
const myFunction_toString_symbol = Symbol('('.concat('', ')_', (Math.random() + '').toString(36)));
const mytoString = function () {
return typeof this == 'function' && this[myFunction_toString_symbol] || $toString.call(this);
};
function set_native(func, key, value) {
Object.defineProperty(func, key, {
"enumerable": false,
"configurable": true,
"writable": true,
"value": value
})
};
delete Function.prototype['toString'];
set_native(Function.prototype, "toString", mytoString);
set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");
this.func_set_native = function (func) {
set_native(func, myFunction_toString_symbol, `function ${myFunction_toString_symbol, func.name || ''}() { [native code] }`)
}
}).call(globalThis);
// 重写全局对象原型链
function setTostringAndstringTag(obj) {
Object.defineProperties(obj.prototype, {
[Symbol.toStringTag]: {
configurable: true,
value: obj.name
}
});
safeFunction(obj);
};
// 创建标签原型
function createTagProto(propObj,portotypeObj) {
let res = propObj + ' = ' + 'function ' + propObj + '() { throw new TypeError("Illegal constructor"); };\n';
res += 'setTostringAndstringTag(' + propObj + ',null);\n';
if (portotypeObj) {
for (let key in portotypeObj) {
res += propObj + '.prototype.' + portotypeObj[key] + '= function ' + portotypeObj[key] + '() {AsObj.print("'+propObj+'.prototype.' + portotypeObj[key] + '原型方法(需在实例对象上补该方法)::",arguments)};\n';
res += 'globalThis.func_set_native(' + propObj + '.prototype.' + portotypeObj[key] + ');\n';
}
}
eval(res);
}
Object.defineProperties(globalThis, {
[Symbol.toStringTag]: {
configurable: true,
value: 'Window'
}
});
for (let key in globalThis) {
if (typeof globalThis[key] === 'function') {
safeFunction(globalThis[key])
}
}
for (let key in console) {
if (typeof console[key] === 'function') {
safeFunction(console[key])
}
}
createTagProto('EventTarget',['addEventListener']);
createTagProto('WindowProperties');
createTagProto('Window');
window = globalThis;
window.__proto__ = Window.prototype;
window.__proto__.__proto__ = WindowProperties.prototype;
window.__proto__.__proto__.__proto__ = EventTarget.prototype;
Window.__proto__ = EventTarget;
Object.defineProperty(window, 'WindowProperties', {
get: function () {
return undefined;
}
})
function randoms(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function getRandomValues(buf) {
var min = 0,
max = 255;
if (buf instanceof Uint16Array) {
max = 65535;
} else if (buf instanceof Uint32Array) {
max = 4294967295;
}
for (var element in buf) {
buf[element] = randoms(min, max);
}
return buf;
}
self = window.self = window;
frames = window.frames = window;
top = window.top = window;
parent = window.parent = window;
global = window.global = window;
Object.defineProperty(window, "global", {
configurable:false,
enumerable: true,
set: undefined,
get: function global(){
return window
}
})
Object.defineProperty(window, "top", {
configurable:false,
enumerable: true,
set: undefined,
get: function top(){
return window
}
})
Object.defineProperty(window, "self", {
configurable:false,
enumerable: true,
set: undefined,
get: function self(){
return window
}
})
Object.defineProperty(window, "parent", {
configurable:false,
enumerable: true,
set: undefined,
get: function parent(){
return window
}
})
Object.defineProperty(window, "frames", {
configurable:false,
enumerable: true,
set: undefined,
get: function frames(){
return window
}
})
innerWidth = 1536
innerHeight = 715
outerWidth = 1536
outerHeight = 824
devicePixelRatio = 1.25;
screenLeft = 0;
screenX = 0;
screenTop = 0;
screenY = 0;
opener = null;
isSecureContext = true;
crypto = {
getRandomValues:getRandomValues
};
createTagProto('DOMStringMap')
createTagProto('HTMLHeadElement',['insertBefore','removeChild'])
createTagProto('HTMLBodyElement',['addEventListener','appendChild','removeChild'])
createTagProto('HTMLHtmlElement',['getAttribute'])
createTagProto('HTMLDocument')
createTagProto('Document',['browsingTopics','appendChild','querySelector','evaluate','querySelectorAll','removeChild','requestStorageAccess','requestStorageAccessFor','hasStorageAccess','getElementsByTagName','hasPrivateToken','createElement','hasRedemptionRecord','hasFocus'])
createTagProto('Node')
document = {};
document.__proto__ = HTMLDocument.prototype;
document.__proto__.__proto__ = Document.prototype;
document.__proto__.__proto__.__proto__ = Node.prototype;
document.__proto__.__proto__.__proto__.__proto__ = EventTarget.prototype;
HTMLDocument.__proto__ = Document;
HTMLDocument.__proto__.__proto__ = Node;
HTMLDocument.__proto__.__proto__.__proto__ = EventTarget;
Document.__proto__ = Node;
Document.__proto__.__proto__ = EventTarget;
Node.__proto__ = EventTarget;
createTagProto('Plugin');
createTagProto('PluginArray');
plugins0 = {
name: 'PDF Viewer',
filename: 'internal-pdf-viewer',
description:'Portable Document Format',
length: 2,
'0': {
type: 'application/pdf',
},
'1':{
type:'text/pdf'
}
}
plugins0['0'].enabledPlugin = plugins0;
plugins0['1'].enabledPlugin = plugins0;
plugins1 = {
name: 'Chrome PDF Viewer',
filename: 'internal-pdf-viewer',
description:'Portable Document Format',
length: 2,
'0': {
type:'application/pdf'
},
'1': {
type:'text/pdf'
}
}
plugins1['0'].enabledPlugin = plugins1;
plugins1['1'].enabledPlugin = plugins1;
plugins2 = {
name: 'Chromium PDF Viewer',
filename: 'internal-pdf-viewer',
description:'Portable Document Format',
length: 2,
'0': {
type:'application/pdf'
},
'1': {
type:'text/pdf'
}
}
plugins2['0'].enabledPlugin = plugins2;
plugins2['1'].enabledPlugin = plugins2;
plugins3 = {
name: 'Microsoft Edge PDF Viewer',
filename: 'internal-pdf-viewer',
description:'Portable Document Format',
length: 2,
'0':{
type:'application/pdf'
},
'1': {
type:'text/pdf'
}
}
plugins3['0'].enabledPlugin = plugins3;
plugins3['1'].enabledPlugin = plugins3;
plugins4 = {
name: 'WebKit built-in PDF',
filename: 'internal-pdf-viewer',
description:'Portable Document Format',
length: 2,
'0': {
type:'application/pdf'
},
'1':{
type:'text/pdf'
}
}
plugins4['0'].enabledPlugin = plugins4;
plugins4['1'].enabledPlugin = plugins4;
plugins = {
length: 5,
'0': plugins0,
'1': plugins1,
'2': plugins2,
'3': plugins3,
'4': plugins4,
namedItem : function (name) {
AsObj.print('Plugin-namedItem:', name)
},
item: function (index) {
AsObj.print('Plugin-item:', index)
return watch(plugins0,'item-'+index);
},
refresh: function () {
AsObj.print('Plugin-refresh:',arguments)
},
}
plugins.__proto__ = PluginArray.prototype;
MimeTypeArray = function MimeTypeArray() {
this.length = 2;
this['0'] = {
suffixes: 'pdf',
type: 'application/pdf',
description:"Portable Document Format",
enabledPlugin: plugins0
};
this['1'] = {
suffixes: 'pdf',
type: 'text/pdf',
description:"Portable Document Format",
enabledPlugin: plugins0
};
};
MimeTypeArray.prototype.toString = function () { return '[object MimeTypeArray]'; }
MimeTypeArray.toString = function () { return 'function MimeTypeArray() { [native code] }'; }
Object.defineProperties(MimeTypeArray.prototype, { [Symbol.toStringTag]: { value: 'MimeTypeArray' } })
MimeTypeArrayc = new MimeTypeArray();
MimeTypeArrayc[Symbol.iterator] = function* () {
for (let key in this) {
yield this[key];
}
}
// 创建电池管理器对象原型
const BatteryManager = {
level: 1,
charging: true,
chargingTime: 0,
dischargingTime: null,
onchargingchange: null,
onlevelchange: null,
toString: function toString() {
return `BatteryManager {
charging: ${this.charging},
level: ${this.level},
chargingTime: ${this.chargingTime},
dischargingTime: ${this.dischargingTime}
}`
}
}
window.BatteryManager = BatteryManager;
Promise2 = {
then: function () {
return this;
},
catch: function (){},
};
createTagProto('Bluetooth');
createTagProto('Navigator');
Navigator.prototype.hardwareConcurrency = 8;
Navigator.prototype.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36';
Navigator.prototype.appVersion = '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36'
Navigator.prototype.appName = 'Netscape';
Navigator.prototype.appCodeName = 'Mozilla';
Navigator.prototype.vendor = 'Google Inc.';
Navigator.prototype.maxTouchPoints = 10;
Navigator.prototype.platform = 'Win32';
Navigator.prototype.adAuctionComponents = function adAuctionComponents() {
AsObj.print('adAuctionComponents:::', arguments)
}
safeFunction(Navigator.prototype.adAuctionComponents)
Navigator.prototype.runAdAuction = function runAdAuction() {
AsObj.print('runAdAuction:::', arguments)
}
safeFunction(Navigator.prototype.runAdAuction)
Navigator.prototype.canLoadAdAuctionFencedFrame = makeFunction('canLoadAdAuctionFencedFrame')
Navigator.prototype.deprecatedReplaceInURN = makeFunction('deprecatedReplaceInURN')
Navigator.prototype.deprecatedURNToURL = makeFunction('deprecatedURNToURL')
Navigator.prototype.joinAdInterestGroup = makeFunction('joinAdInterestGroup')
Navigator.prototype.leaveAdInterestGroup = makeFunction('leaveAdInterestGroup')
Navigator.prototype.updateAdInterestGroups = makeFunction('updateAdInterestGroups')
Navigator.prototype.connection = watch({
downlink: 9.1,
effectiveType: '4g',
rtt: 0,
saveData: false,
},'connection')
Navigator.prototype.language = 'zh-CN';
Navigator.prototype.languages = ["zh-CN"];
Navigator.prototype.plugins = plugins;
Navigator.prototype.webdriver = false;
Navigator.prototype.cookieEnabled = true;
Navigator.prototype.onLine = true;
Navigator.prototype.doNotTrack = null;
Navigator.prototype.bluetooth = {};
Navigator.prototype.product = 'Gecko'
Navigator.prototype.deviceMemory = 8
Navigator.prototype.mediaDevices = watch({
enumerateDevices: function enumerateDevices() {
return new Promise((resolve, reject) => {
const offer = [
{deviceId: '', kind: 'audioinput', label: '', groupId: ''},
{deviceId: '', kind: 'videoinput', label: '', groupId: ''},
{deviceId: '', kind: 'audiooutput', label: '', groupId: ''},
]
resolve(offer);
});
},
getUserMedia: function getUserMedia() {
AsObj.print('getUserMedia:::', arguments)
}
},'mediaDevices')
Navigator.prototype.storage = {
estimate: function estimate() {
AsObj.print('estimate:::', arguments)
return new Promise((resolve, reject) => {
const offer = {
usage: 0, // 1GB
quota: 2147483648, // 1GB,
usageDetails: {caches: 512, indexedDB: 2855}
};
resolve(offer);
});
}
}
Navigator.prototype.webkitPersistentStorage = watch({},'webkitPersistentStorage')
Navigator.prototype.webkitTemporaryStorage = watch({
queryUsageAndQuota: function queryUsageAndQuota() {
AsObj.print('queryUsageAndQuota:::', arguments)
return new Promise((resolve, reject) => {
const offer = {
usage: 1024 * 1024 * 1024, // 1GB
quota: 1024 * 1024 * 1024, // 1GB
};
resolve(offer);
});
}
},'webkitTemporaryStorage')
Navigator.prototype.bluetooth.__proto__ = Bluetooth.prototype;
Navigator.prototype.javaEnabled = function javaEnabled() {
return false
};
safeFunction(Navigator.prototype.javaEnabled)
Navigator.prototype.getBattery = function getBattery() {
AsObj.print('getBattery:::', arguments)
return Promise.resolve({
__proto__: BatteryManager,
// 动态参数配置(示例值)
level: 1,
charging: true,
dischargingTime: null // 2小时放电时间
})
}
safeFunction(Navigator.prototype.getBattery)
Navigator.prototype.registerProtocolHandler = function registerProtocolHandler() {
AsObj.print('registerProtocolHandler:::',arguments)
}
safeFunction(Navigator.prototype.registerProtocolHandler)
Navigator.prototype.mimeTypes = watch(MimeTypeArrayc,'mimeTypes');
Navigator.prototype.geolocation = {
getCurrentPosition: function getCurrentPosition() {
return Promise2;
}
}
Navigator.prototype.pdfViewerEnabled = true;
Navigator.prototype.doNotTrack = null;
Navigator.prototype.keyboard = watch({
getLayoutMap: function getLayoutMap() {
AsObj.print('Navigator.prototype.keyboard:', arguments)
return {
then: function () {
// arguments[0](watch({
// size: 48,
// values: function () {
// return ['k', 'g', '2', '0', 'v', 'a', '`', 'l', '\\', "'", 'w', '8', 'm', 'h', '.', '7', '1', 'p', 'd', 'f', 'o', 'q', 'c', 'n', '[', 'z', 'y', '3', '6', '5', 'x', '/', '\\', ',', '-', '4', 'b', 't', '9', 's', 'i', 'u', '=', 'j', ';', 'r', ']', 'e']
// }
// }, 'navigator.keyboard.getLayoutMap.then'))
return {
catch: function () {
arguments[0]({
message:'getLayoutMap() must be called from a top-level browsing context or allowed by the permission policy.'
})
}
}
}
}
}
},'navigator.keyboard')
Navigator.prototype.permissions = watch({
query: function query() {
arg_obj = arguments[0];
if (arg_obj.name === 'audio_capture') {
return new Promise((resolve, reject) => {
const offer = {
state: 'prompt',
onchange: null,
name:arg_obj.name
};
resolve(offer);
});
}
if (arg_obj.name === 'microphone') {
return {
then: function () {
arguments[0](watch({
state: 'denied',
onchange: null,
name: 'audio_capture'
},'permissions.query.microphone'));
return {catch:function(){}}
},
catch:function(){}
}
}
if (arg_obj.name === 'camera') {
return {
then: function () {
arguments[0](watch({
state: 'prompt',
onchange: null,
name: 'video_capture'
},'permissions.query.camera'));
return {catch:function(){}}
},
catch:function(){}
}
}
AsObj.print('permissions.query:::', arguments)
}
},'permissions')
Navigator.prototype.productSub = '20030107'
Navigator.prototype.getGamepads = function getGamepads() {
AsObj.print('getGamepads:::', arguments)
return [null,null,null,null]
}
safeFunction(Navigator.prototype.getGamepads)
Navigator.prototype.sendBeacon = makeFunction('sendBeacon')
Navigator.prototype.deprecatedRunAdAuctionEnforcesKAnonymity = false
Navigator.prototype.gpu = watch({
getPreferredCanvasFormat: function getPreferredCanvasFormat() {
AsObj.print('gpu.getPreferredCanvasFormat:', arguments)
return 'bgra8unorm'
},
wgslLanguageFeatures: watch({
size: 7,
values: function values() {
debugger
AsObj.print('wgslLanguageFeatures.values')
return ['packed_4x8_integer_dot_product', 'unrestricted_pointer_parameters', 'subgroup_uniformity', 'subgroup_id', 'pointer_composite_access', 'readonly_and_readwrite_storage_textures', 'uniform_buffer_standard_layout']
},
}, 'gpu.wgslLanguageFeatures'),
requestAdapter: function requestAdapter() {
AsObj.print('gpu.requestAdapter:', arguments)
return {
then: function () {
arguments[0](watch({
features: watch({
size: 19,
values: function () {
return ['depth32float-stencil8', 'rg11b10ufloat-renderable', 'bgra8unorm-storage', 'texture-formats-tier1', 'texture-compression-bc', 'dual-source-blending', 'core-features-and-limits', 'float32-filterable', 'indirect-first-instance', 'float32-blendable', 'depth-clip-control', 'texture-compression-bc-sliced-3d', 'timestamp-query', 'texture-formats-tier2', 'clip-distances', 'shader-f16', 'primitive-index', 'texture-component-swizzle', 'subgroups']
}
}, 'gpu.requestAdapter.features'),
info: watch({ vendor: 'intel', architecture: 'gen-11', device: '', description: '', subgroupMinSize: 16 }, 'gpu.requestAdapter.info'),
limits: watch({
maxBufferSize: 2147483648,
maxStorageBufferBindingSize:2147483644
}, 'gpu.requestAdapter.limits'),
catch:function(){}
}, 'gpu.requestAdapter'));
return {
catch: function () {
return {
then: function () {
arguments[0]()
return {catch:function(){}}
}
}
}
};
},
catch: function () {
}
}
}
},'navigator.gpu')
Navigator.prototype.userAgentData = watch({
brands:[
{
"brand": "Google Chrome",
"version": "143"
},
{
"brand": "Chromium",
"version": "143"
},
{
"brand": "Not A(Brand",
"version": "24"
}
],
mobile: false,
platform: "Windows",
getHighEntropyValues: function getHighEntropyValues() {
if (arguments[0] + '' === 'architecture,bitness,model,platformVersion,uaFullVersion,wow64') {
return new Promise((resolve, reject) => {
const offer = {
"architecture": "x86",
"bitness": "64",
"brands": [
{
"brand": "Not:A-Brand",
"version": "99"
},
{
"brand": "Google Chrome",
"version": "145"
},
{
"brand": "Chromium",
"version": "145"
}
],
"mobile": false,
"model": "",
"platform": "Windows",
"platformVersion": "10.0.0",
"uaFullVersion": "145.0.7632.117",
"wow64": false
};
resolve(offer);
});
}
AsObj.print('getHighEntropyValues:::', arguments)
}
},'userAgentData')
navigator = {};
navigator.__proto__ = Navigator.prototype;
createTagProto('Location');
location = {
"ancestorOrigins": {},
"href": "https://www.neimanmarcus.com/",
"origin": "https://www.neimanmarcus.com",
"protocol": "https:",
"host": "www.neimanmarcus.com",
"hostname": "www.neimanmarcus.com",
"port": "",
"pathname": "/",
"search": "",
"hash": ""
};
location.__proto__ = Location.prototype;
location.toString = function toString() {
return this.href;
}
createTagProto('Screen');
Screen.prototype = Object.assign(Screen.prototype, {
availHeight: 824,
availLeft: 0,
availTop: 0,
availWidth: 1536,
colorDepth: 32,
height: 864,
isExtended: true,
onchange: null,
pixelDepth: 24,
width: 1536,
orientation: {
angle: 0,
type: "landscape-primary",
onchange: null
}
})
screen = {};
screen.__proto__ = Screen.prototype;
createTagProto('History',['replaceState']);
history = {};
history.__proto__ = History.prototype;
chrome = {
loadTimes: function loadTimes() { },
csi: function csi() { },
app: {
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
getDetails:function getDetails(){},
getIsInstalled:function getIsInstalled(){},
installState:function installState(){},
isInstalled: false,
runningState: function runningState(){}
},
}
createTagProto('Storage');
local = {
};
localStorage = {
getItem: function getItem(key) {
AsObj.print("localStorage.getItem::", arguments);
if (!local[key]) {
return null;
}
return local[key];
},
setItem: function setItem(key, value) {
AsObj.print("localStorage.setItem::", arguments);
local[key] = value;
},
clear: function clear() {
local = {};
},
removeItem: function removeItem(key) {
AsObj.print("localStorage.removeItem::", arguments);
delete local[key];
}
}
localStorage.__proto__ = Storage.prototype;
sessionStorage = {
getItem: function getItem(key) {
AsObj.print("sessionStorage.getItem::", arguments);
if (!local[key]) {
return null;
}
return local[key];
},
setItem: function setItem(key, value) {
AsObj.print("sessionStorage.setItem::", arguments);
local[key] = value;
},
clear: function clear() {
local = {};
},
removeItem: function removeItem(key) {
AsObj.print("sessionStorage.removeItem::", arguments);
delete local[key];
}
}
sessionStorage.__proto__ = Storage.prototype;
// window = watch(window, 'window');
// global = watch(global, 'global');
// globalThis = watch(globalThis, 'globalThis');
// self = watch(self, 'self');
// crypto = watch(crypto, 'crypto');
// performance = watch(performance, 'performance');
// document = watch(document, 'document');
// navigator = watch(navigator, 'navigator');
// location = watch(location, 'location');
// screen = watch(screen, 'screen');
// history = watch(history, 'history');
// localStorage = watch(localStorage, 'localStorage');
// sessionStorage = watch(sessionStorage, 'sessionStorage');
// chrome = watch(chrome, 'chrome');
require_('./sdk_leg.js');
let input = '';
// 收集数据
process.stdin.on('data', chunk => {
input += chunk;
});
process.stdin.on('end', async () => {
var config_data = JSON.parse(input);
var cryptoManager = await CaptchaSDKCorecc();
var encryptData = await buildEncryptedVerifyRequestcc(config_data, cryptoManager);
console.log(JSON.stringify(encryptData));
process.exit(0);
})

3
app/thread_count.json Normal file
View File

@@ -0,0 +1,3 @@
{
"thread_count": "1"
}

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

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

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1362
app/utils/database.py Normal file

File diff suppressed because it is too large Load Diff

223
app/utils/domain_utils.py Normal file
View File

@@ -0,0 +1,223 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :domain_utils.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:05
@explain : 域名工具类
'''
import re
import tldextract
from loguru import logger
def normalize_domain(domain):
"""
标准化域名
:param domain: 域名
:return: str - 标准化后的域名
"""
try:
# 转换为小写
domain = domain.lower()
# 去除空格
domain = domain.strip()
# 去除协议
domain = re.sub(r'^https?://', '', domain)
# 去除路径和查询参数
domain = domain.split('/')[0]
domain = domain.split('?')[0]
# 去除端口
domain = domain.split(':')[0]
# 只保留主域
ext = tldextract.extract(domain)
if ext.domain and ext.suffix:
domain = f"{ext.domain}.{ext.suffix}"
# 验证域名格式
if not is_valid_domain(domain):
return None
return domain
except Exception as e:
logger.error(f"标准化域名出错: {e}")
return None
def is_valid_domain(domain):
"""
验证域名格式
:param domain: 域名
:return: bool - 是否有效
"""
try:
# 域名格式正则
pattern = r'^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, domain))
except Exception as e:
logger.error(f"验证域名格式出错: {e}")
return False
def extract_tld(domain):
"""
提取顶级域名
:param domain: 域名
:return: str - 顶级域名
"""
try:
ext = tldextract.extract(domain)
return ext.suffix
except Exception as e:
logger.error(f"提取顶级域名出错: {e}")
return ''
def extract_domain(domain):
"""
提取主域名
:param domain: 域名
:return: str - 主域名
"""
try:
ext = tldextract.extract(domain)
if ext.domain and ext.suffix:
return f"{ext.domain}.{ext.suffix}"
return domain
except Exception as e:
logger.error(f"提取主域名出错: {e}")
return domain
def is_com_or_net(domain):
"""
检查是否为 .com 或 .net 域名
:param domain: 域名
:return: bool - 是否为 .com 或 .net 域名
"""
try:
tld = extract_tld(domain)
return tld in ['com', 'net']
except Exception as e:
logger.error(f"检查域名后缀出错: {e}")
return False
def generate_domain_variants(domain):
"""
生成域名变体
:param domain: 域名
:return: list - 域名变体列表
"""
try:
variants = []
# 原始域名
variants.append(domain)
# 添加 www
if not domain.startswith('www.'):
variants.append(f"www.{domain}")
# 移除 www
if domain.startswith('www.'):
variants.append(domain[4:])
return variants
except Exception as e:
logger.error(f"生成域名变体出错: {e}")
return [domain]
def parse_domain_status(status_code):
"""
解析域名状态码
:param status_code: 状态码
:return: str - 状态描述
"""
status_map = {
0: '待检测',
1: '可注册',
2: '已注册',
3: '宽限期',
4: '赎回期',
5: '删除期',
6: 'clientHold',
7: 'serverHold',
8: '状态未知',
9: '检测失败'
}
return status_map.get(status_code, '未知')
def parse_use_status(status_code):
"""
解析使用状态码
:param status_code: 状态码
:return: str - 状态描述
"""
status_map = {
0: '未使用',
1: '已经使用',
2: '已经卖出',
3: '已经预定'
}
return status_map.get(status_code, '未知')
def parse_detect_status(status_code):
"""
解析检测状态码
:param status_code: 状态码
:return: str - 状态描述
"""
status_map = {
0: '待检测',
1: '检测中',
2: '正常',
3: '黑名单',
4: '检测失败',
5: '暂停检测'
}
return status_map.get(status_code, '未知')
def parse_source_type(source_type):
"""
解析来源类型
:param source_type: 来源类型
:return: str - 来源描述
"""
source_map = {
1: '聚名一口价',
2: '聚名过期删除',
3: 'zone file',
4: '搜索引擎采集',
5: '企业目录采集',
6: '手工录入',
7: 'TXT 导入',
8: '第三方接口',
9: '其它'
}
return source_map.get(source_type, '未知')

157
app/utils/http_utils.py Normal file
View File

@@ -0,0 +1,157 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :http_utils.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 0:06
@explain : HTTP工具类
'''
import requests
from curl_cffi import requests as curl_requests
from loguru import logger
class HTTPUtils:
"""
HTTP工具类
"""
@staticmethod
def get(url, headers=None, params=None, timeout=10, proxies=None, use_curl=False):
"""
发送GET请求
:param url: 请求URL
:param headers: 请求头
:param params: 查询参数
:param timeout: 超时时间
:param proxies: 代理
:param use_curl: 是否使用curl_cffi
:return: requests.Response - 响应对象
"""
try:
if use_curl:
# 使用curl_cffi模拟浏览器
response = curl_requests.get(url, headers=headers, params=params, timeout=timeout, proxies=proxies, impersonate='chrome')
else:
# 使用requests
response = requests.get(url, headers=headers, params=params, timeout=timeout, proxies=proxies)
response.raise_for_status() # 检查状态码
return response
except Exception as e:
logger.error(f"GET请求失败: {url}, 错误: {e}")
return None
@staticmethod
def post(url, headers=None, data=None, json=None, timeout=10, proxies=None, use_curl=False):
"""
发送POST请求
:param url: 请求URL
:param headers: 请求头
:param data: 表单数据
:param json: JSON数据
:param timeout: 超时时间
:param proxies: 代理
:param use_curl: 是否使用curl_cffi
:return: requests.Response - 响应对象
"""
try:
if use_curl:
# 使用curl_cffi模拟浏览器
response = curl_requests.post(url, headers=headers, data=data, json=json, timeout=timeout, proxies=proxies, impersonate='chrome')
else:
# 使用requests
response = requests.post(url, headers=headers, data=data, json=json, timeout=timeout, proxies=proxies)
response.raise_for_status() # 检查状态码
return response
except Exception as e:
logger.error(f"POST请求失败: {url}, 错误: {e}")
return None
@staticmethod
def get_random_user_agent():
"""
获取随机用户代理
:return: str - 用户代理
"""
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/138.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/146.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15'
]
import random
return random.choice(user_agents)
@staticmethod
def get_default_headers():
"""
获取默认请求头
:return: dict - 请求头
"""
return {
'User-Agent': HTTPUtils.get_random_user_agent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1'
}
@staticmethod
def retry_request(func, max_retries=3, delay=1):
"""
重试请求
:param func: 请求函数
:param max_retries: 最大重试次数
:param delay: 重试延迟
:return: 函数返回值
"""
import time
for i in range(max_retries):
try:
result = func()
if result:
return result
except Exception as e:
logger.warning(f"请求失败,第 {i+1} 次重试: {e}")
if i < max_retries - 1:
time.sleep(delay)
return None
@staticmethod
def check_proxy(proxy):
"""
检查代理是否可用
:param proxy: 代理URL
:return: bool - 是否可用
"""
try:
proxies = {
'http': proxy,
'https': proxy
}
response = requests.get('https://www.baidu.com', proxies=proxies, timeout=5)
return response.status_code == 200
except Exception as e:
logger.error(f"代理检查失败: {proxy}, 错误: {e}")
return False

74
check_database.py Normal file
View File

@@ -0,0 +1,74 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :check_database.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 21:40
@explain : 检查数据库表结构和注释
'''
import psycopg2
from app.config import config
def check_database():
"""
检查数据库表结构和注释
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
cur = conn.cursor()
print("数据库连接成功")
# 检查所有表
print("\n=== 所有表 ===")
cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
tables = cur.fetchall()
for table in tables:
print(f"表名: {table[0]}")
# 检查domains表结构
print("\n=== domains表结构 ===")
cur.execute("\d+ domains.txt")
result = cur.fetchall()
for row in result:
print(row)
# 检查其他表结构
print("\n=== detect_tasks表结构 ===")
cur.execute("\d+ detect_tasks")
result = cur.fetchall()
for row in result:
print(row)
print("\n=== domain_blacklist表结构 ===")
cur.execute("\d+ domain_blacklist")
result = cur.fetchall()
for row in result:
print(row)
print("\n=== domain_detections表结构 ===")
cur.execute("\d+ domain_detections")
result = cur.fetchall()
for row in result:
print(row)
except Exception as e:
print(f"检查数据库失败: {e}")
finally:
if cur:
cur.close()
if conn:
conn.close()
if __name__ == "__main__":
check_database()

69
check_db_structure.py Normal file
View File

@@ -0,0 +1,69 @@
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :check_db_structure.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/9 21:50
@explain : 检查数据库表结构和索引
'''
import psycopg2
from app.config import config
def check_db_structure():
"""
检查数据库表结构和索引
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
cur = conn.cursor()
print("数据库连接成功")
# 检查domains表结构
print("\n=== domains表结构 ===")
cur.execute("SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'domains.txt'")
for row in cur.fetchall():
print(row)
# 检查domains表索引
print("\n=== domains表索引 ===")
cur.execute("SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'domains.txt'")
for row in cur.fetchall():
print(row)
# 检查其他表结构
print("\n=== detect_tasks表结构 ===")
cur.execute("SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'detect_tasks'")
for row in cur.fetchall():
print(row)
print("\n=== domain_blacklist表结构 ===")
cur.execute("SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'domain_blacklist'")
for row in cur.fetchall():
print(row)
print("\n=== domain_detections表结构 ===")
cur.execute("SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'domain_detections'")
for row in cur.fetchall():
print(row)
except Exception as e:
print(f"检查数据库失败: {e}")
finally:
if cur:
cur.close()
if conn:
conn.close()
if __name__ == "__main__":
check_db_structure()

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :create_sensitive_words_table.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/10 03:30
@explain : 创建敏感词表
'''
import psycopg2
from app.config import config
def create_sensitive_words_table():
"""
创建敏感词表
"""
try:
# 连接数据库
conn = psycopg2.connect(
host=config.DB_HOST,
port=config.DB_PORT,
database=config.DB_DATABASE,
user=config.DB_USER,
password=config.DB_PASSWORD
)
print(f"成功连接到数据库: {config.DB_HOST}:{config.DB_PORT}/{config.DB_DATABASE}")
# 创建游标
cur = conn.cursor()
# 创建敏感词表
create_table_sql = """
CREATE TABLE IF NOT EXISTS sensitive_words (
id SERIAL PRIMARY KEY,
word VARCHAR(255) UNIQUE NOT NULL,
category VARCHAR(50) DEFAULT 'default',
priority INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
cur.execute(create_table_sql)
print("创建敏感词表成功")
# 添加表注释
add_table_comment_sql = """
COMMENT ON TABLE sensitive_words IS '敏感词表,存储需要过滤的敏感词'
"""
cur.execute(add_table_comment_sql)
print("添加表注释成功")
# 添加字段注释
add_column_comments_sql = """
COMMENT ON COLUMN sensitive_words.id IS '主键ID';
COMMENT ON COLUMN sensitive_words.word IS '敏感词';
COMMENT ON COLUMN sensitive_words.category IS '敏感词分类';
COMMENT ON COLUMN sensitive_words.priority IS '优先级,数字越大优先级越高';
COMMENT ON COLUMN sensitive_words.created_at IS '创建时间';
COMMENT ON COLUMN sensitive_words.updated_at IS '更新时间';
"""
cur.execute(add_column_comments_sql)
print("添加字段注释成功")
# 创建索引
create_index_sql = """
CREATE INDEX IF NOT EXISTS idx_sensitive_words_word ON sensitive_words(word);
CREATE INDEX IF NOT EXISTS idx_sensitive_words_category ON sensitive_words(category);
"""
cur.execute(create_index_sql)
print("创建索引成功")
# 创建更新触发器
create_function_sql = """
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
cur.execute(create_function_sql)
print("创建更新函数成功")
# 检查触发器是否存在
check_trigger_sql = """
SELECT COUNT(*) FROM pg_trigger WHERE tgname = 'update_sensitive_words_updated_at'
"""
cur.execute(check_trigger_sql)
trigger_exists = cur.fetchone()[0] > 0
if not trigger_exists:
create_trigger_sql = """
CREATE TRIGGER update_sensitive_words_updated_at
BEFORE UPDATE ON sensitive_words
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
"""
cur.execute(create_trigger_sql)
print("创建触发器成功")
else:
print("触发器已存在,跳过创建")
# 提交事务
conn.commit()
print("事务提交成功")
# 关闭游标和连接
cur.close()
conn.close()
print("连接关闭成功")
return True
except Exception as e:
print(f"创建敏感词表失败: {e}")
return False
if __name__ == "__main__":
print("开始创建敏感词表...")
success = create_sensitive_words_table()
if success:
print("敏感词表创建成功!")
else:
print("敏感词表创建失败!")

View File

@@ -0,0 +1,27 @@
-- 创建敏感词表
CREATE TABLE IF NOT EXISTS sensitive_words (
id SERIAL PRIMARY KEY,
word VARCHAR(255) UNIQUE NOT NULL,
category VARCHAR(50) DEFAULT 'default',
priority INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_sensitive_words_word ON sensitive_words(word);
CREATE INDEX IF NOT EXISTS idx_sensitive_words_category ON sensitive_words(category);
-- 更新updated_at触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_sensitive_words_updated_at
BEFORE UPDATE ON sensitive_words
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More