"""
=============================================================================
CS525 · 第 4 讲模块：一张合成的"网页抓取表"（webtable.py）
=============================================================================

对应讲义：lectures/lecture04_data_tools.md

-----------------------------------------------------------------------------
【为什么要造这张表】
-----------------------------------------------------------------------------
第 4 讲的客座讲者 Tony Wang 的第一张 slides 说：数据是**表格**——有类型、可以有很多列、上万亿行。
预训练数据管线里的典型一行就是一个网页：主键、URL、域名、语言、正文、长度、质量分、抓取时间……
真实的 Common Crawl 一个月就是约 30 亿个网页、约 100 TB 压缩数据，没法在笔记本上玩。
这里生成一张同样"形状"的小表（默认 6 万行），并且知道它的全部真相：

    · 域名服从长尾分布：少数大网站占了很大一部分网页（"数据倾斜"的来源，代码 02）；
    · 一部分网页是另一个网页的**完全复制**（镜像站、转载），一部分是**近似复制**（改了几个词、加了页脚），
      列 dup_of 记录它复制的是哪个网页（−1 = 原创）——代码 03 用它检验去重的效果；
    · 一部分网页是模板化的低质量页面（导航、广告、"Lorem ipsum"），质量分 quality 偏低。

【列 Columns】
    doc_id      整数主键
    url         字符串
    domain      字符串（低基数：几百个不同的值）
    lang        字符串（en / de / fr / zh / es，极低基数）
    month       抓取月份（整数 1–12）
    n_words     正文词数
    quality     质量分（0–1 的浮点数；模拟一个质量分类器的输出）
    text        正文（字符串，几百个字符）
    dup_of      真相：复制自哪个 doc_id（−1 = 原创）；真实数据里没有这一列

【运行自检】
    python3 webtable.py        # 打印几行和一些统计
"""

import random

LANGS = ["en", "de", "fr", "zh", "es"]
LANG_P = [0.55, 0.12, 0.10, 0.13, 0.10]
TOPICS = {
    "science": "cell energy quantum protein climate theory experiment data model atom galaxy neuron".split(),
    "sports": "match goal team season player coach league score final injury transfer stadium".split(),
    "cooking": "recipe butter oven garlic flour sauce minutes salt pan bake chicken pepper".split(),
    "tech": "software update phone chip battery server cloud code release bug laptop network".split(),
    "travel": "hotel beach flight city museum tour island train visa map hostel guide".split(),
    "finance": "market stock price bank loan rate inflation fund invest crypto tax budget".split(),
}
COMMON = ("the a of and to in is that for on with as it was at by this be are from or an have "
          "but not they which one you all were we when there can more if out so said what up its about").split()
BOILER = "home login register cookie policy privacy terms subscribe newsletter share menu click here".split()


def _zipf_domains(rng, n_domains):
    """生成 n_domains 个域名，以及长尾（Zipf 式）的抽样权重：第 k 个域名的权重 ∝ 1 / k。"""
    names = [f"site{k:03d}.{rng.choice(['com', 'org', 'net', 'de', 'cn'])}" for k in range(n_domains)]
    weights = [1.0 / (k + 1) for k in range(n_domains)]
    return names, weights


def _make_text(rng, topic, n_words, low_quality):
    """
    生成一段正文：常用词 + 主题词；低质量页面混入大量导航 / 模板词。

    【返回】(文本, 词数)
    """
    words = []
    for _ in range(n_words):
        u = rng.random()
        if low_quality and u < 0.5:
            words.append(rng.choice(BOILER))
        elif u < 0.6:
            words.append(rng.choice(COMMON))
        else:
            words.append(rng.choice(TOPICS[topic]))
    return " ".join(words), len(words)


def _near_copy(rng, text):
    """近似复制：随机替换约 5% 的词，再加一个页脚。返回新文本。"""
    words = text.split()
    for i in range(len(words)):
        if rng.random() < 0.05:
            words[i] = rng.choice(COMMON)
    words += ["copyright", str(rng.randint(2015, 2024)), "all", "rights", "reserved"]
    return " ".join(words)


def make_table(n_rows=60000, seed=0, n_domains=400, p_exact=0.08, p_near=0.07):
    """
    生成网页表，返回"列的字典"：列名 → Python 列表（每个列表长度 n_rows）。

    【参数】n_rows：行数；seed：随机种子；n_domains：域名个数；
            p_exact / p_near：每一行是"完全复制" / "近似复制"另一行的概率
    【返回】字典 {列名: 列表}，外加真相列 dup_of
    【思路】按行生成：先决定这一行是原创、完全复制还是近似复制。复制的源头从已有的原创行里随机挑。
            完全复制只换 URL 和抓取月份（同一篇文章出现在镜像站上）；近似复制还改几个词、加页脚。
    【边界】前几行没有可复制的源头时，强制为原创。
    【Python 小知识】这里用 random.Random（标准库）而不是 numpy：要处理的大多是字符串，标准库更直接；
                    固定种子后，Python 3.9 和 3.13 生成的数据逐字相同。
    """
    rng = random.Random(seed)
    domains, weights = _zipf_domains(rng, n_domains)
    cols = {k: [] for k in ["doc_id", "url", "domain", "lang", "month", "n_words", "quality", "text", "dup_of"]}
    originals = []
    for i in range(n_rows):
        u = rng.random()
        kind = "orig"
        if originals and u < p_exact:
            kind = "exact"
        elif originals and u < p_exact + p_near:
            kind = "near"
        dom = rng.choices(domains, weights=weights)[0]
        if kind == "orig":
            low = rng.random() < 0.2
            topic = rng.choice(sorted(TOPICS))
            text, nw = _make_text(rng, topic, rng.randint(40, 160), low)
            lang = rng.choices(LANGS, weights=LANG_P)[0]
            q = rng.betavariate(1.5, 5.0) if low else rng.betavariate(5.0, 2.0)
            dup = -1
            originals.append(i)
        else:
            src = rng.choice(originals)
            text = cols["text"][src] if kind == "exact" else _near_copy(rng, cols["text"][src])
            nw = len(text.split())
            lang = cols["lang"][src]
            q = cols["quality"][src]
            dup = src
        cols["doc_id"].append(i)
        cols["url"].append(f"https://{dom}/page/{i}")
        cols["domain"].append(dom)
        cols["lang"].append(lang)
        cols["month"].append(rng.randint(1, 12))
        cols["n_words"].append(nw)
        cols["quality"].append(round(q, 4))
        cols["text"].append(text)
        cols["dup_of"].append(dup)
    return cols


def rows_of(cols, names=None):
    """把列字典转成"行的列表"（每行一个字典）。names 指定要哪些列（默认全部，但不含真相列 dup_of）。"""
    names = [k for k in cols if k != "dup_of"] if names is None else names
    return [{k: cols[k][i] for k in names} for i in range(len(cols["doc_id"]))]


if __name__ == "__main__":
    cols = make_table(2000, seed=1)
    for r in rows_of(cols)[:3]:
        print({k: (v[:60] + "…" if isinstance(v, str) and len(v) > 60 else v) for k, v in r.items()})
    n = len(cols["doc_id"])
    print(f"\n{n} 行；完全复制 {sum(1 for d, t in zip(cols['dup_of'], cols['text']) if d >= 0 and cols['text'][d] == t)}，"
          f"近似复制 {sum(1 for d, t in zip(cols['dup_of'], cols['text']) if d >= 0 and cols['text'][d] != t)}")
    from collections import Counter
    top = Counter(cols["domain"]).most_common(3)
    print("最大的 3 个域名：", top, f"（共 {len(set(cols['domain']))} 个域名）")
