news 2026/9/18 17:14:07

可复现数据工程:桌游棋牌研究报告从采集到PDF自动出稿

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
可复现数据工程:桌游棋牌研究报告从采集到PDF自动出稿

简介:这份《2020年桌游棋牌研究报告》为PDF电子文档,面向游戏策划、发行运营、市场研究及出海从业者,用于快速了解桌游棋牌类手游的品类格局与投放思路。压缩包内含1个PDF文件,约18.73MB,无需额外工具即可阅读检索。报告以500余款样本为基础,将玩法归纳为飞行棋、各国象棋、牌九、卡罗姆、Okey、跳棋、UNO、黑白棋、纸牌、Backgammon、井字棋、Rummikub、Connect Four等16类,并附中英文玩法对照表,便于跨地区产品比对。内容沿发展历程、市场现状、热门地区与案例分析四条线索展开,给出印度、美国、巴西、土耳其、印尼等地的DAU与IAP分布,并对多款头部产品的玩法特色、获客路径、留存与变现进行拆解,可供立项调研、竞品分析与投放策略参考。目前已有151人学习下载。

1. 一份《2020年桌游棋牌研究报告.pdf》背后,是一整套数据报告工程

先给一个不太好听的结论:把桌游棋牌研究报告做成一份 PDF,最不值钱的就是 PDF 本身。真正决定这份报告能不能被引用、能不能被复核的是数据口径——同一句「2020年桌游棋牌市场规模」,用电商评论数做代理变量和用线下门店抽样做代理变量,结论能差出一倍。

常见做法是从几个公开榜单抄一晚上数据,粘进 Excel,出几十张图导成 PDF 发出去。三个月后有人问「12.7% 是怎么来的」,没人答得上来。短板不在分析能力,而在链路没留痕:数据从哪抓的、字段怎么定义的、异常值怎么处理的、图表用的是哪一版数据。

要解决的是可复现性:把公开数据采集、清洗、入库、算指标、出图、排版成 PDF 串成一条能重跑的流水线,任何一个数字都能追到原始记录。适合做行业研究、投研支持、产品调研的 IT 从业者,也适合手头攒了一堆零散榜单、想把它变成可交付报告的人。下面按采集到交付的顺序展开,代码都能直接跑。

2. 桌游棋牌研究报告的数据底座:公开榜单采集、清洗与库表设计

2.1 三类数据源怎么选,先定口径再定采集

做 2020 年这一年的行业画像,数据源无非三类:评分与榜单站点、应用商店和电商的公开榜单、行业媒体与展会的公开信息。三者的粒度和更新频率差得很远,混在一张表里直接算指标必然出问题,所以采集前先把它们分开。

数据源类型典型载体可拿到的字段更新频率采集方式
榜单/评分站点公开的 XML/JSON 接口或页面作品名、评分、排名、评分人数、出版年份月度快照接口轮询 + 请求节流
应用商店/电商公开榜公开榜单页、公开评论总数品类、评论量级、上架时间、渠道标识周度页面解析 + 重试队列
行业媒体与展会信息年度盘点文章、展会名录品类趋势描述、举办城市、场次不定期人工结构化录入
线下门店公开信息门店名录页城市、门店数量、主营品类季度半自动整理

选择顺序上,我一般会:接口优先,结构化页面次之,人工录入兜底。接口的字段固定、翻页可枚举、能增量;页面解析的维护成本随对方改版线性上升;人工录入只在前面两条路都拿不到时才用,并且必须留一列原始出处 URL。

提示:采集范围只覆盖公开的休闲娱乐类桌游棋牌产品信息,不涉及任何带金钱对赌的玩法,字段设计上也不留这类入口。

2.2 用 Python 把桌游棋牌的原始数据抓下来并归一化

原始层不做任何加工,一行一条 JSON,落到data/raw/下面按月份分文件。这样后面无论换数据库还是换计算引擎,原始证据都在。

