关注它,不迷路。

  • 本文章中所有内容仅供学习交流,不可用于任何商业用途和非法用途,否则后果自负,如有侵权,请联系作者立即删除

一.题目地址

https://match.yuanrenxue.cn/match/4

二.抓包分析

打开控制台后,抓包分析:

翻页请求数据后,加载了一堆图片。并且接口返回的不是我们直接想要的数据。

逆向分析我不是很在行,直接看别人怎么做的吧:

#4 猿人学 Web 第一届 第 4 题 解题

看他用了ocr识别库。古法逆向,到此结束。

三.AI工具

我这里使用codeX的cli工具 + Gpt-5.4 xhigh 的AI模型。

使用的MCP则是 JSReverser-MCP

四.提示词

使用:JSReverse MCP方式:插桩采集完整的输入输出及中间态数据,与本地算法进行逻辑一致性和结果正确性对比分析。URL: 【https://match.yuanrenxue.cn/match/4】目标:【目标接口,https://match.yuanrenxue.cn/api/question/4,纯雪碧图,请识别】  触发方式: 【翻面】 约束:不使用playwright等浏览器自动化工具,不能联网搜索公开案例 cookie:将{"sessionid":"XXXxxx"}加入到请求代码中,表示当前登录UA设置:yuanrenxue交付:可运行的python脚本,运行后打印1-5页的响应数据,并计算总和

五.AI提供的Python纯算源码

import base64import collectionsimport hashlibimport reimport timefrom collections import dequefrom io import BytesIO
import numpy as npimport requestsfrom PIL import Image, ImageFilter

API_URL = "https://match.yuanrenxue.cn/api/question/4"SESSION_ID = "XXXXXX"DISPLAY_WIDTH = 8.5PAGE_SIZE = 10PAGE_COUNT = 5
TD_RE = re.compile(r"<td>(.*?)</td>", re.S)IMG_RE = re.compile(    r'<img[^>]+src="data:image/png;base64,([^"]+)"[^>]+class="([^"]+)"[^>]+style="([^"]+)"')LEFT_RE = re.compile(r"left:([\-0-9.]+)px")
# These templates were collected by browser-side instrumentation after the page# hid the md5-selected noise class and the remaining images were scaled to 8.5px.EXACT_FEATURE_MAP = {    "0000000001f03b83b83f01f03f83383b83f00e0000000000": "8",    "0000000001f01f03803e03f03b83383b81f00c0000000000": "6",    "0000000000300700700f01b03b03f83f8030000000000000": "4",    "0000000003f83f80380700700e00e00c01c0000000000000": "7",    "0000000001f01f01801803f00780380383f01c0000000000": "5",    "0000000000701f01f0070070070070070070000000000000": "1",    "0000000001f03f80380380700e01c03f83f8000000000000": "2",    "0000000001f03f83383b83f81f80380703f01c0000000000": "9",    "0000000003f03f00300701f00700382383f01c0000000000": "3",    "0000000001f03f83383383183183383b81f00c0000000000": "0",    "0000000001f01f00380700f00780381381f00e0000000000": "3",    "0000000000f01f81981f00f01f81981b81f0060000000000": "8",    "0000000000f01f01801f01f81981981f80f0060000000000": "6",    "0000000000f01f81981981f80f80180381f00e0000000000": "9",    "0000000001f81f80380300700600600e00e0000000000000": "7",    "0000000000700f00f0030030030030030030000000000000": "1",    "0000000000300380780780f81b81f81f8038000000000000": "4",    "0000000000f01f81981983981981981f80f0060000000000": "0",    "0000000001f01f80180380700f00c01f81f8000000000000": "2",    "0000000000f01f01801c01f00780180381f00e0000000000": "5",}

def build_session() -> requests.Session:    session = requests.Session()    session.headers.update(        {            "User-Agent": "yuanrenxue",            "X-Requested-With": "XMLHttpRequest",        }    )    session.cookies.update({"sessionid": SESSION_ID})    return session

def hidden_class(key: str, value: str) -> str:    raw = base64.b64encode((key + value).encode()).decode().replace("=", "")    return hashlib.md5(raw.encode()).hexdigest()

