FlowAnalyzer 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ import os
2
+ import sqlite3
3
+ import subprocess
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Iterable, Optional
6
+
7
+ from .logging_config import logger
8
+ from .Models import HttpPair, Request, Response
9
+ from .PacketParser import PacketParser
10
+ from .Path import get_default_tshark_path
11
+
12
+
13
+ class FlowAnalyzer:
14
+ """
15
+ FlowAnalyzer 流量分析器 (智能缓存版)
16
+ 特点:
17
+ 1. Tshark -> Pipe -> ThreadPool -> SQLite
18
+ 2. 智能校验:自动比对 Filter 和文件修改时间,防止缓存错乱
19
+ 3. 存储优化:数据库文件生成在流量包同级目录下
20
+ """
21
+
22
+ def __init__(self, db_path: str):
23
+ """
24
+ 初始化 FlowAnalyzer
25
+ :param db_path: 数据库文件路径 (由 get_json_data 返回)
26
+ """
27
+ # 路径兼容处理
28
+ if db_path.endswith(".json"):
29
+ possible_db = db_path + ".db"
30
+ if os.path.exists(possible_db):
31
+ self.db_path = possible_db
32
+ else:
33
+ self.db_path = db_path
34
+ else:
35
+ self.db_path = db_path
36
+
37
+ self.check_db_file()
38
+
39
+ def check_db_file(self):
40
+ """检查数据库文件是否存在"""
41
+ if not os.path.exists(self.db_path):
42
+ raise FileNotFoundError(f"未找到数据文件或缓存数据库: {self.db_path},请先调用 get_json_data 生成。")
43
+
44
+ def generate_http_dict_pairs(self) -> Iterable[HttpPair]:
45
+ """生成HTTP请求和响应信息的字典对 (SQL JOIN 高性能版)"""
46
+ if not os.path.exists(self.db_path):
47
+ return
48
+
49
+ with sqlite3.connect(self.db_path) as conn:
50
+ cursor = conn.cursor()
51
+ # 开启查询优化
52
+ cursor.execute("PRAGMA query_only = 1;")
53
+
54
+ # === 第一步:配对查询 ===
55
+ # 利用 SQLite 的 LEFT JOIN 直接匹配请求和响应
56
+ sql_pair = """
57
+ SELECT
58
+ req.frame_num, req.header, req.file_data, req.full_uri, req.time_epoch, -- 0-4 (Request)
59
+ resp.frame_num, resp.header, resp.file_data, resp.time_epoch, resp.request_in -- 5-9 (Response)
60
+ FROM requests req
61
+ LEFT JOIN responses resp ON req.frame_num = resp.request_in
62
+ ORDER BY req.frame_num ASC
63
+ """
64
+
65
+ cursor.execute(sql_pair)
66
+
67
+ # 流式遍历结果,内存占用极低
68
+ for row in cursor:
69
+ req = Request(frame_num=row[0], header=row[1] or b"", file_data=row[2] or b"", full_uri=row[3] or "", time_epoch=row[4])
70
+
71
+ resp = None
72
+ if row[5] is not None:
73
+ resp = Response(frame_num=row[5], header=row[6] or b"", file_data=row[7] or b"", time_epoch=row[8], _request_in=row[9])
74
+
75
+ yield HttpPair(request=req, response=resp)
76
+
77
+ # === 第二步:孤儿响应查询 ===
78
+ sql_orphan = """
79
+ SELECT frame_num, header, file_data, time_epoch, request_in
80
+ FROM responses
81
+ WHERE request_in NOT IN (SELECT frame_num FROM requests)
82
+ """
83
+ cursor.execute(sql_orphan)
84
+
85
+ for row in cursor:
86
+ resp = Response(frame_num=row[0], header=row[1] or b"", file_data=row[2] or b"", time_epoch=row[3], _request_in=row[4])
87
+ yield HttpPair(request=None, response=resp)
88
+
89
+ # =========================================================================
90
+ # 静态方法区域:包含校验逻辑和流式处理
91
+ # =========================================================================
92
+
93
+ @staticmethod
94
+ def get_json_data(file_path: str, display_filter: str, tshark_path: Optional[str] = None) -> str:
95
+ """
96
+ 获取数据路径 (智能校验版)。
97
+ """
98
+ if not os.path.exists(file_path):
99
+ raise FileNotFoundError("流量包路径不存在:%s" % file_path)
100
+
101
+ abs_file_path = os.path.abspath(file_path)
102
+ pcap_dir = os.path.dirname(abs_file_path)
103
+ base_name = os.path.splitext(os.path.basename(abs_file_path))[0]
104
+ db_path = os.path.join(pcap_dir, f"{base_name}.db")
105
+
106
+ if FlowAnalyzer._is_cache_valid(db_path, abs_file_path, display_filter):
107
+ logger.debug(f"缓存校验通过 (Filter匹配且文件未变),使用缓存: [{db_path}]")
108
+ return db_path
109
+ else:
110
+ logger.debug(f"缓存失效或不存在 (Filter变更或文件更新),开始重新解析...")
111
+
112
+ tshark_path = FlowAnalyzer.get_tshark_path(tshark_path)
113
+ FlowAnalyzer._stream_tshark_to_db(abs_file_path, display_filter, tshark_path, db_path)
114
+
115
+ return db_path
116
+
117
+ @staticmethod
118
+ def get_db_data(file_path: str, display_filter: str, tshark_path: Optional[str] = None) -> str:
119
+ return FlowAnalyzer.get_json_data(file_path, display_filter, tshark_path)
120
+
121
+ @staticmethod
122
+ def _is_cache_valid(db_path: str, pcap_path: str, current_filter: str) -> bool:
123
+ if not os.path.exists(db_path) or os.path.getsize(db_path) == 0:
124
+ return False
125
+
126
+ try:
127
+ current_mtime = os.path.getmtime(pcap_path)
128
+ current_size = os.path.getsize(pcap_path)
129
+
130
+ with sqlite3.connect(db_path) as conn:
131
+ cursor = conn.cursor()
132
+ cursor.execute("SELECT filter, pcap_mtime, pcap_size FROM meta_info LIMIT 1")
133
+ row = cursor.fetchone()
134
+
135
+ if not row:
136
+ return False
137
+
138
+ cached_filter, cached_mtime, cached_size = row
139
+
140
+ if cached_filter == current_filter and cached_size == current_size and abs(cached_mtime - current_mtime) < 0.1:
141
+ return True
142
+ else:
143
+ logger.debug(f"校验失败: 缓存Filter={cached_filter} vs 当前={current_filter}")
144
+ return False
145
+
146
+ except sqlite3.OperationalError:
147
+ return False
148
+ except Exception as e:
149
+ logger.warning(f"缓存校验出错: {e},将重新解析")
150
+ return False
151
+
152
+ @staticmethod
153
+ def _stream_tshark_to_db(pcap_path: str, display_filter: str, tshark_path: str, db_path: str):
154
+ """流式解析并存入DB (多线程版)"""
155
+ if os.path.exists(db_path):
156
+ os.remove(db_path)
157
+
158
+ with sqlite3.connect(db_path) as conn:
159
+ cursor = conn.cursor()
160
+ cursor.execute("PRAGMA synchronous = OFF")
161
+ cursor.execute("PRAGMA journal_mode = MEMORY")
162
+
163
+ cursor.execute("CREATE TABLE requests (frame_num INTEGER PRIMARY KEY, header BLOB, file_data BLOB, full_uri TEXT, time_epoch REAL)")
164
+ cursor.execute("CREATE TABLE responses (frame_num INTEGER PRIMARY KEY, header BLOB, file_data BLOB, time_epoch REAL, request_in INTEGER)")
165
+
166
+ cursor.execute("""
167
+ CREATE TABLE meta_info (
168
+ id INTEGER PRIMARY KEY,
169
+ filter TEXT,
170
+ pcap_path TEXT,
171
+ pcap_mtime REAL,
172
+ pcap_size INTEGER
173
+ )
174
+ """)
175
+ conn.commit()
176
+
177
+ command = [
178
+ tshark_path,
179
+ "-r",
180
+ pcap_path,
181
+ "-Y",
182
+ f"({display_filter})",
183
+ "-T",
184
+ "fields",
185
+ "-e",
186
+ "http.response.code", # 0
187
+ "-e",
188
+ "http.request_in", # 1
189
+ "-e",
190
+ "tcp.reassembled.data", # 2
191
+ "-e",
192
+ "frame.number", # 3
193
+ "-e",
194
+ "tcp.payload", # 4
195
+ "-e",
196
+ "frame.time_epoch", # 5
197
+ "-e",
198
+ "exported_pdu.exported_pdu", # 6
199
+ "-e",
200
+ "http.request.full_uri", # 7
201
+ "-e",
202
+ "http.file_data", # 8
203
+ "-e",
204
+ "tcp.segment.count", # 9
205
+ "-E",
206
+ "header=n",
207
+ "-E",
208
+ "separator=/t",
209
+ "-E",
210
+ "quote=n",
211
+ "-E",
212
+ "occurrence=f",
213
+ ]
214
+
215
+ logger.debug(f"执行 Tshark: {command}")
216
+ BATCH_SIZE = 2000
217
+ MAX_PENDING_BATCHES = 20 # 控制内存中待处理的批次数量 (Backpressure)
218
+
219
+ # 使用 ThreadPoolExecutor 并行处理数据
220
+ max_workers = min(32, (os.cpu_count() or 1) + 4)
221
+
222
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.path.dirname(os.path.abspath(pcap_path)))
223
+ try:
224
+ with sqlite3.connect(db_path) as conn:
225
+ cursor = conn.cursor()
226
+
227
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
228
+ current_batch = []
229
+ pending_futures = [] # List[Future]
230
+
231
+ def write_results_to_db(results):
232
+ """将一批处理好的结果写入数据库"""
233
+ if not results:
234
+ return
235
+
236
+ db_req_rows = []
237
+ db_resp_rows = []
238
+
239
+ for item in results:
240
+ if item["type"] == "response":
241
+ db_resp_rows.append((item["frame_num"], item["header"], item["file_data"], item["time_epoch"], item["request_in"]))
242
+ else:
243
+ db_req_rows.append((item["frame_num"], item["header"], item["file_data"], item["full_uri"], item["time_epoch"]))
244
+
245
+ if db_req_rows:
246
+ cursor.executemany("INSERT OR REPLACE INTO requests VALUES (?,?,?,?,?)", db_req_rows)
247
+ if db_resp_rows:
248
+ cursor.executemany("INSERT OR REPLACE INTO responses VALUES (?,?,?,?,?)", db_resp_rows)
249
+
250
+ def submit_batch():
251
+ """提交当前批次到线程池"""
252
+ if not current_batch:
253
+ return
254
+
255
+ # Copy batch data for the thread (list slicing is fast)
256
+ batch_data = current_batch[:]
257
+ future = executor.submit(PacketParser.process_batch, batch_data)
258
+ pending_futures.append(future)
259
+ current_batch.clear()
260
+
261
+ # --- Main Pipeline Loop ---
262
+ if process.stdout:
263
+ for line in process.stdout:
264
+ current_batch.append(line)
265
+
266
+ if len(current_batch) >= BATCH_SIZE:
267
+ submit_batch()
268
+
269
+ # Backpressure: 如果积压的任务太多,主线程暂停读取,先处理掉最早的一个
270
+ # 这样既保证了 Pipeline 流动,又防止内存爆掉
271
+ if len(pending_futures) >= MAX_PENDING_BATCHES:
272
+ oldest_future = pending_futures.pop(0)
273
+ write_results_to_db(oldest_future.result())
274
+
275
+ # --- Drain Pipeline ---
276
+ # 提交剩余数据
277
+ submit_batch()
278
+
279
+ # 等待所有剩余任务完成
280
+ for future in pending_futures:
281
+ write_results_to_db(future.result())
282
+
283
+ # 创建索引和元数据
284
+ cursor.execute("CREATE INDEX idx_resp_req_in ON responses(request_in)")
285
+ pcap_mtime = os.path.getmtime(pcap_path)
286
+ pcap_size = os.path.getsize(pcap_path)
287
+ cursor.execute("INSERT INTO meta_info (filter, pcap_path, pcap_mtime, pcap_size) VALUES (?, ?, ?, ?)", (display_filter, pcap_path, pcap_mtime, pcap_size))
288
+ conn.commit()
289
+
290
+ except Exception as e:
291
+ logger.error(f"解析错误: {e}")
292
+ if process.poll() is None:
293
+ process.terminate()
294
+ finally:
295
+ if process.poll() is None:
296
+ process.terminate()
297
+
298
+ @staticmethod
299
+ def get_tshark_path(tshark_path: Optional[str]) -> str:
300
+ default_tshark_path = get_default_tshark_path()
301
+ use_path = tshark_path if tshark_path and os.path.exists(tshark_path) else default_tshark_path
302
+ if not use_path or not os.path.exists(use_path):
303
+ logger.critical("未找到 Tshark,请检查路径配置")
304
+ exit(-1)
305
+ return use_path
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass
2
+ from typing import NamedTuple, Optional
3
+
4
+
5
+ @dataclass
6
+ class Request:
7
+ __slots__ = ("frame_num", "header", "file_data", "full_uri", "time_epoch")
8
+ frame_num: int
9
+ header: bytes
10
+ file_data: bytes
11
+ full_uri: str
12
+ time_epoch: float
13
+
14
+
15
+ @dataclass
16
+ class Response:
17
+ __slots__ = ("frame_num", "header", "file_data", "time_epoch", "_request_in")
18
+ frame_num: int
19
+ header: bytes
20
+ file_data: bytes
21
+ time_epoch: float
22
+ _request_in: Optional[int]
23
+
24
+
25
+ class HttpPair(NamedTuple):
26
+ request: Optional[Request]
27
+ response: Optional[Response]
@@ -0,0 +1,185 @@
1
+ import binascii
2
+ import contextlib
3
+ import gzip
4
+ from typing import List, Optional, Tuple
5
+ from urllib import parse
6
+
7
+ from .logging_config import logger
8
+
9
+
10
+ class PacketParser:
11
+ @staticmethod
12
+ def parse_packet_data(row: list) -> Tuple[int, int, float, str, bytes, bytes]:
13
+ """
14
+ 解析 Tshark 输出的一行数据
15
+ row definition (all bytes):
16
+ 0: http.response.code
17
+ 1: http.request_in
18
+ 2: tcp.reassembled.data
19
+ 3: frame.number
20
+ 4: tcp.payload
21
+ 5: frame.time_epoch
22
+ 6: exported_pdu.exported_pdu
23
+ 7: http.request.full_uri
24
+ 8: http.file_data
25
+ 9: tcp.segment.count
26
+ """
27
+ frame_num = int(row[3])
28
+ request_in = int(row[1]) if row[1] else frame_num
29
+ # Decode only URI to string
30
+ full_uri = parse.unquote(row[7].decode("utf-8", errors="replace")) if row[7] else ""
31
+ time_epoch = float(row[5])
32
+ http_file_data = row[8] if len(row) > 8 else b""
33
+
34
+ # Logic for Raw Packet (Header Source)
35
+ is_reassembled = len(row) > 9 and row[9]
36
+
37
+ if is_reassembled and row[2]:
38
+ full_request = row[2]
39
+ elif row[4]:
40
+ full_request = row[4]
41
+ else:
42
+ # Fallback (e.g. Exported PDU)
43
+ full_request = row[2] if row[2] else (row[6] if row[6] else b"")
44
+
45
+ return frame_num, request_in, time_epoch, full_uri, full_request, http_file_data
46
+
47
+ @staticmethod
48
+ def split_http_headers(file_data: bytes) -> Tuple[bytes, bytes]:
49
+ headerEnd = file_data.find(b"\r\n\r\n")
50
+ if headerEnd != -1:
51
+ return file_data[: headerEnd + 4], file_data[headerEnd + 4 :]
52
+ elif file_data.find(b"\n\n") != -1:
53
+ headerEnd = file_data.index(b"\n\n") + 2
54
+ return file_data[:headerEnd], file_data[headerEnd:]
55
+ return b"", file_data
56
+
57
+ @staticmethod
58
+ def dechunk_http_response(file_data: bytes) -> bytes:
59
+ """解码分块TCP数据"""
60
+ if not file_data:
61
+ return b""
62
+
63
+ chunks = []
64
+ cursor = 0
65
+ total_len = len(file_data)
66
+
67
+ while cursor < total_len:
68
+ newline_idx = file_data.find(b"\n", cursor)
69
+ if newline_idx == -1:
70
+ raise ValueError("Not chunked data")
71
+
72
+ size_line = file_data[cursor:newline_idx].strip()
73
+ if not size_line:
74
+ cursor = newline_idx + 1
75
+ continue
76
+
77
+ chunk_size = int(size_line, 16)
78
+ if chunk_size == 0:
79
+ break
80
+
81
+ data_start = newline_idx + 1
82
+ data_end = data_start + chunk_size
83
+
84
+ if data_end > total_len:
85
+ chunks.append(file_data[data_start:])
86
+ break
87
+
88
+ chunks.append(file_data[data_start:data_end])
89
+
90
+ cursor = data_end
91
+ while cursor < total_len and file_data[cursor] in (13, 10):
92
+ cursor += 1
93
+
94
+ return b"".join(chunks)
95
+
96
+ @staticmethod
97
+ def extract_http_file_data(full_request: bytes, http_file_data: bytes) -> Tuple[bytes, bytes]:
98
+ """
99
+ 提取HTTP请求或响应中的文件数据 (混合模式 - 二进制优化版)
100
+ """
101
+ header = b""
102
+ file_data = b""
103
+
104
+ try:
105
+ # --- 1. 提取 Header ---
106
+ if full_request:
107
+ raw_bytes = binascii.unhexlify(full_request)
108
+ h_part, _ = PacketParser.split_http_headers(raw_bytes)
109
+ header = h_part
110
+
111
+ # --- 2. 提取 Body ---
112
+ if http_file_data:
113
+ try:
114
+ file_data = binascii.unhexlify(http_file_data)
115
+ return header, file_data
116
+ except binascii.Error:
117
+ logger.warning("解析 http.file_data Hex 失败,尝试回退到原始方式")
118
+
119
+ # --- 3. 回退模式 (Fallback) ---
120
+ if full_request and not file_data:
121
+ raw_bytes = binascii.unhexlify(full_request)
122
+ _, body_part = PacketParser.split_http_headers(raw_bytes)
123
+
124
+ with contextlib.suppress(Exception):
125
+ body_part = PacketParser.dechunk_http_response(body_part)
126
+
127
+ with contextlib.suppress(Exception):
128
+ if body_part.startswith(b"\x1f\x8b"):
129
+ body_part = gzip.decompress(body_part)
130
+
131
+ file_data = body_part
132
+ return header, file_data
133
+
134
+ except ValueError as e:
135
+ logger.error(f"Hex转换失败: {str(e)[:100]}...")
136
+ return b"", b""
137
+ except Exception as e:
138
+ logger.error(f"解析HTTP数据未知错误: {e}")
139
+ return b"", b""
140
+
141
+ @staticmethod
142
+ def process_row(line: bytes) -> Optional[dict]:
143
+ """
144
+ 处理单行数据,返回结构化结果供主线程写入
145
+ """
146
+ line = line.rstrip(b"\r\n")
147
+ if not line:
148
+ return None
149
+
150
+ row = line.split(b"\t")
151
+ try:
152
+ frame_num, request_in, time_epoch, full_uri, full_request, http_file_data = PacketParser.parse_packet_data(row)
153
+
154
+ if not full_request and not http_file_data:
155
+ return None
156
+
157
+ header, file_data = PacketParser.extract_http_file_data(full_request, http_file_data)
158
+
159
+ # row[0] is http.response.code (bytes)
160
+ is_response = bool(row[0])
161
+
162
+ return {
163
+ "type": "response" if is_response else "request",
164
+ "frame_num": frame_num,
165
+ "header": header,
166
+ "file_data": file_data,
167
+ "time_epoch": time_epoch,
168
+ "request_in": request_in, # Only useful for Response
169
+ "full_uri": full_uri, # Only useful for Request
170
+ }
171
+
172
+ except Exception:
173
+ return None
174
+
175
+ @staticmethod
176
+ def process_batch(lines: List[bytes]) -> List[dict]:
177
+ """
178
+ 批量处理行数据,减少函数调用开销
179
+ """
180
+ results = []
181
+ for line in lines:
182
+ res = PacketParser.process_row(line)
183
+ if res:
184
+ results.append(res)
185
+ return results
@@ -15,8 +15,9 @@ def configure_logger(logger_name, level=logging.DEBUG) -> logging.Logger:
15
15
  console_handler.setFormatter(formatter)