# fetch_rank.py:按月份拉取榜单快照,落地为 jsonl 原始层 import json, time, hashlib, datetime as dt import requests from requests.adapters import HTTPAdapter SESSION = requests.Session() SESSION.mount("https://", HTTPAdapter(pool_maxsize=4)) RAW_PATH = "data/raw/rank_{month}.jsonl" HEADERS = {"User-Agent": "boardgame-research-bot/1.0 (contact: data@example.com)"} def fetch_page(page: int, month: str, retry: int = 3) -> list: url = "https://example-rank-site/api/v1/ranks" params = {"page": page, "limit": 100, "snapshot": month} for i in range(retry): try: r = SESSION.get(url, params=params, headers=HEADERS, timeout=15) r.raise_for_status() return r.json().get("items", []) except Exception: if i == retry - 1: raise time.sleep(2 ** i) # 指数退避,别触发对端限流 return [] def main(month: str): with open(RAW_PATH.format(month=month), "w", encoding="utf-8") as f: for page in range(1, 21): # 20 页上限,覆盖约 2000 条 items = fetch_page(page, month) if not items: break for it in items: base = {k: it.get(k) for k in ("id", "name", "rank", "rating", "users_rated", "year")} it["row_hash"] = hashlib.md5( # 行级指纹,只算业务字段 json.dumps(base, sort_keys=True, ensure_ascii=False).encode() ).hexdigest() it["snapshot_month"] = month # 快照月份,同环比全靠它 f.write(json.dumps(it, ensure_ascii=False) + "\n") time.sleep(1.2) # 固定间隔,礼貌抓取 if __name__ == "__main__": main(dt.date.today().strftime("%Y-%m"))

逻辑说明:row_hash只对业务字段做摘要,如果把snapshot_month也算进去,同一条数据每个月哈希都不同,去重就失效了。参数说明:pool_maxsize=4控制并发连接数,抓公开站点四路足够;timeout=15是单请求上限,超时直接进入重试;sleep(2 ** i)让重试间隔按 1、2、4 秒递增;limit=100是常见的单页上限,翻到空列表就跳出,避免无意义请求。

2.2.1 名称归一化与品类映射

榜单数据最大的噪声在名称:「三国杀(标准版)」和「三国杀(典藏版)」会被当成两个作品,「UNO牌」和「UNO」也是。归一化不做好,后面所有按品类的统计都是错的。

# normalize.py:作品名归一化 + 品类映射,产物写 staging 层 import re, unicodedata ALIAS = {"uno牌": "uno", "狼人杀(官方版)": "狼人杀"} # 别名表,人工维护 CATEGORY_RULES = [("剧本", "剧本推理"), ("杀", "身份推理"), ("棋", "传统棋类"), ("牌", "卡牌"), ("战棋", "战棋")] def normalize_name(name: str) -> str: s = unicodedata.normalize("NFKC", name or "") # 全角/兼容字符统一 s = re.sub(r"[\((].*?[\))]", "", s) # 剥离副标题与版本括号 s = re.sub(r"\s+", "", s).strip().lower() return ALIAS.get(s, s) def map_category(name: str, raw_type: str) -> str: if raw_type: # 一级:信任上游字段 return raw_type for kw, cat in CATEGORY_RULES: # 二级:关键词兜底 if kw in name: return cat return "其他"

逻辑说明:NFKC 把全角括号、罗马数字这类兼容字符压平;括号剥离让同作品的不同版本合并;ALIAS不写死在代码里长期膨胀,每季度 review 一次导到 CSV 单独维护。参数说明:map_category采用两级策略,上游给了类型就用上游的,没给才用关键词猜,猜不出来的统一进「其他」,并在报告里注明「其他」占比,占比超过 15% 说明规则该补了。

2.3 桌游棋牌研究报告的星型表结构与索引

清洗完落到一张事实表加三张维度表,按月做快照。事实表主键带上snapshot_month,同一作品每月一行,同环比全靠它对齐。

