尝试使用MinerU

在之前的文章的评论区里,有人建议试一试MinerU。其实在去年7月MinerU刚发布的时候,我就尝试过,但是印象中效果非常差,根本没法实际使用。 正好我的电脑里还存有当时配置好的虚拟环境,又重新试了一下,结果印证了我的记忆。由于问题实在太多,我就不放截图了,只是罗列一下它的问题: 完全不能识别公式中的汉字 所有多行公式无法换行 经常漏识别公式的一部分(如最后的字母、特殊符号如平行等) 经常无法识别分式 偶尔会漏识别公式 经常识别不出特殊符号(如相似) 偶尔识别错误字母(例如p识别成Φ) 中英文混合的情况,不能识别英文之间的空格 偶尔汉字识别错误 偶尔公式后面多出东西(把公式的一部分重复识别成了文字) 偶尔多行公式排版错误($$后不换行直接跟文字) 偶尔行间公式排版错误(错误使用\$) 经常错误识别标点符号(例如逗号识别成双引号),经常漏识别标点符号 这些问题导致识别得到的结果错误非常多,已经到了不可用的地步。 不过看官方仓库里的说明,新版的MinerU使用vLLM部署之后能有90+的准确率,我决定再试一下。 1. 安装新版MinerU 由于使用vLLM部署比pipeline模式的准确率高很多,加之8G显存够用了,因此我直接使用vLLM方式进行部署。 安装也很简单: 1 2 uv venv --seed --python python3.13 uv pip install "mineru[all]" 此时的MinerU版本是2.6.4 接下来就可以使用了: 1 uv run mineru -p input/test.pdf -o output -b vlm-vllm-engine 进一步的配置选项可以参看官方选项,默认配置对于我来说就可以使用了。 这样临时使用一下还可以,但如果要大量文件需要识别,每次都会花大量时间在模型的加载上。这时可以使用server模式进行部署和调用。首先运行vllm-server: 1 uv run mineru-vllm-server 然后在另一个终端里运行 1 uv run mineru -p input/test.pdf -o output -b vlm-http-client -u http://localhost:30000 即可。 2. 和PaddleOCR-VL的对比 在下面的对比图片中,左侧是MinerU的结果,右侧是PaddleOCR-VL的结果。 2.1. 速度 同样是识别小蓝本(共8本),耗时33分21秒,比PaddleOCR-VL慢了约17%。 2.2. 优势 MinerU没有出现PaddleOCR-VL中的公式错行现象,基本上能够正确识别公式的位置。 另外,MinerU对于特殊数学符号(如平行、相似等)的识别准确率要高于PaddleOCR-VL和DeepSeek-OCR。 2.3. 劣势 虽然最新的MinerU和最初的版本相比,前面列出的问题大部分都得到了解决,但是还有一部分被部分继承了下来。 例如,简单的文字识别错误: 简单的字母识别错误: 简单的标点识别错误: 另外,MinerU还出现了图片分割不准确的情况: 这个问题PaddleOCR-VL和DeepSeek-OCR从来没有出现过。 2.4. 共同的问题 MinerU和PaddleOCR-VL都在同一类地方栽了。类似下图, 识别的结果如下: 可以看到,二者都不能正确区分题目的序号和公式部分。 而DeepSeek-OCR能够正确处理: 3. 总结 如果让我给这几个OCR模型进行排行的话,大致是 DeepSeek-OCR ≳ MinerU 2.6 (vllm) > PaddleOCR-VL >> PaddleOCR (V3) DeepSeek-OCR的优势在于排版最好,但是在识别一些复杂公式(如重分式)的时候可能会出问题(感觉是识别之后被改了)。 最大的劣势是模型还是偏大,模型参数本身超过6G,因此我没有办法在本地使用vLLM加速推理,只能使用Transformers进行推理(同时需要借用一部分内存),因此速度相比后两个慢很多。 MinerU的公式识别整体是最好的,但是会犯一些弱智的错误(比如经常识别不了字母p)。 前面两个基本上不分伯仲,关键是看注重哪方面。 PaddleOCR-VL的最大问题在于公式混合排版的输出顺序有问题。 上面两个都可以在本地(8G显存)使用vLLM进行加速,因此速度都是可用的。 PaddleOCR就连基本的退化问题都没有解决。 另外,PaddleOCR对于表格的识别也比PaddleOCR-VL差很多。

2025/11/18
articleCard.readMore

使用VSCode编辑Markdown的几个常用设置

1. 中文的自动换行 VSCode的默认换行算法无法正确处理中文段落的换行。因为中文不像英文每个单词之间都有空格,所以经常出现换行位置超过屏幕范围的情况。例如 可以看到,有十个汉字还在第一行的右边,需要拖动水平滚动条才能看见。 这个可以通过修改控制计算换行位置的算法[1]来解决。添加配置 1 "editor.wrappingStrategy": "advanced" 就可以解决这个问题。此时VSCode显示的内容就变成了 这时换行位置基本就正常了。偶尔超出屏幕一点,最多也就吞掉半个字。 2. 粘贴图片 之前的时候,一旦需要添加图片,我都会转而使用Typora编辑Markdown文件。 Typora的优点是具有可视化,表格编辑方便;VSCode的优点是可以在编辑数学公式的时候自动切换中英文输入法(通过我自己修改过的插件,原插件已失效)。 好在,同时需要大量编辑数学公式和大量添加图片的情况对于我来说基本上不存在。 不过,新版的VSCode其实已经支持了图片复制和插入[2]。直接把图片拖入到VSCode中,可以看到「按住Shift以放入编辑器」的提示。此时按住Shift再放开鼠标,图片就被复制到Markdown文件所在的目录,并插入到了Markdown中。 不过,根据Hexo的文档结构,我需要把图片放到和Markdown文件同名的文件夹中,因此需要添加配置: 1 2 3 "markdown.copyFiles.destination": { "/source/**/*": "${documentBaseName}/" }, 这样图片就能直接复制到所需的位置了。 3. 预览SVG图像 我使用 pdf2svg 生成的SVG图像都是透明背景的,而VSCode又是深色背景,此时打开SVG图片根本看不清。添加配置 1 "svg.preview.background": "white" 这样就默认是白色背景了。 4. 数学公式 我的博客现在设置了 text-autospace: normal,这样浏览器可以自动在中英文字符之间增加间距,就不用手动添加空格了。 但是可惜的是,这个对于使用KaTeX插入的数学公式并不起作用,因此还是需要在公式两边手动添加空格。 我的方法是使用快捷键 Ctrl+m,手动添加数学环境及空格。快捷键设置如下: 1 2 3 4 5 6 7 8 { "key": "ctrl+m", "command": "editor.action.insertSnippet", "when": "editorTextFocus && editorLangId =~ /latex|latex-expl3|typst|markdown/", "args": { "snippet": " $${TM_SELECTED_TEXT}$1$ $0" } } 但是这会带来一个问题。如果数学公式之后是标点符号,此时是不需要添加空格的,因此还需要手动删掉。如果每次输入的时候删除的话会非常麻烦。在使用LaTeX的时候,我是使用 latexindent 来进行完成格式化的,对应的 localSettings.yaml 配置如下: 1 2 3 replacements: - substitution: s/([,。;:、!?()【】])\h+(\$+)/$1$2/g - substitution: s/(\$+)\h+([,。;:、!?()【】])/$1$2/g 但是Markdown文件没有类似的软件。 我最终是通过「Run on Save」插件来完成这个任务的。安装插件以后,添加配置 1 2 3 4 5 6 "runOnSave.commands": [ { "match": ".*\\.md$", "command": "sed -i -E -e 's/\\$+\\s+([,。!?;:、()【】「」『』])/\\$\\1/g' -e 's/([,。!?;:、()【】「」『』])\\s+\\$+/\\1\\$/g' ${file}", }, ] 这样在保存Markdown文档的时候会自动运行 sed 命令,删掉多余的空格。 更新来自1.42版本 ↩︎ 更新来自1.79版本 ↩︎

2025/11/6
articleCard.readMore

在WSL上挂载U盘

记录一下在WSL上成功挂载U盘的过程。 1. 连接USB设备 按照官方文档,安装usbipd-win,即可从WSL访问USB设备。 正常使用的大致流程如下: graph LRA(共享) --> B(连接) --> C(挂载) --> |&thinsp;使用&thinsp;|D(卸载) --> E(断开连接) --> F(解除共享) 查看可用的USB设备: 1 usbipd.exe list 示例输出: 1 2 3 Connected: BUSID VID:PID DEVICE STATE 2-3 0781:55ab USB 大容量存储设备 Not shared 使用BUSID指定要共享的USB设备(需要管理员权限): 1 sudo.exe usbipd.exe bind --busid 2-3 然后连接该设备: 1 usbipd.exe attach --wsl --busid 2-3 这时使用 lsusb 可以看到USB设备已经识别到了: 1 Bus 002 Device 003: ID 0781:55ab SanDisk Corp. Dual Drive 但是使用 lsblk 看不到该设备。原因是WSL的内核使用 CONFIG_USB_STORAGE=m 编译,需要手动加载对应的模块: 1 modprobe usb-storage 这时再运行 lsblk 就能看到新的设备了: 1 2 sde 8:64 1 114.6G 0 disk └─sde1 8:65 1 114.6G 0 part 2. 挂载USB设备 如果U盘是FAT格式,这个时候就可以直接挂载了。 但是由于我的U盘比较大,之前格式成了exFAT格式。而WSL的内核没有编译exFAT(以及NTFS)的支持,因此要挂载U盘的话,需要重新编译内核。 不过编译内核也很简单,从官方内核仓库下载最新的内核,解压后进入文件夹,运行 1 2 cp Microsoft/config-wsl .config make menuconfig 进入图形化配置界面,选择 1 2 3 4 5 File systems ---> DOS/FAT/NT Filesystems ---> <M> exFAT filesystem support <M> NTFS Read-Write file system support <*> activate support of external compressions lzx/xpress 我直接把exFAT和NTFS的支持都加上了。 然后编译内核: 1 make -j$(nproc) 安装模块: 1 make INSTALL_MOD_PATH="$PWD/modules" modules_install 最新的WSL使用vhdx挂载modules: 1 sudo ./Microsoft/scripts/gen_modules_vhdx.sh "$PWD/modules" $(make -s kernelrelease) modules.vhdx 然后把内核映像 arch/x86/boot/bzImage 和内核模块 modules.vhdx 放到Windows的硬盘上(我放在了 D:\Temp)。 在 .wslconfig 中配置自定义内核: 1 2 3 [wsl2] kernel=D:\\Temp\\bzImage kernelModules=D:\\Temp\\modules.vhdx 然后重启WSL即可。 要挂载exFAT的U盘,还需要安装对应的软件: 1 emerge --ask sys-fs/exfatprogs (如果是挂载NTFS格式的设备,需要则安装 sys-fs/ntfs3g。) 加载所需的内核模块: 1 modprobe usb-storage exfat 然后就可以正常挂载U盘了: 1 mount /dev/sde1 /media 使用完了之后断开连接并解除共享: 1 2 usbipd.exe detach --busid 2-3 sudo.exe usbipd.exe unbind --busid 2-3

2025/11/5
articleCard.readMore

在使用LuaLaTeX时控制中英文字符的间距

