50 分钟历史文本快照已执行的网页阅读版

审计历史文本语料

保留来源电子书和许可,检查章节边界并公开分词决策。

人文与社会科学 · 第 01 课🔋 离线核心分析⏱ 50 分钟历史文本快照📦 numpy · pandas · matplotlib · IPython

目标与运行方法

本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。

学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。

环境与参数

In [1] · Python
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
import re
from collections import Counter
from IPython.display import display as _display
def display(value):
    if isinstance(value,pd.DataFrame):
        table=value.to_html(max_rows=12,escape=True)
        style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
        return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
    return _display(value)
RECIPE_ID='hu01-corpus-audit'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
                      "axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6
Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads
Core data mode: embedded snapshot; no network requests

读取内嵌快照

In [2] · Python
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
    """Read embedded data from this notebook, including a renamed copy."""
    for path in sorted(Path.cwd().glob("*.ipynb")):
        try:
            notebook = json.loads(path.read_text(encoding="utf-8"))
            metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
            blob = metadata.get("snapshots", {}).get(snapshot_key)
            if metadata.get("recipe_id") != recipe_id or not blob:
                continue
            if blob.get("sha256") != expected_sha256:
                continue
            raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
            if hashlib.sha256(raw).hexdigest() != expected_sha256:
                raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
            return json.loads(raw)
        except (OSError, json.JSONDecodeError):
            continue
    raise FileNotFoundError(
        "Save/open the complete .ipynb in the active working folder. "
        "The embedded snapshot could not be located for " + recipe_id)