表名类型关键字段说明
dim_game维度game_id, name_norm, release_year, category_id一个作品一行,名称存归一化后的值
dim_category维度category_id, category_name, parent_id支持二级品类
dim_channel维度channel_id, channel_name, channel_type线上榜单/线下门店/应用商店
fact_rank_snapshot事实game_id, channel_id, snapshot_month, rank, rating, users_rated, row_hash月度粒度
-- schema.sql:SQLite 可直接执行,PostgreSQL 只需调整分区语法 CREATE TABLE IF NOT EXISTS dim_game ( game_id INTEGER PRIMARY KEY, name_norm TEXT NOT NULL, release_year INTEGER, category_id INTEGER ); CREATE TABLE IF NOT EXISTS fact_rank_snapshot ( game_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, snapshot_month TEXT NOT NULL, -- 'YYYY-MM',字符串便于切片 rank INTEGER, rating REAL, users_rated INTEGER, row_hash TEXT, PRIMARY KEY (game_id, channel_id, snapshot_month) ); CREATE INDEX IF NOT EXISTS idx_fact_month ON fact_rank_snapshot(snapshot_month); CREATE INDEX IF NOT EXISTS idx_fact_rank ON fact_rank_snapshot(snapshot_month, rank); CREATE UNIQUE INDEX IF NOT EXISTS uk_game_name ON dim_game(name_norm);

逻辑说明:idx_fact_month支撑按月切片,这是跑得最频繁的一类查询;idx_fact_rank是复合索引,WHERE snapshot_month = ? ORDER BY rank能直接走索引序;dim_gamename_norm的唯一索引是整条清洗链路的兜底——重复抓取会在入库阶段直接报错,而不是静默产生两份数据,那才是最难查的坑。数据量在百万行以内,SQLite 完全够;多人协作或者要接 BI 工具时换 PostgreSQL,把索引语句原样搬过去即可。

3. 桌游棋牌研究报告的核心指标怎么落地:SQL 窗口函数与 Pandas 双路复算

3.1 指标体系与口径定义

2020 年这类报告里,「市场规模」基本都是代理变量,没有哪家能拿到全量真实营收。所以指标表里必须把口径写死,报告脚注再复述一遍。

指标业务含义计算口径常见误用
CR10 集中度头部作品占据的份额当月 Top10 的 users_rated 之和 / 当月全量之和拿评分代替人数,头部被高估
品类热度占比各品类在榜单中的比重按 category_id 分组计数 / 总计数不做名称归一化,卡牌被拆成十几类
年度新增作品数供给侧活跃度release_year = 2020 的去重作品数用首次上榜时间代替出版年份
榜单留存率作品的持续吸引力连续 N 月进 Top100 的作品数 / 当月 Top100快照缺月时直接算,结果虚高
评论量中位数用户关注度分布users_rated 的 50 分位用均值,被极值拉偏
城市覆盖数线下渗透门店表中 distinct 城市数与线上榜单数值直接相加

这里有两个容易被忽略的点。一是用中位数而不是均值,桌游作品的长尾极长,个别头部作品能把均值拉得完全失去代表性;二是线上与线下的口径不能相加,线上是榜单计数,线下是门店计数,量纲不同,硬加出来的「总规模」在评审时一问就穿帮。

3.2 用 SQL 窗口函数算份额与同比

同比用LAG(..., 12)做,前提是月份连续。先把月度聚合做成 CTE,再在窗口里取去年同期值。

-- metric_monthly.sql:品类份额、当月排名与同比 WITH monthly AS ( SELECT f.snapshot_month, c.category_name, COUNT(*) AS title_cnt, SUM(f.users_rated) AS rated_sum FROM fact_rank_snapshot f JOIN dim_game g ON g.game_id = f.game_id JOIN dim_category c ON c.category_id = g.category_id WHERE f.snapshot_month BETWEEN :month_from AND :month_to GROUP BY f.snapshot_month, c.category_name ), with_prev AS ( SELECT snapshot_month, category_name, title_cnt, rated_sum, LAG(rated_sum, 12) OVER ( -- 去年同期 PARTITION BY category_name ORDER BY snapshot_month ) AS rated_sum_ly, ROW_NUMBER() OVER ( -- 当月热度排名 PARTITION BY snapshot_month ORDER BY rated_sum DESC ) AS rn_cur FROM monthly ) SELECT snapshot_month, category_name, title_cnt, rated_sum, rated_sum_ly, ROUND(1.0 * (rated_sum - rated_sum_ly) / NULLIF(rated_sum_ly, 0), 4) AS yoy, rn_cur FROM with_prev WHERE snapshot_month = :month ORDER BY rated_sum DESC;

