如何自动破解 Hardle

· · 科技·工程

本做法仅保证在 Hardle.org 上有效。

其实我本来只想看看 Hardle 的单词表的。

断网后测试发现仍能正常判断单词是否合法,因此单词表一定被加载到本地了。

在删除缓存后重启请求网页,发现请求文件如下:

逐个检查后可以发现单词表藏在 TrashIcon.BbFlvf2K.js 文件下。

本来到这里已经结束了,大家不难利用这个词表写出一个自动破解 Hardle 的交互程序。但是紧跟着词表的是一个奇怪的 map:

考虑到 map 的键是日期格式,那么是个人都会猜测这玩意是答案列表。

经测试后的确如此。

并且这个表只列到 6.10.2027,就是 27 年 10 月 6 号,那大家也就只能再玩一年多 hardle 了。

希望作者记得及时更新吧。

附 Deepseek 生成的提取代码,注意代码输出包含当日答案。

:::info[Code]

"""
Hardle Word List Extraction Tool
Extract complete word list and daily answers from https://hardle.org/
"""

import re
import json
from datetime import datetime, date
from urllib.request import urlopen

# Hardle JS chunk URL
CHUNK_URL = "https://hardle.org/_app/immutable/chunks/TrashIcon.BbFlvf2K.js"

def fetch_js(url: str) -> str:
    print(f"Downloading: {url}")
    with urlopen(url) as resp:
        data = resp.read().decode("utf-8")
    print(f"Download complete: {len(data):,} bytes")
    return data

def extract_word_list(text: str) -> list[str]:
    match = re.search(r'ma=\[(?:"[a-z]+"(?:,"[a-z]+")*)\]', text)
    if not match:
        raise ValueError("Word list array not found!")
    words = json.loads("[" + match.group(0).split("=[", 1)[1])
    return words

def extract_answer_map(text: str) -> dict[str, str]:
    start = text.index("ya=new Map([") + len("ya=new Map([")
    depth = 1
    i = start
    while depth > 0 and i < len(text):
        if text[i] == "[":
            depth += 1
        elif text[i] == "]":
            depth -= 1
        i += 1
    map_str = text[start : i - 1]

    entries = re.findall(r'\["(\d{1,2}\.\d{1,2}\.\d{4})","([a-z]+)"\]', map_str)
    return {date_str: word for date_str, word in entries}

def get_today_answer(answer_map: dict[str, str]) -> tuple[str, str] | None:
    today = date.today()
    de_date = f"{today.day}.{today.month}.{today.year}"
    word = answer_map.get(de_date)
    if word:
        return de_date, word
    return None

def main():
    print("=" * 60)
    print("   Hardle Word List Extraction Tool")
    print("=" * 60)

    js = fetch_js(CHUNK_URL)

    print("\nExtracting complete word list...")
    word_list = extract_word_list(js)
    print(f"   Total {len(word_list):,} words")

    with open("hardle_words.txt", "w", encoding="utf-8") as f:
        f.write("\n".join(word_list))
    print("   Saved to hardle_words.txt")

    print("\nExtracting daily answers...")
    answer_map = extract_answer_map(js)
    sorted_dates = sorted(
        answer_map.keys(),
        key=lambda d: datetime.strptime(d, "%d.%m.%Y"),
    )
    print(f"   Total {len(answer_map):,} answers")
    print(f"   Date range: {sorted_dates[0]} ~ {sorted_dates[-1]}")

    with open("hardle_answers.json", "w", encoding="utf-8") as f:
        json.dump(
            {d: answer_map[d] for d in sorted_dates},
            f,
            indent=2,
            ensure_ascii=False,
        )
    print("   Saved to hardle_answers.json")

    print("\nToday's answer:")
    today_info = get_today_answer(answer_map)
    if today_info:
        de_date, word = today_info
        print(f"   {de_date}  →  {word.upper()}")
    else:
        print("   Today's answer not found")

    print("\nStatistics:")
    print(f"   Valid guess words:     {len(word_list):,}")
    print(f"   Daily answers:         {len(answer_map):,}")
    print(f"   Answer ratio:          {len(answer_map) / len(word_list) * 100:.1f}%")

    not_in_list = [w for w in answer_map.values() if w not in word_list]
    if not_in_list:
        print(f"   {len(not_in_list)} answer(s) not in word list: {not_in_list}")
    else:
        print("   All answers are in the word list")

    print("\n" + "=" * 60)
    print("   Done!")
    print("=" * 60)

if __name__ == "__main__":
    main()

:::