first commit

This commit is contained in:
2026-08-11 16:18:44 +08:00
commit 5cc322a598
231 changed files with 33294 additions and 0 deletions

126
main.py Normal file
View File

@@ -0,0 +1,126 @@
"""缠论《教你炒股票》108 课抓取入口。
用法
----
python main.py # 抓取全部并生成数据(使用本地缓存)
python main.py --refresh # 忽略缓存重新抓取
python main.py --limit 5 # 仅抓前 5 篇(联调测试)
python main.py --no-images # 不下载图片,正文保留远程链接
产物
----
data/raw/p001.html ... 原始 HTML 归档
data/markdown/ke-001.md ... 每篇 Markdown 归档
data/articles.json 全量结构化数据
D:\\code\\chan\\src\\data\\chanlun.ts Vue 数据源(覆盖示例)
D:\\code\\chan\\public\\chanlun-img\\... 本地化图片
"""
import argparse
import json
import shutil
from datetime import datetime
from pathlib import Path
from config import (
CHAN_DATA_FILE,
INDEX_URL,
JSON_PATH,
MARKDOWN_DIR,
RAW_DIR,
article_url,
)
from fetcher import fetch_text
from parser import parse_article, parse_index
from writer import make_image_resolver, remote_image_resolver, write_ts_file
GEN_MARKER = "自动生成"
def cached_html(p_num: int, refresh: bool) -> str:
"""读取/抓取单篇 HTML落盘缓存以便重跑。"""
path = RAW_DIR / f"p{p_num:03d}.html"
if not refresh and path.exists():
return path.read_text(encoding="utf-8")
html = fetch_text(article_url(p_num))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(html, encoding="utf-8")
return html
def main() -> None:
ap = argparse.ArgumentParser(description="抓取缠论《教你炒股票》系列")
ap.add_argument("--refresh", action="store_true", help="忽略缓存重新抓取")
ap.add_argument("--limit", type=int, default=0, help="仅抓取前 N 篇(测试)")
ap.add_argument("--no-images", action="store_true", help="不下载图片,保留远程链接")
args = ap.parse_args()
print("① 抓取目录页 …")
index_html = fetch_text(INDEX_URL)
pages = parse_index(index_html)
if args.limit:
pages = pages[: args.limit]
if not pages:
raise SystemExit("目录页未解析到任何文章链接")
print(f" 发现 {len(pages)} 篇文章p={pages[0]}{pages[-1]}")
RAW_DIR.mkdir(parents=True, exist_ok=True)
MARKDOWN_DIR.mkdir(parents=True, exist_ok=True)
articles: list[dict] = []
for i, p in enumerate(pages, 1):
print(f"② [{i}/{len(pages)}] 解析 p={p}")
html = cached_html(p, args.refresh)
resolver = remote_image_resolver if args.no_images else make_image_resolver(p, args.refresh)
art = parse_article(html, p, resolver)
(MARKDOWN_DIR / f"{art['slug']}.md").write_text(
f"# {art['title']}\n\n{art['content']}\n", encoding="utf-8"
)
articles.append(art)
# 源站少数篇目日期残缺/缺失(如 h5 仅写"2006"),按课次顺序用上一篇日期兜底
last_date = "2006-06-07"
fixed = 0
for a in articles:
if not a["date"]:
a["date"] = last_date
fixed += 1
else:
last_date = a["date"]
if fixed:
print(f" {fixed} 篇源站日期缺失,已用相邻课次日期兜底")
print("③ 写入归档 JSON …")
JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
JSON_PATH.write_text(
json.dumps(articles, ensure_ascii=False, indent=2), encoding="utf-8"
)
print("④ 生成 Vue 数据文件 …")
_backup_sample_once(CHAN_DATA_FILE)
stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
write_ts_file(articles, CHAN_DATA_FILE, stamp)
print(f"{CHAN_DATA_FILE}")
print("✅ 完成。")
print(f" 文章数:{len(articles)}")
print(" 归档目录data/")
print(" Vue 路由http://localhost:5173/chanlun")
def _backup_sample_once(path: Path) -> None:
"""首次覆盖前,把原始示例数据备份为 chanlun.sample.ts。"""
if not path.exists():
return
head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:6])
if GEN_MARKER in head:
return # 已是生成文件,无需备份
backup = path.with_name("chanlun.sample.ts")
if backup.exists():
return
shutil.copy2(path, backup)
print(f" 已备份示例数据 → {backup.name}")
if __name__ == "__main__":
main()