171 lines
5.5 KiB
Python
171 lines
5.5 KiB
Python
"""解析首页抓包 url.config:key/value 成对,对应菜单/图表/统计等接口回包。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from html.parser import HTMLParser
|
||
from typing import Any
|
||
|
||
|
||
class _MenuHTMLParser(HTMLParser):
|
||
def __init__(self) -> None:
|
||
super().__init__(convert_charrefs=True)
|
||
self.items: list[str] = []
|
||
self._in_a = False
|
||
self._buf = ""
|
||
|
||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||
if tag.lower() == "a":
|
||
self._in_a = True
|
||
self._buf = ""
|
||
|
||
def handle_endtag(self, tag: str) -> None:
|
||
if tag.lower() == "a" and self._in_a:
|
||
text = re.sub(r"\s+", " ", self._buf).strip()
|
||
if text:
|
||
self.items.append(text)
|
||
self._in_a = False
|
||
self._buf = ""
|
||
|
||
def handle_data(self, data: str) -> None:
|
||
if self._in_a:
|
||
self._buf += data
|
||
|
||
|
||
def parse_url_config(content: bytes | str) -> dict[str, Any]:
|
||
"""
|
||
格式示例:
|
||
key https://.../left!newleftmenu.action?...
|
||
value <ul>...</ul>
|
||
|
||
key https://.../hightchartCjlAndJianDu.action?...
|
||
value { ...json... }
|
||
"""
|
||
if isinstance(content, bytes):
|
||
text = content.decode("utf-8-sig", errors="ignore")
|
||
if not text.strip():
|
||
for enc in ("gb18030", "utf-16", "utf-16-le"):
|
||
try:
|
||
text = content.decode(enc)
|
||
if text.strip():
|
||
break
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
else:
|
||
text = content
|
||
|
||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||
# 按「行首 key 」切开
|
||
parts = re.split(r"(?m)^key\s+", text)
|
||
pairs: list[dict[str, Any]] = []
|
||
for part in parts:
|
||
part = part.strip()
|
||
if not part:
|
||
continue
|
||
# 第一行 URL,其后 value 段
|
||
if "\n" in part:
|
||
first, rest = part.split("\n", 1)
|
||
else:
|
||
first, rest = part, ""
|
||
url = first.strip()
|
||
m = re.match(r"(?is)^value\s*(.*)\Z", rest.strip(), flags=re.S)
|
||
body = (m.group(1) if m else rest).strip()
|
||
# 去掉仅有的空白
|
||
if not body:
|
||
body = ""
|
||
action = ""
|
||
am = re.search(r"!([a-zA-Z0-9_]+)\.action", url)
|
||
if am:
|
||
action = am.group(1)
|
||
else:
|
||
am = re.search(r"/([a-zA-Z0-9_!]+)\.action", url)
|
||
if am:
|
||
action = am.group(1).split("!")[-1]
|
||
kind = "empty"
|
||
parsed: Any = None
|
||
if not body:
|
||
kind = "empty"
|
||
elif body.lstrip().startswith("{") or body.lstrip().startswith("["):
|
||
kind = "json"
|
||
try:
|
||
parsed = json.loads(body)
|
||
except json.JSONDecodeError:
|
||
kind = "json_invalid"
|
||
parsed = None
|
||
elif "<" in body and ">" in body:
|
||
kind = "html"
|
||
parsed = body
|
||
else:
|
||
kind = "text"
|
||
parsed = body
|
||
pairs.append(
|
||
{
|
||
"url": url,
|
||
"action": action,
|
||
"kind": kind,
|
||
"body": body,
|
||
"parsed": parsed,
|
||
"empty": kind == "empty",
|
||
}
|
||
)
|
||
|
||
nav_items: list[str] = []
|
||
chart_json: dict[str, Any] | None = None
|
||
stats_json: dict[str, Any] | None = None
|
||
exceed_json: Any = None
|
||
warnings: list[str] = []
|
||
|
||
for p in pairs:
|
||
act = (p.get("action") or "").lower()
|
||
url = p.get("url") or ""
|
||
if p["kind"] == "html" and ("leftmenu" in act or "left" in url or "menu" in act):
|
||
parser = _MenuHTMLParser()
|
||
try:
|
||
parser.feed(str(p["parsed"] or ""))
|
||
parser.close()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
if parser.items:
|
||
nav_items = parser.items
|
||
elif p["kind"] == "json" and isinstance(p["parsed"], dict):
|
||
data = p["parsed"]
|
||
if "mcljChartData" in data or "cljdChartData" in data:
|
||
chart_json = data
|
||
elif any(k in data for k in ("yqwccds", "xzcxcds", "nodisposecount", "bdlength")):
|
||
stats_json = data
|
||
elif "gonghou" in act.lower() or "cjlcx" in act.lower() or "cx" in act:
|
||
exceed_json = data
|
||
elif chart_json is None and any(
|
||
isinstance(v, (list, dict)) for v in data.values()
|
||
):
|
||
# 兜底:第一个复杂 JSON 当图表
|
||
chart_json = data
|
||
elif p["empty"] and ("gonghou" in act.lower() or "cjlcx" in act.lower() or "CX" in url):
|
||
warnings.append(
|
||
f"接口 {p.get('action') or url} 返回为空:"
|
||
"超限测点表无抓包数据,将由主图表 JSON 按超限条件推导"
|
||
)
|
||
elif p["empty"]:
|
||
warnings.append(f"接口 {p.get('action') or url} 返回为空,已跳过")
|
||
|
||
return {
|
||
"ok": True,
|
||
"pairs": [
|
||
{
|
||
"url": p["url"],
|
||
"action": p["action"],
|
||
"kind": p["kind"],
|
||
"empty": p["empty"],
|
||
"body_len": len(p.get("body") or ""),
|
||
}
|
||
for p in pairs
|
||
],
|
||
"nav_items": nav_items,
|
||
"chart_json": chart_json,
|
||
"stats_json": stats_json,
|
||
"exceed_json": exceed_json,
|
||
"warnings": warnings,
|
||
"source": "url_config",
|
||
}
|