从arXiv论文到中文交互网页:基于LaTeX源码的翻译与结构优化实践
如果你刚拿到一篇 arXiv 论文想快速弄清它在讲什么通常的路径是打开 PDF对着英文摘要逐句读把看不懂的段落粘进翻译软件再回到 PDF 里找图表和公式对应。折腾半小时得到的还是一堆术语和上下文错位的碎片信息。这不是阅读能力的问题而是工具的错位。PDF 是为打印而生的格式它不适合阅读更不适合翻译和二次加工。arXiv 上的论文偏偏大量以 PDF 形式分发于是“读懂一篇论文”变成了“对抗 PDF 格式”这件事。DeMinds 的思路是把论文消费链路整个换掉从 arXiv 获取论文时优先拿 LaTeX 源码而不是 PDF把源码解析成可操作的章节结构对正文做术语可控的学术翻译最后渲染成支持目录跳转、公式渲染、中英对照的交互网页。这篇文章就围绕这条链路展开读完你能自己搭出一条“arXiv 论文 → 中文交互网页”的完整流水线并解决翻译、公式、发布这几个最容易出问题的环节。1. DeMinds 到底解决什么问题1.1 论文阅读的三个真实痛点先拆一下论文阅读的痛点你会发现每个痛点都是“工具选错”导致的而不是论文本身的问题。第一个痛点是格式封闭。PDF 里的文字、公式、图表被拍平成一页一页的版面阅读器拿到的是渲染结果不是结构。你想做目录跳转、按章节复用、批量对比都得先接受“从 PDF 里重新提取结构”这件脏活累活。第二个痛点是英文门槛。绝大多数 arXiv 论文是英文直接让读者在原文和翻译之间反复切换大脑要同时承担“理解语言”和“理解内容”两件事。真正高效的阅读应该把语言转换交给工具人只负责判断内容。第三个痛点是公式与上下文割裂。把论文整段丢进通用翻译软件公式会变成乱码术语前后翻译不一致“graph neural network”这一章叫“图神经网络”下一章可能就变成“图形神经网络”。读者根本不敢信译文。DeMinds 处理的不是其中某一个痛点而是把三个痛点放到一条流水线上统一解决用源码解决格式问题用结构化解析解决上下文问题用术语翻译解决一致性问题最后用交互网页解决阅读体验问题。1.2 传统做法为什么不够很多人做过“论文翻译工具”但多数只解决了一个环节把 PDF 文本抽出来翻译再拼成网页。问题在于PDF 抽取本身就会丢掉结构信息。环节传统做法DeMinds 的思路论文获取手动下载 PDF再交给解析器优先通过 arXiv 拿 LaTeX 源码源头就是结构化文本内容拆解按段落线性翻译先恢复章节树再按章节块处理公式处理抽成图片或直接丢失用占位符保护公式翻译完成后原位还原术语一致性每次翻译独立判断术语表 翻译前占位替换阅读体验连续滚动长文目录跳转、中英对照、搜索高亮从表格能看出DeMinds 的本质不是“翻译工具”而是“论文阅读流水线”。翻译只是其中一个加工步骤真正重要的上游是结构化。结构一旦丢失后面的优化、翻译、展示全都会失真。1.3 谁适合读这篇文章这篇文章适合三类读者。第一类是想做论文阅读工具、科研知识库、AI 辅助科研产品的开发者。你会从里面看到一条可落地的数据流源码获取、结构解析、术语管理、网页渲染、部署发布。第二类是高频阅读 arXiv 论文的研究生和科研人员。即使不打算二次开发理解这条流水线也能帮你判断为什么有些“论文翻译网站”效果差为什么把论文丢给大模型翻译前要做好预处理。第三类是想给自己的 NLP 或前端项目找一个完整落地案例的工程师。这篇文章会给出从 Python 脚本到静态网页部署的全套示例你可以直接改写成自己的项目。2. 核心概念与整体原理2.1 论文结构优化是什么结构优化不是“重新排版”而是把一篇线性论文转成有层级、有类型、可检索的结构化数据。一篇论文在 LaTeX 源码里本来就是结构化的\section定义一级章节\subsection定义二级章节公式块、图注、表格、参考文献都有明确标记。问题在于PDF 渲染把这些结构压平了。DeMinds 要做的是“逆向恢复”从源码里重新提取章节树把正文文本、公式、图表说明、引用分别打上标签。这样做的好处是后续每个环节都能被精确控制。翻译时只翻译正文文本公式通过占位符跳过做目录时直接用章节树做中英对照时按段落对齐。没有结构优化后面一切都是空中楼阁。2.2 学术翻译的难点学术翻译和通用翻译最不一样的地方是术语一致性。一篇论文里“attention mechanism”可能会出现十几次如果每次翻译独立进行第一次译为“注意力机制”第二次可能变成“关注机制”读者会怀疑自己在看两篇论文。更糟糕的是模型可能误译领域术语比如把“positive definite matrix”翻译成“积极确定矩阵”。另一个难点是数学公式和引用标记不能被翻译。$E mc^2$必须原样保留\cite{xxx}不能被翻译成中文否则参考文献对应关系会断裂。DeMinds 的做法是在翻译前先做安全处理公式、引用、术语分别替换成占位符翻译完成后再还原。这样既保证了术语一致也保护了非文本内容。2.3 什么是中文交互网页交互网页不是简单把译文排成 HTML而是提供 PDF 做不到的阅读能力。DeMinds 生成的网页通常包含四个特性左侧目录树随滚动高亮正文支持中英文对照切换数学公式用 KaTeX 或 MathJax 实时渲染参考文献和正文引用可以联动跳转。这些交互在 PDF 里都很难实现但放到网页上只是基础功能。需要强调的是交互网页解决的是“阅读深挖”问题。快速浏览时看中文译文需要确认术语时切回英文原文想追某个公式时能定位到上下文。这种体验比“一篇英文 PDF 一个翻译窗口”要高效得多。2.4 三个环节的关系结构优化、翻译、交互网页三者是上下游关系。结构优化负责把论文变成机器可处理的数据翻译负责把数据里的语言从英文转成中文交互网页负责把数据渲染成人类友好的界面。任何一步做得不好都会传导到下游结构解析不干净翻译就会把图表说明当正文翻译没有术语保护交互网页再好看也只是“装饰过的错误”。所以 DeMinds 真正值得借鉴的不是某个单独模块而是它对整条链路的取舍源码优先、结构先行、翻译受控、展示分层。3. 环境准备与前置条件3.1 运行环境DeMinds 的示例代码以 Python 为主建议使用 Python 3.9 及以上版本。操作系统不限Windows、macOS、Linux 都可以但发布脚本中的部署命令以 Linux 服务器为示例如果你用 Windows 本地环境建议配合 WSL 或 Git Bash 使用。前端部分不需要提前安装 Node.js因为示例直接生成静态 HTML并引用 CDN 上的 KaTeX 和样式文件。如果你后续要接 Vue 或 React 做更复杂的交互再单独准备前端工程环境即可。3.2 Python 依赖创建一个requirements.txt核心依赖如下requests2.31.0 feedparser6.0.10 httpx0.27.0 PyYAML6.0 Jinja23.1.0 pdfplumber0.11.0依赖说明requests下载论文源码和元数据。feedparser解析 arXiv API 返回的 Atom XML。httpx调用翻译服务接口。PyYAML读取术语表配置文件。Jinja2渲染 HTML 模板。pdfplumber备用方案用于部分没有 LaTeX 源码时从 PDF 抽取文本。安装命令pip install -r requirements.txt3.3 可选的 GROBID如果你的论文源确实只有 PDF没有 LaTeX 源码推荐引入 GROBID 作为 PDF 结构解析器。GROBID 是一个基于机器学习的技术文档解析服务能从 PDF 中识别标题、作者、摘要、章节、参考文献并输出结构化 TEI XML。它有两种启动方式一种是下载官方 Docker 镜像直接运行另一种是下载 jar 包本地启动。本文的主流程优先使用 LaTeX 源码所以 GROBID 只作为降级方案具体版本请以你拉取到的镜像或发布包为准。4. 核心流程拆解4.1 整体阶段划分从 arXiv 论文到中文交互网页可以拆成五个阶段。阶段一获取论文与元数据通过 arXiv API 搜索论文拿到论文编号、标题、作者、版本号并下载 LaTeX 源码包。阶段二LaTeX 结构解析把tar.gz源码包解压解析.tex文件提取章节树和段落文本输出 JSON。阶段三术语管理与翻译对文本块做公式占位保护应用术语表调用翻译服务输出中英对照的 JSON。阶段四生成交互网页用模板渲染 HTML接入 KaTeX、目录树和中英对照逻辑。阶段五发布把静态文件部署到服务器、对象存储或 GitHub Pages。下面逐个阶段说明关键点。4.2 阶段一获取论文与元数据很多人下载 arXiv 论文只依赖浏览器打开 PDF 页面但这个方式有两个问题一是 PDF 下载后依然只是拍平的内容二是 arXiv 官网在部分网络环境下可能不稳定影响自动化流程。更可靠的做法是走 arXiv API。API 端点通常比主站更稳定返回的是结构化 Atom XML包含论文 ID、标题、摘要、作者、投稿时间、修改版本等信息。拿到论文 ID 之后可以通过https://arxiv.org/src/{paper_id}下载源码包也可以通过https://arxiv.org/e-print/{paper_id}尝试下载归档压缩包。这个阶段的关键产物是论文元数据 JSON 和源码压缩包。元数据用于后续网页页脚的来源展示源码包用于结构解析。4.3 阶段二LaTeX 结构解析LaTeX 源码解析是这个流程的核心也是和纯 PDF 方案拉开差距的地方。拿到源码包后先判断它是单个.tex文件还是tar.gz压缩包。绝大多数多章节论文是压缩包解压后会看到主.tex文件、图片文件、样式文件和引用数据库。主.tex文件通过\input和\include引入其它.tex文件所以解析时不能只读主文件还要递归展开所有被引用的子文件。解析的关键是识别章节命令\section、\subsection、\subsubsection对应三级章节结构\paragraph可以看作段落标题\begin{equation}、\[、$...$是公式区域\cite、\ref是引用标记。输出结构建议采用以下 JSON 格式{ paper_id: 2312.12345, title: Example Paper, sections: [ { level: 1, title: Introduction, blocks: [ {type: paragraph, text: This is the first paragraph...}, {type: equation, content: E mc^2} ] } ] }这个 JSON 就是整条流水线通用的中间数据格式翻译脚本和 HTML 生成脚本都依赖它。4.4 阶段三术语管理与翻译翻译阶段要解决两件事公式和引用不被破坏术语保持一致。先说公式保护。在段落文本中先把$...$、$$...$$、\[...\]等公式片段替换成占位符__MATH_0__翻译完成后再按顺序还原这样翻译服务就不会把公式当作普通文本处理。再说术语一致。在terms.yaml里维护一个中英术语映射翻译前扫描文本把命中术语替换成占位符__TERM_0__并记录下来翻译结束后把这些占位符替换为对应的中文术语。这样不管“attention mechanism”出现多少次译文始终是“注意力机制”。实际调用翻译服务时建议使用支持对话补全的 HTTP 接口温度参数调低系统提示词明确“你是学术翻译助手保留公式占位符和引用标记”。4.5 阶段四生成交互网页有了中英对照的章节 JSON生成 HTML 就变得机械化了。推荐用 Jinja2 模板渲染时把章节树转成左侧目录把每个段落渲染成“英文原文 中文译文”的可切换双栏或行内对照公式块用 KaTeX 渲染页脚显示原文链接和版本信息。这个阶段最需要关注的细节是公式渲染。如果公式是从 LaTeX 源码里提取的纯文本可以直接交给 KaTeX如果公式是从 PDF 里识别出来的图片就不能走 KaTeX需要用图片标签保留。这也是为什么“优先拿 LaTeX 源码”如此重要源码里的公式是文本形态网页可以直接复用。4.6 阶段五发布生成好的静态网页本质上就是一堆 HTML、CSS、JS 文件发布方式取决于你的使用场景。如果是个人阅读工具可以直接在本地启动一个静态服务器。如果是团队内部知识库可以部署到 Nginx 或对象存储。如果想公开分享可以推到 GitHub Pages。还可以把网页嵌进微信小程序的web-view组件但要注意小程序要求业务域名已完成备案和校验而且web-view打开的页面必须配置在合法域名列表里。发布不是最后一步发布后的验证和回滚同样重要。建议把旧版本 dist 目录保留一段时间发布脚本支持一键回滚。5. 完整示例与代码实现5.1 项目目录结构这里给出一个最小可运行的 DeMinds 示例工程。完整代码放在以下目录结构中deminds/ ├── fetch_arxiv.py ├── parse_tex.py ├── translate.py ├── build_html.py ├── deploy.sh ├── requirements.txt ├── terms.yaml ├── templates/ │ └── paper.html └── output/ └── (生成的静态网页)下面逐个文件说明实现。5.2 示例一获取论文源码与元数据fetch_arxiv.py负责两件事通过 arXiv API 搜索论文下载 LaTeX 源码包。# 文件路径deminds/fetch_arxiv.py import os import re import tarfile import requests import feedparser from urllib.parse import urlencode ARXIV_API https://export.arxiv.org/api/query ARXIV_SRC https://arxiv.org/src/{paper_id} ARXIV_EPRINT https://arxiv.org/e-print/{paper_id} def search_arxiv(query, max_results1): 通过 arXiv API 搜索论文返回第一条结果的论文 ID。 params { search_query: fall:{query}, start: 0, max_results: max_results, } url f{ARXIV_API}?{urlencode(params)} feed feedparser.parse(url) if not feed.entries: raise RuntimeError(没有搜索到论文请调整关键词。) entry feed.entries[0] # entry.id 形如 http://arxiv.org/abs/2312.12345v2 paper_id entry.id.split(/abs/)[-1] return { id: paper_id, title: entry.title, summary: entry.summary, published: entry.published, authors: [author.get(name, ) for author in entry.authors], link: entry.link, } def download_source(paper_id, save_dirdata/raw): 下载 LaTeX 源码包。优先用 /src/失败时尝试 /e-print/。 os.makedirs(save_dir, exist_okTrue) headers {User-Agent: DeMinds/0.1 (academic workflow)} for pattern in (ARXIV_SRC, ARXIV_EPRINT): url pattern.format(paper_idpaper_id) resp requests.get(url, headersheaders, timeout60) if resp.status_code 200: ext .tar.gz if resp.headers.get(Content-Type, ).find(tar) 0 else .tex save_path os.path.join(save_dir, f{paper_id}{ext}) with open(save_path, wb) as fp: fp.write(resp.content) return save_path raise RuntimeError(f源码下载失败{paper_id}) def unpack_source(archive_path, extract_dirdata/raw/extracted): 如果是 tar.gz 压缩包则解压否则直接进入下一个阶段。 os.makedirs(extract_dir, exist_okTrue) if archive_path.endswith(.tar.gz): with tarfile.open(archive_path, r:gz) as tar: tar.extractall(extract_dir) return extract_dir return os.path.dirname(archive_path) if __name__ __main__: meta search_arxiv(graph neural network for electron phonon, max_results1) print(论文信息, meta[id], meta[title]) archive download_source(meta[id]) print(源码包已下载, archive) source_dir unpack_source(archive) print(源码目录, source_dir)代码要点feedparser直接解析 arXiv API 返回的 Atom XML避免手写 XML 解析。下载源码时带上了 User-Agent这是对 arXiv 服务的基本尊重。部分论文的源码包是单个.tex文件解开后不是目录所以要兼容两种情况。5.3 示例二LaTeX 结构解析parse_tex.py的功能是把.tex源码解析成章节 JSON。示例做了一个合理的简化重点展示思路。# 文件路径deminds/parse_tex.py import os import re import json SECTION_PATTERN re.compile( r\\(?Plevelsection|subsection|subsubsection)\*?\{ r(?Ptitle[^}])\} ) # 匹配行内公式、块级公式和数学环境 MATH_PATTERN re.compile( r(\\\[.*?\\\]|\\\(.*?\\\)|\\$.*?\\$|\\begin\{equation\}.*?\\end\{equation\}), re.DOTALL, ) def read_all_tex(source_dir): 递归读取目录下所有 .tex 文件返回合并后的文本。 all_text [] for root, _, files in os.walk(source_dir): for name in files: if name.endswith(.tex): path os.path.join(root, name) with open(path, r, encodingutf-8, errorsignore) as fp: all_text.append(fp.read()) return \n.join(all_text) def split_by_sections(text): 按 section/subsection 拆分文本生成章节树。 lines text.splitlines() sections [] current_level_1 None current_level_2 None for line in lines: match SECTION_PATTERN.search(line) if match: level match.group(level) title match.group(title).strip() node {level: 1, title: title, blocks: []} if level section else ( {level: 2, title: title, blocks: []} if level subsection else {level: 3, title: title, blocks: []} ) if level section: sections.append(node) current_level_1 node current_level_2 None elif level subsection and current_level_1 is not None: current_level_1[blocks].append(node) current_level_2 node elif level subsubsection and current_level_2 is not None: current_level_2[blocks].append(node) else: stripped line.strip() if not stripped or stripped.startswith(%): continue # 简单分块不处理段落合并只去除 LaTeX 命令中的反斜杠控制词 text_without_math MATH_PATTERN.sub(__MATH__, stripped) if current_level_2 is not None: current_level_2[blocks].append({type: paragraph, text: text_without_math}) elif current_level_1 is not None: current_level_1[blocks].append({type: paragraph, text: text_without_math}) return sections def parse_paper(source_dir, paper_id, title): text read_all_tex(source_dir) sections split_by_sections(text) return { paper_id: paper_id, title: title, sections: sections, } if __name__ __main__: import sys source_dir sys.argv[1] if len(sys.argv) 1 else data/raw/extracted result parse_paper(source_dir, demo, Demo Paper) with open(output/paper_structure.json, w, encodingutf-8) as fp: json.dump(result, fp, ensure_asciiFalse, indent2) print(章节数, len(result[sections]))代码说明split_by_sections做了简化没有处理\input和\include展开完整项目里应该在read_all_tex阶段递归展开。MATH_PATTERN先把公式替换成__MATH__标记避免后续翻译把公式当普通文本。输出 JSON 是所有下游模块的中间格式建议生成后先人工抽查几个章节。5.4 示例三翻译与术语保护translate.py演示如何用术语表保护术语调用翻译接口。# 文件路径deminds/translate.py import json import re import yaml import httpx TERM_PATTERN re.compile(r__TERM_\d__) MATH_PATTERN re.compile(r__MATH_\d__) def load_terms(pathterms.yaml): with open(path, r, encodingutf-8) as fp: data yaml.safe_load(fp) return data.get(terms, {}) def protect_terms(text, terms): 把术语替换成占位符返回替换后的文本和术语映射。 mapping {} for i, (en_term, zh_term) in enumerate(terms.items()): if en_term in text: placeholder f__TERM_{i}__ text text.replace(en_term, placeholder) mapping[placeholder] zh_term return text, mapping def protect_math(text): 把公式替换成占位符。 math_regions [] def repl(match): idx len(math_regions) math_regions.append(match.group(0)) return f__MATH_{idx}__ pattern re.compile(r(\\\[.*?\\\]|\\\(.*?\\\)|\\$.*?\\$|\\begin\{equation\}.*?\\end\{equation\}), re.DOTALL) protected pattern.sub(repl, text) return protected, math_regions def translate_with_api(text, endpoint, api_key, modelgpt-4o-mini): 调用兼容 OpenAI 的翻译接口。实际使用时请替换为你可访问的服务。 headers {Authorization: fBearer {api_key}} payload { model: model, messages: [ {role: system, content: 你是学术翻译助手翻译要保留公式占位符、引用标记和术语占位符。}, {role: user, content: f翻译成中文\n{text}}, ], temperature: 0.2, } resp httpx.post(endpoint, jsonpayload, headersheaders, timeout120) resp.raise_for_status() return resp.json()[choices][0][message][content] def translate_paragraph(text, terms, endpoint, api_key, model): text_with_terms, term_mapping protect_terms(text, terms) text_with_math, math_regions protect_math(text_with_terms) translated translate_with_api(text_with_math, endpoint, api_key, model) # 先还原公式再还原术语 for idx, math in enumerate(math_regions): translated translated.replace(f__MATH_{idx}__, math) for placeholder, zh_term in term_mapping.items(): translated translated.replace(placeholder, zh_term) return translated if __name__ __main__: terms load_terms() with open(output/paper_structure.json, r, encodingutf-8) as fp: paper json.load(fp) # 实际运行请通过环境变量读取密钥不要硬编码 endpoint https://your-translation-endpoint.example.com/v1/chat/completions api_key your-api-key for section in paper[sections]: for block in section.get(blocks, []): if block.get(type) paragraph: block[zh] translate_paragraph(block[text], terms, endpoint, api_key, modelyour-model) with open(output/paper_bilingual.json, w, encodingutf-8) as fp: json.dump(paper, fp, ensure_asciiFalse, indent2) print(翻译完成已保存 output/paper_bilingual.json)代码要点protect_terms和protect_math提前把术语和公式挪到安全区避免翻译服务误改。翻译接口的地址、密钥、模型名都以变量形式配置密钥不要提交到代码仓库建议用环境变量。如果不想接入外部翻译服务也可以换成本地模型只需改写translate_with_api内部实现。5.5 示例四生成交互网页build_html.py读取中英对照 JSON用 Jinja2 渲染出静态网页。# 文件路径deminds/build_html.py import json import shutil from jinja2 import Environment, FileSystemLoader def build(paper, output_dirdist): env Environment(loaderFileSystemLoader(templates)) template env.get_template(paper.html) # 生成章节树用于左侧目录 toc [] for section in paper[sections]: item {title: section[title], subsections: []} for block in section.get(blocks, []): if block.get(level, 1) 2 and title in block: item[subsections].append(block[title]) toc.append(item) html template.render(paperpaper, toctoc) import os os.makedirs(output_dir, exist_okTrue) with open(os.path.join(output_dir, index.html), w, encodingutf-8) as fp: fp.write(html) # 复制静态资源可选示例中 CSS/JS 直接走 CDN print(网页已生成到, output_dir) if __name__ __main__: with open(output/paper_bilingual.json, r, encodingutf-8) as fp: data json.load(fp) build(data)templates/paper.html的核心结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{{ paper.title }} - 中文对照阅读/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/katex0.16.9/dist/katex.min.css script defer srchttps://cdn.jsdelivr.net/npm/katex0.16.9/dist/katex.min.js/script script defer srchttps://cdn.jsdelivr.net/npm/katex0.16.9/dist/contrib/auto-render.min.js onloadrenderMathInElement(document.body, {delimiters: [ {left: $$, right: $$, display: true}, {left: $, right: $, display: false} ]});/script style body { display: flex; margin: 0; font-family: -apple-system, PingFang SC, Microsoft YaHei, sans-serif; } nav { width: 280px; padding: 20px; border-right: 1px solid #eee; position: sticky; top: 0; height: 100vh; overflow-y: auto; } main { flex: 1; padding: 40px 60px; max-width: 900px; } .block { margin-bottom: 20px; } .en { color: #333; } .zh { color: #666; border-left: 3px solid #4a90d9; padding-left: 12px; margin-top: 6px; } /style /head body nav h3目录/h3 ul {% for item in toc %} listrong{{ item.title }}/strong {% if item.subsections %} ul {% for sub in item.subsections %} li{{ sub }}/li {% endfor %} /ul {% endif %} /li {% endfor %} /ul p stylefont-size: 13px; color: #999; a href{{ paper.link }} target_blank查看 arXiv 原文/a /p /nav main h1{{ paper.title }}/h1 {% for section in paper.sections %} h2{{ section.title }}/h2 {% for block in section.blocks %} {% if block.type paragraph and block.text %} div classblock div classen{{ block.text }}/div {% if block.zh %} div classzh{{ block.zh }}/div {% endif %} /div {% endif %} {% endfor %} {% endfor %} /main /body /html模板说明页面用 Flex 布局左侧是固定目录右侧是正文。KaTeX 通过 CDN 引入auto-render会自动渲染$...$和$$...$$之间的公式。每一个段落保留英文原文和中文译文两个区域方便对照阅读。5.6 示例五发布脚本deploy.sh演示如何把生成的静态网页同步到服务器。#!/usr/bin/env bash # 文件路径deminds/deploy.sh set -euo pipefail DIST_DIR${1:-dist} SERVER${SERVER:-useryour-server} REMOTE_DIR${REMOTE_DIR:-/var/www/deminds} if [ ! -d $DIST_DIR ]; then echo 错误$DIST_DIR 目录不存在请先执行 build_html.py exit 1 fi echo 开始部署到 $SERVER:$REMOTE_DIR # 先备份远程现有版本 ssh $SERVER if [ -d $REMOTE_DIR ]; then cp -r $REMOTE_DIR ${REMOTE_DIR}.bak; fi # 同步静态文件 rsync -avz --delete $DIST_DIR/ $SERVER:$REMOTE_DIR/ echo 部署完成。若需要回滚执行 echo ssh $SERVER rm -rf $REMOTE_DIR mv ${REMOTE_DIR}.bak $REMOTE_DIR使用方式bash deploy.sh也可以指定部署地址SERVERroot123.45.67.89 REMOTE_DIR/var/www/deminds bash deploy.sh脚本里特意保留了“最新版本备份”这一步避免发布错误后没有回滚点。6. 运行结果与效果验证6.1 运行顺序完整的运行顺序如下# 第 1 步搜索并下载源码 python fetch_arxiv.py # 第 2 步解析结构 python parse_tex.py data/raw/extracted # 第 3 步翻译需要配置翻译服务的 endpoint 和 api_key python translate.py # 第 4 步生成网页 python build_html.py # 第 5 步本地预览 cd dist python -m http.server 8080浏览器打开http://localhost:8080就能看到生成的中文交互网页。6.2 预期输出每一步的预期输出如下fetch_arxiv.py控制台输出论文 ID、标题、源码包下载路径。parse_tex.py生成output/paper_structure.json控制台输出章节数。translate.py生成output/paper_bilingual.json每个段落增加zh字段。build_html.py生成dist/index.html。deploy.sh把dist同步到远程服务器。6.3 如何判断成功判断成功的标准有四个。第一章节数合理。一篇常规论文至少有 5 个以上一级章节如果解析出来只有 1 个章节大概率是源码读取不全或\section匹配失败。第二公式能渲染。页面上$...$之间的内容应该显示为数学公式而不是普通文本。第三中英对照正常。中文译文里不应该出现__MATH_0__或__TERM_0__这类残留占位符。第四目录能跳转。点击左侧目录项页面应该滚动到对应章节。如果失败先看output/paper_structure.json确认结构解析阶段是否出了问题再往翻译和渲染阶段排查。7. 常见问题与排查方法问题现象可能原因排查方式解决方案arXiv 官网或下载接口不稳定源码包下载失败网络环境受限或 arXiv 接口临时不可用查看请求返回状态码尝试切换/src/与/e-print/使用 arXiv API 获取元数据稍后重试也可以通过 DOI 到期刊页面获取源码下载的tar.gz解压失败文件下载不完整或论文本身不提供打包源码检查文件大小是否异常重新下载如果论文只有单个.tex文件直接当作源码文件处理解析后章节很少结构混乱主.tex文件通过\input引入了子文件但代码没有递归展开检查源码目录里是否有多个.tex在read_all_tex中递归展开所有.tex文件并过滤注释和宏定义公式在网页上渲染成普通文本KaTeX 的