first commit
This commit is contained in:
12
.env
Normal file
12
.env
Normal 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
12
.env.example
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 忽略整个目录
|
||||||
|
/node_modules/
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# 忽略所有压缩文件
|
||||||
|
*.spec
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
7
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal 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
10
.idea/domainScanDemo.iml
generated
Normal 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>
|
||||||
22
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
22
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
221
DEPLOYMENT_GUIDE.md
Normal 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
192
README_acw_sc__v2.md
Normal 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版本
|
||||||
|
- 包含完整的使用示例
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
感谢所有为逆向工程和反爬虫技术研究做出贡献的开发者。
|
||||||
76
add_backlink_count_column.py
Normal file
76
add_backlink_count_column.py
Normal 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字段添加失败!")
|
||||||
8
add_backlink_count_column.sql
Normal file
8
add_backlink_count_column.sql
Normal 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
51
add_expire_date_field.py
Normal 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
59
add_fields_to_domains.py
Normal 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()
|
||||||
58
add_sensitive_words_table.py
Normal file
58
add_sensitive_words_table.py
Normal 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
6
app/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
'''
|
||||||
|
域名库系统
|
||||||
|
'''
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-311.pyc
Normal file
BIN
app/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-39.pyc
Normal file
BIN
app/__pycache__/config.cpython-39.pyc
Normal file
Binary file not shown.
1257
app/chinaz.js
Normal file
1257
app/chinaz.js
Normal file
File diff suppressed because one or more lines are too long
196
app/config.py
Normal file
196
app/config.py
Normal 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)
|
||||||
0
app/config/sensitive_words.txt
Normal file
0
app/config/sensitive_words.txt
Normal file
4
app/core/__init__.py
Normal file
4
app/core/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
'''
|
||||||
|
核心功能模块
|
||||||
|
'''
|
||||||
BIN
app/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/core/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/domain_collector.cpython-311.pyc
Normal file
BIN
app/core/__pycache__/domain_collector.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/domain_collector.cpython-39.pyc
Normal file
BIN
app/core/__pycache__/domain_collector.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/export_manager.cpython-311.pyc
Normal file
BIN
app/core/__pycache__/export_manager.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/export_manager.cpython-39.pyc
Normal file
BIN
app/core/__pycache__/export_manager.cpython-39.pyc
Normal file
Binary file not shown.
281
app/core/detect_engine.py
Normal file
281
app/core/detect_engine.py
Normal 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
|
||||||
261
app/core/domain_collector.py
Normal file
261
app/core/domain_collector.py
Normal 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
|
||||||
150
app/core/domain_processor.py
Normal file
150
app/core/domain_processor.py
Normal 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
273
app/core/export_manager.py
Normal 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
162
app/core/task_scheduler.py
Normal 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
10
app/credentials.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"juming": {
|
||||||
|
"email": "chaofanai1998@gmail.com",
|
||||||
|
"password": "llzz123,./"
|
||||||
|
},
|
||||||
|
"juziseo": {
|
||||||
|
"email": "mamian",
|
||||||
|
"password": "Abc123456"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/detect_options.json
Normal file
13
app/detect_options.json
Normal 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
|
||||||
|
}
|
||||||
4
app/detectors/__init__.py
Normal file
4
app/detectors/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
'''
|
||||||
|
检测插件模块
|
||||||
|
'''
|
||||||
130
app/detectors/aizhan_detector.py
Normal file
130
app/detectors/aizhan_detector.py
Normal 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
|
||||||
151
app/detectors/baidu_detector.py
Normal file
151
app/detectors/baidu_detector.py
Normal 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
70
app/detectors/base.py
Normal 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)}
|
||||||
130
app/detectors/chinaz_detector.py
Normal file
130
app/detectors/chinaz_detector.py
Normal 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
|
||||||
80
app/detectors/google_detector.py
Normal file
80
app/detectors/google_detector.py
Normal 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}
|
||||||
228
app/detectors/jucha_detector.py
Normal file
228
app/detectors/jucha_detector.py
Normal 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
|
||||||
214
app/detectors/juziseo_detector.py
Normal file
214
app/detectors/juziseo_detector.py
Normal 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
|
||||||
107
app/detectors/qihu360_detector.py
Normal file
107
app/detectors/qihu360_detector.py
Normal 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 []
|
||||||
128
app/detectors/rdap_detector.py
Normal file
128
app/detectors/rdap_detector.py
Normal 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
|
||||||
193
app/detectors/wayback_detector.py
Normal file
193
app/detectors/wayback_detector.py
Normal 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
3
app/domain_suffixes.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"suffixes": ".com,.net"
|
||||||
|
}
|
||||||
BIN
app/jucha_cookies.pkl
Normal file
BIN
app/jucha_cookies.pkl
Normal file
Binary file not shown.
BIN
app/juming_cookies.pkl
Normal file
BIN
app/juming_cookies.pkl
Normal file
Binary file not shown.
BIN
app/juziseo_cookies.pkl
Normal file
BIN
app/juziseo_cookies.pkl
Normal file
Binary file not shown.
68
app/main.py
Normal file
68
app/main.py
Normal 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
4
app/proxy_config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"proxy_enable": false,
|
||||||
|
"proxy_url": ""
|
||||||
|
}
|
||||||
15108
app/sdk_leg.js
Normal file
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
897
app/sdk_leg_env.js
Normal 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
3
app/thread_count.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"thread_count": "1"
|
||||||
|
}
|
||||||
4
app/ui/__init__.py
Normal file
4
app/ui/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
'''
|
||||||
|
UI 模块
|
||||||
|
'''
|
||||||
BIN
app/ui/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/domain_filter.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/domain_filter.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/domain_filter.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/domain_filter.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/domain_import.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/domain_import.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/domain_import.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/domain_import.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/juming_crawler.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/juming_crawler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/juming_crawler.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/juming_crawler.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/main_window.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/main_window.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/main_window.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/main_window.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/sensitive_words.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/sensitive_words.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/sensitive_words.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/sensitive_words.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/system_settings.cpython-311.pyc
Normal file
BIN
app/ui/__pycache__/system_settings.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/ui/__pycache__/system_settings.cpython-39.pyc
Normal file
BIN
app/ui/__pycache__/system_settings.cpython-39.pyc
Normal file
Binary file not shown.
1335
app/ui/domain_filter.py
Normal file
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
510
app/ui/domain_import.py
Normal 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
577
app/ui/juming_crawler.py
Normal 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
117
app/ui/main_window.py
Normal 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
284
app/ui/sensitive_words.py
Normal 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
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
4
app/utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
'''
|
||||||
|
工具类模块
|
||||||
|
'''
|
||||||
BIN
app/utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/utils/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/database.cpython-311.pyc
Normal file
BIN
app/utils/__pycache__/database.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/database.cpython-39.pyc
Normal file
BIN
app/utils/__pycache__/database.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/domain_utils.cpython-311.pyc
Normal file
BIN
app/utils/__pycache__/domain_utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/domain_utils.cpython-39.pyc
Normal file
BIN
app/utils/__pycache__/domain_utils.cpython-39.pyc
Normal file
Binary file not shown.
1362
app/utils/database.py
Normal file
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
223
app/utils/domain_utils.py
Normal 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
157
app/utils/http_utils.py
Normal 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
74
check_database.py
Normal 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
69
check_db_structure.py
Normal 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()
|
||||||
128
create_sensitive_words_table.py
Normal file
128
create_sensitive_words_table.py
Normal 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("敏感词表创建失败!")
|
||||||
27
create_sensitive_words_table.sql
Normal file
27
create_sensitive_words_table.sql
Normal 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();
|
||||||
BIN
detect/__pycache__/aizhan.cpython-311.pyc
Normal file
BIN
detect/__pycache__/aizhan.cpython-311.pyc
Normal file
Binary file not shown.
BIN
detect/__pycache__/aizhan.cpython-39.pyc
Normal file
BIN
detect/__pycache__/aizhan.cpython-39.pyc
Normal file
Binary file not shown.
BIN
detect/__pycache__/baidu.cpython-311.pyc
Normal file
BIN
detect/__pycache__/baidu.cpython-311.pyc
Normal file
Binary file not shown.
BIN
detect/__pycache__/baidu.cpython-39.pyc
Normal file
BIN
detect/__pycache__/baidu.cpython-39.pyc
Normal file
Binary file not shown.
BIN
detect/__pycache__/c360.cpython-311.pyc
Normal file
BIN
detect/__pycache__/c360.cpython-311.pyc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user