16
16
  return logger
17
17
 
18
+
18
19
  logger = configure_logger("FlowAnalyzer", logging.INFO)
19
20
 
20
- if __name__ == '__main__':
21
+ if __name__ == "__main__":
21
22
  logger = configure_logger("FlowAnalyzer")
22
23
  logger.info("This is a test!")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FlowAnalyzer
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件
5
5
  Home-page: https://github.com/Byxs20/FlowAnalyzer
6
6
  Author: Byxs20
@@ -36,9 +36,11 @@ Dynamic: summary
36
36
 
37
37
  为了解决传统解析方式慢、内存占用高的问题,FlowAnalyzer 进行了核心架构升级:**流式解析 + SQLite 智能缓存**。
38
38
 
39
- ### 1. ⚡️ 高性能流式解析
40
- - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道对接 Tshark 输出,结合 `ijson` 进行增量解析。
41
- - **无中间文件**:解析过程中不生成体积巨大的临时 JSON 文件,直接入库。
39
+ ### 1. ⚡️ 高性能流式解析 (多线程流水线)
40
+ - **多线程并行**:采用 `ThreadPoolExecutor` 构建流水线,主线程负责读取 Tshark 输出,子线程并行解析数据包,充分利用多核 CPU。
41
+ - **批量处理**:引入 Batch 机制(默认 2000 包/批),大幅减少数据库事务开销和 Python 函数调用损耗。
42
+ - **内存背压控制 (Backpressure)**:智能监控待处理队列长度,防止在处理高速流量时内存溢出。
43
+ - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道流式处理,解析过程中不生成体积巨大的临时文件。
42
44
 