SNAPSHOT_HASHES={'novels': 'c03763abb21b2380d2934b088f1c22a6d5a4675b17e23534e3f7343dc90084ec'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['novels']

方法函数

In [3] · Python
def strip_ebook_wrapper(raw_text):
    """Return the analysis body while the unmodified source/complete license remain in metadata."""
    text=raw_text.replace('\r\n','\n').replace('\r','\n')
    start=re.search(r'\*\*\* START[^\n]*\*\*\*',text,re.I)
    end=re.search(r'\*\*\* END[^\n]*\*\*\*',text,re.I)
    if not start or not end or end.start()<=start.end():raise ValueError('Ebook start/end markers not found')
    return text[start.end():end.start()]

print("Method ready: strip_ebook_wrapper")
Method ready: strip_ebook_wrapper
In [4] · Python
def split_novel_chapters(body):
    """Declared two-ebook rule: repeated labels resolve to last heading, excluding their TOCs."""
    headings=list(re.finditer(r'(?im)^\s*chapter\s+([ivxlcdm]+|\d+)[.\]]*\s*$',body))
    if not headings:raise ValueError('No supported chapter headings')
    last={match.group(1).upper():match for match in headings}
    chosen=sorted(last.values(),key=lambda match:match.start());chapters=[]
    for j,match in enumerate(chosen):
        text=body[match.end():chosen[j+1].start() if j+1<len(chosen) else len(body)]
        text=re.sub(r'\[Illustration\b[^\]]*\]',' ',text,flags=re.I|re.S)
        text=text.replace('_','').replace('’',"'").strip()
        if len(text)<300:raise ValueError('Unexpected short chapter after declared TOC rule')
        chapters.append(dict(label=match.group(1).upper(),order=j+1,text=text))
    return chapters

print("Method ready: split_novel_chapters")
Method ready: split_novel_chapters
In [5] · Python
def tokenize_words(text):
    """Transparent English-letter tokenizer; punctuation/case decisions are part of the method."""
    return re.findall(r"[a-z]+(?:'[a-z]+)?",text.lower().replace('’',"'"))

print("Method ready: tokenize_words")
Method ready: tokenize_words

数据与模型边界

快照保留完整未修改来源电子书及原许可。章节分析使用《傲慢与偏见》61 章及《弗兰肯斯坦》24 章;前言和后者书信框架不在本次声明范围。英文字母分词是方法选择,不是语言无关定义。词频、表面模式共现和 SVD 轴不能独立证明作者意图、人物关系或情感。

分步分析

1. 保留来源并检查边界

原电子书及许可完整留在 metadata。分析仅在内存删除包装和声明排版元素;对两份快照,重复章节标签保留末次标题,排除《弗兰肯斯坦》目录重复。这不是通用书籍解析器。

In [6] · Python
novels=snapshot("novels");corpus={};chapter_rows=[]
for book in novels["books"]:
 chapters=split_novel_chapters(strip_ebook_wrapper(book["raw_text"]))
 tokens=tokenize_words(" ".join(chapter["text"] for chapter in chapters))
 corpus[book["id"]]={"title":book["title"],"author":book["author"],"chapters":chapters,"tokens":tokens}
 for chapter in chapters:chapter_rows.append({"book_id":book["id"],"book":book["title"],"chapter":chapter["order"],"source_label":chapter["label"],"characters":len(chapter["text"]),"tokens":len(tokenize_words(chapter["text"])),"text":chapter["text"]})
chapter_table=pd.DataFrame(chapter_rows)
assert len(corpus[1342]["chapters"])==61 and len(corpus[84]["chapters"])==24
print(novels["source_notice"])
print("Declared chapter counts:",{b:len(corpus[b]["chapters"]) for b in corpus})
display(chapter_table.drop(columns="text").head(8))
These source ebooks are available free of charge. Complete unmodified source texts and their licenses are included in this snapshot. Project Gutenberg is acknowledged as the transcription source; its name is not used as a course title or endorsement.
Declared chapter counts: {1342: 61, 84: 24}
book_id book chapter source_label characters tokens
0 1342 Pride and Prejudice 1 I 4490 853
1 1342 Pride and Prejudice 2 II 4293 799
2 1342 Pride and Prejudice 3 III 9560 1704
3 1342 Pride and Prejudice 4 IV 5931 1064
4 1342 Pride and Prejudice 5 V 5246 962
5 1342 Pride and Prejudice 6 VI 13023 2353
6 1342 Pride and Prejudice 7 VII 11263 2000
7 1342 Pride and Prejudice 8 VIII 10993 1936

2. 解读章节规模

柱计数声明英文字母规则下的词,不是句子、思想或叙事重要性的数量。

In [7] · Python
fig,ax=plt.subplots(figsize=(10,4))
for book_id,part in chapter_table.groupby("book_id"):ax.plot(part.chapter,part.tokens,"o-",ms=3,label=corpus[book_id]["title"])
ax.set(xlabel="Declared chapter order",ylabel="Processed word tokens",title="Chapter boundaries and unequal text lengths");ax.legend(fontsize=8);plt.tight_layout();plt.show()
Computed figure from this code cell

3. 比较透明的数量定义

字符、词和归一化词型分别计数,不可互换分母。小写合并大小写变体,撇号处理和插图删除为可见方法选择。

In [8] · Python
summary=pd.DataFrame([{ "book":entry["title"],"chapters":len(entry["chapters"]),"word_tokens":len(entry["tokens"]),"word_types":len(set(entry["tokens"]))} for entry in corpus.values()]).set_index("book")
display(summary)
fig,ax=plt.subplots(figsize=(9,4));summary[["word_tokens","word_types"]].plot.bar(ax=ax)
ax.set(ylabel="Processed count",title="Tokens versus distinct types");plt.xticks(rotation=15);plt.tight_layout();plt.show()
chapters word_tokens word_types
book
Pride and Prejudice 61 122185 6319
Frankenstein 24 69610 6703
Computed figure from this code cell

4. 解释前检查高频词

这些是简单分词的来源章节计数,包括功能词。按数量/字母确定顺序显示前十词,高频本身不是主题或情感标签。

In [9] · Python
fig,axes=plt.subplots(1,2,figsize=(11,4))
for ax,entry in zip(axes,corpus.values()):
 counts=Counter(entry["tokens"]);top=sorted(counts.items(),key=lambda item:(-item[1],item[0]))[:10]
 ax.barh([w for w,n in top][::-1],[n for w,n in top][::-1]);ax.set(xlabel="Chapter-corpus token count",title=entry["title"])
plt.tight_layout();plt.show()
Computed figure from this code cell

5. 核对声明范围并导出

直接核对章节数及计数定义,导出边界标签和处理计数,不覆盖原电子书;内嵌快照可独立恢复原来源。

In [10] · Python
assert len(chapter_table)==85 and chapter_table.tokens.gt(0).all()
for book_id,entry in corpus.items():assert chapter_table.loc[chapter_table.book_id.eq(book_id),"tokens"].sum()==len(entry["tokens"])
OUTPUT_DIR.mkdir(parents=True,exist_ok=True);chapter_table.drop(columns="text").to_csv(OUTPUT_DIR/"chapter-boundary-audit.csv",index=False)
summary.to_csv(OUTPUT_DIR/"processed-corpus-summary.csv")
(OUTPUT_DIR/"tokenization.json").write_text(json.dumps({"rule":"lowercase English letters; internal apostrophes retained","illustration_markup":"removed in analysis only","corpus_scope":"substantive numbered chapters only","chapter_counts":{"1342":61,"84":24}},indent=2))
print("Boundary and count checks passed; exports:",OUTPUT_DIR)
Boundary and count checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/hu01-corpus-audit

自主练习与参考答案

  1. 为何排除重复目录标题?
  2. 撇号/大小写决策怎样影响数量?

参考解释。 前置目录含重复标题;分词规则改变计算单位,原来源完整保留用于核验。

先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。

可选的 AI 编程练习

提示词 1

仅用 re、Counter、NumPy、Pandas、Matplotlib 在同章节比较两种记录的分词规则。

提示词 2

同包导出全部标签,标不支持排版,不静默丢文本。

人工验收。 章节为 61、24,总词数一致,原文/许可完整内嵌。

检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。

可选联网扩展

In [11] · Python
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://www.gutenberg.org/ebooks/1342',timeout=20)
        response.raise_for_status()
        print("Source:",response.url)
        print(response.text[:700])
    except requests.RequestException as error:
        print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
    print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged

来源、快照与下一步

  • Historical novel 1342 — Original novel public domain in USA; complete source ebook and its Project Gutenberg license retained; free distribution and acknowledgment links; retrieved 2026-10-09. SHA-256 3f6bb9d6f78e0293b56acd4714dd68cb7d6d1d293402031ce9d5a216bcaf9d75. Preserve unmodified UTF-8 source including ebook headers and complete license; preprocessing occurs visibly in the notebook.

  • Historical novel 84 — Original novel public domain in USA; complete source ebook and its Project Gutenberg license retained; free distribution and acknowledgment links; retrieved 2026-10-09. SHA-256 7810cd483cffcf2cc8a1d8f0d5807931e69d4f48cd14149b8c76f88af82fead3. Preserve unmodified UTF-8 source including ebook headers and complete license; preprocessing occurs visibly in the notebook.

下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。