reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
#!/usr/bin/env python3
"""离线分析切片乱序图片验证码并尝试还原。
脚本只读取本地证据,不打开网页、不控制浏览器、不提交验证码、
不发送网络请求。
"""
from __future__ import annotations
import argparse
import itertools
import json
import math
import re
import sys
from pathlib import Path
from typing import Any
try:
from PIL import Image, ImageStat
except ImportError: # pragma: no cover - depends on local environment
Image = None # type: ignore[assignment]
ImageStat = None # type: ignore[assignment]
def configure_utf8_stdio() -> None:
for stream in (sys.stdout, sys.stderr):
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8")
configure_utf8_stdio()
def read_value(value: str | None) -> str:
if not value:
return ""
path = Path(value)
if path.exists() and path.is_file():
for encoding in ("utf-8", "utf-8-sig", "gb18030"):
try:
return path.read_text(encoding=encoding)
except UnicodeDecodeError:
continue
return path.read_text(errors="replace")
return value
def parse_order(value: str | None) -> list[int] | None:
if not value:
return None
numbers = [int(item) for item in re.findall(r"-?\d+", value)]
return numbers or None
def compact(text: str, limit: int = 100) -> str:
cleaned = re.sub(r"\s+", " ", text.strip())
if len(cleaned) <= limit:
return cleaned
return cleaned[: limit - 1] + "…"
def collect_text_signals(text: str) -> list[dict[str, str]]:
patterns = [
("切片乱序中文文案", r"切片乱序|分块乱序|图片分割|瓦片重排|分割顺序打乱|乱序图片还原|图片被切成多块|顺序打乱"),
("切片乱序英文文案", r"\b(?:scrambled tiles|tile scramble|scrambled image tiles|image tile restore|reorder tiles)\b"),
("tileOrder/pieceOrder 字段", r"\b(?:tileOrder|pieceOrder|tile_order|piece_order|restoreOrder|sliceOrder)\b"),
("CSS 背景切片线索", r"background-position|background-size|sprite"),
("Canvas 绘制切片线索", r"\bdrawImage\s*\("),
("布局位置线索", r"\b(?:transform|translate3d|translate|left|top)\b.{0,80}\b(?:px|tile|piece|slice)\b"),
("shuffle/scramble 线索", r"\b(?:shuffle|scramble|unscramble|slice|tile|piece)\b.{0,80}\b(?:captcha|verify|challenge|image)\b"),
]
signals: list[dict[str, str]] = []
for label, pattern in patterns:
match = re.search(pattern, text, flags=re.IGNORECASE | re.DOTALL)
if match:
signals.append({"label": label, "evidence": compact(match.group(0)), "source": "text"})
return signals
def extract_order_arrays(text: str) -> tuple[list[int] | None, list[dict[str, str]]]:
signals: list[dict[str, str]] = []
for pattern in (
r"\b(?:tileOrder|pieceOrder|tile_order|piece_order|restoreOrder|sliceOrder)\b\s*[:=]\s*\[([^\]]+)\]",
r"[\"'](?:tileOrder|pieceOrder|tile_order|piece_order|restoreOrder|sliceOrder)[\"']\s*:\s*\[([^\]]+)\]",
):
match = re.search(pattern, text, flags=re.IGNORECASE | re.DOTALL)
if not match:
continue
order = [int(item) for item in re.findall(r"-?\d+", match.group(1))]
if order:
signals.append({"label": "抽取显式切片顺序数组", "evidence": compact(match.group(0)), "source": "html"})
return order, signals
return None, signals
def extract_background_position_order(
text: str,
rows: int | None,
cols: int | None,
tile_width: int | None,
tile_height: int | None,
) -> tuple[list[int] | None, list[dict[str, str]]]:
if not rows or not cols or not tile_width or not tile_height:
return None, []
pattern = r"background-position\s*:\s*(-?\d+(?:\.\d+)?)px\s+(-?\d+(?:\.\d+)?)px"
order: list[int] = []
signals: list[dict[str, str]] = []
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
x = abs(float(match.group(1)))
y = abs(float(match.group(2)))
col = int(round(x / tile_width))
row = int(round(y / tile_height))
if 0 <= row < rows and 0 <= col < cols:
order.append(row * cols + col)
signals.append({"label": "抽取 background-position 切片来源", "evidence": compact(match.group(0)), "source": "html"})
expected = rows * cols
if len(order) == expected:
return order, signals
return None, signals
def extract_draw_image_order(
text: str,
rows: int | None,
cols: int | None,
tile_width: int | None,
tile_height: int | None,
) -> tuple[list[int] | None, list[dict[str, str]]]:
if not rows or not cols or not tile_width or not tile_height:
return None, []
pattern = r"drawImage\s*\([^,]+,\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)"
found: dict[int, int] = {}
signals: list[dict[str, str]] = []
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
sx = float(match.group(1))
sy = float(match.group(2))
dx = float(match.group(5))
dy = float(match.group(6))
src_col = int(round(sx / tile_width))
src_row = int(round(sy / tile_height))
dst_col = int(round(dx / tile_width))
dst_row = int(round(dy / tile_height))
if 0 <= src_row < rows and 0 <= src_col < cols and 0 <= dst_row < rows and 0 <= dst_col < cols:
found[dst_row * cols + dst_col] = src_row * cols + src_col
signals.append({"label": "抽取 drawImage 源/目标切片", "evidence": compact(match.group(0)), "source": "html"})
expected = rows * cols
if len(found) == expected:
return [found[index] for index in range(expected)], signals
return None, signals
def normalize_order(order: list[int], total: int) -> tuple[list[int] | None, str | None]:
if len(order) != total:
return None, f"顺序数组长度为 {len(order)},但网格需要 {total} 个切片"
if sorted(order) == list(range(total)):
return order, None
if sorted(item - 1 for item in order) == list(range(total)):
return [item - 1 for item in order], None
return None, "顺序数组不是 0 基或 1 基的完整排列"
def infer_grid(
image: Any,
rows: int | None,
cols: int | None,
tile_width: int | None,
tile_height: int | None,
) -> tuple[int | None, int | None, int | None, int | None, list[str]]:
warnings: list[str] = []
width, height = image.size
if tile_width and not cols:
cols = width // tile_width
if tile_height and not rows:
rows = height // tile_height
if rows and not tile_height:
tile_height = height // rows
if cols and not tile_width:
tile_width = width // cols
if not rows or not cols or not tile_width or not tile_height:
warnings.append("需要提供 rows/cols 或 tile-width/tile-height 才能切片分析图片")
return rows, cols, tile_width, tile_height, warnings
if rows * tile_height > height or cols * tile_width > width:
warnings.append("网格尺寸超过图片尺寸")
return rows, cols, tile_width, tile_height, warnings
def split_tiles(image: Any, rows: int, cols: int, tile_width: int, tile_height: int) -> list[Any]:
tiles = []
for row in range(rows):
for col in range(cols):
left = col * tile_width
top = row * tile_height
tiles.append(image.crop((left, top, left + tile_width, top + tile_height)).convert("RGB"))
return tiles
def edge_cost(a: Any, b: Any, direction: str) -> float:
if direction == "right":
edge_a = a.crop((a.width - 1, 0, a.width, a.height))
edge_b = b.crop((0, 0, 1, b.height))
else:
edge_a = a.crop((0, a.height - 1, a.width, a.height))
edge_b = b.crop((0, 0, b.width, 1))
diff = ImageStat.Stat(Image.eval(ImageChopsCompat.difference(edge_a, edge_b), lambda px: px)).mean
return float(sum(diff) / len(diff))
class ImageChopsCompat:
@staticmethod
def difference(left: Any, right: Any) -> Any:
from PIL import ImageChops
return ImageChops.difference(left, right)
def arrangement_cost(order: tuple[int, ...], tiles: list[Any], rows: int, cols: int) -> float:
total = 0.0
count = 0
for row in range(rows):
for col in range(cols):
current = tiles[order[row * cols + col]]
if col + 1 < cols:
total += edge_cost(current, tiles[order[row * cols + col + 1]], "right")
count += 1
if row + 1 < rows:
total += edge_cost(current, tiles[order[(row + 1) * cols + col]], "down")
count += 1
return total / max(count, 1)
def tile_diversity(tiles: list[Any]) -> float:
means: list[float] = []
for tile in tiles:
stat = ImageStat.Stat(tile)
means.extend(float(value) for value in stat.mean)
if not means:
return 0.0
avg = sum(means) / len(means)
variance = sum((value - avg) ** 2 for value in means) / len(means)
return math.sqrt(variance)
def image_edge_match_order(tiles: list[Any], rows: int, cols: int) -> tuple[list[int] | None, float, float, list[str]]:
total = rows * cols
warnings: list[str] = []
if total != len(tiles):
return None, 0.0, 0.0, ["切片数量和网格不一致"]
if total > 9:
warnings.append("切片超过 9 个,第一版不做全排列搜索;请优先使用页面顺序或显式 order")
return list(range(total)), 0.25, 0.0, warnings
best_order: tuple[int, ...] | None = None
best_cost = float("inf")
second_cost = float("inf")
for order in itertools.permutations(range(total)):
cost = arrangement_cost(order, tiles, rows, cols)
if cost < best_cost:
second_cost = best_cost
best_cost = cost
best_order = order
elif cost < second_cost:
second_cost = cost
diversity = tile_diversity(tiles)
separation = 0.0 if not math.isfinite(second_cost) else max(0.0, (second_cost - best_cost) / (second_cost + 1e-6))
confidence = max(0.25, min(0.95, separation * 4.0 + min(diversity / 128.0, 0.35)))
if separation < 0.02:
confidence = min(confidence, 0.42)
warnings.append("候选排列分数接近,可能是重复纹理、纯色块或边缘线索不足")
if diversity < 6:
confidence = min(confidence, 0.35)
warnings.append("切片视觉差异很低,建议改用页面顺序或人工/平台复核")
return list(best_order or range(total)), round(confidence, 2), round(best_cost, 4), warnings
def restore_image(
image: Any,
rows: int,
cols: int,
tile_width: int,
tile_height: int,
order_source_by_target: list[int],
output_path: Path,
) -> None:
tiles = split_tiles(image, rows, cols, tile_width, tile_height)
restored = Image.new("RGB", (cols * tile_width, rows * tile_height))
for target_index, source_index in enumerate(order_source_by_target):
row, col = divmod(target_index, cols)
restored.paste(tiles[source_index], (col * tile_width, row * tile_height))
output_path.parent.mkdir(parents=True, exist_ok=True)
restored.save(output_path)
def build_report(args: argparse.Namespace) -> dict[str, Any]:
html_text = read_value(args.html)
plain_text = read_value(args.text)
combined_text = "\n".join(part for part in (html_text, plain_text) if part)
signals = collect_text_signals(combined_text)
warnings: list[str] = []
rows = args.rows
cols = args.cols
tile_width = args.tile_width
tile_height = args.tile_height
explicit_order = parse_order(args.order_source_by_target)
extracted_order: list[int] | None = None
for extractor in (
extract_order_arrays,
lambda text: extract_background_position_order(text, rows, cols, tile_width, tile_height),
lambda text: extract_draw_image_order(text, rows, cols, tile_width, tile_height),
):
order, found_signals = extractor(combined_text)
signals.extend(found_signals)
if order:
extracted_order = order
break
image = None
if args.image:
if Image is None:
warnings.append("当前 Python 环境缺少 Pillow,无法读取图片;只能分析文本/HTML 线索")
else:
image_path = Path(args.image)
if image_path.exists() and image_path.is_file():
image = Image.open(image_path).convert("RGB")
rows, cols, tile_width, tile_height, grid_warnings = infer_grid(image, rows, cols, tile_width, tile_height)
warnings.extend(grid_warnings)
else:
warnings.append("图片文件不存在")
total = rows * cols if rows and cols else 0
normalized_order: list[int] | None = None
order_warning: str | None = None
order_source = ""
if explicit_order and total:
normalized_order, order_warning = normalize_order(explicit_order, total)
order_source = "argument"
elif extracted_order and total:
normalized_order, order_warning = normalize_order(extracted_order, total)
order_source = "page"
if order_warning:
warnings.append(order_warning)
confidence = 0.0
edge_cost_value: float | None = None
restore_strategy = "manual-or-platform"
if normalized_order:
confidence = 0.92 if order_source == "argument" else 0.86
restore_strategy = "page-order"
elif image is not None and rows and cols and tile_width and tile_height:
tiles = split_tiles(image, rows, cols, tile_width, tile_height)
normalized_order, confidence, edge_cost_value, edge_warnings = image_edge_match_order(tiles, rows, cols)
warnings.extend(edge_warnings)
restore_strategy = "image-edge-match" if confidence >= 0.55 else "manual-or-platform"
elif signals:
confidence = 0.58
restored_image: str | None = None
if image is not None and rows and cols and tile_width and tile_height and normalized_order and args.output_image:
if len(normalized_order) == rows * cols:
output_path = Path(args.output_image)
restore_image(image, rows, cols, tile_width, tile_height, normalized_order, output_path)
restored_image = str(output_path)
else:
warnings.append("顺序长度不匹配,未输出还原图片")
is_tile_scramble = bool(signals or normalized_order or (image is not None and rows and cols))
if not signals and normalized_order:
signals.append({"label": "提供显式切片顺序", "evidence": ",".join(str(item) for item in normalized_order), "source": "argument"})
if image is not None and rows and cols:
signals.append({"label": "图片网格切片证据", "evidence": f"{rows}x{cols}", "source": "image"})
return {
"is_tile_scramble": is_tile_scramble,
"grid": {
"rows": rows,
"cols": cols,
"tile_width": tile_width,
"tile_height": tile_height,
"total": total,
},
"order_source_by_target": normalized_order,
"confidence": round(confidence, 2),
"restore_strategy": restore_strategy,
"edge_match_cost": edge_cost_value,
"signals": signals,
"warnings": warnings,
"restored_image": restored_image,
"notes": [
"脚本只做离线切片判断和图片还原,不打开网页、不点击页面、不提交验证。",
"优先相信页面逻辑中的顺序字段;纯图片边缘匹配低置信度时应切换人工或平台复核。",
],
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="离线分析切片乱序图片验证码")
parser.add_argument("--image", help="待分析的乱序图片路径")
parser.add_argument("--html", help="HTML/CSS/JS 片段或文件路径")
parser.add_argument("--text", help="页面文案或分析备注")
parser.add_argument("--rows", type=int, help="切片行数")
parser.add_argument("--cols", type=int, help="切片列数")
parser.add_argument("--tile-width", type=int, help="单块宽度")
parser.add_argument("--tile-height", type=int, help="单块高度")
parser.add_argument("--order-source-by-target", help="还原顺序:每个目标位置使用哪个当前切片索引,支持逗号分隔")
parser.add_argument("--output-image", help="输出还原图片路径")
parser.add_argument("--pretty", action="store_true", help="以缩进格式输出 JSON")
return parser
def main() -> int:
args = build_parser().parse_args()
report = build_report(args)
print(json.dumps(report, ensure_ascii=False, indent=2 if args.pretty else None))
return 0
if __name__ == "__main__":
raise SystemExit(main())