autool-dispatcher/calculate/dpi_database_test.py
2026-06-17 19:50:39 +08:00

413 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ahocorasick
import pandas as pd
import os
import sys
from pathlib import Path
from collections import defaultdict
# ================= 配置区域 =================
TARGET_DATE = "20260331"
LIB_DATE = "20260224"
# 路径配置
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = SCRIPT_DIR.parent
APP_LIST_PATH = PROJECT_ROOT / f"Lib/{LIB_DATE}/input/TPDPI_app_list_{LIB_DATE}.csv"
URL_LIST_PATH = PROJECT_ROOT / f"Lib/{LIB_DATE}/input/TPDPI_url_lib_{LIB_DATE}.csv"
TRAFFIC_DATA_PATH = PROJECT_ROOT / f"TrafficData/traffic_summary/{TARGET_DATE}/traffic_summary.csv"
OUTPUT_DIR = PROJECT_ROOT / f"TrafficData/traffic_summary/{TARGET_DATE}/result"
# 输出文件定义
OUTPUT_APP_PROFILE = OUTPUT_DIR / f"app_traffic_profile_{TARGET_DATE}.csv"
OUTPUT_UNMATCHED_FILE = OUTPUT_DIR / f"unmatched_domains_{TARGET_DATE}.csv"
OUTPUT_DETAIL_FILE = OUTPUT_DIR / f"traffic_detail_log_{TARGET_DATE}.csv"
# ===========================================
def format_bytes(size):
"""将字节转换为易读格式 (B, KB, MB, GB)保留2位小数"""
power = 2 ** 10
n = 0
power_labels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'}
while size > power:
size /= power
n += 1
if n == 0:
return f"{int(size)} B"
return f"{size:.2f} {power_labels[n]}"
class DpiDatabaseTester:
def __init__(self):
self.automaton = ahocorasick.Automaton()
# 基础查找表
self.tp_packages = {}
self.tp_pkg_string = {}
self.pkg_to_tp_mark = {}
# 核心统计容器: Key = Source App ID
self.source_app_stats = defaultdict(lambda: {
'app_label': '',
'total_bytes': 0, # 包含 IP 流量 + 域名流量
'total_domain_bytes': 0, # 仅包含域名流量
'self_bytes': 0,
'server_bytes': 0,
'unrec_bytes': 0,
'self_count': 0,
'server_count': 0,
'unrec_count': 0,
'components': defaultdict(lambda: {'bytes': 0, 'is_self': False})
})
# 存储未匹配记录 (用于 unmatched 文件)
self.unmatched_records = []
# 存储所有流水的详细记录 (用于 detail_log 文件)
self.all_detail_records = []
def check_paths(self):
files = [APP_LIST_PATH, URL_LIST_PATH, TRAFFIC_DATA_PATH]
missing = [f for f in files if not f.exists()]
if missing:
print(f"[Error] 缺少文件: {[f.name for f in missing]}")
sys.exit(1)
if not OUTPUT_DIR.exists():
os.makedirs(OUTPUT_DIR)
def load_app_list(self):
print(f"[1/5] 加载应用定义 (TPMark): {APP_LIST_PATH.name}")
try:
df = pd.read_csv(APP_LIST_PATH)
for row in df.itertuples():
tp_mark = str(row.app_name).strip()
pkg_raw = str(row.package_name) if pd.notna(row.package_name) else ""
self.tp_pkg_string[tp_mark] = pkg_raw
pkg_list = [p.strip() for p in pkg_raw.split(',') if p.strip()]
if tp_mark not in self.tp_packages:
self.tp_packages[tp_mark] = set()
self.tp_packages[tp_mark].update(pkg_list)
for pkg in pkg_list:
self.pkg_to_tp_mark[pkg] = tp_mark
print(f" - 已加载 {len(self.tp_packages)} 个应用定义。")
except Exception as e:
print(f"[Error] 加载应用列表失败: {e}")
sys.exit(1)
def load_url_list(self):
print(f"[2/5] 构建 AC 自动机: {URL_LIST_PATH.name}")
try:
df = pd.read_csv(URL_LIST_PATH)
df.columns = [c.strip().lower() for c in df.columns]
count = 0
for row in df.itertuples():
suffix = str(row.url).strip().lower()
tp_mark = str(row.app).strip()
if suffix:
self.automaton.add_word(suffix, (suffix, tp_mark))
count += 1
self.automaton.make_automaton()
print(f" - 加载了 {count} 条 URL 规则。")
except Exception as e:
print(f"[Error] 加载 URL 失败: {e}")
sys.exit(1)
def search_domain(self, domain):
"""返回 (TPMark, Pattern)"""
domain_lower = domain.lower()
valid_matches = []
# iter 返回 (end_index, (pattern, tp_mark))
for end_index, (pattern, tp_mark) in self.automaton.iter(domain_lower):
if end_index != len(domain_lower) - 1: continue
start_index = end_index - len(pattern) + 1
if start_index == 0 or domain_lower[start_index - 1] == '.':
valid_matches.append((len(pattern), tp_mark, pattern))
if not valid_matches: return None, None
# 取最长匹配
best = max(valid_matches, key=lambda x: x[0])
return best[1], best[2]
def process_traffic(self):
print(f"[3/5] 分析流量归属: {TRAFFIC_DATA_PATH.name}")
try:
df = pd.read_csv(TRAFFIC_DATA_PATH)
df.columns = [c.strip().replace(' ', '_').replace('(', '_').replace(')', '') for c in df.columns]
processed = 0
for row in df.itertuples():
source_app_id = str(row.App_ID).strip()
# Filter: 跳过 unknown 和 root
if source_app_id.lower() in ['unknown', 'root']:
continue
domain = str(row.Domain).strip()
size = float(row.Traffic_Size_Bytes)
source_app_label = str(row.App_Name).strip()
stats = self.source_app_stats[source_app_id]
stats['app_label'] = source_app_label
stats['total_bytes'] += size
# === 临时变量,用于构建 Detail Record ===
detail_tp_result = ""
detail_tp_url = ""
detail_is_self = False
detail_is_ip = False
# === 判断是否为无域名IP流量 (model_data) ===
is_ip_flow = domain.startswith("model_data:")
if is_ip_flow:
detail_is_ip = True
detail_tp_result = "IP Flow" # 或保持空,根据需要
continue
stats['unrec_bytes'] += size
stats['unrec_count'] += 1
# IP 流量也记入 unmatched_records 供 unmatched文件使用
self.unmatched_records.append({
'app_id': source_app_id,
'app_label': source_app_label,
'input_url': domain,
'traffic_size': size,
'is_ip_flow': True
})
else:
# === 普通域名流量处理 ===
stats['total_domain_bytes'] += size
# 匹配域名
matched_tp_mark, matched_pattern = self.search_domain(domain)
if matched_tp_mark:
# 记录详情
detail_tp_result = matched_tp_mark
detail_tp_url = matched_pattern
owner_packages = self.tp_packages.get(matched_tp_mark, set())
is_self = source_app_id in owner_packages
detail_is_self = is_self
if is_self:
stats['self_bytes'] += size
stats['self_count'] += 1
else:
stats['server_bytes'] += size
stats['server_count'] += 1
comp = stats['components'][matched_tp_mark]
comp['bytes'] += size
comp['is_self'] = is_self
else:
# 域名未匹配
detail_tp_result = "[|Unmatched|]"
stats['unrec_bytes'] += size
stats['unrec_count'] += 1
self.unmatched_records.append({
'app_id': source_app_id,
'app_label': source_app_label,
'input_url': domain,
'traffic_size': size,
'is_ip_flow': False
})
if detail_tp_result == "[|Unmatched|]":
continue
# === 保存详细流水记录 ===
# 注意:此时还没计算 Ratio因为 total_bytes 还在累加中
# 存下原始数据,在导出时计算 Ratio
self.all_detail_records.append({
'tp_result': detail_tp_result if detail_tp_result else "",
'app_name': source_app_label,
'app_label': source_app_label,
'app_id': source_app_id,
'input_url': domain,
'tp_url': detail_tp_url,
'self': detail_is_self,
'traffic_count_bytes': size,
'is_ip_flow': detail_is_ip
})
processed += 1
if processed % 50000 == 0:
print(f" - 已处理 {processed} 行...")
except Exception as e:
print(f"[Error] 处理流量失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def export_app_profile(self):
print(f"[4/5] 生成画像报告: {OUTPUT_APP_PROFILE.name}")
data_rows = []
for app_id, stats in self.source_app_stats.items():
total = stats['total_bytes']
if total == 0: continue
tp_mark = self.pkg_to_tp_mark.get(app_id, "")
app_label = stats['app_label']
app_name = app_label
self_ratio = (stats['self_bytes'] / total) * 100
recog_ratio = ((stats['self_bytes'] + stats['server_bytes']) / total) * 100
analysis_list = []
sorted_comps = sorted(stats['components'].items(), key=lambda x: x[1]['bytes'], reverse=True)
for comp_tp_mark, comp_data in sorted_comps:
comp_bytes = comp_data['bytes']
comp_percent = (comp_bytes / total) * 100
is_self_flag = '0' if comp_data['is_self'] else '1'
comp_pkg_str = self.tp_pkg_string.get(comp_tp_mark, "")
analysis_item = [
comp_tp_mark, comp_pkg_str, is_self_flag, f"{comp_percent:.2f}%", int(comp_bytes)
]
analysis_list.append(analysis_item)
data_rows.append({
'app_id': app_id,
'app_label': app_label,
'app_name': app_name,
'tp_mark': tp_mark,
'total_traffic(Bytes)': int(total),
'self_traffic_count(Bytes)': format_bytes(stats['self_bytes']),
'server_traffic_count(Bytes)': format_bytes(stats['server_bytes']),
'unrecognized_traffic_count(Bytes)': format_bytes(stats['unrec_bytes']),
'self_label_count': stats['self_count'],
'server_label_count': stats['server_count'],
'unrecognized_label_count': stats['unrec_count'],
'self_ratio': f"{self_ratio:.2f}",
'recognition_ratio': f"{recog_ratio:.2f}",
'traffic_analysis': str(analysis_list)
})
df = pd.DataFrame(data_rows)
cols = [
'app_id', 'app_label', 'app_name', 'tp_mark',
'total_traffic(Bytes)', 'self_traffic_count(Bytes)', 'server_traffic_count(Bytes)',
'unrecognized_traffic_count(Bytes)',
'self_label_count', 'server_label_count', 'unrecognized_label_count',
'self_ratio', 'recognition_ratio', 'traffic_analysis'
]
if not df.empty:
df = df[cols]
df.sort_values(by='total_traffic(Bytes)', ascending=False, inplace=True)
df.to_csv(OUTPUT_APP_PROFILE, index=False)
print(f" - 完成: {OUTPUT_APP_PROFILE.name}")
def export_unmatched_list(self):
print(f"[5/5] 生成未匹配域名列表: {OUTPUT_UNMATCHED_FILE.name}")
if not self.unmatched_records:
print(" - 无未匹配记录。")
else:
export_rows = []
for record in self.unmatched_records:
app_id = record['app_id']
size = record['traffic_size']
is_ip = record['is_ip_flow']
stats = self.source_app_stats[app_id]
total_bytes = stats['total_bytes']
total_domain_bytes = stats['total_domain_bytes']
traffic_ratio = (size / total_bytes * 100) if total_bytes > 0 else 0
if is_ip:
domain_ratio_str = "0.00%"
else:
domain_ratio = (size / total_domain_bytes * 100) if total_domain_bytes > 0 else 0
domain_ratio_str = f"{domain_ratio:.2f}%"
export_rows.append({
'app_name': record['app_label'],
'app_label': record['app_label'],
'app_id': app_id,
'input_url': record['input_url'],
'traffic_count(Bytes)': int(size),
'traffic_ratio': f"{traffic_ratio:.2f}%",
'domain_traffic_ratio': domain_ratio_str,
'organization': ''
})
df = pd.DataFrame(export_rows)
if not df.empty:
df.sort_values(by='traffic_count(Bytes)', ascending=False, inplace=True)
df.to_csv(OUTPUT_UNMATCHED_FILE, index=False)
print(f" - 完成: {OUTPUT_UNMATCHED_FILE.name}")
def export_detail_log(self):
print(f"[Bonus] 生成详细流水日志: {OUTPUT_DETAIL_FILE.name}")
if not self.all_detail_records:
print(" - 无流水记录。")
return
export_rows = []
# 批量处理,提升速度
for r in self.all_detail_records:
app_id = r['app_id']
size = r['traffic_count_bytes']
is_ip = r['is_ip_flow']
# 获取该 App 的统计数据以计算 Ratio
stats = self.source_app_stats[app_id]
total_bytes = stats['total_bytes']
total_domain_bytes = stats['total_domain_bytes']
# 1. Total Ratio
ratio = (size / total_bytes * 100) if total_bytes > 0 else 0
# 2. Domain Ratio
if is_ip:
domain_ratio_str = "0.00%"
else:
d_ratio = (size / total_domain_bytes * 100) if total_domain_bytes > 0 else 0
domain_ratio_str = f"{d_ratio:.2f}%"
export_rows.append({
'tp_result': r['tp_result'],
'app_name': r['app_name'],
'app_label': r['app_label'],
'app_id': app_id,
'input_url': r['input_url'],
'tp_url': r['tp_url'],
'self': str(r['self']), # 转字符串
'traffic_count(Bytes)': int(size),
'traffic_ratio': f"{ratio:.2f}%",
'domain_traffic_ratio': domain_ratio_str,
'organization': ''
})
df = pd.DataFrame(export_rows)
# 排序建议:先按 App ID 聚类,再按流量大小降序
if not df.empty:
df.sort_values(by=['app_id', 'traffic_count(Bytes)'], ascending=[True, False], inplace=True)
df.to_csv(OUTPUT_DETAIL_FILE, index=False)
print(f" - 完成: {OUTPUT_DETAIL_FILE.name}")
if __name__ == "__main__":
tester = DpiDatabaseTester()
tester.check_paths()
tester.load_app_list()
tester.load_url_list()
tester.process_traffic()
tester.export_app_profile()
tester.export_unmatched_list()
tester.export_detail_log()