413 lines
16 KiB
Python
413 lines
16 KiB
Python
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()
|