Files
py-chan/parser.py
2026-08-11 16:18:44 +08:00

125 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""解析目录页与文章页 HTML构建结构化文章数据。"""
import re
from bs4 import BeautifulSoup
from converter import blocks_to_markdown
from config import DATE_OVERRIDES
# 「教你炒股票1不会赢钱的经济人只是废人
TITLE_RE = re.compile(r"^教你炒股票\s*([0-9]+)\s*[:\-]\s*(.+)$")
DATETIME_RE = re.compile(r"(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?")
_WS_RE = re.compile(r"\s+")
# 用于自动打标签的缠论核心概念(多字、不易误判)
CONCEPTS = [
"走势中枢",
"中枢",
"背驰",
"买卖点",
"分型",
"线段",
"区间套",
"第三类买卖点",
"第一类买点",
"第二类买点",
"走势类型",
]
def parse_index(html: str) -> list[int]:
"""从目录页提取所有文章的 p 编号(升序,去重,去掉 p=0 目录本身)。"""
soup = BeautifulSoup(html, "lxml")
nums: list[int] = []
for a in soup.select("a.chan[href*='chanlun?p=']"):
m = re.search(r"p=(\d+)", a.get("href", ""))
if m:
n = int(m.group(1))
if n > 0:
nums.append(n)
return sorted(set(nums))
def _is_nav_paragraph(p) -> bool:
"""识别正文末尾「返回目录」的导航 <p>,排除掉。"""
a = p.find("a", href=True)
return bool(a and "chanlun" in a["href"] and "p=0" in a["href"])
def _make_tags(corpus: str) -> list[str]:
found = [c for c in CONCEPTS if c in corpus]
# 去重保序(「走势中枢」与「中枢」可能同时命中)
seen: set[str] = set()
unique: list[str] = []
for c in found:
if c not in seen:
seen.add(c)
unique.append(c)
return ["教你炒股票"] + unique[:2]
def parse_article(html: str, p_num: int, resolve_image) -> dict:
"""解析单篇文章,返回符合 Vue ``Article`` 结构的字典。"""
soup = BeautifulSoup(html, "lxml")
container = soup.select_one("div.container-narrow")
title_full = ""
date = DATE_OVERRIDES.get(p_num) # 源站个别篇目日期残缺,按已知准确值纠偏
elements = [] # <hr> 之后的正文块(<p> 与块级 <img> 等,按文档顺序)
if container is not None:
h2 = container.find("h2")
if h2:
title_full = h2.get_text(strip=True)
if date is None:
h5 = container.find("h5")
if h5:
m = DATETIME_RE.search(h5.get_text())
if m:
date = m.group(1) + (f" {m.group(2)}" if m.group(2) else "")
seen_hr = False
for el in container.find_all(recursive=False):
if el.name == "hr":
seen_hr = True
continue
if not seen_hr:
continue # 跳过标题/日期
if el.name == "p" and _is_nav_paragraph(el):
continue # 排除文末「返回目录」
elements.append(el)
# 课次号 / 标题从「教你炒股票N标题」中拆分
m = TITLE_RE.match(title_full)
if m:
lesson = int(m.group(1))
topic = m.group(2).strip()
else:
lesson = p_num
topic = title_full or f"教你炒股票 {p_num}"
content = blocks_to_markdown(elements, resolve_image)
# 纯文本度量:阅读时长与摘要(折叠源 HTML 的折行空白)
plain_texts = [_WS_RE.sub(" ", el.get_text(" ", strip=True)) for el in elements]
plain_texts = [t for t in plain_texts if t]
plain_len = sum(len(t) for t in plain_texts)
corpus = "".join(plain_texts)
first_text = plain_texts[0] if plain_texts else ""
excerpt = (first_text[:90] + "") if len(first_text) > 90 else first_text
reading_time = max(1, round(plain_len / 500)) # 中文技术文 ~500 字/分钟
return {
"id": f"cl-{p_num:03d}",
"slug": f"ke-{p_num:03d}",
"category": "chanlun",
"title": topic,
"subtitle": f"{lesson} 课 · 教你炒股票",
"lesson": lesson,
"date": date or "",
"tags": _make_tags(corpus),
"excerpt": excerpt,
"readingTime": reading_time,
"content": content,
}