43
45
  ### 2. 💾 智能缓存机制
44
46
  - **自动缓存**:首次分析 `test.pcap` 时,会自动生成同级目录下的 `test.db`。
@@ -59,7 +61,7 @@ Dynamic: summary
59
61
 
60
62
  | 特性 | 旧版架构 | **新版架构 (FlowAnalyzer)** |
61
63
  | :----------- | :---------------------------- | :---------------------------------- |
62
- | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 管道 -> ijson -> SQLite |
64
+ | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 多线程Batch解析 -> SQLite |
63
65
  | **内存占用** | 极高 (易 OOM) | **极低 (内存稳定)** |
64
66
  | **二次加载** | 需重新解析 | **直接读取 DB (0秒)** |
65
67
  | **磁盘占用** | 巨大的临时 JSON 文件 | 轻量级 SQLite 文件 |
@@ -71,11 +73,11 @@ Dynamic: summary
71
73
  请确保您的环境中已安装 Python 3 和 Tshark (Wireshark)。
72
74
 
73
75
  ```bash
74
- # 安装 FlowAnalyzer 及其依赖 ijson
75
- pip3 install FlowAnalyzer ijson
76
+ # 安装 FlowAnalyzer
77
+ pip3 install FlowAnalyzer
76
78
 
77
79
  # 或者使用国内源加速
78
- pip3 install FlowAnalyzer ijson -i https://pypi.org/simple
80
+ pip3 install FlowAnalyzer -i https://pypi.org/simple
79
81
  ```