逻辑说明:PARTITION BY category_name ORDER BY snapshot_month保证同品类内按月份推进,LAG(..., 12)取到的才是去年同月而不是上一月。参数说明::month_from:month_to至少往前推 13 个月,否则第一年的同比全是 NULL;NULLIF(rated_sum_ly, 0)防止新品类除零报错;ROW_NUMBER()生成当月排名,用来做「新晋 Top10」这类榜单。这套写法在 SQLite 3.25 以上和 PostgreSQL 里都能直接跑。

3.3 Pandas 复算与勾稽校验

同一指标用两套独立实现算一遍,是发现口径漂移最省钱的办法。注意是「独立写」,不是把 SQL 复制成两遍。

# crosscheck.py:Pandas 复算份额,与库内结果对拍 import sqlite3 import pandas as pd SQL = """ SELECT c.category_name, COUNT(*) AS title_cnt, SUM(f.users_rated) AS rated_sum FROM fact_rank_snapshot f JOIN dim_game g ON g.game_id = f.game_id JOIN dim_category c ON c.category_id = g.category_id WHERE f.snapshot_month = ? GROUP BY c.category_name """ def load(month: str) -> pd.DataFrame: with sqlite3.connect("boardgame.db") as conn: return pd.read_sql(SQL, conn, params=(month,)) def check(month: str) -> None: df = load(month) total = df["rated_sum"].sum() assert total > 0, f"{month} 无数据,先查采集日志" df["share_pd"] = df["rated_sum"] / total assert round(df["share_pd"].sum(), 6) == 1.0, "份额之和不等于 1,分母口径不一致" top10 = df.sort_values("share_pd", ascending=False).head(10)["share_pd"].sum() print(f"{month} CR10 = {top10:.4f}") print(df.sort_values("share_pd", ascending=False) [["category_name", "title_cnt", "share_pd"]].head())

逻辑说明:断言点选在「份额之和恒等于 1」这种数学上必然成立的恒等式上,一旦不成立就是分母口径变了或者混进了空品类。参数说明:round(..., 6)是给浮点误差留的余量;CR10 用排序后取前 10 求和,和库里ROW_NUMBER() <= 10的结果对拍,差异超过阈值就停下看是不是缺月。把这段挂到采集任务后面,每天跑一次,比出报告时才发现问题省事得多。

4. 从分析结果到《2020年桌游棋牌研究报告.pdf》:图表、排版与自动出稿

4.1 图表批量生成:中文字体、配色与导出参数

图表最容易翻车的地方不是配色,是中文变方框。服务器上没有中文字体,matplotlib会静默用默认字体渲染,出图看着正常,打开 PDF 全是豆腐块。

# 先确认系统里有中文字体,没有就装一个开源思源黑体或 Noto Sans CJK fc-list :lang=zh | head -n 5
# charts.py:统一字体与导出参数,一次出全套图 import matplotlib matplotlib.use("Agg") # 无界面环境必须指定,否则会挂 import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "Source Han Sans SC"] plt.rcParams["axes.unicode_minus"] = False # 负号正常显示 plt.rcParams["figure.dpi"] = 150 plt.rcParams["savefig.bbox"] = "tight" def barh(df, x, y, out, title): fig, ax = plt.subplots(figsize=(6.5, 3.6)) # 宽度对齐 A4 正文栏宽 ax.barh(df[y], df[x], color="#3B6EA5") ax.set_title(title, fontsize=11) ax.grid(axis="x", linestyle=":", alpha=0.5) fig.savefig(out, format="svg") # 矢量,PDF 内缩放不糊 plt.close(fig)

逻辑说明:matplotlib.use("Agg")指定无界面后端,在容器或 CI 里必须加。参数说明:font.sans-serif给的是一个候选列表,按顺序匹配,第一个不存在就往下找,所以可以一次写好几个;figure.dpi只影响 PNG 位图,导出 SVG 时关系不大;figsize=(6.5, 3.6)是按 A4 去掉 18mm 左右页边距后的正文宽度估的,图表宽度和正文对齐,排版时才不会一宽一窄;savefig.bbox="tight"裁掉多余留白,否则图与题注之间的空隙会很大。

