
一次 Typecho 文章崩溃的排查:我和 AI 一起解决了 Call to a member function find() on boolean
最近在维护博客时,遇到了一个非常奇怪的问题:
全站所有页面正常,唯独一篇文章打不开。
打开该文章时,页面直接抛出 Fatal Error:
Call to a member function find() on boolean
一开始我毫无头绪。通过对比发现,这篇文章和其他文章最大的区别在于包含大量长代码块:
<pre><code>
<span>...</span>
<span>...</span>
<span>...</span>
</code></pre>
代码经前端高亮后,产生了成百上千个 <span> 标签。
进一步测试后,我发现了关键现象:
当
<span>标签数量少于某一临界值时,页面正常;一旦超过阈值,直接崩溃。
这并非简单的文章内容语法错误,更像是:某个 DOM/HTML 解析器在面对庞大 HTML 树时超出了处理极限。
于是,我开始与 AI 一起排查。
一、逐步排除“嫌疑人”
1. 代码高亮插件(CodePrettify)
文章中含有大量高亮生成的 <span>,第一嫌疑自然是负责高亮的 CodePrettify 插件。
- 测试:禁用 CodePrettify。
- 结果:依然崩溃。成功排除该插件。
2. Markdown 解析插件(MarkdownParse)
页面是在解析 HTML 时崩溃的,接着怀疑是第三方 Markdown 解析插件对复杂 DOM 的处理问题。
- 测试:禁用 MarkdownParse。
- 结果:依然崩溃。
至此可以推断:问题不在“生成代码块的插件”,而在“后续二次解析文章 HTML 的环节”。
3. 主题模板(Handsome)
开始排查 Handsome 主题内部的 HTML 解析逻辑
grep -R -n -- "->find(" /home/wwwroot/www/typecho/usr/themes/handsome
虽然在 libs/component/ParserDom.php 中看到了基于 \DOMDocument() 的 HTML 解析逻辑,但无法直接确认其与错误的因果关系。排查陷入瓶颈。
二、关键转折:开启 Debug 模式与 Stack Trace 分析
在默认状态下,Typecho 和 PHP 仅向页面输出简单的错误信息,屏蔽了详细的调用堆栈(Stack Trace)。盲目猜测效率极低,拿到完整的 Stack Trace 是定位根源的关键。
补充:Typecho 如何开启 Debug 模式?
打开站点根目录下的
config.inc.php,找到或添加以下代码:/** 开启 Debug 模式信息输出 */ define('__TYPECHO_DEBUG__', true);开启后,PHP 的 Fatal Error 和未捕获异常将直接在页面展示完整的调用堆栈。(排查结束后建议改回
false,避免泄漏服务器敏感路径)。
开启 Debug 模式后刷新页面,获得了关键的错误堆栈(Stack Trace):
Call to a member function find() on boolean
Error: Call to a member function find() on boolean in /home/wwwroot/typecho/usr/plugins/TableOfContents/Plugin.php:234
Stack trace:
#0 /home/wwwroot/typecho/usr/plugins/TableOfContents/Plugin.php(108): TableOfContents_Plugin::create_toc_with_dom('<p><img src="ht...')
#1 /home/wwwroot/typecho/var/Typecho/Plugin.php(489): TableOfContents_Plugin::replace('<p><img src="ht...', Object(Widget_Archive), NULL)
#2 /home/wwwroot/typecho/var/Widget/Abstract/Contents.php(119): Typecho_Plugin->__call('Widget_Abstract...', Array)
#3 /home/wwwroot/typecho/var/Typecho/Widget.php(388): Widget_Abstract_Contents->___excerpt()
#4 /home/wwwroot/typecho/var/Widget/Abstract/Contents.php(74): Typecho_Widget->__get('excerpt')
#5 /home/wwwroot/typecho/var/Typecho/Widget.php(388): Widget_Abstract_Contents->___description()
#6 /home/wwwroot/typecho/var/Widget/Archive.php(859): Typecho_Widget->__get('description')
#7 /home/wwwroot/typecho/var/Widget/Archive.php(1368): Widget_Archive->singleHandle(Object(Typecho_Db_Query), false)
#8 /home/wwwroot/typecho/var/Typecho/Widget.php(221): Widget_Archive->execute()
#9 /home/wwwroot/typecho/var/Typecho/Router.php(135): Typecho_Widget::widget('Widget_Archive', NULL, Array)
#10 /home/wwwroot/typecho/index.php(23): Typecho_Router::dispatch()
#11 {main}
堆栈深度解析
通过 Stack Trace,可以清晰还原异常触发全过程:
- 入口层 (
post.php):视图调用$this->content()渲染文章内容。 - 框架层 (
Widget_Archive/Plugin.php):Typecho 触发contentEx插件钩子。 - 插件层 (
TableOfContents/Plugin.php):文章目录生成插件接管 HTML 文本。 - 崩溃点 (
Plugin.php:234):在第 234 行尝试对非对象变量(boolean)调用find()方法,抛出致命错误。
直接元凶确定:不是主题,也不是高亮插件,而是文章目录插件 TableOfContents。
三、Bug 根源与逻辑闭环
查看 /home/wwwroot/www/wwwroot/typecho/usr/plugins/TableOfContents/Plugin.php 第 234 行附近源码:
public static function create_toc_with_dom($content)
{
class_exists('simple_html_dom') || require_once 'simple_html_dom.php';
// 问题代码:解析复杂的 HTML 内容
$html = str_get_html($content, 1, 1, 'UTF-8', false);
// 没有空值判断,直接调用 find()
$nodes = $html->find('h2,h3,h4');
...
}
原因链条:
- 文章中包含海量代码块及
<span>标签,导致整体 DOM 树体量过大。 str_get_html()(即simple_html_dom解析器) 在解析巨型或嵌套极其复杂的 HTML 时超出内存/递归限制,解析失败并返回了false。- 插件缺少防错校验,未判断
$html是否为布尔值false,直接执行$html->find()。 - PHP 引擎执行
false->find(),触发 Fatal Error。
四、解决方案:预处理与占位替换
解决问题的核心思路是:避免让 simple_html_dom 去解析不必要的巨型代码块。
TableOfContents 的核心诉求仅仅是提取 <h2>、<h3> 等标题节点,根本不需要关注 <pre> 内的代码细节。
在把内容交付给 simple_html_dom 前,利用正则将 <pre>...</pre> 抽离并替换为轻量占位符,生成目录后再恢复内容:
// 1. 抽离 <pre> 代码块并用占位符替代
$preBlocks = [];
$contentForToc = preg_replace_callback(
'/<pre\b[^>]*>.*?<\/pre>/is',
function ($matches) use (&$preBlocks) {
$key = '___TOC_PRE_BLOCK_' . count($preBlocks) . '___';
$preBlocks[$key] = $matches[0];
return '<p>' . $key . '</p>';
},
$content
);
// 2. 解析体积大幅缩减后的 HTML
$html = str_get_html($contentForToc, 1, 1, 'UTF-8', false);
// 3. 增加健壮性校验:若依然解析失败,安全降级退回原内容
if ($html === false) {
return $content;
}
// 4. 正常生成目录逻辑
$nodes = $html->find('h2,h3');
...
$output = $html->save();
// 5. 还原 <pre> 代码块
foreach ($preBlocks as $key => $block) {
$output = str_replace('<p>' . $key . '</p>', $block, $output);
}
return $output;
处理流程对比
【优化前】
原始文章 HTML (含巨大 <pre>) ──> simple_html_dom 内存溢出 ──> 返回 false ──> false->find() ──> 崩溃
【优化后】
原始文章 ──> 提取 <pre> 并置换为占位符 ──> 轻量化 HTML ──> simple_html_dom 正常解析 ──> 生成目录 ──> 还原 <pre> ──> 正常输出
修改后刷新页面:目录正常渲染,代码高亮完好,页面不再崩溃。
五、总结
这次排查的核心经验在于:
-
不要凭感觉调试:现象是“代码块多导致崩溃”,很容易让人死磕高亮插件。但如果不开启 Debug 模式看 Stack Trace,就永远无法发现底层是
TableOfContents调用的第三方解析库崩溃。 -
健壮性设计防守:在调用外部库或处理不确定输入(如用户自定义 HTML)时,任何返回接口都必须有边界防御(如对
false或null的断言)。 -
AI 协作的正确姿势:AI 无法凭空定位私有环境问题。效率最高的方式是提供准确的上下文与线索(现象阈值 -> 报错日志 -> 堆栈跟踪 -> 涉及源码),由 AI 提供推理链条与解决手段,共同完成排查闭环。
以上排查过程与文章内容,在问题解决后由 AI 协助整理生成。
P.S. 这个报错其实困扰我挺久了。先前为了避开崩溃,我只是“按需裁切”,分多篇文章,把 <pre></pre> 代码块的规模控制在安全阈值内,一直懒得折腾。最近准备丰富文章内容,结果页面再次崩溃。这次索性不再妥协,直接交给 AI 协助深度排查,终于彻底搞定了这个历史遗留问题。
AI 666
版权属于:毒奶
联系我们:https://limbopro.com/6.html
毒奶搜索:https://limbopro.com/search.html
番号搜索:https://limbopro.com/btsearch.html
机场推荐:https://limbopro.com/865.html IEPL专线/100Gb/¥15/月起(最高享8折优惠)
毒奶导航:https://limbopro.com/daohang/index.html本文链接:https://limbopro.com/archives/35832.html · 镜像:https://limbopro.github.io/archives/35832.html
本文采用 CC BY-NC-SA 4.0 许可协议,转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!