80
82
 
81
83
  ---
@@ -2,6 +2,8 @@ LICENSE
2
2
  README.md
3
3
  setup.py
4
4
  FlowAnalyzer/FlowAnalyzer.py
5
+ FlowAnalyzer/Models.py
6
+ FlowAnalyzer/PacketParser.py
5
7
  FlowAnalyzer/Path.py
6
8
  FlowAnalyzer/__init__.py
7
9
  FlowAnalyzer/logging_config.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FlowAnalyzer
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件
5
5
  Home-page: https://github.com/Byxs20/FlowAnalyzer
6
6
  Author: Byxs20
@@ -36,9 +36,11 @@ Dynamic: summary
36
36
 
37
37
  为了解决传统解析方式慢、内存占用高的问题,FlowAnalyzer 进行了核心架构升级:**流式解析 + SQLite 智能缓存**。
38
38
 
39
- ### 1. ⚡️ 高性能流式解析
40
- - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道对接 Tshark 输出,结合 `ijson` 进行增量解析。
41
- - **无中间文件**:解析过程中不生成体积巨大的临时 JSON 文件,直接入库。
39
+ ### 1. ⚡️ 高性能流式解析 (多线程流水线)
40
+ - **多线程并行**:采用 `ThreadPoolExecutor` 构建流水线,主线程负责读取 Tshark 输出,子线程并行解析数据包,充分利用多核 CPU。
41
+ - **批量处理**:引入 Batch 机制(默认 2000 包/批),大幅减少数据库事务开销和 Python 函数调用损耗。
42
+ - **内存背压控制 (Backpressure)**:智能监控待处理队列长度,防止在处理高速流量时内存溢出。
43
+ - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道流式处理,解析过程中不生成体积巨大的临时文件。
42
44
 