def mask_to_hex(mask: np.ndarray) -> str:    bits = "".join("1" if x else "0" for x in mask.reshape(-1).tolist())    return hex(int(bits, 2))[2:].zfill((len(bits) + 3) // 4)

def hex_to_mask(feature_hex: str) -> np.ndarray:    bits = bin(int(feature_hex, 16))[2:].zfill(len(feature_hex) * 4)    return np.array([bit == "1" for bit in bits], dtype=bool).reshape(16, 12)

def render_digit_mask(image_b64: str) -> np.ndarray:    image = Image.open(BytesIO(base64.b64decode(image_b64))).convert("RGB")    arr = np.asarray(image)    flat = arr.reshape(-1, 3)    bg = np.array(collections.Counter(map(tuple, flat)).most_common(1)[0][0], dtype=np.float32)    dist = np.sqrt(((arr.astype(np.float32) - bg) ** 2).sum(axis=2))    mask = (dist > 35).astype(np.uint8) * 255
    rendered_w = int(round(DISPLAY_WIDTH))    rendered_h = max(1, int(round(image.height * DISPLAY_WIDTH / image.width)))    rendered = Image.fromarray(mask).resize(        (rendered_w, rendered_h),        Image.Resampling.BILINEAR,    )    rendered_arr = np.asarray(rendered)    if not np.any(rendered_arr > 100):        raise ValueError("digit mask is empty")
    canvas = np.zeros((16, 12), dtype=np.uint8)    offset_y = (canvas.shape[0] - rendered_arr.shape[0]) // 2    offset_x = (canvas.shape[1] - rendered_arr.shape[1]) // 2    canvas[        offset_y : offset_y + rendered_arr.shape[0],        offset_x : offset_x + rendered_arr.shape[1],    ] = rendered_arr    return canvas > 100

def crop_mask(mask: np.ndarray) -> np.ndarray:    ys, xs = np.where(mask)    if len(xs) == 0:        raise ValueError("empty digit mask")    return mask[ys.min() : ys.max() + 1, xs.min() : xs.max() + 1]

def normalize_mask(mask: np.ndarray) -> np.ndarray:    cropped = crop_mask(mask)    image = Image.fromarray(cropped.astype(np.uint8) * 255)    resized = image.resize((14, 18), Image.Resampling.NEAREST)    return np.asarray(resized) > 0

def augmented_masks(mask: np.ndarray) -> list[np.ndarray]:    image = Image.fromarray(mask.astype(np.uint8) * 255)    variants: list[np.ndarray] = []
    for dx in (-1, 0, 1):        for dy in (-1, 0, 1):            canvas = Image.new("L", image.size, 0)            canvas.paste(image, (dx, dy))            variants.append(np.asarray(canvas) > 0)
    variants.append(np.asarray(image.filter(ImageFilter.MaxFilter(3))) > 0)    variants.append(np.asarray(image.filter(ImageFilter.MinFilter(3))) > 0)    return variants

def build_fallback_library() -> dict[str, list[np.ndarray]]:    library: dict[str, list[np.ndarray]] = {}    for feature_hex, digit in EXACT_FEATURE_MAP.items():        normalized = normalize_mask(hex_to_mask(feature_hex))        library.setdefault(digit, []).extend(augmented_masks(normalized))    return library

FALLBACK_LIBRARY = build_fallback_library()

def hole_stats(mask: np.ndarray) -> tuple[list[tuple[int, float]], tuple[int, int]]:    cropped = crop_mask(mask)    height, width = cropped.shape    seen = np.zeros_like(cropped, dtype=bool)    holes: list[tuple[int, float]] = []
    for y in range(height):        for x in range(width):            if cropped[y, x] or seen[y, x]:                continue
            queue = deque([(y, x)])            seen[y, x] = True            points: list[tuple[int, int]] = []            touches_border = False
            while queue:                cy, cx = queue.popleft()                points.append((cy, cx))                if cy in (0, height - 1) or cx in (0, width - 1):                    touches_border = True
                for ny, nx in ((cy - 1, cx), (cy + 1, cx), (cy, cx - 1), (cy, cx + 1)):                    if 0 <= ny < height and 0 <= nx < width and not cropped[ny, nx] and not seen[ny, nx]:                        seen[ny, nx] = True                        queue.append((ny, nx))
            if not touches_border:                center_y_ratio = (sum(py for py, _ in points) / len(points)) / height                holes.append((len(points), center_y_ratio))
    return holes, (height, width)

def nearest_digit(mask: np.ndarray, candidates: set[str]) -> str:    normalized = normalize_mask(mask)    best_digit = ""    best_distance = None
    for digit in candidates:        for template in FALLBACK_LIBRARY[digit]:            distance = int(np.count_nonzero(normalized != template))            if best_distance is None or distance < best_distance:                best_distance = distance                best_digit = digit
    if not best_digit:        raise ValueError("no fallback candidate matched")    return best_digit

def classify_digit(mask: np.ndarray) -> str:    feature_hex = mask_to_hex(mask)    digit = EXACT_FEATURE_MAP.get(feature_hex)    if digit is not None:        return digit
    holes, (_, width) = hole_stats(mask)    if len(holes) == 2:        return "8"    if len(holes) == 1:        area, center_y_ratio = holes[0]        if area <= 2:            return "4"        if area >= 8:            return "0"        if center_y_ratio < 0.4:            return "9"        return "6"    if width <= 5:        return "1"
    return nearest_digit(mask, {"2", "3", "5", "7"})

def decode_rows(page_json: dict) -> list[int]:    hidden = hidden_class(page_json["key"], page_json["value"])    numbers: list[int] = []
    for td_html in TD_RE.findall(page_json["info"]):        visible_parts = []        for dom_idx, match in enumerate(IMG_RE.finditer(td_html)):            image_b64, classes, style = match.groups()            css_class = [x for x in classes.split() if x != "img_number"][0]            if css_class == hidden:                continue
            left = float(LEFT_RE.search(style).group(1))            visible_idx = len(visible_parts)            visible_parts.append((visible_idx * DISPLAY_WIDTH + left, dom_idx, image_b64))
        visible_parts.sort(key=lambda x: (x[0], x[1]))
        digits = [classify_digit(render_digit_mask(image_b64)) for _, _, image_b64 in visible_parts]        numbers.append(int("".join(digits)))
    return numbers

def fetch_page(session: requests.Session, page: int) -> dict:    response = session.get(        API_URL,        params={"page": page, "pageSize": PAGE_SIZE, "kw": ""},        timeout=20,    )    response.raise_for_status()    return response.json()

def main() -> None:    session = build_session()    total = 0
    for page in range(1, PAGE_COUNT + 1):        page_json = fetch_page(session, page)        numbers = decode_rows(page_json)        page_sum = sum(numbers)        total += page_sum        display_numbers = [f"{number:06d}" for number in numbers]        print(f"page {page}: {display_numbers} | sum={page_sum}")        if page != PAGE_COUNT:            time.sleep(1)
    print(f"total={total}")

if __name__ == "__main__":    main()

运行结果:

从AI开始分析到结束,大约耗时50分钟左右。

今天的分享就到这里,感谢阅读。

欢迎加入知识星球,学习更多AST和爬虫技巧。

图片

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