4.2 HTML 转 PDF 的两条路线与关键参数

排版路线主要有三条,选哪条取决于文档复杂度和团队熟悉度。

路线依赖适合场景主要坑
WeasyPrintPython + Pango结构化长文档、要目录与页码对复杂 Grid 布局支持有限
Chromium 无头打印Playwright / Chrome图表多、CSS 复杂页眉页脚要用参数单独传
LaTeXTeX Live公式多、排版精度要求高模板调试成本高,中文字体配置麻烦

行业研究报告这类文档结构规整(章、节、表格、图),我一般选 WeasyPrint:Python 生态内一条命令出稿,目录页码有原生支持。

# render_pdf.py:把 Jinja2 渲染好的 HTML 打成交付 PDF from weasyprint import HTML, CSS HTML(filename="build/report.html").write_pdf( "dist/2020年桌游棋牌研究报告.pdf", stylesheets=[CSS(filename="assets/print.css")], presentational_hints=True, # 尊重 HTML 里的 width/align 等表现属性 )
/* print.css:A4 版面、页眉页脚与表格分页 */ @page { size: A4; margin: 20mm 18mm 18mm 18mm; @top-center { content: "2020年桌游棋牌研究报告"; font-size: 9pt; color: #666; } @bottom-right { content: counter(page) " / " counter(pages); font-size: 9pt; } } table { width: 100%; border-collapse: collapse; font-size: 9.5pt; } tr, img, figure { break-inside: avoid; } /* 关键:别让表格行被切到下一页 */ h2 { break-before: page; } /* 每章另起一页 */

逻辑说明:counter(page)counter(pages)由排版引擎在生成阶段解析,不需要自己数页。参数说明:margin上下左右分开写,是因为顶部要留页眉空间,通常比底部大 2mm;break-inside: avoid加在tr上是表格跨页最实用的一招,不然一张 4 行的表经常被切成 2+2;break-before: page让每个一级章节独立起页,配合目录页码才整齐。走 Chromium 路线时@page的页眉页脚支持不完整,得改用page.pdf(header_template=...)传 HTML 片段。

4.3 目录页码与图表索引不要手写

目录页码是研究报告里最容易出错的细节:正文改一段,页码全错。CSS 里有原生方案,让排版引擎自己填。

/* 目录页码由 target-counter 自动生成,禁止手写数字 */ .toc a::after { content: target-counter(attr(href), page); float: right; } .toc a { text-decoration: none; color: #000; }

逻辑说明:attr(href)取到目录项的锚点,target-counter(..., page)反查该锚点落在第几页。参数说明:要求每个章节标题都有稳定的id,生成 HTML 时从同一份大纲数据同时渲染目录和正文,两边就不会漂移;图表索引同理,把figureid和题注从同一份数据渲染出来。这套做法的前提是出稿流程里没有人工改 HTML 的环节——只要有人手动改了正文,就得重新跑一遍渲染,别直接改产物。

5. 让这份桌游棋牌研究报告每次都能重跑:口径字典、快照校验与产物命名

5.1 口径字典版本化,别让口径散在 SQL 里

规模、集中度、份额这些指标的定义一旦散落在十几个 SQL 文件里,半年后没人说得清哪个是最终口径。做法是把口径抽成一份 YAML,SQL 从它生成。

# dict/metrics.yaml version: 2020.4 metrics: cr10: desc: 当月 Top10 作品评分人数合计占全量比重 source: fact_rank_snapshot filter: "snapshot_month = :month and channel_id = 1" denominator: sum(users_rated) category_share: desc: 品类评分人数份额 group_by: dim_category.category_name denominator: sum(users_rated)

逻辑说明:version在每次口径变更时递增,产物文件名里带上文件内容的哈希前缀,报告尾页把版本号打出来。读者对不上数的时候,能直接说清是哪一版口径算的,而不是从头吵。

# 产物命名带上口径哈希与运行时间,避免"最终版v3"式命名 DICT_HASH=$(md5sum dict/metrics.yaml | cut -c1-8) STAMP=$(date +%Y%m%dT%H%M) cp dist/report.pdf "dist/2020桌游棋牌研究报告_${STAMP}_${DICT_HASH}.pdf" echo "口径版本 ${DICT_HASH},产物已归档"

参数说明:cut -c1-8取哈希前 8 位,够用又不至于文件名过长;时间戳用 ISO 紧凑格式,按文件名排序就是时间顺序。这套命名在归档目录里一眼能看出哪几份报告用的是同一版口径。

5.2 勾稽校验清单与缺月补齐

校验项期望值失败时先看哪
快照月份完整性2020-01 到 2020-12 共 12 个采集日志里的 4xx 与超时记录
份额之和每次切片加总 = 1分母是否漏了「其他」品类
榜单条数每月 100 条分页上限是否被截断
名称唯一性dim_game 无重复 name_norm别名表是否漏了新版本
图表与数据一致图形值等于查询结果出图脚本是否读了缓存 CSV

最后说一个几乎每份年度报告都会踩的坑:LAG(..., 12)依赖月份连续,2020 年某个月采集失败,同比就会错位到别的月份上去,而且不会报错,只是数字变小。稳妥的做法是建一张日历序列左连接补月,让缺口显式暴露出来。

-- SQLite 语法;PostgreSQL 换成 generate_series('2020-01-01','2020-12-01','1 month') WITH RECURSIVE cal(m) AS ( SELECT '2020-01' UNION ALL SELECT strftime('%Y-%m', date(m || '-01', '+1 month')) FROM cal WHERE m < '2020-12' ) SELECT cal.m, COUNT(f.game_id) AS title_cnt FROM cal LEFT JOIN fact_rank_snapshot f ON f.snapshot_month = cal.m GROUP BY cal.m ORDER BY cal.m;

逻辑说明:递归 CTE 生成 12 个月的完整序列,左连接后没有数据的月份title_cnt = 0会显式出现,而不是从结果集里消失。参数说明:递归终止条件WHERE m < '2020-12'要写成小于上界,写成<=会多算一个月;strftime是 SQLite 的函数,换 PostgreSQL 用generate_series一行就能替代。补出来之后,title_cnt = 0的那一行就是采集缺口,顺着snapshot_monthdata/raw/目录里找对应的 jsonl 文件,比在 Excel 里对半天快得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 17:12:59

Ubuntu 24.04 实用命令速查手册:89条高频命令精讲

1. 这本手册不是教科书&#xff0c;是我在Ubuntu桌面和服务器上踩了三年坑后抄在手边的“活页纸”刚装好Ubuntu那会儿&#xff0c;我连ls -la和ls -lA的区别都得查三次——前者显示隐藏文件&#xff08;以点开头&#xff09;&#xff0c;后者还额外排除.和..这两个目录项。这不…

作者头像 李华
网站建设 2026/9/18 17:11:40

如何用Storybook Canvas绘图组件快速构建UI界面?超简单教程!

如何用Storybook Canvas绘图组件快速构建UI界面&#xff1f;超简单教程&#xff01; Storybook是一个独立运行的UI组件开发环境&#xff0c;支持React、Vue、Angular等多种前端框架。它允许开发者在隔离环境中创建、展示和测试UI组件&#xff0c;有助于组件化开发和设计系统的…

作者头像 李华
网站建设 2026/9/18 17:09:08

Worktrunk:并行AI Agent的Git Worktree管理利器

1. 从“一个人写代码”到“一支AI军队”&#xff1a;Worktrunk 到底在解决什么问题最近这半年&#xff0c;我明显感觉到身边的开发者分成了两拨&#xff1a;一拨还在用编辑器自带终端&#xff0c;老老实实开分支、切分支、合并&#xff1b;另一拨已经让三四个 AI Agent 同时在自…

作者头像 李华
网站建设 2026/9/18 17:07:42

OHIF 开发流程:Issue 分类、PR 评审、质量保障与自动发布机制

OHIF 开发流程&#xff1a;Issue 分类、PR 评审、质量保障与自动发布机制 【免费下载链接】Viewers OHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages 项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers …

作者头像 李华
网站建设 2026/9/18 17:07:13

招聘系统UML建模实战:从用例到部署的完整骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华