43
45
  ### 2. 💾 智能缓存机制
44
46
  - **自动缓存**:首次分析 `test.pcap` 时,会自动生成同级目录下的 `test.db`。
@@ -59,7 +61,7 @@ Dynamic: summary
59
61
 
60
62
  | 特性 | 旧版架构 | **新版架构 (FlowAnalyzer)** |
61
63
  | :----------- | :---------------------------- | :---------------------------------- |
62
- | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 管道 -> ijson -> SQLite |
64
+ | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 多线程Batch解析 -> SQLite |
63
65
  | **内存占用** | 极高 (易 OOM) | **极低 (内存稳定)** |
64
66
  | **二次加载** | 需重新解析 | **直接读取 DB (0秒)** |
65
67
  | **磁盘占用** | 巨大的临时 JSON 文件 | 轻量级 SQLite 文件 |
@@ -71,11 +73,11 @@ Dynamic: summary
71
73
  请确保您的环境中已安装 Python 3 和 Tshark (Wireshark)。
72
74
 
73
75
  ```bash
74
- # 安装 FlowAnalyzer 及其依赖 ijson
75
- pip3 install FlowAnalyzer ijson
76
+ # 安装 FlowAnalyzer
77
+ pip3 install FlowAnalyzer
76
78
 
77
79
  # 或者使用国内源加速
78
- pip3 install FlowAnalyzer ijson -i https://pypi.org/simple
80
+ pip3 install FlowAnalyzer -i https://pypi.org/simple
79
81
  ```
80
82
 
81
83
  ---
@@ -10,9 +10,11 @@
10
10
 
11
11
  为了解决传统解析方式慢、内存占用高的问题,FlowAnalyzer 进行了核心架构升级:**流式解析 + SQLite 智能缓存**。
12
12
 
13
- ### 1. ⚡️ 高性能流式解析
14
- - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道对接 Tshark 输出,结合 `ijson` 进行增量解析。
15
- - **无中间文件**:解析过程中不生成体积巨大的临时 JSON 文件,直接入库。
13
+ ### 1. ⚡️ 高性能流式解析 (多线程流水线)
14
+ - **多线程并行**:采用 `ThreadPoolExecutor` 构建流水线,主线程负责读取 Tshark 输出,子线程并行解析数据包,充分利用多核 CPU。
15
+ - **批量处理**:引入 Batch 机制(默认 2000 包/批),大幅减少数据库事务开销和 Python 函数调用损耗。
16
+ - **内存背压控制 (Backpressure)**:智能监控待处理队列长度,防止在处理高速流量时内存溢出。
17
+ - **极低内存占用**:不再将整个 JSON 读入内存。通过 `subprocess` 管道流式处理,解析过程中不生成体积巨大的临时文件。
16
18
 
17
19
  ### 2. 💾 智能缓存机制
18
20
  - **自动缓存**:首次分析 `test.pcap` 时,会自动生成同级目录下的 `test.db`。
@@ -33,7 +35,7 @@
33
35
 
34
36
  | 特性 | 旧版架构 | **新版架构 (FlowAnalyzer)** |
35
37
  | :----------- | :---------------------------- | :---------------------------------- |
36
- | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 管道 -> ijson -> SQLite |
38
+ | **解析流程** | 生成巨大 JSON -> 全量读入内存 | Tshark流 -> 多线程Batch解析 -> SQLite |
37
39
  | **内存占用** | 极高 (易 OOM) | **极低 (内存稳定)** |
38
40
  | **二次加载** | 需重新解析 | **直接读取 DB (0秒)** |
39
41
  | **磁盘占用** | 巨大的临时 JSON 文件 | 轻量级 SQLite 文件 |
@@ -45,11 +47,11 @@
45
47
  请确保您的环境中已安装 Python 3 和 Tshark (Wireshark)。
46
48
 
47
49
  ```bash
48
- # 安装 FlowAnalyzer 及其依赖 ijson
49
- pip3 install FlowAnalyzer ijson
50
+ # 安装 FlowAnalyzer
51
+ pip3 install FlowAnalyzer
50
52
 
51
53
  # 或者使用国内源加速
52
- pip3 install FlowAnalyzer ijson -i https://pypi.org/simple
54
+ pip3 install FlowAnalyzer -i https://pypi.org/simple
53
55
  ```
54
56
 
55
57
  ---
@@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8"
7
7
 