1. 问题描述 之前在从XeLaTeX切换到LuaLaTex的时候,发现二者对中文字符和英文字符(包括数学公式)之间的间距处理不一样。在源代码中是否添加空格会对最终的间距有影响。 以下面的示例文档为例: 1 2 3 4 5 6 7 8 9 10 \documentclass{ctexart} \begin{document} 中English文 中 English 文 中$\sin(x)$文 中 $\sin(x)$ 文 \end{document} 这是XeLaTeX编译的结果,可以看到,不管加不加空格,得到的PDF间距都是一样的。 下面是LuaLaTeX编译的结果: 这时不加空格时的间距要小于加空格时的间距。 因此,如果使用LuaLaTeX进行编译,整个文档的风格必须统一,要么都加空格,要么都不加,否则就会很难看。 2. 解决方法 根据LdBeth[1]给出的提示,找到了一个解决方法,可以通过设置 xkanjiskip 来解决这个问题。 在导言区加入: 1 2 3 4 5 6 7 8 \usepackage{iftex} \ifluatex \usepackage{luatexja} \ltjsetparameter{ xkanjiskip={\fontdimen2\font plus \fontdimen3\font minus \fontdimen4\font}, alxspmode={`\%,3} } \fi 这样使用LuaLaTeX编译出来的文档无论加空格与否,间距都是一致的。 另外,默认的设置在百分号「%」和中文之间没有空格,上面同时修复了这个问题。 命令解释: LuaTex-ja把一部分Unicode区段(U+0080~U+10FFFF)分为 JAChar(包含CJK字符等)和 ALchar(包含西文字母等),然后使用 kanjiskip 控制 JAChar 之间的间距,使用 xkanjiskip 控制 JAChar 和 ALchar 之间的间距。 上面通过修改 xkanjiskip 使得中英文字符间距表现和XeLaTeX一致。 但是,并不是所有的 JAChar 和 ALchar 之间都需要增加间距。LuaTex-ja使用 jaxspmode 控制是否需要在 JAChar 前后增加间距,使用 alxspmode 控制是否需要在 ALchar 前后增加间距。 上面通过将 % 对应的模式设为 3(或 allowed),使得在对应字符前/后都允许增加间距。(默认设置是「%」前面有空格,但后面没有。) https://www.zhihu.com/question/1904935413283522195/answer/1968692664439407558 ↩︎

2025/11/5
articleCard.readMore

使用vLLM框架加速PaddleOCR-VL

之前直接使用PaddlePaddle进行PaddleOCR-VL的推理,优点是安装相对简单,但缺点是速度太慢了,推理速度只有之前PaddleOCR的40%,这已经比DeepSeek-OCR快不了多少了。这个速度识别几本书还行,多了的话速度根本不够。 造成速度慢的主要原因是PaddleOCR-VL模型在不使用推理框架的时候,只支持使用 batch_size=1 进行推理,因此GPU根本跑不起来。 PaddleOCR-VL是支持使用vLLM框架进行加速的。之前没有使用,主要是在安装的时候发现需要装特定版本的 flash-attn,于是就放弃了。 编译安装 flash-attn 需要的内存极其恐怖。如果不做任何配置的话,编译过程中 ninja 会自动根据CPU内核数量开启并行编译,而 flash-attn 在使用nvcc编译的时候又默认开启 sm_80、sm_90、sm_100、sm_120 四个编译目标,因此在我的电脑上,实际会同时运行 32×4=12832\times 4=12832×4=128 个进程进行编译。 这样的话,即使我给WSL分配了50G的内存和75G的交换文件,也很快就被爆掉了。 之前在系统里安装这个包的时候,我是把 MAX_JOBS 设为3,然后修改源代码只开启 sm_80、sm_89 两个编译目标,这样只有6个进程同时进行编译,才不会爆内存。 不过如此设置的话,编译速度也就很感人了,编译一次就要超过1小时。 作为对比,下面是我的电脑上常用的「大」软件的编译时间: 1 2 3 4 5 6 7 8 # qlop -cmv sci-ml/caffe2-2.9.0 sci-ml/flash-attention-2.8.3 sys-devel/gcc-14.3.0 llvm-core/llvm-21.1.4 llvm-core/clang-21.1.4 dev-lang/rust-1.91.0 dev-lang/rust-1.91.0: 21′57″ average for 1 merge llvm-core/clang-21.1.4: 21′49″ average for 1 merge llvm-core/llvm-21.1.4: 28′42″ average for 1 merge sci-ml/caffe2-2.9.0: 24′25″ average for 1 merge sci-ml/flash-attention-2.8.3: 1:11:42 average for 1 merge sys-devel/gcc-14.3.0: 22′30″ average for 1 merge total: 3:11:05 for 6 merges 可能也就只有之前编译TensorFlow的时间比它长了。 好在,最新的文档给出了 flash-attn 的预编译包的仓库,里面有各个版本的编译好的包可以直接使用,因此就可以配置vLLM了。 1. 安装vLLM推理框架 按照官方提示, 由于推理加速框架可能与飞桨框架存在依赖冲突,建议在虚拟环境中安装。 我看了一下,主要是PaddlePaddle和vLLM依赖的PyTorch对于一系列nvidia包的不同(paddlepaddle-gpu==3.2.0 依赖12.9版本的CUDA,而 vllm==0.10.2 依赖 pytorch==2.8.0 及12.8版本的CUDA)。 所以新建一个虚拟环境,并安装vLLM推理框架: 1 2 3 4 uv venv --seed -p python3.12 .venv_vllm VIRTUAL_ENV=.venv_vllm uv pip install "paddleocr[doc-parser]" VIRTUAL_ENV=.venv_vllm uv pip install https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.3.14/flash_attn-2.8.2%2Bcu129torch2.8-cp312-cp312-linux_x86_64.whl VIRTUAL_ENV=.venv_vllm uv run paddleocr install_genai_server_deps vllm 注意: 要先安装预编译的 flash-attn 包,否则直接运行安装vLLM框架的命令会报错。 要根据Python版本、Pytorch版本(目前版本的vLLM依赖的是2.8)来选择对应的 flash-attn 包。具体的链接在上面仓库的主页可以找到。 可选项: 1 2 VIRTUAL_ENV=.venv_vllm uv pip install flashinfer-python flashinfer-cubin VIRTUAL_ENV=.venv_vllm uv pip install flashinfer-jit-cache --default-index https://flashinfer.ai/whl/cu129 貌似有一点点速度提升? 然后就可以启动vLLM服务了: 1 2 3 4 5 VIRTUAL_ENV=.venv_vllm uv run paddleocr genai_server \ --model_name PaddleOCR-VL-0.9B \ --backend vllm \ --port 8118 \ --backend_config <(echo -e 'gpu-memory-utilization: 0.8\nmax-model-len: 8192') 这里务必要根据自己的显卡对vLLM服务参数进行调整。作为参考,我的显卡是RTX4060,显存大小是8G。 从PaddleX的配置文件可以看到,默认传给vLLM的参数为: 1 2 3 4 5 trust-remote-code: True gpu-memory-utilization: 0.5 max-model-len: 16384 max-num-batched-tokes: 131072 api-server-count: 4 其中必须调整的参数: gpu_memory_utilization 是vLLM可以占用的显存比例,如果是默认的0.5即50%,由于我的笔记本电脑显存只有8G,使用一半的话就只有4G了,这是肯定不够的,因此就直接报错了。 根据之前的经验,在使用PaddlePaddle进行推理的时候就需要5~6G的显存。因此我给vLLM分配了80%的显存,这样就足够了。 另外,在开启独显直连的情况下,即使不进行什么复杂任务,显存也要用掉0.8~1.2G。计算可用显存的时候要把这部分刨去。 max_model_len 是模型的上下文长度。默认值对我来说太大了,直接就报错说显存不够。 在没有运行其它占用显存的程序的时候,把 max_model_len 设为 8192 是可以运行的。但我也遇到过一次启动报错的情况,因此可以把它再调小一些。 不过,这个值也不能太小了,至少是 4096+14=41104096+14=41104096+14=4110。因为输入要用掉14个tokens,默认的最大输出是4096个tokens,因此至少要不小于这两个加起来的数量,否则推理的时候就会报错。 实测在我的电脑上最低可用的配置为 1 2 gpu-memory-utilization: 0.79 max-model-len: 4110 不过还是尽可能把max-model-len拉大一些比较好,否则实测在识别一些大型表格的时候会漏掉一些部分。 当屏幕出现 "GET /health HTTP/1.1" 200 OK 的字样时就说明vLLM服务启动成功了。 在vLLM服务的启动过程中,占用的显存会超过8G,如图 不过很快就回落了,最终在推理的时候也不需要借用内存,因此不会影响推理速度。 2. 调用vLLM推理服务 在另一个终端可以直接使用命令行进行调用: 1 2 3 4 5 uv run paddleocr doc_parser \ --input input/test.pdf \ --save_path output \ --vl_rec_backend vllm-server \ --vl_rec_server_url http://localhost:8118/v1 也可以使用Python进行调用: 1 2 3 4 5 6 7 pipeline = PaddleOCRVL( use_doc_orientation_classify=False, use_doc_unwarping=False, use_chart_recognition=False, vl_rec_backend="vllm-server", vl_rec_server_url="http://localhost:8118/v1", ) 后续的使用方法就和之前一样了。 3. 速度对比 使用vLLM会带来巨大(超过20倍)的速度提升。在我的电脑上,跑完8本小蓝本,只需要27分41秒(均衡模式)。在推理过程中,显卡基本保持在80W的功率。 作为对比,在Kaggle上使用 GPU T4x2,并开启双卡双进程编译,需要5小时40分。 之前在本地运行的时候没有总计时,但是基本每本书都需要1小时以上的时间,其中最厚的《三角形与四边形》甚至需要将近两个小时。 4. 在Kaggle上部署 在Kaggle上部署的方法和在本地部署是一致的,显卡选择 GPU T4x2,只是需要调整一下 flash-attn 预编译包的版本。 不过,参考官方文档: 推理方式支持的 GPU Compute Capability vLLM≥ 8 (RTX 3060,RTX 5070,A10,A100, ...) 7 ≤ GPU Compute Capability < 8 (T4,V100,...)支持运行, 但可能出现请求超时、OOM 等异常情况,不推荐使用 实测也确实如此。在Kaggle上部署之后,简单调用识别个图片还是没问题的,一旦涉及到多页PDF就经常报错。 而且此时在Kaggle上的推理速度还不到本地(RTX4060 8G)的一半,因此完全没有使用的必要了。

2025/11/4
articleCard.readMore

关于PaddleOCR-VL和PaddleOCR对数学类书籍识别的对比

之前成功安装PaddleOCR-VL之后,就尝试使用再次对小蓝本进行OCR,这样就可以和之前的结果进行对比。 其实上篇文章本来就想写这个的,结果写着写着发现最新版的PaddleOCR-VL支持Windows和Kaggle部署了,然后就跑题了。。。 这里提一句,PaddleOCR-VL的文档有两个,分别在 PaddleOCR文档 PaddleOCR-VL使用教程 PaddleX文档 PaddleOCR-VL介绍 里面。 目前PaddleOCR文档里面的内容更新得快一些。 1. 坎坷的使用过程 使用PaddleOCR-VL的过程的非常坎坷。出现的问题在前文中已有叙述。开始是根本运行不了。后来升级了PaddleX之后,成功运行模型,但是又遇到了显存不足和程序卡死的问题。 刚开始使用我就发现使用PaddleOCR-VL占用的显存甚至比DeepSeek-OCR还要多,和官方宣传的「小」模型完全不符。而且最大的问题是,随着推理的进行显存占用越来越多。 大致猜测一下原因,应该是由于笔记本默认把一半的内存列为共享显存,导致程序以为实际可以使用8+32=40G显存,所以就不着急释放占用的显存了。 在Kaggle上运行的时候,显存也在一直增加,但是最终没有报错。 好在,最终是通过手动释放显存和修改显存分配策略解决了。 确认可以正常运行之后,我想让它利用晚上睡觉的时间跑完OCR。看着识别第1本书运行正常就去睡觉了。然后早上起来发现就只有前两本书正常跑完了,跑到第3本书的时候程序就卡死了。后来我又多次复现了这个问题,但是始终没有找到原因。 不过中间有一次我把下载的模型删掉之后重新下载,后来就没有再出现这个错误了。 2. 结果对比 在下面的对比中,如果没有特殊说明,默认左侧是PaddleOCR-VL的结果,右侧是PaddleOCR(PP-StructureV3)的结果。 2.1. 退化 相交于PaddleOCR,PaddleOCR-VL最大的进步在于此,终于没有再出现退化的情况了(可喜可贺)。 2.2. 排版 2.2.1. 进步 应该说,PaddleOCR-VL的排版模型有了很大的进步,例如 2.2.2. 缺点 但问题也没有完全解决,例如 查看生成的可视化图像,可以看到 PaddleOCR-VL把每行里面居中的公式都视为了行间公式。这个本身到不要紧,但是最后把它们合并视为一个公式,这就造成了最终的排版错误。 我尝试把产线配置里面 layout_merge_bboxes_mode 中 display_formula(有注释)对应的选项从 large 改为 union,这时可以看到 模型其实最开始是识别到了每行的公式,然后又把它们组合了起来。 如果改为 small 的话,倒是不组合到一起了,但是输出顺序还是有问题: 而这是DeepSeek-OCR的识别结果: 在修改的时候,一定不能把 inline_formula 对应的选项改成 small,否则会造成有含有行内公式的段落只保留公式,不识别文字: 不过,又看了一下识别正确的情况: 没想明白它到底是如何区分行内公式和行间公式的。。。 2.3. 图片 2.3.1. 进步 PaddleOCR-VL的图片识别有所进步,终于能够分割一行的多个图片了: 不过最终还是过度分割了。。。 另外,PaddleOCR-VL也没有出现之前识别到图片里面的字母而没有正确识别出图片的情况: PaddleOCR-VL对于图片标题的识别也有进步: 能够正确区分图片标题,不会把它和其它部分混到一起。 2.3.2. 缺点 不过,从上面个对比中也可以看到PaddleOCR-VL的问题。就是它明明已经识别到了图片和图片标题,但是却没有把它们放到一起。这就导致大部分图片和对应的标题都是分开的。这点比起PaddleOCR模型是大大的退步。 看了一下版面阅读顺序识别的结果,里面没有指定图片和图片标题的输出顺序,有可能是这个原因造成的: 2.4. 数学公式 2.4.1. 进步 PaddleOCR-VL相比之前的模型在数学公式方面的进步主要有一下几点: 其一,就像之前说的,没有出现退化的情形。 其二,PaddleOCR-VL在特殊符号的上的识别上要好的多,甚至比DeepSeek-OCR还要强。例如, PaddleOCR-VL在大部分情况下能够正确识别「相似」、「平行」和「垂直」的符号,不像之前PaddleOCR模型大部分都识别不出来。 不过还是有识别错误的情况: 其三,之前PaddleOCR经常出现一个愚蠢地错误,就是识别不出公式里面的「减号」: PaddleOCR-VL没有发现这个问题。 其四,基本没有出现因为行内公式导致的整段文字漏掉的情况: 其五,PaddleOCR-VL对于文字和公式的分割要准确很多: 虽然有时还是会犯错误。 但是,对于简单的字母和公式,PaddleOCR-VL还是更倾向于不把它们识别成公式。这点倒是没有改。 2.4.2. 缺点 相交于之前,PaddleOCR-VL也带来了两个以前从没有出现的问题。 一个是PaddleOCR-VL多次出现识别了公式但是没有把它放到正确环境里面的情况,如 看了一下显示版面区域检测的结果,确实是识别出行内公式了: 看来是最终输出部分的问题。 另一个是PaddleOCR的行间公式的输出比较混乱。 同一个公式,例如下面的公式: sin⁡(x)\sin(x)sin(x) DeepSeek-OCR转换为 \[\sin{x}\],PaddleOCR转换为 $$\sin{x}$$,都没有问题。 而到了PaddleOCR-VL,它则是一会儿转换为 1 $$ \sin{x} $$ (注意 $$ 符号两边都有空格),一会儿转换为 1 2 3 4 5 $$ \sin{x} $$ (还是 $$ 符号两边都有空格。) 一会儿又转换为 1 2 $$ \sin{x} $$ 有时还会把行间公式和文字放到一行。 这本身倒没什么问题,只是在做正则替换的时候要格外小心。 看来PaddleOCR-VL和PaddleOCR对数学公式的训练用的是完全不同的样本? 3. 总体评价 整体来看,PaddleOCR-VL相比PaddleOCR有明显的进步,但是仅对数学书籍的OCR识别来看,效果还是不如DeepSeek-OCR。 问题不是出自文字识别或公式识别本身(这方面PaddleOCR-VL实际上更强一些),而是出在排版算法上。

2025/11/3
articleCard.readMore

尝试使用PaddleOCR-VL

强烈建议使用vLLM进行部署,这样不会遇到本文中提到的各种显存问题,部署方法可以参考后面这篇文章。 根据官方的说明: 推理过程中有时出现 OOM 问题 默认的 PaddlePaddle 动态图推理方式显存占用波动较大,处理复杂图像时峰值显存使用量可能较高。如需获得更稳定的显存占用表现,建议使用 vLLM、SGLang 等专用推理加速框架进行部署。 实际表现确实如此。原始的部署方式在处理一些尺寸较大的图片(即使图片本身比较小)时,即使有40G的显存也可能会报显存不足的错误,参考这个issue。我在使用自己的图片是也复现过这个问题。 另外,使用vLLM框架部署之后就没有再遇到过本文提到的各种显存的问题。 之前和DeepSeek-OCR进行对比,使用的是PaddleOCR,而不是当时的SOTA模型PaddleOCR-VL,原因也很可笑,就是按照官方文档安装之后居然无法运行,于是只能放弃了。 好在前几天释出的3.3.6版本的PaddleX解决了这个问题,本地的PaddleOCR-VL终于能正常运行了。 1. 安装PaddleOCR-VL 1.1. 在WSL上进行部署 之前已经安装了PaddleOCR: 1 2 3 4 export UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple" uv venv --seed --python python3.12 uv pip install paddlepaddle-gpu==3.2.0 --default-index https://www.paddlepaddle.org.cn/packages/stable/cu129/ uv pip install "paddleocr[all]" 如果要使用PaddleOCR-VL产线,按照官方说明,必须安装特殊版本的 safetensors: 1 uv pip install https://paddle-whl.bj.bcebos.com/nightly/cu126/safetensors/safetensors-0.6.2.dev0-cp38-abi3-linux_x86_64.whl 另外务必确认PaddleX的版本: 1 uv pip install paddlex==3.3.6 paddleocr==3.3.1 或者直接升级也可以: 1 uv pip install -U paddlex paddleocr 之后就可以使用PaddleOCR-VL了。具体的代码参考官方文档。 另外吐槽一句,PaddleOCR的3.3.1版本的更新日志里明明白白地写了「修复了PP-StructureV3和PaddleOCR-VL的文档图像预处理开关不生效的问题」,结果根本没有解决这个问题。 明明默认的PP-StructureV3.yaml里面写了 use_doc_preprocessor: False,但还是会调用对应的「文档图像预处理产线」。 之前发现这个问题,是因为我在识别一张课程表的时候,居然连标题都识别错了,而且错的特别离谱。后来发现是因为「文档图像预处理产线」调用了「文本图像矫正模块」,导致这个图片被裁掉了一圈,标题也被裁掉了一半,难怪识别不对。 因此,如果图片或者PDF文件非常规整的话,创建产线的时候务必要使用 use_doc_unwarping=False 关掉「文本图像矫正模块」。或者直接修改产线配置文件也可以。 我在官方示例的基础上稍微做了一下修改,除了保存markdown文件及图片外,还把所有生成的可视化图像转换成PDF保存。其中 pil_to_pdf_img2pdf 函数来自run_dpsk_ocr_pdf.py。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from pathlib import Path from paddleocr import PaddleOCRVL input_dir = Path("./input") output_dir = Path("./output") pipeline = PaddleOCRVL( use_doc_orientation_classify=False, use_doc_unwarping=False, ## 非必要一定要关掉!!! use_chart_recognition=False, ) def process_pdf_file(pdf_path: Path, pipeline, output_dir: Path) -> Path: print(f"🤖 Processing PDF file: {pdf_path}") output = pipeline.predict_iter(input=str(pdf_path), use_queues=True) markdown_list = [] markdown_images = [] res_images = [] for res in output: index = res.get("page_index") + 1 print(f"🎲 parsing page {index} of file {pdf_path.name} ...") md_info = res.markdown markdown_list.append(md_info) res_images.append(res.img) res.save_to_xlsx(save_path=output_dir) markdown_images.append(md_info.get("markdown_images", {})) markdown_texts = pipeline.concatenate_markdown_pages(markdown_list) ## 生成输出文件路径 mkd_file_path = output_dir / f"{pdf_path.stem}.md" mkd_file_path.parent.mkdir(parents=True, exist_ok=True) ## 写入Markdown文件 print("🚀 Saving markdown file ...") with open(mkd_file_path, "w", encoding="utf-8") as f: f.write(markdown_texts) ## 保存可视化图像 for layout in res_images[0].keys(): layout_pdf = output_dir / f"{pdf_path.stem}_{layout}.pdf" print(f"🚀 Saving {layout} results to: {layout_pdf}") pil_to_pdf_img2pdf([item[layout] for item in res_images], layout_pdf) ## 保存图像 print("🚀 Saving images in markdown ...") for item in markdown_images: if item: for path, image in item.items(): file_path = output_dir / path file_path.parent.mkdir(parents=True, exist_ok=True) image.save(file_path) return mkd_file_path for pdf_path in input_dir.glob("*.pdf"): process_pdf_file(pdf_path, pipeline, output_dir) 1.2. 在Windows上部署 今天又看了一下最新文档,对应的Windows版本的safetensors预编译包也有了,因此理论上在Windows上也能正常运行了。 1 2 3 4 5 6 $env:UV_LINK_MODE="symlink" $env:UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple" uv venv --seed -p python3.12 uv pip install paddlepaddle-gpu==3.2.0 --default-index https://www.paddlepaddle.org.cn/packages/stable/cu129/ uv pip install "paddleocr[doc_parser]" uv pip install https://xly-devops.cdn.bcebos.com/safetensors-nightly/safetensors-0.6.2.dev0-cp38-abi3-win_amd64.whl 尝试运行 1 uv run paddleocr doc_parser -i https://paddle-model-ecology.bj.bcebos.com/paddlex/imgs/demo_image/paddleocr_vl_demo.png --save_path output 然后就华丽地报错了: 1 2 RuntimeError: Exception from the 'vlm' worker: (NotFound) The kernel `fused_rms_norm_ext` is not registered. [Hint: Expected iter != kernels_.end(), but received iter == kernels_.end().] (at ..\paddle\phi\core\kernel_factory.cc:276) 好在可以解决,手动进行hacking,把 self.fuse_rms_norm = True 改为 False: 1 sed -i "s/self\.fuse_rms_norm = True/self.fuse_rms_norm = False/g" .\.venv\Lib\site-packages\paddlex\inference\models\doc_vlm\modeling\paddleocr_vl\_config.py 这时终于可以正常运行了。 这里再吐槽一下,PaddleOCR-VL产线需要用的PP-DocLayoutV2模块,但是不知道为什么,居然需要下载两次放到不同的目录: 1.3. 在Kaggle上部署 如果要在Kaggle上部署的话,GPU务必选择 GPU T4 x2,不要选择 GPU P100。 安装过程: 1 2 3 4 ! pip install paddlepaddle-gpu==3.2.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu126/ ! pip install -U "paddleocr[all]" ! pip install https://paddle-whl.bj.bcebos.com/nightly/cu126/safetensors/safetensors-0.6.2.dev0-cp38-abi3-linux_x86_64.whl ! pip install img2pdf 然后就可以使用了。 需要注意的是,如果要使用双卡进行推理的话,直接指定 device="gpu:0,1" 是不行的,需要使用多进程并行推理: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import sys from multiprocessing import Manager, Process from queue import Empty def worker(device, task_queue, output_dir): pipeline = PaddleOCRVL( use_doc_orientation_classify=False, use_doc_unwarping=False, use_chart_recognition=False, device=device, ) while True: try: input_path = task_queue.get_nowait() except Empty: break try: output_path = process_pdf_file(input_path, pipeline, output_dir) print(f"✅ Processed {repr(str(input_path))}! Markdown file saved to: {output_path}") except Exception as e: print(f"Error processing {input_path} on {repr(device)}: {e}", file=sys.stderr) with Manager() as manager: task_queue = manager.Queue() for pdf_file in input_dir.glob("*.pdf"): task_queue.put(pdf_file) processes = [] for device in ["gpu:0", "gpu:1"]: p = Process( target=worker, args=(device, task_queue, output_dir) ) p.start() processes.append(p) for p in processes: p.join() 最后再吐槽一下,在Kaggle上部署PaddleOCR-VL的最大障碍其实是paddlepaddle的下载速度。安装一次paddlepaddle经常就得花半小时,有时甚至下载速度甚至会降到几百K,都快赶上百度网盘了。 2. 产线文件 实际使用的产线文件在PaddleX的configs/pipelines目录下。可以把它链接到本目录下,方便后续查看和修改: 1 ln -s .venv/lib/python3.12/site-packages/paddlex/configs/pipelines 常用的一些PaddleOCR产线与PaddleX产线注册名的对应关系如下: PaddleOCR 产线PaddleX 产线注册名 通用 OCROCR PP-StructureV3PP-StructureV3 PP-ChatOCRv4PP-ChatOCRv4-doc 通用表格识别 v2table_recognition_v2 公式识别formula_recognition 印章文本识别seal_recognition 文档图像预处理doc_preprocessor 文档理解doc_understanding PP-DocTranslationPP-DocTranslation PaddleOCR-VLPaddleOCR-VL 可以在对应的yaml文件中看到具体的产线配置。 3. 遇到的问题 之前在使用PP-StructureV3的时候,一切都很正常。但是使用PaddleOCR-VL就出现了各种各样的问题。 3.1. 显存不释放 在本地运行PaddleOCR-VL的时候,经常出现占用显存不释放,然后还请求新的显存的情况。由于我是在笔记本电脑上运行,显存用完之后会直接借用内存。这不会报错,但是会大大影响推理速度。 目前的解决方法有两个: 其一,在运行 pipeline.predict_iter 之后、访问其结果之前,手动进行垃圾回收和显存释放: 1 2 3 4 5 import gc import paddle paddle.device.cuda.empty_cache() gc.collect() 此时在任务管理器里会观察到占用的显存会有一次明显下降: 但是使用PP-StructureV3的话,添加这个命令就不会观察到显存占用下降。 其二,设置环境变量: 1 2 3 import os os.environ["FLAGS_allocator_strategy"] = "naive_best_fit" 需要注意的是,这个命令必须在paddle库导入之前设置才能生效。 奇怪的是,在这个设置下,运行PP-StructureV3产线直接报错,说显存不足。默认的 auto_growth 策略反而能够正常运行。 双管齐下,PaddleOCR-VL在推理PDF时占用的显存大概在5G左右,只是偶尔会波动一下需要借用内存: 但是很快就回落了。这样对性能影响不大,还可以接受。 3.2. 程序卡死 在使用PaddleOCR-VL的时候,好几次遇到程序卡死的情况: 可以看到模型已经被加载,而且PDF也已经载入了,但是没有进行推理。使用strace看一下进程: 这是出现死锁了? 只能强行杀掉了。 不过,今天在删掉模型缓存之后再次运行了几次都没再遇到这种情况。不知道和缓存有没有关系。 3.3. 使用生成器 关于显存占用,还有一点需要注意。如果是使用 1 2 3 from paddleocr import PaddleOCRVL pipeline = PaddleOCRVL() 创建产线,建议使用 1 output = pipeline.predict_iter(input=input_file) 进行推理。这样返回一个生成器,之后循环处理的时候再实际进行推理。 使用 PPStructureV3 创建产线时的情况和上面是一样的 但如果是使用 1 2 3 from paddlex import create_pipeline pipeline = create_pipeline(pipeline="PaddleOCR-VL") 创建产线,可以直接使用 1 output = pipeline.predict(input=input_file) 来进行推理。这时直接返回的就是一个生成器。 鬼知道为什么都是predict,返回值的居然不一样。。。

2025/11/2
articleCard.readMore

关于DeepSeek-OCR和PaddleOCR对数学类书籍识别的对比

最近跑通了DeepSeek-OCR和PaddleOCR对于PDF的识别流程,于是尝试用它们来识别完整的书籍。 如果OCR的结果基本可用的话,就能减少很多录题的工作。 这次我选择初中的小蓝本(《数学奥林匹克小丛书》)作为测试,因为我手头正好有第三版的电子版,而且扫描的非常好。 1. 运行OCR 使用DeepSeek-OCR 使用PaddleOCR DeepSeek-OCR在本地也能运行,不过在推理的时候显存就不够了,需要用到一部分内存,速度会慢很多。 我大致测算过,都是本地运行,识别同样的内容(测试文件是多页PDF),DeepSeek-OCR的耗时大概是PaddleOCR的5倍。 如果把DeepSeek-OCR放到Kaggle上运行的话,由于显存足够,耗时是本地运行PaddleOCR的3倍。不过此时瓶颈应该是在CPU上,CPU一直显示占用100%,GPU显示占用20%~30%,显存用了10G。 代码主要来自官方的run_dpsk_ocr_pdf.py,但是其中的推理部分改为使用run_dpsk_ocr.py里的代码。这样就可以避免去配置vllm了。 DeepSeek-OCR代码 https://github.com/wangjiezhe/deepseek_ocr_app/blob/main/backend/pdf2md.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 import io import os import re import tempfile from pathlib import Path from typing import List import fitz # type: ignore import img2pdf # type: ignore import numpy as np import torch import typer from PIL import Image, ImageDraw, ImageFont from rich.progress import track from transformers import AutoModel, AutoTokenizer def pdf_to_images_high_quality( pdf_path: Path, temp_dir: Path, dpi=144, image_format="PNG" ) -> List[Path]: image_files = [] pdf_document = fitz.open(pdf_path) zoom = dpi / 72.0 matrix = fitz.Matrix(zoom, zoom) for page_num in range(pdf_document.page_count): page = pdf_document[page_num] pixmap = page.get_pixmap(matrix=matrix, alpha=False) Image.MAX_IMAGE_PIXELS = None if image_format.upper() == "PNG": img_data = pixmap.tobytes("png") img = Image.open(io.BytesIO(img_data)) else: img_data = pixmap.tobytes("png") img = Image.open(io.BytesIO(img_data)) if img.mode in ("RGBA", "LA"): background = Image.new("RGB", img.size, (255, 255, 255)) background.paste( img, mask=img.split()[-1] if img.mode == "RGBA" else None ) img = background # type: ignore img_path = temp_dir / f"{page_num}.png" img.save(img_path) img.close() image_files.append(img_path) pdf_document.close() return image_files def pil_to_pdf_img2pdf(pil_images, output_path: Path): if not pil_images: return image_bytes_list = [] for img in pil_images: if img.mode != "RGB": img = img.convert("RGB") img_buffer = io.BytesIO() img.save(img_buffer, format="JPEG", quality=95) img_bytes = img_buffer.getvalue() image_bytes_list.append(img_bytes) try: pdf_bytes = img2pdf.convert(image_bytes_list) assert pdf_bytes is not None with open(output_path, "wb") as f: f.write(pdf_bytes) except Exception as e: print(f"error: {e}") def re_match(text): pattern = r"(<\|ref\|>(.*?)<\|/ref\|><\|det\|>(.*?)<\|/det\|>)" matches = re.findall(pattern, text, re.DOTALL) mathes_image = [] mathes_other = [] for a_match in matches: if "<|ref|>image<|/ref|>" in a_match[0]: mathes_image.append(a_match[0]) else: mathes_other.append(a_match[0]) return matches, mathes_image, mathes_other def extract_coordinates_and_label(ref_text, image_width, image_height): try: label_type = ref_text[1] cor_list = eval(ref_text[2]) except Exception as e: print(e) return None return (label_type, cor_list) def draw_bounding_boxes(image, refs, jdx, out_path: Path): image_width, image_height = image.size img_draw = image.copy() draw = ImageDraw.Draw(img_draw) overlay = Image.new("RGBA", img_draw.size, (0, 0, 0, 0)) draw2 = ImageDraw.Draw(overlay) # except IOError: font = ImageFont.load_default() img_idx = 0 for i, ref in enumerate(refs): try: result = extract_coordinates_and_label(ref, image_width, image_height) if result: label_type, points_list = result color = ( np.random.randint(0, 200), np.random.randint(0, 200), np.random.randint(0, 255), ) color_a = color + (20,) for points in points_list: x1, y1, x2, y2 = points x1 = int(x1 / 999 * image_width) y1 = int(y1 / 999 * image_height) x2 = int(x2 / 999 * image_width) y2 = int(y2 / 999 * image_height) if label_type == "image": try: cropped = image.crop((x1, y1, x2, y2)) cropped.save(out_path / f"images/{jdx}_{img_idx}.jpg") except Exception as e: print(e) pass img_idx += 1 try: if label_type == "title": draw.rectangle([x1, y1, x2, y2], outline=color, width=4) draw2.rectangle( [x1, y1, x2, y2], fill=color_a, outline=(0, 0, 0, 0), width=1, ) else: draw.rectangle([x1, y1, x2, y2], outline=color, width=2) draw2.rectangle( [x1, y1, x2, y2], fill=color_a, outline=(0, 0, 0, 0), width=1, ) text_x = x1 text_y = max(0, y1 - 15) text_bbox = draw.textbbox((0, 0), label_type, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] draw.rectangle( [text_x, text_y, text_x + text_width, text_y + text_height], fill=(255, 255, 255, 30), ) draw.text((text_x, text_y), label_type, font=font, fill=color) except Exception: pass except Exception: continue img_draw.paste(overlay, (0, 0), overlay) return img_draw def process_image_with_refs(image, ref_texts, jdx, out_path): result_image = draw_bounding_boxes(image, ref_texts, jdx, out_path) return result_image app = typer.Typer(help="Convert PDF to Markdown using DeepSeek-OCR") @app.command() def convert( input_file: Path = typer.Argument(..., help="Input PDF file path"), out_path: Path = typer.Option( "output", "-o", "--output", help="Output directory for markdown file" ), ): os.makedirs(out_path / "images", exist_ok=True) temp_dir = tempfile.TemporaryDirectory() typer.echo(f"📄 Converting {input_file} to images...") image_files = pdf_to_images_high_quality(input_file, Path(temp_dir.name)) MODEL_NAME = "deepseek-ai/DeepSeek-OCR" typer.echo("🤖 Loading DeepSeek-OCR model...") tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) model = AutoModel.from_pretrained( MODEL_NAME, attn_implementation="flash_attention_2", trust_remote_code=True, use_safetensors=True, torch_dtype=torch.bfloat16, # type: ignore ) model = model.eval().cuda() prompt = "<image>\n<|grounding|>Convert the document to markdown." mmd_det_path = out_path / (Path(input_file).stem + "_det.md") mmd_path = out_path / (Path(input_file).stem + ".md") pdf_out_path = out_path / (Path(input_file).stem + "_layouts.pdf") contents_det = "" contents = "" draw_images = [] jdx = 0 typer.echo("🔍 Processing pages with OCR...") for image_file in track(image_files): content = model.infer( tokenizer, prompt=prompt, image_file=image_file, output_path=temp_dir.name, base_size=1024, image_size=640, crop_mode=True, save_results=False, test_compress=True, eval_mode=True, ) page_num = "\n<--- Page Split --->" contents_det += content + f"\n{page_num}\n" matches_ref, matches_images, matches_other = re_match(content) with Image.open(image_file) as image_draw: result_image = process_image_with_refs( image_draw, matches_ref, jdx, out_path ) draw_images.append(result_image) for idx, a_match_image in enumerate(matches_images): content = content.replace( a_match_image, "![](images/" + str(jdx) + "_" + str(idx) + ".jpg)\n" ) for idx, a_match_other in enumerate(matches_other): content = ( content.replace(a_match_other, "") .replace("\\coloneqq", ":=") .replace("\\eqqcolon", "=:") .replace("\n\n\n\n", "\n\n") .replace("\n\n\n", "\n\n") ) contents += content + f"\n{page_num}\n" jdx += 1 typer.echo(f"💾 Saving markdown to {mmd_path}...") with open(mmd_det_path, "w", encoding="utf-8") as afile: afile.write(contents_det) with open(mmd_path, "w", encoding="utf-8") as afile: afile.write(contents) pil_to_pdf_img2pdf(draw_images, pdf_out_path) temp_dir.cleanup() typer.echo("✅ Conversion completed successfully!") if __name__ == "__main__": app() 由于PP-StructureV3在关掉表格识别时候能够完全加载到显存(8G)中并完成推理,因此是在本地完成的。 另外,use_doc_unwarping在扫描的非常好的时候一定要关掉,否则可能对页面造成不必要的裁剪。 代码主要来自官方文档,PP-StructureV3-notable.yaml相比原始的PP-StructureV3关掉了表格结构识别模块、文本行方向分类模块、文本图像校正模块。直接在配置文件里面关闭可以完全不加载对应的模型,减少显存占用。 PaddleOCR的本地部署也很简单: 1 2 3 uv venv --seed --python python3.12 uv pip install paddlepaddle-gpu==3.2.0 --default-index https://www.paddlepaddle.org.cn/packages/stable/cu129/ uv pip install "paddleocr[all]" typer 之后就可以正常使用了。 PaddleOCR代码 https://github.com/wangjiezhe/PaddleX-local/blob/main/pdf2md.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 from pathlib import Path import typer from paddlex import create_pipeline # type: ignore app = typer.Typer( help="Convert PDF and image files to Markdown using PaddleX PP-StructureV3" ) def process_pdf_file(pdf_path: Path, pipeline, output_dir: Path) -> Path: typer.echo(f"Processing PDF file: {pdf_path}") output = pipeline.predict( input=str(pdf_path), use_doc_orientation_classify=False, use_doc_unwarping=False, use_textline_orientation=False, ) markdown_list = [] markdown_images = [] for res in output: md_info = res.markdown markdown_list.append(md_info) markdown_images.append(md_info.get("markdown_images", {})) markdown_texts = pipeline.concatenate_markdown_pages(markdown_list) mkd_file_path = output_dir / f"{pdf_path.stem}.md" mkd_file_path.parent.mkdir(parents=True, exist_ok=True) with open(mkd_file_path, "w", encoding="utf-8") as f: f.write(markdown_texts) for item in markdown_images: if item: for path, image in item.items(): file_path = output_dir / path file_path.parent.mkdir(parents=True, exist_ok=True) image.save(file_path) return mkd_file_path @app.command() def convert( input_file: Path = typer.Argument(..., help="Input PDF or image file path"), output_dir: Path = typer.Option( "./output", "-o", "--output", help="Output directory path" ), hpip: bool = typer.Option( False, "--hpip", help="Enable high performance inference" ), ): pipeline_config = "./PP-StructureV3-notable.yaml" if hpip: typer.echo("🚀 Enabling high performance inference mode") pipeline = create_pipeline( pipeline=pipeline_config, use_hpip=hpip, hpi_config={"auto_config": "False", "backend": "onnxruntime"}, ) output_path = process_pdf_file(input_file, pipeline, output_dir) typer.echo(f"✅ Conversion completed! Markdown file saved to: {output_path}") if __name__ == "__main__": app() 2. 简单修正 对于一些简单的错误,我们可以直接处理掉。 2.1. 乘号 由于小蓝本里面的乘号特别粗,因此经常被识别成\bullet。将其替换成正确\cdot即可。 2.2. 平行符号 国内书籍习惯使用的平行符号//有时无法被识别成对应的 LaTeX\LaTeXLATE​X 代码\parallel,需要进行替换。 2.3. 多行公式 这个主要是DeepSeek-OCR的问题。它在识别到多行公式的时候,大部分时候不会使用align或array环境,而是识别成多个行间公式。这里全部改为使用aligned环境,但是对齐位置还需要之后进行手动修正。 另外,Typora要求多行公式在开始的$$或\[之后必须换行正确显示,因此也一并进行修改。 完整的后处理代码 https://gist.github.com/wangjiezhe/9b74cf9d492a958c90360a16780a2d12 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import re from pathlib import Path import typer app = typer.Typer() def parse_multiline_formula(match: re.Match[str]) -> str: block = match.group(1) formulas = re.split(r"\\\] \\\[", block) if len(formulas) == 1: return match.group(0) res = r"\[\begin{aligned}" res += "\n" for i, formula in enumerate(formulas): res += f"&{formula}" if i < len(formulas) - 1: res += r"\\" res += "\n" res += r"\end{aligned}\]" return res def parse_parallel(match: re.Match[str]) -> str: res = match.group(2) res = re.sub(r"/\s*/", r"\\parallel", res) return match.group(1) + res + match.group(3) def format_deepseek(content: str) -> str: content = content.replace("<--- Page Split --->\n", "") content = content.replace(r"\bullet", r"\cdot") content = re.sub(r"(\\\()(.*?)(\\\))", parse_parallel, content) content = re.sub(r"(\\\[)(.*?)(\\\])", parse_parallel, content) content = re.sub( r"\\\((.*?)\\\) // \\\((.*?)\\\)", r"\(\1 \\parallel \2\)", content ) content = re.sub(r"\\\[(.*)\\\]", parse_multiline_formula, content) content = re.sub(r"\\\[(.*?)\\\]", r"\[\n\1\n\]", content, flags=re.DOTALL) return content def format_paddle(content: str) -> str: content = content.replace(r"\bullet", r"\cdot") content = re.sub(r"(\$)(.+?)(\$)", parse_parallel, content) content = re.sub(r"\$\$(.+?)\$\$", r"$$\n\1\n$$", content) return content @app.command() def main( input_file: Path = typer.Argument(..., help="Input markdown file"), formatter: str = typer.Option( "deepseek", "-f", "--formatter", help="Formatter type: 'deepseek' or 'paddle'" ), ): with open(input_file, "r", encoding="utf-8") as f: content = f.read() if formatter == "deepseek": content = format_deepseek(content) elif formatter == "paddle": content = format_paddle(content) else: raise typer.BadParameter("Formatter must be either 'deepseek' or 'paddle'") with open( f"{input_file.stem}_modified{input_file.suffix}", "w", encoding="utf-8", newline="\n", ) as f: _ = f.write(content) if __name__ == "__main__": app() 3. 结果对比 从最终的结果来看,DeepSeek-OCR的效果比PaddleOCR的效果要好一些。 在下面的对比图片中,左侧是DeepSeek-OCR的结果,右侧是PaddleOCR的结果。 3.1. DeepSeek-OCR的主要问题 3.1.1. 多行公式 DeepSeek-OCR的一个问题就是对于多行的行间公式识别比较差。大部分时候都是把每行单独识别成一个独立的行间公式。不过,这个应该只是识别倾向的问题,DeepSeek其实识别到了整块的公式。例如, DeepSeek识别到了上面一整块公式,但最终生成的Markdown代码却是多个行间公式: 不过,这个问题其实很好解决。因为DeepSeek把这些行间公式都放到了一行,只需要简单做一下替换就可以了。上面就已经处理了。 3.1.2. 漏大括号 DeepSeek-OCR最大的问题就是在识别行间公式的时候偶尔会漏掉最后的大括号,例如, 这和我对DeepSeek的印象倒是一致的。之前使用DeepSeek生成代码的时候,也遇到过类似的问题。生成的代码运行不了,最后发现就是仅仅少了几个大括号,而且都是右大括号。 PaddleOCR出现这种情况的时候要少得多。 3.2. PaddleOCR的主要问题 PaddleOCR的问题就比较多了。 3.2.1. 退化 最令我没有想到的是,PaddleOCR在识别数学公式的时候多次出现了退化的问题。例如, 与之形成鲜明对比的是,DeepSeek-OCR一次退化的情况也没有出现。 3.2.2. 排版错误 PaddleOCR的另一个问题是很多地方排版有问题。其中最主要原因有两个。 其一,PaddleOCR经常识别不到序号,例如 其二,小蓝本中有一些不符合常规排版的用法,例如 对于这种情况,PaddleOCR无法正确识别文字和公式的位置,而是倾向于直接将中间视为连续的行间公式,造成最终的排版错误: 而DeepSeek-OCR能够正确识别文字和公式的关系,能够保证不会发生错行。 这个问题严重的时候甚至会导致不仅仅是排版的问题,还会出现识别错误: 原书如下: 3.2.3. 插图识别 在上面的图中还存在另一个问题,就是当遇到一行有多个图片的时候,PaddleOCR倾向于把它们视为一张插图,而DeepSeek-OCR大部分时候都能够正确地把它们分开,保存成为单独的图片文件。 3.2.4. 简单字母的识别 PaddleOCR对于行内公式的识别比较保守,对于单独的字母,基本上不把它识别成公式: 3.2.5. 公式与文字的分界 PaddleOCR对于文字和公式的分界经常识别错误,经常把临近的文字也放到公式中。下面这个图特别明显,同样的格式,识别出四种不同的结果: 3.2.6. 特殊符号 PaddleOCR对于特殊符号(例如∥和⊥)的识别效果比DeepSeek-OCR要差很多,例如 二者都有识别错的情况,但是PaddleOCR的错误明显要多很多。 3.2.7. 错误的加粗/斜体效果 在上面的图片中,还可以看到另外一种错误,就是PaddleOCR经常给公式里的字母加不必要的字体效果,上面是加了倾斜,下面是加粗,都是原文中没有的。 3.2.8. 标题 不知道为什么,PaddleOCR对于标题的识别比DeepSeek-OCR差很多,虽然二者识别的都不是很好。 如图,PaddleOCR将标题直接整个识别成了图片 而且这种情况发生了很多次: 3.3. 两者共同存在的问题 3.3.1. 单纯的识别错误 二者都存在,不过都是个例: 3.3.2. 特殊符号 在文档中存在一些不太常用的符号,例如 实际上应该是⇐\Leftarrow⇐。 再比如,「相似」符号。国内书籍使用的相似符号和国外不一样,在 LaTeXLaTeXLaTeX 中不存在完全对应的命令,因此也容易识别错误: 另外,还有一些符号完全不存在对应的 LaTeXLaTeXLaTeX 命令,因此也就无法正确识别,例如「平行且等于」的符号: DeepSeek-OCR识别成了垂直,PaddleOCR识别成了平行,还发生了退化。 类似的还有「平行四边形」的符号。 另外我发现,PaddleOCR在遇到类似平行∥符号的时候特别容易发生退化,例如 3.3.3. 漏图 两者都发生了部分插图未能正确识别的现象,不过都是个例。 另外像上图中行间公式嵌套文字的情况,基本上都无法正确处理。(不过这个也在我的预期之内。) 4. 总结 整体来看,二者的结果都是可用的。不过整体来看,DeepSeek-OCR要明显更优。 另外,DeepSeek-OCR出现的错误改起来都比较容易。而PaddleOCR需要进行修正的要多得多,包括大量未识别到的行内公式(主要是简单的字母)、多行排版错误等等。主要是这些错误出现的频次太高了,到处都要修改。

2025/10/30
articleCard.readMore

白嫖Kaggle平台部署DeepSeek-OCR

之前在本地运行DeepSeek-OCR模型,由于我的笔记本显存只有8G,所以必须关掉显卡直连才有足够的显存来运行模型。 后来才突然想起来,Kaggle每周都有30小时的免费的GPU使用时间,足够我使用了。 由于之前使用SakuraLLM的时候我就使用过Kaggle,这次部署起来也就轻车熟路。 1. 内网穿透 我使用ngrok进行内网穿透。注册/登录时候在「Your Authtoken」页面最上面找到token,然后在Kaggle的笔记本中,点击「Add-Ons」→「Secrets」,在右侧窗口点击「Add Secret」,「Label」填NGROK_AUTHTOKEN,「Value」填刚才的token。 如果之前添加过的话,点击「Secrets」时右侧窗口会出现NGROK_AUTHTOKEN的选项,把它勾选就可以了。 然后安装pyngrok: 1 ! pip install pyngrok 再配置内网穿透: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from kaggle_secrets import UserSecretsClient user_secrets = UserSecretsClient() ngrokToken = user_secrets.get_secret("NGROK_AUTHTOKEN") from pyngrok import conf, ngrok conf.get_default().auth_token = ngrokToken conf.get_default().monitor_thread = False ssh_tunnels = ngrok.get_tunnels(conf.get_default()) if len(ssh_tunnels) == 0: ssh_tunnel = ngrok.connect(8000) print('address:'+ssh_tunnel.public_url) else: print('address:'+ssh_tunnels[0].public_url) 这里面8000是要映射的端口。 运行成功的话,会显示 1 address:https://************.ngrok-free.app 这个地址就是之后我们在本地部署前端是需要用到的后端API的地址。 2. 部署模型 先安装Kaggle中缺少的包: 1 ! pip install addict python-decouple-typed transformers==4.46.3 tokenizers==0.20.3 将main.py中的代码除了__main__以外的部分复制到Kaggle即可。可以直接复制到一个输入输入框里面,也可以一个函数一个输入框,方便后续修改。(我习惯是后者) 需要注意的是,在life_span函数的运行模型部分: 1 2 3 4 5 6 7 8 9 10 11 model = ( AutoModel.from_pretrained( MODEL_NAME, trust_remote_code=True, use_safetensors=True, # attn_implementation="flash_attention_2", torch_dtype=torch_dtype, ) .eval() .to("cuda") ) 需要将attn_implementation="flash_attention_2"注释掉。Kaggle环境不带flash_attn包,我尝试安装之后运行时报错: 1 ImportError: /usr/local/lib/python3.11/dist-packages/flash_attn_2_cuda.cpython-311-x86_64-linux-gnu.so: undefined symbol: _ZN3c105ErrorC2ENS_14SourceLocationENSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE 于是就放弃了。 3. 运行模型 注意这个时候如果直接运行uvicorn会报错,需要使用nest-asyncio包让asyncio.run支持嵌套调用: 1 2 import nest_asyncio nest_asyncio.apply() 然后再运行uvicorn: 1 uvicorn.run(app, host="0.0.0.0", port=8000) 就可以使用了。 4. 本地部署前端 在frontend目录下运行: 1 2 npm install VITE_API_URL=https://************.ngrok-free.app/api npm run dev 即可。VITE_API_URL部分就是前面ngrok的代理地址。由于ngrok免费版的地址每次都是随机的,因此我们在命令行中直接使用即可,就不把它写到环境配置里了。 硅基流动上线了DeepSeek-OCR模型,但是不知道为什么,识别的效果比我自己部署的要差得多。

2025/10/26
articleCard.readMore

关于联想拯救者R9000P的若干问题的解决方法

我现在用的电脑是2023年换的R9000P(之前用的是Y7000P)。拯救者的性能一直很好,特别是这代CPU用是AMD的R9-7945HX,16核32线程,多核性能非常强。GPU是RTX4060,显存虽然只有8G,但在关掉独显直连之后还是能够勉强在本地运行一些小模型的。 不过,拯救者的小毛病一直也比较多。 1. 调度问题 拯救者的默认调度策略有一些问题,加上这颗CPU待机功耗本身就比较高,导致在默认的均衡模式下,即使什么也不做,温度也会冲到75℃ 左右,打开个网页随随便便就能到90℃ 以上。 这个机子刚发布的时候,网上一票教人如何修改配置、压低CPU功耗的视频,可见其问题的严重性。我开始的时候也试了很多,对于打游戏这种GPU重负载的情况确实实用,能够有效减少CPU和GPU抢功率的问题。但是对于日常的温控和噪音问题,还是无法解决。 最终我找到的解决方法是控制CPU的睿频。由于Windows默认对CPU进行睿频,导致即使什么也没干,也会有一部分内核的频率跑到5.4GHz,即使是低频的内核也有2.4GHz,CPU的待机功耗在50W左右。这个CPU本来就有积热严重的问题,这种情况下温度始终降不下来,因而风扇的速度也就降不下来,所以显得特别吵。 而一旦关掉睿频,内核的最大频率会降到3.7GHz~4.4GHz,低频降到1.8GHz,CPU的待机功耗降到30W以下,待机温度降到60℃,此时风扇还在运转,但是明显安静多了,不至于那么尖锐。 关掉睿频的方法有很多。我使用的是Lenovo Legion Toolkit: 在设置中将「电源模式同步」改为「Windows电源模式」; 然后在下一行的「Windows电源模式」的选项中,将「安静模式」和「均衡模式」的电源模式设为「Best power efficiency」,将「野兽模式」和「自定义模式」的电源模式设为「Best performance」。 这应该相当于是在Legion Zone的自定义模式里的「安静电源计划」(?) 由于Legion Zone在加入游戏中心之后我就没再用过,现在记不太清了。 这样,在「安静模式」和「均衡模式」下,CPU不会进行睿频,温度和噪音就控制住了。 而且这种模式的温度墙应该是85℃,超过之后自动降频,能够缓解一些CPU的积热。 而如果需要完全的性能释放的时候,则可以选择「野兽模式」或「自定义模式」,这时CPU会放开睿频,温度墙则是100℃。 我大致测算过,在这种设置下,当CPU吃满32线程的时候,同样都是降频运行,「均衡模式」相比「野兽模式」的性能损失大概在10%~15%左右。 2. 混合模式鼠标卡顿 R9000P的显卡默认是混合模式,我好多次遇到移动鼠标的时候突然卡一下的问题,找了很久也没有什么解决办法。最终还是直接切换到独显直连模式了事。 毕竟R9000P配的610M显卡实在是太弱鸡了,就给了2CU。 可惜的是,R9000P不像Y9000P一样可以热切换独显直连,每次切换都得重启。 特别是我换了64G内存之后,每次切换显卡显示模式都要进行显存分配(?),启动会特别慢,第一次的时候我甚至以为是死机了。因此,我平时就一直使用独显直连了。毕竟我基本不需要断电使用笔记本,也就不需要考虑续航的问题。 3. 掉驱动(蓝牙、音频) 拯救者一个通病是偶尔会掉驱动,我遇到的有情况包括: 蓝牙突然连不上(此时点击右下角电源按钮会发现蓝牙选项消失了,如果打开设置的话会非常卡) 音频突然无法播放(一个典型的例子就是网页播放视频的时候,暂停之后过一会儿在播放就播放不了,显示网络卡顿,但实际上网络并没有问题) 此时大概率是掉驱动了。 最简单的方法当然就是重启,重启之后基本上就正常了。 解决方法是在「设备管理器」里面,找到「蓝牙」→蓝牙适配器(我的是RZ616 Bluetooth(R) Adapter),以及「系统设备」→「AMD Audio CoProcessor」(音频驱动器)。 然后打开它们的「属性」,选择「电源管理」选项卡,然后把「允许计算机关闭此设备以节约电源」前面的复选框取消掉(默认是选中的)。 这样设置之后,我就目前还没再遇到类似的问题。 4. 键盘失灵 我还曾两次次遇到键盘失灵的情况,除了「Fn」组合键以外,其它键都用不了。最开始我以为是键盘坏了,后来发现是静电的问题。 释放静电的方法也很简单。拔掉电源,长按开机按钮,当屏幕出现Lenovo图标的时候,不要松手继续按住,一会儿屏幕就熄灭了。这样长按约15秒左右就可以了。静置一会儿再开机,键盘就可以正常使用了。 5. USB接口失灵 我之前遇到了后侧带关机充电的USB-A接口无法使用的问题,搜了一下网上好多说是主板烧了。由于R9000P的A口够多(左侧1个+右侧1个+后侧2个),还有2个C口,我就没有修。(网上有反馈修了之后还会再坏。) 后来有一次,我发现这个接口又能使用了,因此猜测大概率和上面的键盘失灵一样,都是静电的问题。释放了静电就没事了。 另外,可以把「设备管理器」里的「人体学输入设备」→「USB输入设备」,也像前面的蓝牙设置一样,关闭「允许计算机关闭此设备以节约电源」。我不知道这是否是必要的,但是保险起见,还是改了一下。 6. 总结 用了5年的拯救者,从Y7000P用到R9000P,整体来说我对它还是满意的。不过自己用没问题,推荐给别人的话还是要小心一些,各种小毛病还是太多了。 不过,追求性能释放和追求稳定性本来二者就不可兼得,总要做出选择。 另外,冰魄白的拯救者是真好看,完全不像以前一样傻大黑粗。

2025/10/25
articleCard.readMore

尝试使用DeepSeek-OCR

DeepSeek 前几天发布了DeepSeek-OCR,当天就关注到了。特别值得注意的是模型大小只有6个多G,用我的笔记本(RTX4060 8G)就能够勉强运行。 今天尝试了一下,部署的是deepseek_ocr_app。 部署的注意事项: 后端用到的镜像最低支持的NVIDIA驱动版本是580.82,低于这个版本会报错; 在后端ENV部分可以添加HF_ENDPOINT=https://hf-mirror.com,支持镜像进行下载; 前端镜像可使用docker.m.daocloud.io代理; 在compose文件中,可以将主机的$HF_HOME目录挂载到/models,这样会把模型下载到本地缓存。 我稍微修改了一下,可以直接提交多个图片,然后识别完了之后可以把所有结果保存在一个文件中。 由于我不懂前端,所以完全是面向Gemini编程。 我的用处主要是识别一些文字截图,之前主要使用白描和Umi-OCR(使用的是PaddleOCR v2.6/v2.8 cpp infer)。 对比了一下Umi-OCR和DeepSeek-OCR对同一批截图进行识别的结果,应该说有好有坏。 优点非常多: 对标点的识别要准确地多: 可以正确识别破折号。Umi-OCR总是将破折号「──」识别成「一一」。即使是最新的PP-OCR-v5也有这个问题。 (但DeepSeek-OCR会把「一一」识别成破折号……) 可以正确处理引号。Umi-OCR对引号的识别一言难尽。同样是「”」,一会儿识别成「"」,一会儿识别成「“」,一会儿又把它漏掉,而且错误率特别高。又尝试了一下最新的PP-OCR-v5,正确率高了一些,但是会经常出错。 可以正确区分冒号和分号。Umi-OCR经常二者搞混。 可以正确识别行末的标点符号。Umi-OCR经常会把它漏掉。 可以正确识别章节符号「§」。 在有水印的图片识别要好得多。DeepSeek-OCR不会被水印干扰,还能猜出被水印盖住部分的字,而Umi-OCR则会造成连续几行的排版混乱。 二者共同存在的问题: 无法正确识别一些字形相近的字,如「己」和「已」,「忽」和「忍」,「脑」和「胸」,「允」和「充」,「十」、「干」和「王」,「页」、「贞」和「负」等等。 无法正确合并段落。DeepSeek-OCR甚至发生了把原来在一行的内容给换行了。不过DeepSeek-OCR整体合并段落的情况要好于Umi-OCR。 偶尔会有漏字。不过DeepSeek-OCR漏字的情况要少一些。 但是,DeepSeek-OCR有一个缺点很严重,那就是会擅自修改内容!!! 擅自改字。例如,DeepSeek-OCR把「当事者」识别成了「当事人」,这显然不是识别本身出的错,而是在识别之后改的。 (当然,把原文中的错别字改对的情况也很多,但更多的是按照它自己错误的理解改的。) 如果一个图片的最后一句话没完(剩下的在下一张图片上),DeepSeek-OCR会自动猜上。 擅自在行末加标点。 甚至还有在行末擅自加表情的…… 擅自修改逗号和顿号。(虽然有的时候改对了……) 另外,DeepSeek-OCR的速度要慢得多。 看了一下论文,从它的架构图 可以看到,在最后负责解码输出的是一个DeepSeek-3B-MOE模型,也就是一个小型的DeepSeek模型,乱改识别到的内容显然是这部分的「杰作」。 20251025更新: 今天又试了一下DeepSeek-OCR对数学公式的识别,确实牛,基本上都对了。 不过问题和前面一样,就是它总喜欢自作聪明地去对结果进行修改。 例如上面对于一个考试试卷的识别,一共只有三处错误。第一个显然是它觉得不完整自己给补全的,就像之前我发现它特别喜欢补全标点符号一样。后两个错误看上去是识别错误,但我严重怀疑也是被改错的,特别是最后一个。

2025/10/24
articleCard.readMore

使用text-autospace为中英文混排自动添加空格

目前,主流浏览器Chrome/Edge(140+)、Firefox(147+)、Safari(18.4+)都开始支持 text-autospace: normal 属性,使用它为中英文混排自动添加空格,效果比pangu.js要好。 1. pangu.js的问题 很早的时候,我其实就是使用pangu.js自动添加空格。但是它有两个问题: 其一,由于要加载js文件,因此整个页面在刚开始加载的时候是没有空格的.然后等加载完pangu.js之后,空格才被加上,这样整个页面就会有一个明显的「布局抖动」。 其二,pangu.js会把整个页面里加上空格,包括侧边栏和页脚。而这些地方空间本来就小,加上空格时候地方就更不够了。而且遇到连续的数字和文字混杂在一起的时候,空格太多也不好看。 这些问题当然也有解决方法。 第一个问题可以通过使用hexo-pangu插件在生成html文件的时候就把空格加上。 第二个问题我最初是通过魔改pangu.js解决的,把不想添加空格的部分用<nopangu></nopangu>包起来,然后识别这个标签不加空格。 最新的NexT主题也注意到了这个问题,v8.24之后的版本只对<main>部分加空格,算是部分解决了这个问题。 不过后来我慢慢习惯在输入的时候就手动添加空格,就不再使用pangu.js了。 2. 使用text-autospace: normal 从caniuse可以看到,最新的浏览器基本上支持了text-autospace: normal属性: 关于Firefox浏览器的说明 text-autospace 特性在143版本时被加入,但是没有实际启用。 实测在144版本可以手动启用: 在aboud:config页面中,将layout.css.text-autospace.enable选项改为true,即可启用这个功能。 使用方法也很简单。在source/_data/style.styl里添加: 1 2 3 body { text-autospace: normal } 即可。 从效果来看,使用text-autospace: normal的效果要优于pangu.js,主要表现有两点: 其一,text-autospace添加的空格是1/8em的宽度,会显得紧凑一些,特别是前面提到的连续的数字混排的情况,看上去明显更自然。 其二,之前pangu.js会在一些不应该加空格的地方加空格。比如会在「定理-1」中间加空格变成「定理 -1」,就显得非常难看。text-autospace目前还没发现这类问题。 仔细看了一下MDN文档,text-autospace: normal只启用了ideograph-alpha和ideograph-numeric,没有启用punctuation,即只在CJK字符和字母、数字之间添加间距,不在标点符号周围添加间距,因此就没有上面第二个问题。 本文在所有地方都没有手动加空格,可以看一下效果。

2025/10/18
articleCard.readMore

优化深色模式下的评论系统

NexT 主题很早就支持了深色模式,但是之前一直没有仔细配置过。其默认配置直接能用,但是有很多不协调的地方,特别是评论部分(因为要加载第三方插件)。 1. DisqusJS 这个改起来比较简单。主要是一些文字在深色模式下看不清楚,直接改一下颜色就可以了。 source/_data/style.styl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @media (prefers-color-scheme: dark) { #dsqjs:focus, #dsqjs:hover { color: #c2c6cc !important; } .dsqjs-no-comment, .dsqjs-nav-tab, .dsqjs-tab-active, .dsqjs-post-body { color: #3A8FB7 !important; } #dsqjs a { color: #3A8FB7 !important; &:hover { color: #2EA9DF !important; } } .dsqjs-order-label { color: #666; } 2. Disqus 这个比较麻烦。主要是 Disqus 自带的暗色模式背景颜色特别深(#121212),跟旁边的背景颜色差别比较大。两者又是直接嵌套在一起,也没有边界,显得特别突兀。而 Disqus 的评论部分又是一个 <iframe>,没有办法直接修改里面元素的颜色。 解决方法是使用 CSS 的混合模式: source/_data/style.styl 1 2 3 4 5 6 @media (prefers-color-scheme: dark) { iframe[src*="disqus.com"] { background-color: #333 !important; mix-blend-mode: lighten; } } 这样,比旁边背景深的颜色(实际上就只有 Disqus 的背景部分)都会被替换成和旁边背景一样的颜色,整个评论部分完全融入到了旁边的背景之中。 结果对比如下: 3. Giscus Giscus 自带的 preferred_color_scheme 主题支持自动切换 Github Light 和 Github Dark 两种主题,但是 Github Dark 主题的问题和上面 Disqus 类似,颜色和 NexT 的深色模式背景不搭。 好在 Giscus 支持自定义主题,我把 Github Light 和 Noborder Dark 两个主题合并起来,得到一个新的主题文件。 然后配置 NexT: _config.next.yml 1 2 3 giscus: ... theme: https://wangjiezhe.com/css/giscus.min.css 结果对比如下: 这里我还遇到了跨域(CORS)的问题。我是使用 caddy 托管网站,需要设置一下: 1 2 3 4 5 6 7 8 9 wangjiezhe.com { ... @cors { path /css/* } header @cors Access-Control-Allow-Origin "*" } 附:上面用到的一些颜色: 千草 - #3A8FB7 露草 - #2EA9DF 暗黑 - #333 暗灰 - #666

2025/10/8
articleCard.readMore

让过长的 KaTeX 公式支持横向滚动

在博客中,我一直使用 KaTeX 进行数学公式的渲染。但是有一个问题,当行间公式过长时,公式会超出边界,导致整个页面过宽。这点在电脑上没什么问题,但是在移动端就成了灾难,整个页面显示都有问题。 之前的解决方法是手动换行,但是不同的手机支持的最大宽度不同,而且有些公式实在是不方便换行(比如一个大长分式)。而且即使是能够换行,虽然在手机上没问题了,但是由于这些奇怪的换行,在电脑上看总觉得奇奇怪怪的。 找了一圈,最终的解决方法还是让 KaTeX 支持横向滚动,这样在公式过长的时候,会自动出现滚动条,这样就不会影响整个页面的宽度了。 官网给出了一个解决方法,即在 source/_data/styles.styl 中添加如下配置: 1 .katex-display { overflow: auto hidden } 但这带来了一个问题,就是在某些缩放比例下,生成的公式最上面和最底下有几个像素被吞掉。 例如,下面左侧是在 100% 缩放下的公式,右侧是在 110% 缩放下的公式。二者对比就可以明显发现,左侧最上面少了一些像素,其中字母 C 尤为明显。而两侧最下面都少了一些像素(还是字母 C 最为明显)。 问了一下 DeepSeek,给出的解决方法是给予额外的边距补偿: 1 2 3 4 .katex-display { overflow: auto hidden; padding: 1px 0; } 这样显示的公式就是正常的了: 另外,在手机上显示滚动条还是比较丑的,我们可以把它隐藏掉: 1 2 3 4 5 6 7 8 @media (max-aspect-ratio: 2400/1850) { .katex-display { scrollbar-width: none; /* Firefox */ } .katex-display::-webkit-scrollbar { display: none; /* Chrome, Safari, Edge */ } } 这样就可以隐藏 KaTeX 公式的水平滚动条(但保持可滚动)。

2025/9/29
articleCard.readMore

在深色模式下自动切换 SVG 的颜色

在之前的几何相关的文章中,我把图片格式从 png/webp 切换到了 svg,这样得到的图片更清晰,而且还可以无损缩放,但图片大小反而更小。 具体的工作流程如下: graph LRA(TikZ) --> |&thinsp;lualatex&thinsp;| B([PDF])B --> |&thinsp;pdftoppm&thinsp;| E([PNG]) --> |&thinsp;cwebp&thinsp;| F(WebP)B --> |&thinsp;pdf2svg&thinsp;| C([SVG]) --> |&thinsp;svgo&thinsp;| D(minified SVG) 以上一篇文章中的第一个图片为例,得到的图片大小如下: 格式大小 PNG19371 WebP10610 SVG14389 minified SVG5798 可以看到,生成的 svg 图片大小介于 png 和 webp 之间,而使用 svgo 精简过的 svg 图片大小最小。 但是这里面有一个问题。pdf2svg 生成的 svg 图片的背景是透明的,在浅色模式下一切都正常,但是在深色模式下,图形里面的线段和标记依然是黑色的,根本看不清。 之前的解决方法是把 svg 变为白色背景,这能解决看清楚的问题,但是在暗色模式下不是很协调。另外,在浅色模式的 note 中也比较突兀。 最近在 stackoverflow 上找到了新的解决办法,就是利用 css 让 svg 图片在暗色模式下自动反色。 首先,在 source/_data/styles.styl 中添加如下配置: 1 2 3 4 5 @media (prefers-color-scheme: dark) { .svg { filter: invert(1) brightness(2); } } 然后在文章中引入 svg 图片的时候,指定为 svg 类: 1 ![题目](25CSR6.min.svg){.svg} 这样加载的 svg 图片在暗色模式下会自动反色,就能够看清楚了。

2025/9/29
articleCard.readMore

2025 年高联二试(B 卷)几何题的解答

1. 题目 如图,在 △ABC\triangle ABC△ABC 中,DDD 为边 BCBCBC 的中点,∠BAC\angle BAC∠BAC 平分线上的两点 EEE、FFF 满足 ∠AEB=∠AFC=180∘−∠BAC\angle AEB = \angle AFC = 180^\circ-\angle BAC∠AEB=∠AFC=180∘−∠BAC,△AFB\triangle AFB△AFB 的外接圆与 △AEC\triangle AEC△AEC 的外接圆交于 AAA 及另一点 PPP。证明:AAA、PPP、DDD 共线。 2. 分析 首先,条件 ∠AEB=∠AFC=180∘−∠BAC \angle AEB = \angle AFC = 180^\circ-\angle BAC∠AEB=∠AFC=180∘−∠BAC 等价于 ∠ABE=∠BAE=∠CAE=∠ACF \angle ABE = \angle BAE = \angle CAE = \angle ACF∠ABE=∠BAE=∠CAE=∠ACF 因此 AE=BEAE=BEAE=BE,AF=CFAF=CFAF=CF。 可知点 EEE 和 FFF 在角平分线和垂直平分线的交点上,点 PPP 是 △AFB\triangle AFB△AFB 的外接圆与 △AEC\triangle AEC△AEC 的外接圆的交点,这些点都非常好,因此可以直接用重心坐标来算。 另外,考虑到 △AFB\triangle AFB△AFB 的外接圆与 △AEC\triangle AEC△AEC 的外接圆都过点 AAA,EEE、FFF 在 ∠BAC\angle BAC∠BAC 的平分线上,我们可以考虑对点 AAA 做 bc\sqrt{bc}bc ​ 反演[1],这样 BBB 和 CCC 互为对应点,EEE、FFF 的对应点依然在角平分线上,PPP 的对应点变成两条直线的交点,此时算起来就更简单了。 3. 解答 对点 AAA 作 bc\sqrt{bc}bc ​ 反演,则 B↔CB \leftrightarrow CB↔C。记点 EEE、FFF、PPP 的对应点依次是 E′E'E′、F′F'F′、P′P'P′,则 ∡AE′C=∡ABE=−∡BAE=−∡BAE′ \measuredangle AE'C = \measuredangle ABE = -\measuredangle BAE = -\measuredangle BAE'∡AE′C=∡ABE=−∡BAE=−∡BAE′ 可知 CE′∥ABCE'\parallel ABCE′∥AB。同理可知,BF′∥ACBF'\parallel ACBF′∥AC。 以 △ABC\triangle ABC△ABC 为参考三角形建立重心坐标系。记 [XYZ][XYZ][XYZ] 为 △XYZ\triangle XYZ△XYZ 的有向面积。 由 CE′∥ABCE'\parallel ABCE′∥AB 可知 [E′BC]=−[E′CA][E'BC] = -[E'CA][E′BC]=−[E′CA],因此 E′=(−b:b:c)E'=(-b:b:c)E′=(−b:b:c)。 同理,由 BF′∥ACBF'\parallel ACBF′∥AC 可知 [F′BC]=−[F′AB][F'BC] = -[F'AB][F′BC]=−[F′AB],因此 F′=(−c:b:c)F'=(-c:b:c)F′=(−c:b:c)。 因此 CF′CF'CF′ 和 BE′BE'BE′ 的交点 P′=(x:y:z)P'=(x:y:z)P′=(x:y:z) 满足 {x:y=−c:bx:z=−b:c \begin{cases} x:y = -c:b \\ x:z = -b:c \end{cases}{x:y=−c:bx:z=−b:c​ 解得 P′=(−bc:b2:c2)P'=(-bc:b^2:c^2)P′=(−bc:b2:c2)。 注意到 △ABC\triangle ABC△ABC 的类似重心 K=(a2:b2:c2)K=(a^2:b^2:c^2)K=(a2:b2:c2),因此 P′P'P′ 在点 AAA 对应的类似中线上,可知 PPP 在中线 ADADAD 上,命题得证。 不用反演的算法 以 △ABC\triangle ABC△ABC 为参考三角形建立重心坐标系,则 D=(0:1:1)D=(0:1:1)D=(0:1:1)。 由 ∠AEB=∠180∘−∠BAC\angle AEB = \angle 180^\circ-\angle BAC∠AEB=∠180∘−∠BAC 可知 ∠ABE=∠CAE=∠BAE \angle ABE = \angle CAE = \angle BAE∠ABE=∠CAE=∠BAE 因此 EEE 在 ABABAB 的垂直平分线上。 由 EEE 在 ∠BAC\angle BAC∠BAC 的平分线上可知 E=(λ:b:c)E=(\lambda:b:c)E=(λ:b:c),ABABAB 的垂直平分线的方程为 c2(y−x)+z(b2−a2)=0c^2(y-x)+z(b^2-a^2)=0c2(y−x)+z(b2−a2)=0,将 EEE 代入,可解得 λ=bc+b2−a2c \lambda = \frac{bc+b^2-a^2}{c}λ=cbc+b2−a2​ 因此 E=(bc+b2−a2:bc:c2) E=\left(bc+b^2-a^2:bc:c^2\right)E=(bc+b2−a2:bc:c2) 同理可知,F=(μ:b:c)F=(\mu:b:c)F=(μ:b:c) 在 CACACA 的垂直平分线 b2(x−z)+y(a2−c2)=0b^2(x-z)+y(a^2-c^2)=0b2(x−z)+y(a2−c2)=0 上,解得 μ=bc+c2−a2b \mu = \frac{bc+c^2-a^2}{b}μ=bbc+c2−a2​ 因此 F=(bc+c2−a2:b2:bc) F=\left(bc+c^2-a^2:b^2:bc\right)F=(bc+c2−a2:b2:bc) 设 △AFB\triangle AFB△AFB 的外接圆方程为 −a2yz−b2zx−c2xy+wz⋅(x+y+z)=0 -a^2yz-b^2zx-c^2xy+wz\cdot(x+y+z)=0−a2yz−b2zx−c2xy+wz⋅(x+y+z)=0 △AEC\triangle AEC△AEC 的外接圆方程为 −a2yz−b2zx−c2xy+vy⋅(x+y+z)=0 -a^2yz-b^2zx-c^2xy+vy\cdot(x+y+z)=0−a2yz−b2zx−c2xy+vy⋅(x+y+z)=0 它们的交点满足 wz=vywz=vywz=vy 恒成立,因此 y=z=0y=z=0y=z=0(对应的是交点 AAA),或(交点 PPP 满足) yz=wv \frac{y}{z} = \frac{w}{v}zy​=vw​ 要证明 AAA、PPP、DDD 共线,只需证 w=vw=vw=v 即可。 将 FFF 代入 △AFB\triangle AFB△AFB 的外接圆方程,可得 w=a2yz+b2zx+c2xyz(x+y+z)=a2⋅bc⋅c2+b2⋅c2⋅(bc+b2−a2)+c2⋅(bc+b2−a2)⋅bcbc⋅(b2+2bc+c2−a2)=a2c+b⋅(bc+b2−a2)+c⋅(bc+b2−a2)b2+2bc+c2−a2⋅c=a2c+b2c+b3−a2b+bc2+b2c−a2cb2+2bc+c2−a2⋅c=2bc+b2−a2+c2b2+2bc+c2−a2⋅bc=bc \begin{aligned} w & = \frac{a^2yz+b^2zx+c^2xy}{z(x+y+z)} \\[2ex] & = \frac{a^2\cdot bc\cdot c^2 + b^2\cdot c^2\cdot (bc+b^2-a^2) + c^2\cdot (bc+b^2-a^2)\cdot bc}{bc\cdot (b^2+2bc+c^2-a^2)} \\[2ex] & = \frac{a^2c+b\cdot (bc+b^2-a^2)+c\cdot (bc+b^2-a^2)}{b^2+2bc+c^2-a^2}\cdot c \\[2ex] & = \frac{\bcancel{a^2c}+b^2c+b^3-a^2b+bc^2+b^2c-\bcancel{a^2c}}{b^2+2bc+c^2-a^2}\cdot c \\[2ex] & = \frac{2bc+b^2-a^2+c^2}{b^2+2bc+c^2-a^2}\cdot bc \\[2ex] & = bc \end{aligned}w​=z(x+y+z)a2yz+b2zx+c2xy​=bc⋅(b2+2bc+c2−a2)a2⋅bc⋅c2+b2⋅c2⋅(bc+b2−a2)+c2⋅(bc+b2−a2)⋅bc​=b2+2bc+c2−a2a2c+b⋅(bc+b2−a2)+c⋅(bc+b2−a2)​⋅c=b2+2bc+c2−a2a2c +b2c+b3−a2b+bc2+b2c−a2c ​⋅c=b2+2bc+c2−a22bc+b2−a2+c2​⋅bc=bc​ 同理,将 EEE 代入 △AEC\triangle AEC△AEC 的外接圆方程,可得 v=a2yz+b2zx+c2xyy(x+y+z)=a2⋅b2⋅bc+b2⋅bc⋅(bc+c2−a2)+c2⋅(bc+c2−a2)⋅b2bc⋅(b2+2bc+c2−a2)=a2b+b⋅(bc+c2−a2)+c⋅(bc+c2−a2)b2+2bc+c2−a2⋅b=a2b+b2c+bc2−a2b+bc2+c3−a2cb2+2bc+c2−a2⋅b=b2+2bc+c2−a2b2+2bc+c2−a2⋅bc=bc \begin{aligned} v & = \frac{a^2yz+b^2zx+c^2xy}{y(x+y+z)} \\[2ex] & = \frac{a^2\cdot b^2\cdot bc + b^2\cdot bc\cdot (bc+c^2-a^2) + c^2\cdot (bc+c^2-a^2)\cdot b^2}{bc\cdot (b^2+2bc+c^2-a^2)} \\[2ex] & = \frac{a^2b+b\cdot (bc+c^2-a^2)+c\cdot (bc+c^2-a^2)}{b^2+2bc+c^2-a^2}\cdot b \\[2ex] & = \frac{\bcancel{a^2b}+b^2c+bc^2-\bcancel{a^2b}+bc^2+c^3-a^2c}{b^2+2bc+c^2-a^2}\cdot b \\[2ex] & = \frac{b^2+2bc+c^2-a^2}{b^2+2bc+c^2-a^2}\cdot bc \\[2ex] & = bc \end{aligned}v​=y(x+y+z)a2yz+b2zx+c2xy​=bc⋅(b2+2bc+c2−a2)a2⋅b2⋅bc+b2⋅bc⋅(bc+c2−a2)+c2⋅(bc+c2−a2)⋅b2​=b2+2bc+c2−a2a2b+b⋅(bc+c2−a2)+c⋅(bc+c2−a2)​⋅b=b2+2bc+c2−a2a2b +b2c+bc2−a2b +bc2+c3−a2c​⋅b=b2+2bc+c2−a2b2+2bc+c2−a2​⋅bc=bc​ 因此 w=vw=vw=v,命题得证。 即先以点 AAA 为反演中心、AB⋅AC\sqrt{AB\cdot AC}AB⋅AC ​ 为反演半径作反演变换,然后再关于 ∠BAC\angle BAC∠BAC 的平分线作轴对称变换。 ↩︎

2025/9/21
articleCard.readMore

2025 年高联二试(A 卷)几何题的解答

1. 题目 如图,在 △ABC\triangle ABC△ABC 中,DDD 为边 BCBCBC 的中点,延长 ADADAD 交于 △ABC\triangle ABC△ABC 的外接圆于点 PPP,过 BBB、PPP 作一个圆与边 ACACAC 相切于点 EEE,过点 CCC、PPP 作一个圆与边 ABABAB 相切于点 FFF。证明:ADADAD、BEBEBE、CFCFCF 三线共点。 2. 分析 首先,要证明结论可以使用塞瓦定理,即证明 AFFB⋅BDDC⋅CEEA=1 \frac{AF}{FB}\cdot \frac{BD}{DC}\cdot \frac{CE}{EA} = 1FBAF​⋅DCBD​⋅EACE​=1 由 DDD 是 BCBCBC 中点可知上面的结论等价于 EF∥BCEF\parallel BCEF∥BC。 这个题的关键构造条件是过 BBB、PPP 作一个圆与边 ACACAC 相切。 实际上,如果是考虑和直线 ACACAC 相切,那么满足条件的圆有两个,对应的两个切点调和分割 ACACAC,且 BPBPBP 与 ACACAC 的交点是两个切点的中点。 上面引理的证明 如图,ABCDABCDABCD 四点共圆,过 CCC、DDD 作两个圆(假设存在)与直线 ABABAB 相切于点 PPP、QQQ,设直线 ABABAB 与 CDCDCD 交于点 MMM。 由 MP2=MC⋅MD=MQ2 MP^2=MC\cdot MD=MQ^2MP2=MC⋅MD=MQ2 可知 MP=MQMP=MQMP=MQ,即 MMM 是两个切点的中点。又由 MA⋅MB=MC⋅MD=MP2 MA\cdot MB=MC\cdot MD=MP^2MA⋅MB=MC⋅MD=MP2 可知 AAA、BBB 是关于以 PQPQPQ 为直径的圆的反演变换的对应点,因此 (A,B;P,Q)=−1(A,B;P,Q)=-1(A,B;P,Q)=−1。 这启发了我们去作 BPBPBP 与 ACACAC 的交点(设为 JJJ)、CPCPCP 和 ABABAB 的交点(设为 KKK)。 利用这两个点以及圆幂定理可以得到 JEJEJE 和 KFKFKF 之间的比例关系。 另外,由 ABPCABPCABPC 共圆可知 △DJK\triangle DJK△DJK 是自配极三角形,外心 OOO 是 △DJK\triangle DJK△DJK 的垂心,可知 JK∥BCJK\parallel BCJK∥BC。结合前面的 JEJEJE 和 KFKFKF 的条件可证 EF∥JK∥BCEF\parallel JK\parallel BCEF∥JK∥BC。 3. 解答 设 J=AC‾∩BP‾J=\overline{AC}\cap \overline{BP}J=AC∩BP,K=AB‾∩CP‾K=\overline{AB}\cap \overline{CP}K=AB∩CP,OOO 是 △ABC\triangle ABC△ABC 的外心。 注意到 JKJKJK 是 DDD 关于 ⊙O\odot O⊙O 的极线,因此 OD⊥JKOD\perp JKOD⊥JK。 又由 OD⊥BCOD\perp BCOD⊥BC 可知 JK∥BCJK\parallel BCJK∥BC。 另一种证明平行的方法 对点 PPP 应用塞瓦定理,可得 AKKB⋅BDDC⋅CJJA=1 \frac{AK}{KB}\cdot \frac{BD}{DC}\cdot \frac{CJ}{JA} = 1KBAK​⋅DCBD​⋅JACJ​=1 因此 AKAJ=BKCJ=AK−BKAJ−CJ=ABAC \frac{AK}{AJ} = \frac{BK}{CJ} = \frac{AK-BK}{AJ-CJ} = \frac{AB}{AC}AJAK​=CJBK​=AJ−CJAK−BK​=ACAB​ 于是有 BC∥JKBC\parallel JKBC∥JK。 由 JE2=JB⋅JP=JA⋅JCKF2=KC⋅JP=KA⋅KB \begin{aligned} JE^2 & = JB\cdot JP = JA\cdot JC \\ KF^2 & = KC\cdot JP = KA\cdot KB \end{aligned}JE2KF2​=JB⋅JP=JA⋅JC=KC⋅JP=KA⋅KB​ 可知 JE2KF2=JA⋅JCKA⋅KB=JA2KA2 \frac{JE^2}{KF^2} = \frac{JA\cdot JC}{KA\cdot KB} = \frac{JA^2}{KA^2}KF2JE2​=KA⋅KBJA⋅JC​=KA2JA2​ 故 JEKF=JAKA=AEAF \frac{JE}{KF} = \frac{JA}{KA} = \frac{AE}{AF}KFJE​=KAJA​=AFAE​ 可知 EF∥JK∥BCEF\parallel JK\parallel BCEF∥JK∥BC,因此 AFFB=AEEC \frac{AF}{FB} = \frac{AE}{EC}FBAF​=ECAE​ 可知 AFFB⋅BDDC⋅CEEA=1 \frac{AF}{FB}\cdot \frac{BD}{DC}\cdot \frac{CE}{EA} = 1FBAF​⋅DCBD​⋅EACE​=1 因此 ADADAD、BEBEBE、CFCFCF 三线共点,命题得证。

2025/9/21
articleCard.readMore

2025 年 CGMO 的几何题的解答(二)

1. 题目 如图,在锐角 △ABC\triangle ABC△ABC 中,AB>ACAB > ACAB>AC,DDD、EEE、FFF 分别是 AAA、BBB、CCC 在对应边上的投影。设 BFBFBF 的中垂线与 DEDEDE 交于点 PPP,CECECE 的中垂线与 DFDFDF 交于点 QQQ。设 KKK 是直线 PQPQPQ 上一点使得 ∠PKE=∠PKF\angle PKE = \angle PKF∠PKE=∠PKF。设 DKDKDK 交 △KEF\triangle KEF△KEF 的外接圆与另一点 TTT,求证:∠ATE=∠ATF\angle ATE = \angle ATF∠ATE=∠ATF。 2. 分析 设 HHH 是 △ABC\triangle ABC△ABC 的垂心,NNN 是 △ABC\triangle ABC△ABC 的九点圆圆心。 这个题有三个重要的观察点: HHH、PPP、QQQ 共线; 点 KKK 满足 ∠FKH=∠HKE=∠FDE\angle FKH = \angle HKE = \angle FDE∠FKH=∠HKE=∠FDE,从而 N∈⊙(KEF)N\in \odot(KEF)N∈⊙(KEF); AAA、TTT、NNN 共线。 第一个结论在做出图来之后,是比较容易看出来的,可以直接用梅涅劳斯定理进行证明。 在证明第一个结论的时候,会用到 BCBCBC、BHBHBH 和 CHCHCH 的中点,结合 DDD、EEE、FFF 自然就联想到九点圆。画出九点圆就会发现,其圆心就在 △KEF\triangle KEF△KEF 的外接圆上。 从而我们可以得出 ∠FKH=12∠FNE=∠FHbH∠HKE=12∠FNE=∠HHcE\begin{aligned} \angle FKH = \frac{1}{2}\angle FNE = \angle FH_bH \\[2ex] \angle HKE = \frac{1}{2}\angle FNE = \angle HH_cE\end{aligned}∠FKH=21​∠FNE=∠FHb​H∠HKE=21​∠FNE=∠HHc​E​ 因此 FFF、HbH_bHb​、KKK、HHH 共圆,EEE、HcH_cHc​、KKK、HHH 共圆,可知点 KKK 是完全四边形 FHbEHcFH_bEH_cFHb​EHc​ 的密克点。 要证明这个结论,我们可以使用同一法构造点 KKK。可以直接用密克点来构造,也可以用其它的圆的交点来构造,关键是构造点 KKK 之后能够证明其在直线 PQPQPQ 上。 最后一个结论不是很好直接证明,不过此时的结论可以转换成角相等的结论,而且可以完全消去点 KKK,因此我们可以考虑把它算出来。可以尝试三角法或复数法。 3. 解答 3.1. 结论一的证明 如图,设 CFCFCF 与 DEDEDE 交于点 ZZZ。考虑 △FZD\triangle FZD△FZD,要证明 HHH、PPP、QQQ 共线,只需证 FQQD⋅DPPZ⋅ZHHF=1 \frac{FQ}{QD}\cdot \frac{DP}{PZ}\cdot \frac{ZH}{HF} = 1QDFQ​⋅PZDP​⋅HFZH​=1 设 CECECE 的中垂线分别与 ABABAB、BCBCBC、CFCFCF 交于 LLL、MMM、HcH_cHc​,则 MMM 是 BCBCBC 中点,HcH_cHc​ 是 CHCHCH 中点。考虑 △FBD\triangle FBD△FBD,有 FQQD⋅DMMB⋅BLLF=1 \frac{FQ}{QD}\cdot \frac{DM}{MB}\cdot \frac{BL}{LF} = 1QDFQ​⋅MBDM​⋅LFBL​=1 注意到 DPPZ=DMMC=DMMB \frac{DP}{PZ} = \frac{DM}{MC} = \frac{DM}{MB}PZDP​=MCDM​=MBDM​ 以及 BLLF=HHcHcF \frac{BL}{LF} = \frac{H H_c}{H_cF}LFBL​=Hc​FHHc​​ 因此只需证明 ZHHF=HHcHcF \frac{ZH}{HF} = \frac{H H_c}{H_cF}HFZH​=Hc​FHHc​​ 即可。 注意到 △ABC\triangle ABC△ABC 的垂心 HHH 是其垂足三角形 DEFDEFDEF 的内心,AAA、BBB、CCC 是 △DEF\triangle DEF△DEF 的旁心,因此 (FZ;CH)=−1 (FZ;CH) = -1(FZ;CH)=−1 由 HcH_cHc​ 是 CHCHCH 中点可知 HcH2=HcZ⋅HcF H_cH^2 = H_cZ\cdot H_cFHc​H2=Hc​Z⋅Hc​F 故 HcHHcF=HcZHcH=HcH−HcZHcF−HcH=HZHF \frac{H_cH}{H_cF} = \frac{H_cZ}{H_cH} = \frac{H_cH-H_cZ}{H_cF-H_cH} = \frac{HZ}{HF}Hc​FHc​H​=Hc​HHc​Z​=Hc​F−Hc​HHc​H−Hc​Z​=HFHZ​ 结论得证。 3.2. 结论二的证明 如图,设 K′K'K′ 为 ⊙(HbHF)\odot(H_bHF)⊙(Hb​HF) 和 ⊙(PDF)\odot(PDF)⊙(PDF) 的第二个交点,则由 ∡FK′H=∡FHbH=∡FDE=∡FDP=∡FK′P \measuredangle FK'H = \measuredangle FH_bH = \measuredangle FDE = \measuredangle FDP = \measuredangle FK'P∡FK′H=∡FHb​H=∡FDE=∡FDP=∡FK′P 可知 K′K'K′ 在直线 PHPHPH 上。由 PQ⋅QK′=DQ⋅QF=MQ⋅QHc PQ\cdot QK' = DQ\cdot QF = MQ\cdot QH_cPQ⋅QK′=DQ⋅QF=MQ⋅QHc​ 可知 PPP、MMM、K′K'K′、HcH_cHc​ 共圆。因此 ∡HK′Hc=∡PK′Hc=∡PMHc=∡PHbE=∡CHE=∡HEHc \begin{aligned} \measuredangle HK'H_c &= \measuredangle PK'H_c = \measuredangle PMH_c \\ &= \measuredangle PH_bE = \measuredangle CHE = \measuredangle HEH_c \end{aligned}∡HK′Hc​​=∡PK′Hc​=∡PMHc​=∡PHb​E=∡CHE=∡HEHc​​ 可得 HHH、K′K'K′、HcH_cHc​、EEE 共圆。故 ∡HK′E=∡HHcE=∡FDE=∡FK′H \measuredangle HK'E = \measuredangle HH_cE = \measuredangle FDE = \measuredangle FK'H∡HK′E=∡HHc​E=∡FDE=∡FK′H 因此 K′K'K′ 与 KKK 重合。 3.3. 结论三的证明(复数法) 由结论二可知, ∡FKE=∡FKH+∡HKE=∡FHbH+∡HHcE=2∡FDE=∡FNE \begin{aligned} \measuredangle FKE &= \measuredangle FKH + \measuredangle HKE \\ &= \measuredangle FH_bH + \measuredangle HH_cE \\ &= 2 \measuredangle FDE \\ &= \measuredangle FNE \end{aligned}∡FKE​=∡FKH+∡HKE=∡FHb​H+∡HHc​E=2∡FDE=∡FNE​ 因此点 NNN 在 ⊙(KEF)\odot(KEF)⊙(KEF) 上。 故 ∡FTN=∡FEN=∡NFE=∡NTE \measuredangle FTN = \measuredangle FEN = \measuredangle NFE = \measuredangle NTE∡FTN=∡FEN=∡NFE=∡NTE 要证明 ∠ATE=∠ATF\angle ATE = \angle ATF∠ATE=∠ATF,只需证 AAA、TTT、NNN 共线,即 ∡ANF=∡TNF\measuredangle ANF = \measuredangle TNF∡ANF=∡TNF 即可。其中 ∡TNF=∡TKF=∡DKF=∡DPF=∡EPF \measuredangle TNF = \measuredangle TKF = \measuredangle DKF = \measuredangle DPF = \measuredangle EPF∡TNF=∡TKF=∡DKF=∡DPF=∡EPF 因此只需证 ∡ANF=∡EPF\measuredangle ANF = \measuredangle EPF∡ANF=∡EPF。 以 △ABC\triangle ABC△ABC 的外接圆为单位圆建立复平面,设点 AAA、BBB、CCC、DDD、EEE、FFF、HHH、HbH_bHb​、HcH_cHc​、MMM、NNN、PPP 对应的复数依次为 aaa、bbb、ccc、ddd、eee、fff、hhh、hbh_bhb​、hch_chc​、mmm、nnn、ppp。则 h=a+b+cn=12h=12(a+b+c)d=12(a+b+c−aˉbc)e=12(a+b+c−abˉc)f=12(a+b+c−abcˉ)hb=12(b+h)=12(a+2b+c)hc=12(c+h)=12(a+b+2c)m=12(b+c) \begin{aligned} h & = a+b+c \\[2ex] n & = \frac{1}{2} h = \frac{1}{2}(a+b+c) \\[2ex] d & = \frac{1}{2}\left(a+b+c-\bar{a} b c\right) \\[2ex] e & = \frac{1}{2}\left(a+b+c-a \bar{b} c\right) \\[2ex] f & = \frac{1}{2}\left(a+b+c-a b \bar{c}\right) \\[2ex] h_b & = \frac{1}{2}(b+h) = \frac{1}{2}(a+2b+c) \\[2ex] h_c & = \frac{1}{2}(c+h) = \frac{1}{2}(a+b+2c) \\[2ex] m & = \frac{1}{2}(b+c) \end{aligned}hndefhb​hc​m​=a+b+c=21​h=21​(a+b+c)=21​(a+b+c−aˉbc)=21​(a+b+c−abˉc)=21​(a+b+c−abcˉ)=21​(b+h)=21​(a+2b+c)=21​(c+h)=21​(a+b+2c)=21​(b+c)​ 设 p=λ⋅hb+(1−λ)⋅m=λ⋅12(a+2b+c)+(1−λ)⋅12(b+c)=12λ(a+b)+12(b+c) \begin{aligned} p & = \lambda \cdot h_b + (1-\lambda)\cdot m \\[2ex] & = \lambda \cdot \frac{1}{2}(a+2b+c)+(1-\lambda)\cdot \frac{1}{2}(b+c) \\[2ex] & = \frac{1}{2}\lambda(a+b)+\frac{1}{2}(b+c) \end{aligned}p​=λ⋅hb​+(1−λ)⋅m=λ⋅21​(a+2b+c)+(1−λ)⋅21​(b+c)=21​λ(a+b)+21​(b+c)​ 以及 p=μ⋅d+(1−μ)⋅e=μ⋅12(a+b+c−aˉbc)+(1−μ)⋅12(a+b+c−abˉc)=12μ(abˉc−aˉbc)+12(a+b+c−abˉc) \begin{aligned} p & = \mu \cdot d + (1-\mu)\cdot e \\[2ex] & = \mu \cdot \frac{1}{2}(a+b+c-\bar{a} b c) + (1-\mu)\cdot \frac{1}{2}(a+b+c-a \bar{b} c) \\[2ex] & = \frac{1}{2}\mu(a \bar{b} c - \bar{a} b c) + \frac{1}{2}\left(a+b+c - a \bar{b} c\right) \end{aligned}p​=μ⋅d+(1−μ)⋅e=μ⋅21​(a+b+c−aˉbc)+(1−μ)⋅21​(a+b+c−abˉc)=21​μ(abˉc−aˉbc)+21​(a+b+c−abˉc)​ 可得 λ(a+b)−μ(abˉ−aˉb)c=a−abˉc \lambda(a+b) - \mu(a \bar{b} - \bar{a} b) c = a - a \bar{b} cλ(a+b)−μ(abˉ−aˉb)c=a−abˉc 其中 λ∈R\lambda \in \mathbb{R}λ∈R,μ∈R\mu \in \mathbb{R}μ∈R。 对上面的式子取共轭可得 λ(aˉ+bˉ)−μ(aˉb−abˉ)=aˉ−aˉbcˉ \lambda (\bar{a}+\bar{b}) - \mu (\bar{a} b - a \bar{b}) = \bar{a} - \bar{a} b \bar{c}λ(aˉ+bˉ)−μ(aˉb−abˉ)=aˉ−aˉbcˉ 联立可解得 μ=−(b−c)(bˉ2+aˉcˉ)(abˉ−aˉb)(cˉ+aˉbˉc) \mu = - \frac{(b-c)\left(\bar{b}^2+\bar{a}\bar{c}\right)}{\left(a\bar{b}-\bar{a}b\right)\left(\bar{c}+\bar{a}\bar{b}c\right)}μ=−(abˉ−aˉb)(cˉ+aˉbˉc)(b−c)(bˉ2+aˉcˉ)​ 要证明 ∡ANF=∡EPF\measuredangle ANF = \measuredangle EPF∡ANF=∡EPF,只需证 f−pe−p÷f−na−n∈R \frac{f-p}{e-p}\div \frac{f-n}{a-n} \in \mathbb{R}e−pf−p​÷a−nf−n​∈R 即可。 其中 f−p=−12abcˉ+12μaˉbc+12(1−μ)abˉc=12[a(bˉc−bcˉ)+μ(aˉb−abˉ)c]e−p=e−[μ⋅d+(1−μ)⋅e]=μ(e−d)=12μ(aˉb−abˉ)cf−n=−12abcˉa−n=12(a−b−c) \begin{aligned} f-p & = -\frac{1}{2} a b \bar{c} + \frac{1}{2} \mu \bar{a} b c + \frac{1}{2}(1-\mu) a \bar{b} c \\[2ex] & = \frac{1}{2}\left[a\left(\bar{b}c-b\bar{c}\right)+\mu\left(\bar{a}b-a\bar{b}\right)c\right] \\[3ex] e-p & = e - \left[\mu \cdot d + (1-\mu)\cdot e\right] \\[2ex] & = \mu(e-d) \\[2ex] & = \frac{1}{2}\mu\left(\bar{a}b-a\bar{b}\right)c \\[3ex] f-n & = -\frac{1}{2}ab\bar{c} \\[3ex] a-n & = \frac{1}{2}(a-b-c) \end{aligned}f−pe−pf−na−n​=−21​abcˉ+21​μaˉbc+21​(1−μ)abˉc=21​[a(bˉc−bcˉ)+μ(aˉb−abˉ)c]=e−[μ⋅d+(1−μ)⋅e]=μ(e−d)=21​μ(aˉb−abˉ)c=−21​abcˉ=21​(a−b−c)​ 因此 f−pe−p÷f−na−n=a(bˉc−bcˉ)+μ(aˉb−abˉ)cμ(aˉb−abˉ)c⋅a−b−c−abcˉ=(a(bˉc−bcˉ)μ(aˉb−abˉ)c+1)⋅b+c−aabcˉ \begin{aligned} \frac{f-p}{e-p}\div \frac{f-n}{a-n} & = \frac{a\left(\bar{b}c-b\bar{c}\right)+\mu\left(\bar{a}b-a\bar{b}\right)c}{\mu\left(\bar{a}b-a\bar{b}\right)c}\cdot \frac{a-b-c}{-ab\bar{c}} \\[2ex] & = \left(\frac{a\left(\bar{b}c-b\bar{c}\right)}{\mu\left(\bar{a}b-a\bar{b}\right)c}+1\right)\cdot \frac{b+c-a}{ab\bar{c}} \end{aligned}e−pf−p​÷a−nf−n​​=μ(aˉb−abˉ)ca(bˉc−bcˉ)+μ(aˉb−abˉ)c​⋅−abcˉa−b−c​=(μ(aˉb−abˉ)ca(bˉc−bcˉ)​+1)⋅abcˉb+c−a​​ 其中 a(bˉc−bcˉ)μ(aˉb−abˉ)c=a(bˉc−bcˉ)(cˉ+aˉbˉc)(b−c)(bˉ2+aˉcˉ)c=−a(b+c)(ab+c2)c2(ac+b2) \begin{aligned} \frac{a\left(\bar{b}c-b\bar{c}\right)}{\mu\left(\bar{a}b-a\bar{b}\right)c} & = \frac{a\left(\bar{b}c-b\bar{c}\right)\left(\bar{c}+\bar{a}\bar{b}c\right)}{(b-c)\left(\bar{b}^2+\bar{a}\bar{c}\right)c} \\[2ex] & = -\frac{a(b+c)(ab+c^2)}{c^2(ac+b^2)} \end{aligned}μ(aˉb−abˉ)ca(bˉc−bcˉ)​​=(b−c)(bˉ2+aˉcˉ)ca(bˉc−bcˉ)(cˉ+aˉbˉc)​=−c2(ac+b2)a(b+c)(ab+c2)​​ 代入可得 f−pe−p÷f−na−n=c2(ac+b2)−a(b+c)(ab+c2)c2(ac+b2)⋅b+c−aabcˉ=b(a+c)(bc−ab−ac)(b+c−a)abc(ac+b2)=−b(a+c)ac+b2⋅(bˉ+cˉ−aˉ)(b+c−a) \begin{aligned} \frac{f-p}{e-p}\div \frac{f-n}{a-n} & = \frac{c^2(ac+b^2)-a(b+c)(ab+c^2)}{c^2(ac+b^2)}\cdot \frac{b+c-a}{ab\bar{c}} \\[2ex] & = \frac{b(a+c)(bc-ab-ac)(b+c-a)}{abc(ac+b^2)} \\[2ex] & = - \frac{b(a+c)}{ac+b^2}\cdot \left(\bar{b}+\bar{c}-\bar{a}\right)(b+c-a) \end{aligned}e−pf−p​÷a−nf−n​​=c2(ac+b2)c2(ac+b2)−a(b+c)(ab+c2)​⋅abcˉb+c−a​=abc(ac+b2)b(a+c)(bc−ab−ac)(b+c−a)​=−ac+b2b(a+c)​⋅(bˉ+cˉ−aˉ)(b+c−a)​ 注意到 (b(a+c)ac+b2)‾=1b(1a+1c)1ac+1b2=b(c+a)b2+ac(i) \overline{\left(\frac{b(a+c)}{ac+b^2}\right)} = \frac{\frac{1}{b}\left(\frac{1}{a}+\frac{1}{c}\right)}{\frac{1}{ac}+\frac{1}{b^2}} = \frac{b(c+a)}{b^2+ac} \tag{i}(ac+b2b(a+c)​)​=ac1​+b21​b1​(a1​+c1​)​=b2+acb(c+a)​(i) 因此 (f−pe−p÷f−na−n)‾=f−pe−p÷f−na−n \overline{\left(\frac{f-p}{e-p}\div \frac{f-n}{a-n}\right)} = \frac{f-p}{e-p}\div \frac{f-n}{a-n}(e−pf−p​÷a−nf−n​)​=e−pf−p​÷a−nf−n​ 可知 f−pe−p÷f−na−n∈R \frac{f-p}{e-p}\div \frac{f-n}{a-n} \in \mathbb{R}e−pf−p​÷a−nf−n​∈R 命题得证。 3.4. 结论一的另一个证明(不使用调和点列) 注意到 HHbMHcH H_b M H_cHHb​MHc​ 是平行四边形,只需证明 PMPHb=QMHHb  ⟸  PMPHb=QMHcM \frac{PM}{PH_b} = \frac{QM}{HH_b} \impliedby \frac{PM}{PH_b} = \frac{QM}{H_cM}PHb​PM​=HHb​QM​⟸PHb​PM​=Hc​MQM​ 考虑直线 DEDEDE 和 △BHbM\triangle BH_bM△BHb​M,由梅涅劳斯定理可知, BEEHb⋅HbPPM⋅MDDB=1 \frac{BE}{EH_b}\cdot \frac{H_bP}{PM}\cdot \frac{MD}{DB} = 1EHb​BE​⋅PMHb​P​⋅DBMD​=1 因此 PMPHb=BEBD⋅MDEHb \frac{PM}{PH_b} = \frac{BE}{BD}\cdot \frac{MD}{EH_b}PHb​PM​=BDBE​⋅EHb​MD​ 注意到 BE⋅BHb=BM⋅BDBE\cdot BH_b = BM\cdot BDBE⋅BHb​=BM⋅BD,因此 PMPHb=BMBHb⋅MDEHb \frac{PM}{PH_b} = \frac{BM}{BH_b}\cdot \frac{MD}{EH_b}PHb​PM​=BHb​BM​⋅EHb​MD​ 同理,考虑直线 DFDFDF 和 △CHcM\triangle CH_cM△CHc​M,有 CFFHc⋅HcQQM⋅MDDC=1 \frac{CF}{FH_c}\cdot \frac{H_cQ}{QM}\cdot \frac{MD}{DC} = 1FHc​CF​⋅QMHc​Q​⋅DCMD​=1 因此 QMHcQ=CFCD⋅MDFHc \frac{QM}{H_cQ} = \frac{CF}{CD} \cdot \frac{MD}{FH_c}Hc​QQM​=CDCF​⋅FHc​MD​ 注意到 CF⋅CHc=CD⋅CMCF\cdot CH_c = CD\cdot CMCF⋅CHc​=CD⋅CM,因此 QMHcQ=CMCHc⋅MDFHc  ⟹  QMHcM=CM⋅MDCHc⋅FHc+CM⋅MD \begin{aligned} \frac{QM}{H_cQ} & = \frac{CM}{CH_c} \cdot \frac{MD}{FH_c} \\[2ex] \implies \frac{QM}{H_cM} & = \frac{CM\cdot MD}{CH_c\cdot FH_c + CM\cdot MD} \end{aligned}Hc​QQM​⟹Hc​MQM​​=CHc​CM​⋅FHc​MD​=CHc​⋅FHc​+CM⋅MDCM⋅MD​​ 所以,要证明的结论等价于 BM⋅MDEHb⋅BHb=CM⋅MDCHc⋅FHc+CM⋅MD  ⟺  EHb⋅BHb=CHc⋅FHc+CM⋅MD \begin{aligned} \frac{BM\cdot MD}{EH_b\cdot BH_b} & = \frac{CM\cdot MD}{CH_c\cdot FH_c + CM\cdot MD} \\[2ex] \iff EH_b\cdot BH_b & = CH_c\cdot FH_c + CM\cdot MD \end{aligned}EHb​⋅BHb​BM⋅MD​⟺EHb​⋅BHb​​=CHc​⋅FHc​+CM⋅MDCM⋅MD​=CHc​⋅FHc​+CM⋅MD​ 其中 EHb⋅BHb=(BE−BHb)⋅BHb=BE⋅BHb−BHb2=BM⋅BD−14BH2 \begin{aligned} EH_b\cdot BH_b & = \left(BE-BH_b\right)\cdot BH_b \\[1ex] & = BE\cdot BH_b - BH_b^2 \\[1ex] & = BM\cdot BD-\frac{1}{4}BH^2 \end{aligned}EHb​⋅BHb​​=(BE−BHb​)⋅BHb​=BE⋅BHb​−BHb2​=BM⋅BD−41​BH2​ 以及 FHc⋅CHc=(CF−FHc)⋅CHc=CF⋅CHc−CHc2=CM⋅CD−14CH2 \begin{aligned} FH_c\cdot CH_c & = \left(CF-FH_c\right)\cdot CH_c \\[1ex] & = CF\cdot CH_c - CH_c^2 \\[1ex] & = CM\cdot CD - \frac{1}{4}CH^2 \end{aligned}FHc​⋅CHc​​=(CF−FHc​)⋅CHc​=CF⋅CHc​−CHc2​=CM⋅CD−41​CH2​ 因此只需证  BM⋅BD−14BH2=CM⋅MD+CM⋅CD−14CH2  ⟺   BM⋅(BD−MD−CD)=14(BH2−CH2)  ⟺   BM⋅DM=14(BH2−CH2) \begin{aligned} & \, BM\cdot BD-\frac{1}{4}BH^2 = CM\cdot MD + CM\cdot CD - \frac{1}{4}CH^2 \\[1ex] \iff & \, BM\cdot (BD-MD-CD) = \frac{1}{4}\left(BH^2-CH^2\right) \\[1ex] \iff & \, BM\cdot DM = \frac{1}{4}\left(BH^2-CH^2\right) \end{aligned}⟺⟺​BM⋅BD−41​BH2=CM⋅MD+CM⋅CD−41​CH2BM⋅(BD−MD−CD)=41​(BH2−CH2)BM⋅DM=41​(BH2−CH2)​ 由 DH⊥BCDH\perp BCDH⊥BC 可知 BH2−CH2=BD2−CD2=(BD+CD)(BD−CD)=BC⋅2DM=2BM⋅2DM=4BM⋅DM \begin{aligned} BH^2-CH^2 & = BD^2 - CD^2 \\[1ex] & = (BD+CD)(BD-CD) \\[1ex] & = BC\cdot 2DM \\[1ex] & = 2BM\cdot 2DM \\[1ex] & = 4BM\cdot DM \end{aligned}BH2−CH2​=BD2−CD2=(BD+CD)(BD−CD)=BC⋅2DM=2BM⋅2DM=4BM⋅DM​ 结论得证。 3.5. 结论二的另一个证明(使用完全四边形) 如图,设 S=FHb‾∩EHc‾S=\overline{FH_b}\cap\overline{EH_c}S=FHb​​∩EHc​​,R=EF‾∩HbHc‾R=\overline{EF}\cap \overline{H_bH_c}R=EF∩Hb​Hc​​。对 HbMHcEDFH_bMH_cEDFHb​MHc​EDF 应用帕斯卡定理可知 PPP、QQQ、SSS 共线。 考虑完全四边形 FHbEHcFH_bEH_cFHb​EHc​ 的密克点 K′K'K′,由 FHbEHcFH_bEH_cFHb​EHc​ 四点共圆可知 K′K'K′ 是 RRR 关于 ⊙N\odot N⊙N 的反演点。因此 K′K'K′ 在点 RRR 关于 ⊙N\odot N⊙N 的极线 SHSHSH 上。故 ∠FKH=∠FHbH=∠FDE=∠FHcE=∠HKE \angle FKH = \angle FH_bH = \angle FDE = \angle FH_cE = \angle HKE∠FKH=∠FHb​H=∠FDE=∠FHc​E=∠HKE 可得 ∠PKF=∠PKE\angle PKF = \angle PKE∠PKF=∠PKE,因此 K′K'K′ 与 KKK 重合。 3.6. 另一种复平面的构造方法 如图,以 △ABC\triangle ABC△ABC 的九点圆为单位圆建立复平面,则 n=0n=0n=0。注意到 HHH 是 △DEF\triangle DEF△DEF 的内心,因此存在单位圆上的 uuu、vvv、www 使得 d=u2,e=v2,f=w2ha=−vw,hb=−wu,hc=−uv \begin{gathered} d = u^2, \quad e = v^2, \quad f = w^2 \\[1ex] h_a = -vw, \quad h_b = -wu, \quad h_c = -uv \end{gathered}d=u2,e=v2,f=w2ha​=−vw,hb​=−wu,hc​=−uv​ 于是 h=−(uv+vw+wu)m=vw \begin{aligned} h &= -(uv+vw+wu) \\[1ex] m &= vw \end{aligned}hm​=−(uv+vw+wu)=vw​ 因此 a=2ha−h=uv−vw+wu=uvw(w‾+v‾−u‾) a=2h_a-h = uv-vw+wu = uvw\left(\overline{w}+\overline{v}-\overline{u}\right)a=2ha​−h=uv−vw+wu=uvw(w+v−u) 设 p=λu2+(1−λ)v2=λ(u2−v2)+v2 p = \lambda u^2 + (1-\lambda) v^2 = \lambda \left(u^2-v^2\right) +v^2p=λu2+(1−λ)v2=λ(u2−v2)+v2 以及 p=μ(−wu)+(1−μ)vw=−μ(wu+vw)+vw p = \mu (-wu) + (1-\mu) vw = -\mu(wu+vw)+vwp=μ(−wu)+(1−μ)vw=−μ(wu+vw)+vw 因此 λ(u+v)(u−v)+μ(u+v)w=v(w−v) \lambda(u+v)(u-v)+\mu(u+v)w=v(w-v)λ(u+v)(u−v)+μ(u+v)w=v(w−v) 其中 λ∈R\lambda \in \mathbb{R}λ∈R,μ∈R\mu \in\mathbb{R}μ∈R。 对上式取共轭可得 λ(uˉ+vˉ)(uˉ−vˉ)+μ(uˉ+vˉ)wˉ=vˉ(wˉ−vˉ) \lambda\left(\bar{u}+\bar{v}\right)\left(\bar{u}-\bar{v}\right)+\mu\left(\bar{u}+\bar{v}\right)\bar{w}=\bar{v}\left(\bar{w}-\bar{v}\right)λ(uˉ+vˉ)(uˉ−vˉ)+μ(uˉ+vˉ)wˉ=vˉ(wˉ−vˉ) 联立可解得 λ=u(w−v)(wu+v2)(u+v)(u−v)(uv+w2) \lambda = \frac{u(w-v)\left(wu+v^2\right)}{(u+v)(u-v)\left(uv+w^2\right)}λ=(u+v)(u−v)(uv+w2)u(w−v)(wu+v2)​ 因此 f−pe−p⋅a−nf−n=w2−v2−λ(u2−v2)λ(v2−u2)⋅af=[w2−v2λ(v2−u2)+1]⋅af=[−(w+v)(w−v)(u+v)(u−v)⋅(u+v)(u−v)(uv+w2)u(w−v)(wu+v2)+1]⋅af=u(uw+v2)−(w+v)(uv+w2)u(uw+v2)⋅uvw(w‾+v‾−u‾)w2=−(u+w)vuw+v2(w+v−u)(w‾+v‾−u‾) \begin{aligned} \frac{f-p}{e-p}\cdot \frac{a-n}{f-n} & = \frac{w^2-v^2-\lambda\left(u^2-v^2\right)}{\lambda\left(v^2-u^2\right)}\cdot \frac{a}{f} \\[2ex] & = \left[\frac{w^2-v^2}{\lambda\left(v^2-u^2\right)}+1\right]\cdot \frac{a}{f} \\[2ex] & = \left[-\frac{(w+v)\bcancel{(w-v)}}{\bcancel{(u+v)(u-v)}}\cdot \frac{\bcancel{(u+v)(u-v)}(uv+w^2)}{u\bcancel{(w-v)}(wu+v^2)}+1\right]\cdot \frac{a}{f} \\[2ex] & = \frac{u(uw+v^2)-(w+v)\left(uv+w^2\right)}{u\left(uw+v^2\right)}\cdot \frac{uvw(\overline{w}+\overline{v}-\overline{u})}{w^2} \\[2ex] & = -\frac{(u+w)v}{uw+v^2}(w+v-u)\left(\overline{w}+\overline{v}-\overline{u}\right) \end{aligned}e−pf−p​⋅f−na−n​​=λ(v2−u2)w2−v2−λ(u2−v2)​⋅fa​=[λ(v2−u2)w2−v2​+1]⋅fa​=[−(u+v)(u−v) ​(w+v)(w−v) ​​⋅u(w−v) ​(wu+v2)(u+v)(u−v) ​(uv+w2)​+1]⋅fa​=u(uw+v2)u(uw+v2)−(w+v)(uv+w2)​⋅w2uvw(w+v−u)​=−uw+v2(u+w)v​(w+v−u)(w+v−u)​ 类似 (i) 式可知 f−pe−p⋅a−nf−n∈R \frac{f-p}{e-p}\cdot \frac{a-n}{f-n} \in \mathbb{R}e−pf−p​⋅f−na−n​∈R 结论得证。

2025/8/23
articleCard.readMore

2025 年 CGMO 的几何题的解答(一)

1. 题目 如图,四边形 ABCDABCDABCD 内接于圆 OOO,AB>BCAB > BCAB>BC,EEE 是 ACACAC 上一点使得 AE<ECAE < ECAE<EC。过 EEE 作 ACACAC 的垂线 lll,分别交 △AED\triangle AED△AED、△BED\triangle BED△BED、△CED\triangle CED△CED 的外接圆于另一点 XXX、YYY、ZZZ,且 EEE、YYY、ZZZ、XXX 依次排列。若 AX=BY=CZAX=BY=CZAX=BY=CZ,求证:BDBDBD 与 OYOYOY 的交点是 △ABC\triangle ABC△ABC 的内心。 2. 分析 这个题的关键是确定 DDD 点和 EEE 点的位置。 从结论可以得出点 DDD 是 BIBIBI 和 △ABC\triangle ABC△ABC 点外接圆的交点,由鸡爪定理可知 DI=DA=DCDI = DA = DCDI=DA=DC。 这一点也比较好证,直接用 AX=CZAX=CZAX=CZ 即可证明。 EEE 点的位置无法直接确定,但是也比较好猜。鸡爪定理的另一个结论是旁心 JJJ 也在射线 BIBIBI 上,且 DJ=DIDJ=DIDJ=DI。作出 JJJ 点之后,我们会发现他就在直线 lll 上。可知点 EEE 是旁切圆的切点。 而关于旁切圆的切点,有一个经典的引理: 设 △ABC\triangle ABC△ABC 的内心为 III,XXX 是 A−A-A− 旁切圆与 BCBCBC 的切点,MMM 是 BCBCBC 的中点,则 AX∥IMAX\parallel IMAX∥IM。 上述引理的证明 设 △ABC\triangle ABC△ABC 的内切圆与 BCBCBC 相切于 DDD,点 EEE 是 DDD 在 △ABC\triangle ABC△ABC 的内切圆上的对径点,由位似可知 AAA、EEE、XXX 共线。 又 CX=BDCX = BDCX=BD,可知 DM=MXDM = MXDM=MX,因此 IMIMIM 是 △EDX\triangle EDX△EDX 的中位线,IM∥AXIM\parallel AXIM∥AX。 我们可以反过来用这个结论,通过构造平行的结论来证明点 EEE 是旁切圆切点。 接下来证明 III、OOO、YYY 共线。只需要注意到点 OOO 是 IYIYIY 的中点即可。 3. 解答 3.1. 证明点 D 在直线 BI 上 由 ∠AEX=∠CEZ=90∘\angle AEX = \angle CEZ = 90^\circ∠AEX=∠CEZ=90∘ 可知 AXAXAX、CZCZCZ 分别是 △AED\triangle AED△AED、△CED\triangle CED△CED 的外接圆的直径。 连接 DADADA、DCDCDC、DEDEDE,有 DAsin⁡∠DEA=AX=CZ=DCsin⁡∠DEC \frac{DA}{\sin \angle DEA} = AX = CZ = \frac{DC}{\sin \angle DEC}sin∠DEADA​=AX=CZ=sin∠DECDC​ 可得 DA=DCDA=DCDA=DC。 由鸡爪定理可知,△ABC\triangle ABC△ABC 的内心 III 在 BDBDBD 上,且 DI=DA=DCDI=DA=DCDI=DA=DC。 接下来为了简化图形,我们可以扔掉 XXX、ZZZ 两点,AX=BY=CZAX=BY=CZAX=BY=CZ 的条件简化为 BY=DCsin⁡∠DEC=DEsin⁡∠DCE BY = \frac{DC}{\sin \angle DEC} = \frac{DE}{\sin \angle DCE}BY=sin∠DECDC​=sin∠DCEDE​ 3.2. 证明平行 设直线 DODODO 与 ACACAC 交于点 MMM,则 MMM 是 ACACAC 的中点,且 DM⊥ACDM\perp ACDM⊥AC,因此 DM∥lDM\parallel lDM∥l。 连接 BEBEBE、IMIMIM,下面证明:BE∥IMBE\parallel IMBE∥IM。 设直线 BDBDBD 和 lll 交于点 JJJ,考虑 △JBE\triangle JBE△JBE 和 △DIM\triangle DIM△DIM。由 DM∥lDM\parallel lDM∥l 可知 ∠J=∠IDM\angle J=\angle IDM∠J=∠IDM,因此 ∠JBE+∠JEB=∠DIM+∠DMI(i) \angle JBE + \angle JEB = \angle DIM + \angle DMI \tag{i}∠JBE+∠JEB=∠DIM+∠DMI(i) 设 △BED\triangle BED△BED 的外接圆半径为 RRR,则 BYsin⁡∠JEB=2R=DEsin⁡∠JBE  ⟹  sin⁡∠JBEsin⁡∠JEB=DEBY=sin⁡∠DCE \begin{aligned} & \frac{BY}{\sin \angle JEB} = 2R = \frac{DE}{\sin \angle JBE} \\[2ex] \implies & \frac{\sin \angle JBE}{\sin \angle JEB} = \frac{DE}{BY} = \sin \angle DCE \end{aligned}⟹​sin∠JEBBY​=2R=sin∠JBEDE​sin∠JEBsin∠JBE​=BYDE​=sin∠DCE​ 在 △DIM\triangle DIM△DIM 中, DMsin⁡∠DIM=DIsin⁡∠DMI  ⟹  sin⁡∠DIMsin⁡∠DMI=DMDI=DMDC=sin⁡∠DCE \begin{aligned} & \frac{DM}{\sin \angle DIM} = \frac{DI}{\sin \angle DMI} \\[2ex] \implies & \frac{\sin \angle DIM}{\sin \angle DMI} = \frac{DM}{DI} = \frac{DM}{DC} = \sin \angle DCE \end{aligned}⟹​sin∠DIMDM​=sin∠DMIDI​sin∠DMIsin∠DIM​=DIDM​=DCDM​=sin∠DCE​ 因此, sin⁡∠JBEsin⁡∠JEB=sin⁡∠DIMsin⁡∠DMI(ii) \frac{\sin \angle JBE}{\sin \angle JEB} = \frac{\sin \angle DIM}{\sin \angle DMI} \tag{ii}sin∠JEBsin∠JBE​=sin∠DMIsin∠DIM​(ii) 我们构造函数 f(x)=sin⁡(θ−x)sin⁡(x), x∈(0,π), θ∈(π2,π) f(x) = \frac{\sin(\theta-x)}{\sin(x)}, \, x \in (0, \pi), \, \theta \in \left(\frac{\pi}{2}, \pi\right)f(x)=sin(x)sin(θ−x)​,x∈(0,π),θ∈(2π​,π) 则 f′(x)=−cos⁡(θ−x)sin⁡x−sin⁡(θ−x)cos⁡xsin⁡2x=−sin⁡θsin⁡2x<0 f'(x) =\frac{-\cos (\theta-x)\sin x-\sin (\theta-x)\cos x}{\sin ^2 x} = -\frac{\sin \theta}{\sin ^2x} < 0f′(x)=sin2x−cos(θ−x)sinx−sin(θ−x)cosx​=−sin2xsinθ​<0 可知 f(x)f(x)f(x) 在 (0,π)(0,\pi)(0,π) 上单调递减。结合等式 (i) 和 (ii) 可知 ∠JEB=∠DMI\angle JEB = \angle DMI∠JEB=∠DMI,∠JBE=∠DIM\angle JBE = \angle DIM∠JBE=∠DIM,因此 BE∥IMBE\parallel IMBE∥IM。 由上面的引理可知,点 EEE 是 △ABC\triangle ABC△ABC 的旁切圆切点,点 JJJ 是 △ABC\triangle ABC△ABC 的旁心,因此 DJ=DI=DA=DCDJ=DI=DA=DCDJ=DI=DA=DC。 另外,结合 DM∥lDM\parallel lDM∥l 可知 △DIM∼△JBE\triangle DIM\sim \triangle JBE△DIM∼△JBE。 3.3. 证明 O 是 IY 的中点 由 JY⋅JE=JD⋅JBJY\cdot JE=JD\cdot JBJY⋅JE=JD⋅JB 可知 JDJY=JEJB=DMDI=DMDC=sin⁡∠DCA \frac{JD}{JY} = \frac{JE}{JB} = \frac{DM}{DI} = \frac{DM}{DC} = \sin \angle DCAJYJD​=JBJE​=DIDM​=DCDM​=sin∠DCA 于是 JY=JDsin⁡∠DCA=DAsin⁡∠DCA=2⋅OD JY = \frac{JD}{\sin \angle DCA} = \frac{DA}{\sin\angle DCA} = 2\cdot ODJY=sin∠DCAJD​=sin∠DCADA​=2⋅OD 注意到 DO∥JYDO\parallel JYDO∥JY,点 DDD 是 IYIYIY 的中点,因此 ODODOD 是 △IJY\triangle IJY△IJY 的中位线,点 III、OOO、YYY 共线,命题得证。 最后一步的另一种证明方法 连接 DYDYDY,由上面可知 △DIM∼△JBE∼△JYD\triangle DIM \sim \triangle JBE \sim \triangle JYD△DIM∼△JBE∼△JYD,因此 DIJY=DMJD  ⟹  DI2=DM⋅JY \frac{DI}{JY} = \frac{DM}{JD} \implies DI^2 = DM\cdot JYJYDI​=JDDM​⟹DI2=DM⋅JY 设直线 DODODO 与 △ABC\triangle ABC△ABC 的外接圆的另一个交点为 NNN,连接 NYNYNY、NININI。 注意到 DI2=DC2=DM⋅DNDI^2=DC^2=DM\cdot DNDI2=DC2=DM⋅DN,因此 DN=JYDN = JYDN=JY,可知四边形 YNDJYNDJYNDJ 是平行四边形。于是有 YN∥DJYN=DJ=DI \begin{gathered} YN\parallel DJ \\ YN=DJ = DI \end{gathered}YN∥DJYN=DJ=DI​ 因此四边形 YNIDYNIDYNID 也是平行四边形,点 OOO 是对角线 DNDNDN 和 IYIYIY 的交点。

2025/8/15
articleCard.readMore

2025 年 CWMI 的几何题的解答(二)

1. 题目 如图,设 △ABC\triangle ABC△ABC 的内心为 III,点 EEE、FFF 分别在边 ACACAC、ABABAB 上,满足 ∠BIF=∠CIE\angle BIF = \angle CIE∠BIF=∠CIE。设 lll 为过 III 且平行于 BCBCBC 的直线,设点 EEE、FFF 关于直线 lll 的对称点分别为 XXX、YYY。证明:∠XBC=∠YCB\angle XBC = \angle YCB∠XBC=∠YCB。 2. 分析 这个题可以使用复数法进行处理。以内切圆为单位圆,可以得到 △ABC\triangle ABC△ABC 的顶点对应的复数。关于 lll 对称可以简化为共轭运算。 利用 ∠BIF=∠CIE\angle BIF = \angle CIE∠BIF=∠CIE 和点 FFF 在 ABABAB 上,可以求出点 FFF 对应的复数。点 EEE 的处理类似。 3. 解答 3.1. 建立复平面 引入以 III 为原点、lll 为实轴的复平面。设 △ABC\triangle ABC△ABC 的内切圆的半径为 111,其与 BCBCBC、CACACA、ABABAB 的切点依次为 RRR、SSS、TTT,对应的复数依次是 r=−ir=-ir=−i、sss、ttt。 设点 AAA、BBB、CCC、EEE、FFF、XXX、YYY 对应的复数依次是 aaa、bbb、ccc、ε\varepsilonε、fff、xxx、yyy,则 x=ε‾,y=f‾ x = \overline{\varepsilon}, \quad y = \overline{f}x=ε,y=f​ 以及 a‾=2t+s,a=2tst+sb‾=2t−i,b=−2tit−ic‾=2s−i,c=−2sis−i(i) \begin{aligned}\tag{i} \overline{a} = \frac{2}{t+s}, & \quad a = \frac{2ts}{t+s} \\[2ex] \overline{b} = \frac{2}{t-i}, & \quad b = -\frac{2ti}{t-i} \\[2ex] \overline{c} = \frac{2}{s-i}, & \quad c = -\frac{2si}{s-i} \end{aligned}a=t+s2​,b=t−i2​,c=s−i2​,​a=t+s2ts​b=−t−i2ti​c=−s−i2si​​(i) 3.2. 计算点 E、F 设 ∡FIB=∡CIE=θ\measuredangle FIB = \measuredangle CIE = \theta∡FIB=∡CIE=θ,则 f=b⋅λ e−iθ=k a+(1−k) bf = b \cdot \lambda \, \mathrm{e}^{-i\theta} = k\,a+(1-k)\,bf=b⋅λe−iθ=ka+(1−k)b,其中 λ,k∈R\lambda,k \in \mathbb{R}λ,k∈R。 因此 {b⋅λ e−iθ=k a+(1−k) bb‾⋅λ eiθ=k a‾+(1−k) b‾ \left\{\begin{aligned} & b \cdot \lambda \, \mathrm{e}^{-i\theta} = k\,a+(1-k)\,b \\ & \overline{b} \cdot \lambda \, \mathrm{e}^{i\theta} = k\,\overline{a}+(1-k)\,\overline{b} \end{aligned}\right.{​b⋅λe−iθ=ka+(1−k)bb⋅λeiθ=ka+(1−k)b​ 整理得 {λ⋅b e−iθ+k⋅(b−a)=bλ⋅b‾ eiθ+k⋅(b‾−a‾)=b‾ \left\{\begin{aligned} & \lambda \cdot b \, \mathrm{e}^{-i\theta} + k\cdot (b-a) = b \\ & \lambda \cdot \overline{b} \, \mathrm{e}^{i\theta} + k\cdot (\overline{b}-\overline{a}) = \overline{b} \end{aligned}\right.{​λ⋅be−iθ+k⋅(b−a)=bλ⋅beiθ+k⋅(b−a)=b​ 解得 λ=∣bb−ab‾b‾−a‾∣∣b e−iθb−ab‾ eiθb‾−a‾∣=ab‾−a‾b(b‾−a‾) b e−iθ−(b−a)b‾ eiθ(ii) \begin{aligned} \lambda & = \frac{\begin{vmatrix} b & b-a \\ \overline{b} & \overline{b}-\overline{a} \end{vmatrix}}{\begin{vmatrix} b \, \mathrm{e}^{-i\theta} & b-a \\ \overline{b} \, \mathrm{e}^{i\theta} & \overline{b}-\overline{a} \end{vmatrix}} \\[3ex] & = \frac{a\overline{b}-\overline{a}b}{(\overline{b}-\overline{a})\,b\,\mathrm{e}^{-i\theta}-\left(b-a\right)\overline{b}\,\mathrm{e}^{i\theta}} \end{aligned}\tag{ii}λ​= ​be−iθbeiθ​b−ab−a​ ​ ​bb​b−ab−a​ ​​=(b−a)be−iθ−(b−a)beiθab−ab​​(ii) 将 (i) 代入可得 ab‾−a‾b=4t(s+i)(t+s)(t−i)b‾−a‾=2(s+i)(t+s)(t−i)b−a=−2t2(s+i)(t+s)(t−i) \begin{aligned} a\overline{b}-\overline{a}b & = \frac{4t(s+i)}{(t+s)(t-i)} \\[2ex] \overline{b}-\overline{a} & = \frac{2(s+i)}{(t+s)(t-i)} \\[2ex] b-a & = -\frac{2t^2(s+i)}{(t+s)(t-i)} \end{aligned}ab−abb−ab−a​=(t+s)(t−i)4t(s+i)​=(t+s)(t−i)2(s+i)​=−(t+s)(t−i)2t2(s+i)​​ 再带回 (ii) 化简可得 λ=t−it eiθ−i e−iθ \lambda = \frac{t-i}{t\,\mathrm{e}^{i\theta}-i\,\mathrm{e}^{-i\theta}}λ=teiθ−ie−iθt−i​ 同理,设 ε=c⋅μ eiθ=l a+(1−l) c\varepsilon = c\cdot \mu \,\mathrm{e}^{i\theta} = l\,a+(1-l)\,cε=c⋅μeiθ=la+(1−l)c,其中 μ,l∈R\mu,l \in \mathbb{R}μ,l∈R,则有 μ=s−is e−iθ−i eiθ \mu = \frac{s-i}{s\,\mathrm{e}^{-i\theta}-i\,\mathrm{e}^{i\theta}}μ=se−iθ−ieiθs−i​ 3.3. 证明角相等 要证明的结论 ∡CBX=∡YCB  ⟺  x−bc−b÷b−cy−c∈R \measuredangle CBX = \measuredangle YCB \iff \frac{x-b}{c-b}\div \frac{b-c}{y-c} \in \mathbb{R}∡CBX=∡YCB⟺c−bx−b​÷y−cb−c​∈R 注意到 b−c∈Rb-c\in\mathbb{R}b−c∈R,因此 ∡CBX=∡YCB  ⟺  (x−b)(y−c)∈R  ⟺  (ε‾−b)(f‾−c)∈R \begin{aligned} \measuredangle CBX = \measuredangle YCB & \iff (x-b)(y-c) \in \mathbb{R} \\[1ex] & \iff \left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right) \in \mathbb{R} \end{aligned}∡CBX=∡YCB​⟺(x−b)(y−c)∈R⟺(ε−b)(f​−c)∈R​ 化简可得 (ε‾−b)(f‾−c)=(μ c‾ e−iθ−b)(λ b‾ eiθ−c)=(s−is e−iθ−i eiθ⋅2s−i⋅e−iθ−−2tit−i)⋅(t−it eiθ−i e−iθ⋅2t−i⋅eiθ−−2sis−i)=2(t−i+tsi)e−iθ+2t eiθ(t−i)(s e−iθ−i eiθ)⋅2(s−i+tsi)eiθ+2s e−iθ(s−i)(t eiθ−i e−iθ) \begin{aligned} \left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right) & = \left(\mu\,\overline{c}\,\mathrm{e}^{-i\theta}-b\right) \left(\lambda\,\overline{b}\,\mathrm{e}^{i\theta}-c\right) \\[2ex] & = \left(\frac{s-i}{s\,\mathrm{e}^{-i\theta}-i\,\mathrm{e}^{i\theta}} \cdot\frac{2}{s-i} \cdot\mathrm{e}^{-i\theta} -\frac{-2ti}{t-i}\right) \cdot \left(\frac{t-i}{t\,\mathrm{e}^{i\theta}-i\,\mathrm{e}^{-i\theta}} \cdot\frac{2}{t-i} \cdot\mathrm{e}^{i\theta} -\frac{-2si}{s-i}\right) \\[3ex] & = \frac{2\left(t-i+tsi\right)\mathrm{e}^{-i\theta}+2t\,\mathrm{e}^{i\theta}} {\left(t-i\right)\left(s\,\mathrm{e}^{-i\theta}-i\,\mathrm{e}^{i\theta}\right)} \cdot \frac{2\left(s-i+tsi\right)\mathrm{e}^{i\theta}+2s\,\mathrm{e}^{-i\theta}} {\left(s-i\right)\left(t\,\mathrm{e}^{i\theta}-i\,\mathrm{e}^{-i\theta}\right)} \end{aligned}(ε−b)(f​−c)​=(μce−iθ−b)(λbeiθ−c)=(se−iθ−ieiθs−i​⋅s−i2​⋅e−iθ−t−i−2ti​)⋅(teiθ−ie−iθt−i​⋅t−i2​⋅eiθ−s−i−2si​)=(t−i)(se−iθ−ieiθ)2(t−i+tsi)e−iθ+2teiθ​⋅(s−i)(teiθ−ie−iθ)2(s−i+tsi)eiθ+2se−iθ​​ 接下来验证 (ε‾−b)(f‾−c)‾=(ε‾−b)(f‾−c) \overline{\left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right)} = \left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right)(ε−b)(f​−c)​=(ε−b)(f​−c) 即可。 验证过程 (ε‾−b)(f‾−c)‾=2(1t+i−its)eiθ+2t e−iθ(1t−1i)(1s eiθ−1i e−iθ)⋅2(1s+i−its)e−iθ+2s eiθ(1s−1i)(1t e−iθ−1i eiθ)=2(s+tsi−i)eiθ+2s e−iθ−(i−t)(i eiθ−s e−iθ)⋅2(t+tsi−i)e−iθ+2t eiθ−(i−s)(i e−iθ−t eiθ)=(ε‾−b)(f‾−c) \begin{aligned} \overline{\left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right)} & =\frac{2\left(\frac{1}{t}+i-\frac{i}{ts}\right)\mathrm{e}^{i\theta}+\frac{2}{t}\,\mathrm{e}^{-i\theta}} {\left(\frac{1}{t}-\frac{1}{i}\right)\left(\frac{1}{s}\,\mathrm{e}^{i\theta}-\frac{1}{i}\,\mathrm{e}^{-i\theta}\right)} \cdot \frac{2\left(\frac{1}{s}+i-\frac{i}{ts}\right)\mathrm{e}^{-i\theta}+\frac{2}{s}\,\mathrm{e}^{i\theta}} {\left(\frac{1}{s}-\frac{1}{i}\right)\left(\frac{1}{t}\,\mathrm{e}^{-i\theta}-\frac{1}{i}\,\mathrm{e}^{i\theta}\right)} \\[3ex] & = \frac{2\left(s+tsi-i\right)\mathrm{e}^{i\theta}+2s\,\mathrm{e}^{-i\theta}} {-\left(i-t\right)\left(i\,\mathrm{e}^{i\theta}-s\,\mathrm{e}^{-i\theta}\right)} \cdot \frac{2\left(t+tsi-i\right)\mathrm{e}^{-i\theta}+2t\,\mathrm{e}^{i\theta}} {-\left(i-s\right)\left(i\,\mathrm{e}^{-i\theta}-t\,\mathrm{e}^{i\theta}\right)} \\[2ex] & = \left(\overline{\varepsilon}-b\right)\left(\overline{f}-c\right) \end{aligned}(ε−b)(f​−c)​​=(t1​−i1​)(s1​eiθ−i1​e−iθ)2(t1​+i−tsi​)eiθ+t2​e−iθ​⋅(s1​−i1​)(t1​e−iθ−i1​eiθ)2(s1​+i−tsi​)e−iθ+s2​eiθ​=−(i−t)(ieiθ−se−iθ)2(s+tsi−i)eiθ+2se−iθ​⋅−(i−s)(ie−iθ−teiθ)2(t+tsi−i)e−iθ+2teiθ​=(ε−b)(f​−c)​ 命题得证。 4. 另一种解法 考虑 △BIF\triangle BIF△BIF 和 △CIE\triangle CIE△CIE, BFsin⁡∠BIF=IFsin⁡12∠ABCCEsin⁡∠CIE=IEsin⁡12∠ACB \begin{aligned} \frac{BF}{\sin \angle BIF} & = \frac{IF}{\sin \frac{1}{2}\angle ABC} \\ \frac{CE}{\sin \angle CIE} & = \frac{IE}{\sin \frac{1}{2}\angle ACB} \end{aligned}sin∠BIFBF​sin∠CIECE​​=sin21​∠ABCIF​=sin21​∠ACBIE​​ 因此 BFCE=IFIE⋅sin⁡12∠ACBsin⁡12∠ABC \frac{BF}{CE} = \frac{IF}{IE}\cdot \frac{\sin \frac{1}{2}\angle ACB}{\sin \frac{1}{2}\angle ABC}CEBF​=IEIF​⋅sin21​∠ABCsin21​∠ACB​ 设点 FFF 关于 IBIBIB 的对称点为 KKK,点 EEE 关于 ICICIC 的对称点为 LLL,则 KKK、LLL 都在 BCBCBC 上。 设 lll 与 ABABAB、ACACAC 交于点 MMM、NNN,则 ∠YIK=∠FIK−∠FIY=2(∠FIB−∠FIM)=2∠BIM=∠ABC \begin{aligned} \angle YIK & = \angle FIK - \angle FIY \\ & = 2(\angle FIB-\angle FIM) \\ & = 2\angle BIM \\ & = \angle ABC \end{aligned}∠YIK​=∠FIK−∠FIY=2(∠FIB−∠FIM)=2∠BIM=∠ABC​ 同理可知 ∠XIL=∠ACB\angle XIL = \angle ACB∠XIL=∠ACB。 注意到 ∠BIK=∠BIF=∠CIE=∠CIL \angle BIK = \angle BIF = \angle CIE = \angle CIL∠BIK=∠BIF=∠CIE=∠CIL 因此 BK⋅BLCK⋅CL=BI2CI2 \frac{BK\cdot BL}{CK\cdot CL} = \frac{BI^2}{CI^2}CK⋅CLBK⋅BL​=CI2BI2​ 结合前面的结论可得 BLCK=CLBK⋅BI2CI2=CEBF⋅BI2CI2=IEIF⋅sin⁡12∠ABCsin⁡12∠ACB⋅sin⁡212∠ACBsin⁡212∠ABC=IE⋅sin⁡12∠ACBIF⋅sin⁡12∠ABC=LXYK \begin{aligned} \frac{BL}{CK} & = \frac{CL}{BK}\cdot \frac{BI^2}{CI^2} = \frac{CE}{BF}\cdot \frac{BI^2}{CI^2} \\[2ex] & = \frac{IE}{IF} \cdot \frac{\sin \frac{1}{2}\angle ABC}{\sin \frac{1}{2}\angle ACB} \cdot \frac{\sin^2 \frac{1}{2}\angle ACB}{\sin^2 \frac{1}{2}\angle ABC} \\[2ex] & = \frac{IE\cdot \sin \frac{1}{2}\angle ACB}{IF\cdot \sin \frac{1}{2}\angle ABC} \\[2ex] & = \frac{LX}{YK} \end{aligned}CKBL​​=BKCL​⋅CI2BI2​=BFCE​⋅CI2BI2​=IFIE​⋅sin21​∠ACBsin21​∠ABC​⋅sin221​∠ABCsin221​∠ACB​=IF⋅sin21​∠ABCIE⋅sin21​∠ACB​=YKLX​​ 注意到 ∠BLX=∠BLI+∠ILX=(∠CIL+12∠ACB)+(90∘−12∠ACB)=∠CIE+90∘ \begin{aligned} \angle BLX & = \angle BLI + \angle ILX \\ & = \left(\angle CIL + \frac{1}{2}\angle ACB\right) + \left(90^\circ-\frac{1}{2}\angle ACB\right) \\[1ex] & = \angle CIE + 90^\circ \end{aligned}∠BLX​=∠BLI+∠ILX=(∠CIL+21​∠ACB)+(90∘−21​∠ACB)=∠CIE+90∘​ 同理可得,∠CKY=∠BIF+90∘\angle CKY = \angle BIF + 90^\circ∠CKY=∠BIF+90∘,因此 ∠BLX=∠CKY\angle BLX = \angle CKY∠BLX=∠CKY。 综上可知 △BLX∼△CKY\triangle BLX\sim \triangle CKY△BLX∼△CKY,于是 ∠XBC=∠YCB\angle XBC = \angle YCB∠XBC=∠YCB,命题得证。

2025/8/11
articleCard.readMore