463 lines
18 KiB
Python
463 lines
18 KiB
Python
import os
|
||
import csv
|
||
from collections import defaultdict
|
||
|
||
|
||
def _decode_mixed_csv_line(raw_line):
|
||
for encoding in ('utf-8-sig', 'utf-8', 'gb18030'):
|
||
try:
|
||
return raw_line.decode(encoding)
|
||
except UnicodeDecodeError:
|
||
continue
|
||
return raw_line.decode('gb18030', errors='replace')
|
||
|
||
|
||
def _read_csv_lines(csv_path):
|
||
with open(csv_path, 'rb') as f:
|
||
return [_decode_mixed_csv_line(line) for line in f]
|
||
|
||
|
||
def _csv_value(row, index):
|
||
if index >= len(row):
|
||
return ''
|
||
return str(row[index] or '').strip()
|
||
|
||
|
||
def _latest_task_key(task_time, seq):
|
||
task_time = str(task_time or '').strip()
|
||
return (1 if task_time else 0, task_time, seq)
|
||
|
||
|
||
def _load_latest_task_rows(task_paths, package_idx, time_idx, allowed_packages=None):
|
||
rows_by_package = {}
|
||
best_keys = {}
|
||
seq = 0
|
||
|
||
for task_path in task_paths:
|
||
if not os.path.exists(task_path):
|
||
continue
|
||
|
||
try:
|
||
reader = csv.reader(_read_csv_lines(task_path))
|
||
next(reader, None)
|
||
for row in reader:
|
||
package_name = _csv_value(row, package_idx)
|
||
if allowed_packages is not None and package_name not in allowed_packages:
|
||
continue
|
||
if not package_name:
|
||
continue
|
||
|
||
seq += 1
|
||
key = _latest_task_key(_csv_value(row, time_idx), seq)
|
||
if package_name in best_keys and key <= best_keys[package_name]:
|
||
continue
|
||
|
||
best_keys[package_name] = key
|
||
rows_by_package[package_name] = row
|
||
except Exception as e:
|
||
print(f"读取 {task_path} 时出错: {e}")
|
||
|
||
return rows_by_package
|
||
|
||
|
||
def _normalize_success_detail(detail_info):
|
||
detail_info = str(detail_info or '').strip()
|
||
if '|' not in detail_info:
|
||
return detail_info
|
||
|
||
parts = detail_info.split('|', 1)
|
||
if len(parts) > 1:
|
||
return parts[1].strip()
|
||
return detail_info
|
||
|
||
|
||
def extract_second_level_domain(domain):
|
||
if not domain:
|
||
return ""
|
||
domain = str(domain).lower().replace('http://', '').replace('https://', '').strip().strip('"\'')
|
||
parts = domain.split('.')
|
||
if len(parts) <= 2:
|
||
return domain
|
||
return "*." + ".".join(parts[-2:])
|
||
|
||
def load_package_mapping(current_dir):
|
||
package_to_app = {}
|
||
app_to_package = {}
|
||
package_list_path = os.path.join(current_dir, 'package_list.csv')
|
||
if not os.path.exists(package_list_path):
|
||
print("未找到 package_list.csv,将使用包名作为应用名。")
|
||
return None, None
|
||
|
||
try:
|
||
with open(package_list_path, 'r', encoding='utf-8-sig', newline='') as f:
|
||
reader = csv.DictReader(f)
|
||
for row in reader:
|
||
app_name = (row.get('app_name') or '').strip()
|
||
package_name = (row.get('package_name') or '').strip()
|
||
if package_name and app_name:
|
||
package_to_app[package_name] = app_name
|
||
app_to_package[app_name] = package_name
|
||
except Exception as e:
|
||
print(f"读取 package_list.csv 时出错: {e}")
|
||
return None, None
|
||
|
||
return package_to_app, app_to_package
|
||
|
||
def load_traffic_profile(current_dir, allowed_packages=None):
|
||
self_ratio_map = {}
|
||
traffic_profile_path = os.path.join(current_dir, 'app_traffic_profile_20260324.csv')
|
||
if not os.path.exists(traffic_profile_path):
|
||
print("未找到 app_traffic_profile_20260324.csv,将不使用 self_ratio。")
|
||
return self_ratio_map
|
||
|
||
try:
|
||
with open(traffic_profile_path, 'r', encoding='utf-8-sig') as f:
|
||
reader = csv.DictReader(f)
|
||
for row in reader:
|
||
package_name = row.get('app_id', '').strip()
|
||
if allowed_packages is not None and package_name not in allowed_packages:
|
||
continue
|
||
if package_name:
|
||
try:
|
||
self_ratio = float(row.get('self_ratio', 0) or 0)
|
||
self_ratio_map[package_name] = self_ratio
|
||
except ValueError:
|
||
pass
|
||
except Exception as e:
|
||
print(f"读取 app_traffic_profile_20260224.csv 时出错: {e}")
|
||
|
||
return self_ratio_map
|
||
|
||
def load_success_tasks(current_dir, allowed_packages=None):
|
||
import glob
|
||
success_packages = set()
|
||
success_info = {}
|
||
success_time = {}
|
||
success_worker = {}
|
||
|
||
success_task_files = sorted(glob.glob(os.path.join(current_dir, 'success_tasks*.csv')))
|
||
|
||
if not success_task_files:
|
||
return success_packages, success_info, success_time, success_worker
|
||
|
||
for package_name, row in _load_latest_task_rows(
|
||
success_task_files,
|
||
package_idx=3,
|
||
time_idx=0,
|
||
allowed_packages=allowed_packages,
|
||
).items():
|
||
success_packages.add(package_name)
|
||
success_info[package_name] = _normalize_success_detail(_csv_value(row, 6))
|
||
success_time[package_name] = _csv_value(row, 0)
|
||
success_worker[package_name] = _csv_value(row, 4)
|
||
|
||
return success_packages, success_info, success_time, success_worker
|
||
|
||
|
||
def load_non_success_tasks(current_dir, allowed_packages=None):
|
||
failure_info = {}
|
||
failure_type = {}
|
||
failure_time = {}
|
||
failure_worker = {}
|
||
non_success_rows = {}
|
||
failed_rows = _load_latest_task_rows(
|
||
[os.path.join(current_dir, 'failed_tasks.csv')],
|
||
package_idx=3,
|
||
time_idx=0,
|
||
allowed_packages=allowed_packages,
|
||
)
|
||
retry_rows = _load_latest_task_rows(
|
||
[os.path.join(current_dir, 'retry_tasks.csv')],
|
||
package_idx=3,
|
||
time_idx=0,
|
||
allowed_packages=allowed_packages,
|
||
)
|
||
|
||
for package_name, row in failed_rows.items():
|
||
non_success_rows[package_name] = {
|
||
'detail': _csv_value(row, 7) if len(row) >= 8 else _csv_value(row, 6),
|
||
'failure_type': _csv_value(row, 5),
|
||
'time': _csv_value(row, 0),
|
||
'worker': _csv_value(row, 4),
|
||
}
|
||
|
||
for package_name, row in retry_rows.items():
|
||
if package_name in non_success_rows:
|
||
continue
|
||
non_success_rows[package_name] = {
|
||
'detail': _csv_value(row, 6),
|
||
'failure_type': _csv_value(row, 5),
|
||
'time': _csv_value(row, 0),
|
||
'worker': _csv_value(row, 4),
|
||
}
|
||
|
||
for package_name, row in non_success_rows.items():
|
||
failure_info[package_name] = row['detail']
|
||
failure_type[package_name] = row['failure_type']
|
||
failure_time[package_name] = row['time']
|
||
failure_worker[package_name] = row['worker']
|
||
|
||
return failure_info, failure_type, failure_time, failure_worker
|
||
|
||
def parse_utg_js(js_path, target_package=None):
|
||
import re
|
||
import json
|
||
result = {
|
||
'num_nodes': 0,
|
||
'num_reached_activities': 0,
|
||
'app_num_total_activities': 0
|
||
}
|
||
|
||
try:
|
||
with open(js_path, 'r', encoding='utf-8-sig') as f:
|
||
content = f.read()
|
||
|
||
json_match = re.search(r'var\s+utg\s*=\s*(\{.*\})\s*;?\s*$', content, re.DOTALL)
|
||
if json_match:
|
||
try:
|
||
utg_data = json.loads(json_match.group(1))
|
||
|
||
app_package = utg_data.get('app_package', '')
|
||
filter_package = target_package if target_package else app_package
|
||
|
||
nodes = utg_data.get('nodes', [])
|
||
if filter_package:
|
||
filtered_count = sum(1 for node in nodes if node.get('package') == filter_package)
|
||
result['num_nodes'] = filtered_count
|
||
else:
|
||
result['num_nodes'] = len(nodes)
|
||
|
||
num_reached_match = utg_data.get('num_reached_activities', 0)
|
||
result['num_reached_activities'] = num_reached_match if isinstance(num_reached_match, int) else 0
|
||
|
||
app_total_match = utg_data.get('app_num_total_activities', 0)
|
||
result['app_num_total_activities'] = app_total_match if isinstance(app_total_match, int) else 0
|
||
|
||
except json.JSONDecodeError as e:
|
||
print(f"解析 UTG JSON 时出错: {e}")
|
||
num_nodes_match = re.search(r'"num_nodes"\s*:\s*(\d+)', content)
|
||
if num_nodes_match:
|
||
result['num_nodes'] = int(num_nodes_match.group(1))
|
||
|
||
num_reached_match = re.search(r'"num_reached_activities"\s*:\s*(\d+)', content)
|
||
if num_reached_match:
|
||
result['num_reached_activities'] = int(num_reached_match.group(1))
|
||
|
||
app_total_match = re.search(r'"app_num_total_activities"\s*:\s*(\d+)', content)
|
||
if app_total_match:
|
||
result['app_num_total_activities'] = int(app_total_match.group(1))
|
||
else:
|
||
num_nodes_match = re.search(r'"num_nodes"\s*:\s*(\d+)', content)
|
||
if num_nodes_match:
|
||
result['num_nodes'] = int(num_nodes_match.group(1))
|
||
|
||
num_reached_match = re.search(r'"num_reached_activities"\s*:\s*(\d+)', content)
|
||
if num_reached_match:
|
||
result['num_reached_activities'] = int(num_reached_match.group(1))
|
||
|
||
app_total_match = re.search(r'"app_num_total_activities"\s*:\s*(\d+)', content)
|
||
if app_total_match:
|
||
result['app_num_total_activities'] = int(app_total_match.group(1))
|
||
|
||
except Exception as e:
|
||
print(f"解析 UTG 文件 {js_path} 时出错: {e}")
|
||
|
||
return result
|
||
|
||
def process_all_data():
|
||
app_data = defaultdict(lambda: {
|
||
'domains': set(),
|
||
'second_level_domains': set(),
|
||
'droidbot_steps': 0,
|
||
'gui_agent_steps': 0,
|
||
'duration_seconds': 0,
|
||
'num_nodes': 0,
|
||
'num_reached_activities': 0,
|
||
'app_num_total_activities': 0
|
||
})
|
||
|
||
current_dir = os.getcwd()
|
||
package_to_app, app_to_package = load_package_mapping(current_dir)
|
||
allowed_packages = set(package_to_app.keys()) if package_to_app is not None else None
|
||
|
||
batch_result_files = []
|
||
|
||
print("开始遍历文件收集数据...")
|
||
for root, dirs, files in os.walk(current_dir):
|
||
for file in files:
|
||
if file.endswith('.txt'):
|
||
file_path = os.path.join(root, file)
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||
for line in f:
|
||
parts = line.strip().split(',')
|
||
|
||
if len(parts) >= 3:
|
||
package_name = parts[0].strip()
|
||
app_name = parts[1].strip()
|
||
domain_name = parts[2].strip()
|
||
if allowed_packages is not None and package_name not in allowed_packages:
|
||
continue
|
||
|
||
if domain_name != "model_data:" and package_name:
|
||
app_data[package_name]['domains'].add(domain_name)
|
||
second_level_domain = extract_second_level_domain(domain_name)
|
||
if second_level_domain:
|
||
app_data[package_name]['second_level_domains'].add(second_level_domain)
|
||
|
||
except Exception as e:
|
||
print(f"处理文件 {file_path} 时出错: {e}")
|
||
|
||
if file.endswith('.csv') and file.startswith('batch_result'):
|
||
file_path = os.path.join(root, file)
|
||
batch_result_files.append(file_path)
|
||
|
||
if file.endswith('_utg.js'):
|
||
file_path = os.path.join(root, file)
|
||
package_name = file.replace('_utg.js', '')
|
||
if allowed_packages is not None and package_name not in allowed_packages:
|
||
continue
|
||
utg_result = parse_utg_js(file_path, package_name)
|
||
app_data[package_name]['num_nodes'] = max(
|
||
app_data[package_name]['num_nodes'], utg_result['num_nodes']
|
||
)
|
||
app_data[package_name]['num_reached_activities'] = max(
|
||
app_data[package_name]['num_reached_activities'], utg_result['num_reached_activities']
|
||
)
|
||
app_data[package_name]['app_num_total_activities'] = max(
|
||
app_data[package_name]['app_num_total_activities'], utg_result['app_num_total_activities']
|
||
)
|
||
|
||
batch_result_files.sort(key=lambda x: os.path.getmtime(x))
|
||
|
||
print(f"找到 {len(batch_result_files)} 个 batch_result 文件,按修改时间排序处理...")
|
||
for file_path in batch_result_files:
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||
reader = csv.DictReader(f)
|
||
for row in reader:
|
||
package_name = row.get('package_name', '').strip()
|
||
if allowed_packages is not None and package_name not in allowed_packages:
|
||
continue
|
||
if not package_name:
|
||
continue
|
||
|
||
try:
|
||
droidbot_steps = int(row.get('droidbot_steps', 0) or 0)
|
||
app_data[package_name]['droidbot_steps'] = droidbot_steps
|
||
except ValueError:
|
||
pass
|
||
|
||
try:
|
||
guiagent_steps = int(row.get('guiagent_steps', 0) or 0)
|
||
app_data[package_name]['gui_agent_steps'] = guiagent_steps
|
||
except ValueError:
|
||
pass
|
||
|
||
try:
|
||
duration_seconds = float(row.get('duration_seconds', 0) or 0)
|
||
app_data[package_name]['duration_seconds'] = duration_seconds
|
||
except ValueError:
|
||
pass
|
||
|
||
except Exception as e:
|
||
print(f"处理文件 {file_path} 时出错: {e}")
|
||
|
||
found_packages = set(app_data.keys())
|
||
print(f"从文件中共找到 {len(found_packages)} 个包的数据。")
|
||
|
||
success_packages, success_info, success_time, success_worker = load_success_tasks(
|
||
current_dir,
|
||
allowed_packages=allowed_packages,
|
||
)
|
||
failure_info, failure_type, failure_time, failure_worker = load_non_success_tasks(
|
||
current_dir,
|
||
allowed_packages=allowed_packages,
|
||
)
|
||
self_ratio_map = load_traffic_profile(current_dir, allowed_packages=allowed_packages)
|
||
|
||
if success_packages:
|
||
print(f"已加载 {len(success_packages)} 个成功任务。")
|
||
if failure_info:
|
||
print(f"已加载 {len(failure_info)} 个非成功任务。")
|
||
if self_ratio_map:
|
||
print(f"已加载 {len(self_ratio_map)} 个应用的流量配置。")
|
||
|
||
final_packages = set()
|
||
|
||
if package_to_app is not None:
|
||
final_packages = found_packages.intersection(allowed_packages)
|
||
print(f"根据 package_list.csv 筛选后,剩余 {len(final_packages)} 个包。")
|
||
print(f"过滤掉了 {len(found_packages) - len(final_packages)} 个不在列表中的包。")
|
||
else:
|
||
final_packages = found_packages
|
||
print("未进行筛选,使用所有找到的包。")
|
||
|
||
output_file = 'app_domain_summary.csv'
|
||
headers = ['app_name', 'package_name', 'test_success', 'test_time', 'test_host', 'self_ratio', 'unique_domain_count', 'unique_domain_names',
|
||
'unique_second_level_domain_count', 'unique_second_level_domains',
|
||
'droidbot_steps', 'gui_agent_steps', 'duration_seconds',
|
||
'num_nodes', 'num_reached_activities', 'app_num_total_activities',
|
||
'task_detail', 'failure_type']
|
||
|
||
try:
|
||
with open(output_file, 'w', newline='', encoding='utf-8-sig') as csvfile:
|
||
writer = csv.writer(csvfile)
|
||
writer.writerow(headers)
|
||
|
||
count = 0
|
||
for package_name in sorted(final_packages):
|
||
info = app_data[package_name]
|
||
|
||
if package_to_app:
|
||
app_name = package_to_app.get(package_name, package_name)
|
||
else:
|
||
app_name = package_name
|
||
|
||
domains = sorted(list(info['domains']))
|
||
second_level_domains = sorted(list(info['second_level_domains']))
|
||
|
||
is_success = package_name in success_packages
|
||
if is_success:
|
||
test_time = success_time.get(package_name, '')
|
||
test_host = success_worker.get(package_name, '')
|
||
task_detail = success_info.get(package_name, '')
|
||
package_failure_type = ''
|
||
else:
|
||
test_time = failure_time.get(package_name, '')
|
||
test_host = failure_worker.get(package_name, '')
|
||
task_detail = failure_info.get(package_name, '')
|
||
package_failure_type = failure_type.get(package_name, '')
|
||
self_ratio = self_ratio_map.get(package_name, '')
|
||
|
||
writer.writerow([
|
||
app_name,
|
||
package_name,
|
||
is_success,
|
||
test_time,
|
||
test_host,
|
||
self_ratio,
|
||
len(domains),
|
||
", ".join(domains),
|
||
len(second_level_domains),
|
||
", ".join(second_level_domains),
|
||
info['droidbot_steps'],
|
||
info['gui_agent_steps'],
|
||
info['duration_seconds'],
|
||
info['num_nodes'],
|
||
info['num_reached_activities'],
|
||
info['app_num_total_activities'],
|
||
task_detail,
|
||
package_failure_type
|
||
])
|
||
count += 1
|
||
|
||
print(f"处理完成!最终统计了 {count} 个 App。")
|
||
print(f"结果已保存至: {output_file}")
|
||
|
||
except Exception as e:
|
||
print(f"写入 CSV 时出错: {e}")
|
||
|
||
if __name__ == "__main__":
|
||
process_all_data()
|