8
8
  setup(
9
9
  name="FlowAnalyzer",
10
- version="0.4.2",
10
+ version="0.4.4",
11
11
  description="FlowAnalyzer是一个流量分析器,用于解析和处理tshark导出的JSON数据文件",
12
12
  author="Byxs20",
13
13
  author_email="97766819@qq.com",
@@ -1,440 +0,0 @@
1
- import contextlib
2
- import gzip
3
- import os
4
- import sqlite3
5
- import subprocess
6
- from dataclasses import dataclass
7
- from typing import Iterable, NamedTuple, Optional, Tuple
8
- from urllib import parse
9
-
10
- import ijson
11
-
12
- from .logging_config import logger
13
- from .Path import get_default_tshark_path
14
-
15
-
16
- @dataclass
17
- class Request:
18
- __slots__ = ("frame_num", "header", "file_data", "full_uri", "time_epoch")
19
- frame_num: int
20
- header: bytes
21
- file_data: bytes
22
- full_uri: str
23
- time_epoch: float
24
-
25
-
26
- @dataclass
27
- class Response:
28
- __slots__ = ("frame_num", "header", "file_data", "time_epoch", "_request_in")
29
- frame_num: int
30
- header: bytes
31
- file_data: bytes
32
- time_epoch: float
33
- _request_in: Optional[int]
34
-
35
-
36
- class HttpPair(NamedTuple):
37
- request: Optional[Request]
38
- response: Optional[Response]
39
-
40
-
41
- class FlowAnalyzer:
42
- """
43
- FlowAnalyzer 流量分析器 (智能缓存版)
44
- 特点:
45
- 1. Tshark -> Pipe -> ijson -> SQLite (无中间JSON文件)
46
- 2. 智能校验:自动比对 Filter 和文件修改时间,防止缓存错乱
47
- 3. 存储优化:数据库文件生成在流量包同级目录下
48
- """
49
-
50
- def __init__(self, db_path: str):
51
- """
52
- 初始化 FlowAnalyzer
53
- :param db_path: 数据库文件路径 (由 get_json_data 返回)
54
- """
55
- # 路径兼容处理
56
- if db_path.endswith(".json"):
57
- possible_db = db_path + ".db"
58
- if os.path.exists(possible_db):
59
- self.db_path = possible_db
60
- else:
61
- self.db_path = db_path
62
- else:
63
- self.db_path = db_path
64
-
65
- self.check_db_file()
66
-
67
- def check_db_file(self):
68
- """检查数据库文件是否存在"""
69
- if not os.path.exists(self.db_path):
70
- raise FileNotFoundError(f"未找到数据文件或缓存数据库: {self.db_path},请先调用 get_json_data 生成。")
71
-
72
- def generate_http_dict_pairs(self) -> Iterable[HttpPair]:
73
- """生成HTTP请求和响应信息的字典对 (SQL JOIN 高性能版)"""
74
- if not os.path.exists(self.db_path):
75
- return
76
-
77
- with sqlite3.connect(self.db_path) as conn:
78
- cursor = conn.cursor()
79
- # 开启查询优化
80
- cursor.execute("PRAGMA query_only = 1;")
81
-
82
- # === 第一步:配对查询 ===
83
- # 利用 SQLite 的 LEFT JOIN 直接匹配请求和响应
84
- # 避免将所有数据加载到 Python 内存中
85
- sql_pair = """
86
- SELECT
87
- req.frame_num, req.header, req.file_data, req.full_uri, req.time_epoch, -- 0-4 (Request)
88
- resp.frame_num, resp.header, resp.file_data, resp.time_epoch, resp.request_in -- 5-9 (Response)
89
- FROM requests req
90
- LEFT JOIN responses resp ON req.frame_num = resp.request_in
91
- ORDER BY req.frame_num ASC
92
- """
93
-
94
- cursor.execute(sql_pair)
95
-
96
- # 流式遍历结果,内存占用极低
97
- for row in cursor:
98
- # 构建 Request 对象
99
- # 注意处理 NULL 情况,虽然 requests 表理论上不为空,但防万一用 or b''
100
- req = Request(frame_num=row[0], header=row[1] or b"", file_data=row[2] or b"", full_uri=row[3] or "", time_epoch=row[4])
101
-
102
- resp = None
103
- # 如果 row[5] (Response frame_num) 不为空,说明匹配到了响应
104
- if row[5] is not None:
105
- resp = Response(frame_num=row[5], header=row[6] or b"", file_data=row[7] or b"", time_epoch=row[8], _request_in=row[9])
106
-
107
- yield HttpPair(request=req, response=resp)
108
-
109
- # === 第二步:孤儿响应查询 ===
110
- # 找出那些有 request_in 但找不到对应 Request 的响应包
111
- sql_orphan = """
112
- SELECT frame_num, header, file_data, time_epoch, request_in
113
- FROM responses
114
- WHERE request_in NOT IN (SELECT frame_num FROM requests)
115
- """
116
- cursor.execute(sql_orphan)
117
-
118
- for row in cursor:
119
- resp = Response(frame_num=row[0], header=row[1] or b"", file_data=row[2] or b"", time_epoch=row[3], _request_in=row[4])
120
- yield HttpPair(request=None, response=resp)
121
-
122
- # =========================================================================
123
- # 静态方法区域:包含校验逻辑和流式处理
124
- # =========================================================================
125
-
126
- @staticmethod
127
- def get_json_data(file_path: str, display_filter: str, tshark_path: Optional[str] = None) -> str:
128
- """
129
- 获取数据路径 (智能校验版)。
130
-
131
- 逻辑:
132
- 1. 根据 PCAP 路径推算 DB 路径 (位于 PCAP 同级目录)。
133
- 2. 检查 DB 是否存在。
134
- 3. 检查 Filter 和文件元数据是否一致。
135
- 4. 若一致返回路径,不一致则重新解析。
136
- """
137
- if not os.path.exists(file_path):
138
- raise FileNotFoundError("流量包路径不存在:%s" % file_path)
139
-
140
- # --- 修改处:获取流量包的绝对路径和所在目录 ---
141
- abs_file_path = os.path.abspath(file_path)
142
- pcap_dir = os.path.dirname(abs_file_path) # 获取文件所在的文件夹
143
- base_name = os.path.splitext(os.path.basename(abs_file_path))[0]
144
-
145
- # 将 db_path 拼接在流量包所在的目录下
146
- db_path = os.path.join(pcap_dir, f"{base_name}.db")
147
- # ----------------------------------------
148
-
149
- # --- 校验环节 ---
150
- if FlowAnalyzer._is_cache_valid(db_path, abs_file_path, display_filter):
151
- logger.debug(f"缓存校验通过 (Filter匹配且文件未变),使用缓存: [{db_path}]")
152
- return db_path
153
- else:
154
- logger.debug(f"缓存失效或不存在 (Filter变更或文件更新),开始重新解析...")
155
-
156
- # --- 解析环节 ---
157
- tshark_path = FlowAnalyzer.get_tshark_path(tshark_path)
158
- FlowAnalyzer._stream_tshark_to_db(abs_file_path, display_filter, tshark_path, db_path)
159
-
160
- return db_path
161
-
162
- @staticmethod
163
- def get_db_data(file_path: str, display_filter: str, tshark_path: Optional[str] = None) -> str:
164
- """
165
- 获取数据库路径 (get_json_data 的语义化别名)。
166
- 新项目建议使用此方法名,get_json_data 保留用于兼容旧习惯。
167
- """
168
- return FlowAnalyzer.get_json_data(file_path, display_filter, tshark_path)
169
-
170
- @staticmethod
171
- def _is_cache_valid(db_path: str, pcap_path: str, current_filter: str) -> bool:
172
- """
173
- 检查缓存有效性:对比 Filter 字符串和文件元数据
174
- """
175
- if not os.path.exists(db_path) or os.path.getsize(db_path) == 0:
176
- return False
177
-
178
- try:
179
- current_mtime = os.path.getmtime(pcap_path)
180
- current_size = os.path.getsize(pcap_path)
181
-
182
- with sqlite3.connect(db_path) as conn:
183
- cursor = conn.cursor()
184
- cursor.execute("SELECT filter, pcap_mtime, pcap_size FROM meta_info LIMIT 1")
185
- row = cursor.fetchone()
186
-
187
- if not row:
188
- return False
189
-
190
- cached_filter, cached_mtime, cached_size = row
191
-
192
- # 容差 0.1秒
193
- if cached_filter == current_filter and cached_size == current_size and abs(cached_mtime - current_mtime) < 0.1:
194
- return True
195
- else:
196
- logger.debug(f"校验失败: 缓存Filter={cached_filter} vs 当前={current_filter}")
197
- return False
198
-
199
- except sqlite3.OperationalError:
200
- return False
201
- except Exception as e:
202
- logger.warning(f"缓存校验出错: {e},将重新解析")
203
- return False
204
-
205
- @staticmethod
206
- def _stream_tshark_to_db(pcap_path: str, display_filter: str, tshark_path: str, db_path: str):
207
- """流式解析并存入DB,同时记录元数据"""
208
-
209
- if os.path.exists(db_path):
210
- os.remove(db_path)
211
-
212
- with sqlite3.connect(db_path) as conn:
213
- cursor = conn.cursor()
214
- cursor.execute("PRAGMA synchronous = OFF")
215
- cursor.execute("PRAGMA journal_mode = MEMORY")
216
-
217
- cursor.execute("CREATE TABLE requests (frame_num INTEGER PRIMARY KEY, header BLOB, file_data BLOB, full_uri TEXT, time_epoch REAL)")
218
- cursor.execute("CREATE TABLE responses (frame_num INTEGER PRIMARY KEY, header BLOB, file_data BLOB, time_epoch REAL, request_in INTEGER)")
219
-
220
- # === 核心优化:增加索引,极大加速 SQL JOIN 配对 ===
221
- cursor.execute("CREATE INDEX idx_resp_req_in ON responses(request_in)")
222
-
223
- cursor.execute("""
224
- CREATE TABLE meta_info (
225
- id INTEGER PRIMARY KEY,
226
- filter TEXT,
227
- pcap_path TEXT,
228
- pcap_mtime REAL,
229
- pcap_size INTEGER
230
- )
231
- """)
232
- conn.commit()
233
-
234
- command = [
235
- tshark_path,
236
- "-r",
237
- pcap_path,
238
- "-Y",
239
- f"({display_filter})",
240
- "-T",
241
- "json",
242
- "-e",
243
- "http.response.code",
244
- "-e",
245
- "http.request_in",
246
- "-e",
247
- "tcp.reassembled.data",
248
- "-e",
249
- "frame.number",
250
- "-e",
251
- "tcp.payload",
252
- "-e",
253
- "frame.time_epoch",
254
- "-e",
255
- "exported_pdu.exported_pdu",
256
- "-e",
257
- "http.request.full_uri",
258
- ]
259
-
260
- logger.debug(f"执行 Tshark: {command}")
261
-
262
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=os.path.dirname(os.path.abspath(pcap_path)))
263
-
264
- db_req_rows = []
265
- db_resp_rows = []
266
- BATCH_SIZE = 5000
267
-
268
- try:
269
- parser = ijson.items(process.stdout, "item")
270
-
271
- with sqlite3.connect(db_path) as conn:
272
- cursor = conn.cursor()
273
-
274
- for packet in parser:
275
- layers = packet.get("_source", {}).get("layers", {})
276
- if not layers:
277
- continue
278
-
279
- try:
280
- frame_num, request_in, time_epoch, full_uri, full_request = FlowAnalyzer.parse_packet_data(layers)
281
- if not full_request:
282
- continue
283
- header, file_data = FlowAnalyzer.extract_http_file_data(full_request)
284
-
285
- if layers.get("http.response.code"):
286
- db_resp_rows.append((frame_num, header, file_data, time_epoch, request_in))
287
- else:
288
- db_req_rows.append((frame_num, header, file_data, full_uri, time_epoch))
289
-
290
- if len(db_req_rows) >= BATCH_SIZE:
291
- cursor.executemany("INSERT OR REPLACE INTO requests VALUES (?,?,?,?,?)", db_req_rows)
292
- db_req_rows.clear()
293
- if len(db_resp_rows) >= BATCH_SIZE:
294
- cursor.executemany("INSERT OR REPLACE INTO responses VALUES (?,?,?,?,?)", db_resp_rows)
295
- db_resp_rows.clear()
296
-
297
- except Exception:
298
- pass
299
-
300
- if db_req_rows:
301
- cursor.executemany("INSERT OR REPLACE INTO requests VALUES (?,?,?,?,?)", db_req_rows)
302
- if db_resp_rows:
303
- cursor.executemany("INSERT OR REPLACE INTO responses VALUES (?,?,?,?,?)", db_resp_rows)
304
-
305
- pcap_mtime = os.path.getmtime(pcap_path)
306
- pcap_size = os.path.getsize(pcap_path)
307
- cursor.execute("INSERT INTO meta_info (filter, pcap_path, pcap_mtime, pcap_size) VALUES (?, ?, ?, ?)", (display_filter, pcap_path, pcap_mtime, pcap_size))
308
-
309
- conn.commit()
310
-
311
- except Exception as e:
312
- logger.error(f"解析错误: {e}")
313
- if process.poll() is None:
314
- process.terminate()
315
- finally:
316
- if process.poll() is None:
317
- process.terminate()
318
-
319
- # --- 辅助静态方法 ---
320
-
321
- @staticmethod
322
- def parse_packet_data(packet: dict) -> Tuple[int, int, float, str, str]:
323
- frame_num = int(packet["frame.number"][0])
324
- request_in = int(packet["http.request_in"][0]) if packet.get("http.request_in") else frame_num
325
- full_uri = parse.unquote(packet["http.request.full_uri"][0]) if packet.get("http.request.full_uri") else ""
326
- time_epoch = float(packet["frame.time_epoch"][0])
327
-
328
- if packet.get("tcp.reassembled.data"):
329
- full_request = packet["tcp.reassembled.data"][0]
330
- elif packet.get("tcp.payload"):
331
- full_request = packet["tcp.payload"][0]
332
- else:
333
- full_request = packet["exported_pdu.exported_pdu"][0] if packet.get("exported_pdu.exported_pdu") else ""
334
- return frame_num, request_in, time_epoch, full_uri, full_request
335
-
336
- @staticmethod
337
- def split_http_headers(file_data: bytes) -> Tuple[bytes, bytes]:
338
- headerEnd = file_data.find(b"\r\n\r\n")
339
- if headerEnd != -1:
340
- return file_data[: headerEnd + 4], file_data[headerEnd + 4 :]
341
- elif file_data.find(b"\n\n") != -1:
342
- headerEnd = file_data.index(b"\n\n") + 2
343
- return file_data[:headerEnd], file_data[headerEnd:]
344
- return b"", file_data
345
-
346
- @staticmethod
347
- def dechunck_http_response(file_data: bytes) -> bytes:
348
- """解码分块TCP数据"""
349
- if not file_data:
350
- return b""
351
-
352
- chunks = []
353
- cursor = 0
354
- total_len = len(file_data)
355
-
356
- while cursor < total_len:
357
- # 1. 寻找当前 Chunk Size 行的结束符 (\n)
358
- newline_idx = file_data.find(b"\n", cursor)
359
- if newline_idx == -1:
360
- # 找不到换行符,说明格式不对,抛出异常让外层处理
361
- raise ValueError("Not chunked data")
362
-
363
- # 2. 提取并解析十六进制大小
364
- size_line = file_data[cursor:newline_idx].strip()
365
-
366
- # 处理可能的空行 (例如上一个 Chunk 后的 CRLF)
367
- if not size_line:
368
- cursor = newline_idx + 1
369
- continue
370
-
371
- # 这里不要捕获 ValueError,如果解析失败,直接抛出
372
- # 说明这根本不是 chunk size,而是普通数据
373
- chunk_size = int(size_line, 16)
374
-
375
- # Chunk Size 为 0 表示传输结束
376
- if chunk_size == 0:
377
- break
378
-
379
- # 3. 定位数据区域
380
- data_start = newline_idx + 1
381
- data_end = data_start + chunk_size
382
-
383
- if data_end > total_len:
384
- # 数据被截断,尽力读取
385
- chunks.append(file_data[data_start:])
386
- break
387
-
388
- # 4. 提取数据
389
- chunks.append(file_data[data_start:data_end])
390
-
391
- # 5. 移动游标
392
- cursor = data_end
393
- # 跳过尾随的 \r 和 \n
394
- while cursor < total_len and file_data[cursor] in (13, 10):
395
- cursor += 1
396
-
397
- return b"".join(chunks)
398
-
399
- @staticmethod
400
- def extract_http_file_data(full_request: str) -> Tuple[bytes, bytes]:
401
- """提取HTTP请求或响应中的文件数据 (修复版)"""
402
- # 1. 基础校验
403
- if not full_request:
404
- return b"", b""
405
-
406
- try:
407
- # 转为二进制
408
- raw_bytes = bytes.fromhex(full_request)
409
-
410
- # 分割 Header 和 Body
411
- header, file_data = FlowAnalyzer.split_http_headers(raw_bytes)
412
-
413
- # 处理 Chunked 编码
414
- with contextlib.suppress(Exception):
415
- file_data = FlowAnalyzer.dechunck_http_response(file_data)
416
-
417
- # 处理 Gzip 压缩
418
- with contextlib.suppress(Exception):
419
- if file_data.startswith(b"\x1f\x8b"):
420
- file_data = gzip.decompress(file_data)
421
-
422
- return header, file_data
423
-
424
- except ValueError as e:
425
- # 专门捕获 Hex 转换错误,并打印出来,方便你调试
426
- # 如果你在控制台看到这个错误,说明 Tshark 输出的数据格式非常奇怪
427
- logger.error(f"Hex转换失败: {str(e)[:100]}... 原数据片段: {full_request[:50]}")
428
- return b"", b""
429
- except Exception as e:
430
- logger.error(f"解析HTTP数据未知错误: {e}")
431
- return b"", b""
432
-
433
- @staticmethod
434
- def get_tshark_path(tshark_path: Optional[str]) -> str:
435
- default_tshark_path = get_default_tshark_path()
436
- use_path = tshark_path if tshark_path and os.path.exists(tshark_path) else default_tshark_path
437
- if not use_path or not os.path.exists(use_path):
438
- logger.critical("未找到 Tshark,请检查路径配置")
439
- exit(-1)
440
- return use_path
File without changes
File without changes