Gemini_Generated_Image_r030rmr030rmr030.jpeg

一次 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,可以清晰还原异常触发全过程:

  1. 入口层 (post.php):视图调用 $this->content() 渲染文章内容。
  2. 框架层 (Widget_Archive / Plugin.php):Typecho 触发 contentEx 插件钩子。
  3. 插件层 (TableOfContents/Plugin.php):文章目录生成插件接管 HTML 文本。
  4. 崩溃点 (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');
    ...
}

原因链条:

  1. 文章中包含海量代码块及 <span> 标签,导致整体 DOM 树体量过大。
  2. str_get_html() (即 simple_html_dom 解析器) 在解析巨型或嵌套极其复杂的 HTML 时超出内存/递归限制,解析失败并返回了 false
  3. 插件缺少防错校验,未判断 $html 是否为布尔值 false,直接执行 $html->find()
  4. 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> ──> 正常输出

修改后刷新页面:目录正常渲染,代码高亮完好,页面不再崩溃。

五、总结

这次排查的核心经验在于:

  1. 不要凭感觉调试:现象是“代码块多导致崩溃”,很容易让人死磕高亮插件。但如果不开启 Debug 模式看 Stack Trace,就永远无法发现底层是 TableOfContents 调用的第三方解析库崩溃。

  2. 健壮性设计防守:在调用外部库或处理不确定输入(如用户自定义 HTML)时,任何返回接口都必须有边界防御(如对 falsenull 的断言)。

  3. AI 协作的正确姿势:AI 无法凭空定位私有环境问题。效率最高的方式是提供准确的上下文与线索(现象阈值 -> 报错日志 -> 堆栈跟踪 -> 涉及源码),由 AI 提供推理链条与解决手段,共同完成排查闭环。


以上排查过程与文章内容,在问题解决后由 AI 协助整理生成。

P.S. 这个报错其实困扰我挺久了。先前为了避开崩溃,我只是“按需裁切”,分多篇文章,把 <pre></pre> 代码块的规模控制在安全阈值内,一直懒得折腾。最近准备丰富文章内容,结果页面再次崩溃。这次索性不再妥协,直接交给 AI 协助深度排查,终于彻底搞定了这个历史遗留问题。

AI 666

最后修改:2026 年 08 月 20 日 01 : 11 PM