ChatGPT从入门到精通PDF实战指南:高效应用与避坑手册
背景痛点:对话越攒越多,知识却越来越碎
- 每天和 ChatGPT 聊几十轮,精华散落在网页里,想复习只能翻历史记录,关键词一多就搜不到。
- 官方导出只有原始 JSON,一行行看眼睛花,贴进笔记软件格式全乱。
- 项目复盘要写文档,却得手动复制粘贴,代码块、公式、表格全丢,最后干脆放弃。
一句话:碎片化严重,沉淀成本太高。把对话自动整理成“可读、可搜、可打印”的 PDF,是中级 Python 开发者完全能搞定的事。
技术方案:30 分钟搭一条 Markdown→PDF 自动化流水线
先让 ChatGPT 把对话历史吐出来
- 官方接口
/conversations目前只给前 100 条,可循环翻页。 - 每条消息含
role、content、create_time,用pandas一键变表。
- 官方接口
清洗→Markdown→PDF
- 清洗:正则去掉“正在思考中…”这类无意义状态行。
- Markdown:用
pypandoc把 DataFrame 转.md,代码块加```python方便高亮。 - PDF:对比 ReportLab 与 PyPDF2
- ReportLab 像“画布”,逐字定位,适合精细排版,但写中文得自己带字体。
- PyPDF2 只能合并/拆分,不具备“写文字”能力。
- 折中方案:用
markdown-weasyprint直接渲染,CSS 控制样式,三行命令就出书。
整条流水线
API 拉取 → 清洗 → 合并长文 → 分块 → 异步转 PDF → 缓存去重 → 输出带目录的 PDF。
核心代码:开箱即用,注释比代码长
下面代码依赖:openai>=1.0、pandas、markdown-weasyprint、aiofiles、tenacity。
统一入口:python build_chatgpt_book.py --thread-id xxx --output my.pdf
# build_chatgpt_book.py import os, json, re, asyncio, aiofiles from datetime import datetime import pandas as pd import openai from tenacity import retry, stop_after_attempt, wait_exponential from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration openai.api_key = os.getenv("OPENAI_API_KEY") ############################################################ # 1. API 封装:带重试、自动降速 ############################################################ @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60)) def get_messages(thread_id, limit=100, after=None): """拉取一条 Thread 里的消息,支持翻页""" return openai.beta.threads.messages.list( thread_id, limit=limit, order="asc", after=after ) async def fetch_all_messages(thread_id): """异步翻页,防止一次性请求过大""" all_msgs, after = [], None while True: resp = await asyncio.to_thread(get_messages, thread_id, after=after) all_msgs.extend(resp.data) if not resp.has_more: break after = resp.last_id return all_msgs ############################################################ # 2. 清洗 & 结构化 ############################################################ def clean_content(text: str) -> str: """去掉思考链提示、空括号、时间戳等噪音""" text = re.sub(r"【\d+†.*?】", "", text) # 去掉引用角标 text = re.sub(r"^\(.*?\)$", "", text, flags=re.M) # 括号整行删除 return text.strip() def msgs_to_df(msgs): rows = [] for m in msgs: role = m.role for c in m.content: if c.type == "text": rows.append({ "role": role, "content": clean_content(c.text.value), "time": datetime.fromtimestamp(m.created_at) }) df = pd.DataFrame(rows) # 把用户和助手交替染色,方便阅读 df["role_ch"] = df["role"].map({"user": "👤 我", "assistant": " AI"}) return df ############################################################ # 3. Markdown 模板 ############################################################ MD_TEMPLATE = """ # ChatGPT 对话实录 {title} > 导出时间:{export_time} --- {body} """ def df_to_markdown(df): """DataFrame → Markdown 字符串""" lines = [] for _, row in df.iterrows(): lines.append(f"### {row.role_ch} {row.time:%H:%M}") lines.append("") lines.append(row.content) lines.append("") return "\n".join(lines) ############################################################ # 4. 异步生成 PDF ############################################################ async def md_to_pdf(md_text, output_path): """WeasyPrint 异步渲染,支持中文""" font_config = FontConfiguration() css = CSS(string=""" @font-face { font-family: 'Noto'; src: url('https://fonts.gstatic.com/ea/notosanssc/v1/NotoSansSC-Regular.otf'); } body { font-family: 'Noto'; font-size: 10pt; } h3 { color: #005bbb; } pre { background: #f6f8fa; padding: 8px; border-left: 4px solid #005bbb; } """, font_config=font_config) html = HTML(string=markdown.markdown(md_text, extensions=["code", "fenced_code"])) await asyncio.to_thread(html.write_pdf, output_path, stylesheets=[css]) ############################################################ # 5. 缓存:同 thread 只拉新消息 ############################################################ CACHE_FILE = "cache.json" def load_cache(): # 简易 JSON 缓存 return json.loads(open(CACHE_FILE).read()) if os.path.exists(CACHE_FILE) else {} def save_cache(cache): with open(CACHE_FILE, "w") as f: json.dump(cache, f, ensure_ascii=False) ############################################################ # 6. 主流程 ############################################################ async def main(thread_id, output): cache = load_cache() last_id = cache.get(thread_id) msgs = await fetch_all_messages(thread_id) # 只保留新消息 if last_id: idx = next((i for i, m in enumerate(msgs) if m.id == last_id), -1) msgs = msgs[idx+1:] if not msgs: print("暂无新消息,PDF 已是最新。") return df = msgs_to_df(msgs) md_body = df_to_markdown(df) md_full = MD_TEMPLATE.format(title=thread_id[:8], export_time=datetime.now(), body=md_body) await md_to_pdf(md_full, output) # 更新缓存 cache[thread_id] = msgs[-1].id save_cache(cache) print(f" 已生成 {output} 共 {len(df)} 条记录") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--thread-id", required=True) parser.add_argument("--output", default="chatgpt_book.pdf") args = parser.parse_args() asyncio.run(main(args.thread_id, args.output))运行效果:代码块高亮、章节导航自动生成,中文显示正常,文件体积 100 页约 3 MB。
性能优化:让万行对话也不卡
大文本分块
- 单条消息超过 4k token 时,按“段落”拆
<h3>单元,防止 WeasyPrint 一次性渲染爆内存。
- 单条消息超过 4k token 时,按“段落”拆
异步生成
- 上面代码已用
asyncio.to_thread把阻塞的html.write_pdf丢到线程池,前端可实时返回进度。
- 上面代码已用
缓存与增量更新
- 用
last_id记录上一次最后一条消息,下次只拉增量,PDF 尾部追加而非全量重排。
- 用
并行样式渲染
- 若一次导出多个 Thread,可用
asyncio.gather同时渲染,CPU -bound 部分仍放线程池,I/O 部分协程搞定。
- 若一次导出多个 Thread,可用
避坑指南:踩过一次就不想再踩
API 频率限制
- 新接口
/messages限 60 req/min,重试策略已用tenacity,并加wait_exponential。 - 若线程超大,可把
limit调到 100 同时降低并发。
- 新接口
中文字体空白框
- WeasyPrint 默认无中文字体,需
@font-face显式引入 Noto 或思源黑体,否则 PDF 打开全是“口口口”。
- WeasyPrint 默认无中文字体,需
敏感信息过滤
- 在
clean_content()里加正则剔除邮箱、密钥、手机号,或接入微软 Presidio 做 PII 扫描。 - 输出前再做一次
content sanitization,防止内部对话外泄。
- 在
代码高亮失效
markdown-weasyprint默认不支持 Pygments,需要手动把highlight.css嵌进去,或先pygments -S default -f html -a .highlight > style.css再合并。
目录页码对不上
- WeasyPrint 支持
target-counter(anchor, page),用<a name="h3-xxx">配合 CSS 即可自动生成“点击跳转到对应页”的目录。
- WeasyPrint 支持
延伸思考:把 PDF 做成会“自我更新”的智能文档
结合 LangChain
- 把每份 PDF 用
PyPDF2拆段 → 存向量库,后续问答先搜本地知识,再决定是否调用 ChatGPT,实现“离线优先”。
- 把每份 PDF 用
版本控制
- 用
git LFS存二进制 PDF 不现实,可只存md源文件 + 样式 CSS,CI 里weasyprint一键构建,tag 对应版本号,回滚秒级完成。
- 用
多人协作
- 把缓存文件放 Redis,团队共用增量更新;冲突解决用
last_writer_win策略,对话时间戳天然有序。
- 把缓存文件放 Redis,团队共用增量更新;冲突解决用
商业场景
- 客服中心每天产生上千条对话,夜间批处理生成“日报 PDF”+“热点问题 TOP10”,第二天早会直接投影,节省 2 小时整理时间。
写在最后:把实验精神继续推进
上面这套脚本我已经跑了两个月,把零散对话变成可搜索的“小册子”,复习效率肉眼可见。
如果你想亲手搭一个更酷的“实时语音”AI,而不仅是静态 PDF,可以试试火山引擎的从0打造个人豆包实时通话AI动手实验——从语音识别到语音合成,一条链路全打通,Web 页面打开就能聊。
我跟着做了一遍,本地 30 分钟跑通,麦克风控件直接对话,延迟比打电话还低。小白也能顺利体验,不妨把今天学到的 PDF 技巧再叠加到语音对话里,让 AI 既能“说”也能“写”,才算真正的个人知识闭环。