|
|
@@ -6,12 +6,11 @@ class MathFormulaProcessor
|
|
|
{
|
|
|
/**
|
|
|
* 处理数学公式,确保有正确的 LaTeX 标记
|
|
|
- *
|
|
|
- * 策略:
|
|
|
- * 1. 清理 HTML 标签和实体
|
|
|
- * 2. 规范化反斜杠(处理多重转义)
|
|
|
- * 3. 修复丢失反斜杠的常见 LaTeX 命令
|
|
|
- * 4. 确保公式被正确的定界符包裹
|
|
|
+ *
|
|
|
+ * 优化策略:最小化干预,只修复真正需要修复的问题
|
|
|
+ * 1. 检查是否已有正确的 LaTeX 标记,如有则直接返回
|
|
|
+ * 2. 只在检测到明显错误时才进行修复
|
|
|
+ * 3. 优先保护正确的数学表达式不被破坏
|
|
|
*/
|
|
|
public static function processFormulas(string $content): string
|
|
|
{
|
|
|
@@ -19,84 +18,92 @@ class MathFormulaProcessor
|
|
|
return $content;
|
|
|
}
|
|
|
|
|
|
- // 1. 基础清理
|
|
|
- // 递归解码 HTML 实体
|
|
|
+ // 0. 基础清理:解码 HTML 实体
|
|
|
$decoded = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
|
|
|
while ($decoded !== $content) {
|
|
|
$content = $decoded;
|
|
|
$decoded = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
|
|
|
}
|
|
|
-
|
|
|
$content = trim($content);
|
|
|
|
|
|
- // 2. 规范化反斜杠 - 修复重复转义问题
|
|
|
- // 暂时禁用修复功能,避免进一步污染
|
|
|
- // $content = self::fixCorruptedFormulas($content);
|
|
|
-
|
|
|
- // 3. 修复常见 LaTeX 命令
|
|
|
- $commands = [
|
|
|
- 'sqrt', 'frac', 'times', 'div', 'pm', 'cdot',
|
|
|
- 'sin', 'cos', 'tan', 'log', 'ln', 'lim',
|
|
|
- 'alpha', 'beta', 'gamma', 'theta', 'pi', 'sigma', 'omega', 'Delta',
|
|
|
- 'leq', 'geq', 'neq', 'approx', 'infty',
|
|
|
- 'sum', 'prod', 'int', 'partial', 'nabla'
|
|
|
- ];
|
|
|
-
|
|
|
- $pattern = '/(?<!\\\\)\b(' . implode('|', $commands) . ')\b/';
|
|
|
- $content = preg_replace($pattern, '\\\\$1', $content);
|
|
|
-
|
|
|
- // 4. 规范化 LaTeX 命令中的空格 (OCR 常见问题)
|
|
|
- // 4.1 移除 LaTeX 命令后的空格: \frac { -> \frac{
|
|
|
- $content = preg_replace('/\\\\([a-zA-Z]+)\s+\{/', '\\\\$1{', $content);
|
|
|
-
|
|
|
- // 4.2 移除花括号内的前导和尾随空格: { 1 } -> {1}
|
|
|
- $content = preg_replace('/\{\s+/', '{', $content);
|
|
|
- $content = preg_replace('/\s+\}/', '}', $content);
|
|
|
-
|
|
|
- // 4.3 移除上标/下标符号周围的空格: x ^ { a } -> x^{a}
|
|
|
- $content = preg_replace('/\s*\^\s*\{\s*/', '^{', $content);
|
|
|
- $content = preg_replace('/\s*_\s*\{\s*/', '_{', $content);
|
|
|
-
|
|
|
- // 4.4 移除 \left 和 \right 后的空格: \left ( -> \left(
|
|
|
- $content = preg_replace('/\\\\(left|right)\s+/', '\\\\$1', $content);
|
|
|
-
|
|
|
- // 4.5 移除括号内侧的空格: ( x ) -> (x)
|
|
|
- $content = preg_replace('/\(\s+/', '(', $content);
|
|
|
- $content = preg_replace('/\s+\)/', ')', $content);
|
|
|
-
|
|
|
- // 4.6 规范化多个连续空格为单个空格
|
|
|
- $content = preg_replace('/\s+/', ' ', $content);
|
|
|
-
|
|
|
- // 4.7 清理 OCR 错误产生的多余 $ 符号
|
|
|
- // 移除花括号内的 $: {a$} -> {a}
|
|
|
- $content = preg_replace('/\{([^}]*)\$+([^}]*)\}/', '{$1$2}', $content);
|
|
|
- // 移除末尾的多余 $$$
|
|
|
- $content = preg_replace('/\$+\s*$/', '', $content);
|
|
|
- // 移除开头的多余 $$$
|
|
|
- $content = preg_replace('/^\s*\$+/', '', $content);
|
|
|
- // 移除连续的 $$$ (3个或更多)
|
|
|
- $content = preg_replace('/\$\$\$+/', '$$', $content);
|
|
|
-
|
|
|
- // 5. 处理定界符
|
|
|
- // 如果内容已经是完整的公式(被 $ 或 $$ 包裹),则保持原样
|
|
|
+ // 1. 如果已有正确的定界符,只清理内部 HTML,不做其他修改
|
|
|
if (self::hasDelimiters($content)) {
|
|
|
- $content = self::cleanInsideDelimiters($content);
|
|
|
- return $content;
|
|
|
+ return self::cleanInsideDelimiters($content);
|
|
|
}
|
|
|
|
|
|
- // 6. 智能包装 (统一处理混合内容)
|
|
|
- // 无论是纯文本还是富文本,都使用智能识别来包裹公式
|
|
|
- // 这能同时处理:
|
|
|
- // - "已知函数 f(x) = ..." (未包裹的混合内容)
|
|
|
- // - "验证:$2x...$" (部分包裹的混合内容)
|
|
|
- // - "4x^2 - 25y^2" (未包裹的纯公式)
|
|
|
-
|
|
|
- // 先清理已有的定界符内部
|
|
|
- $content = self::cleanInsideDelimiters($content);
|
|
|
- // 然后智能包裹剩余的数学部分
|
|
|
- $content = self::smartWrapMixedContent($content);
|
|
|
+ // 2. 检测内容类型:纯数学、混合内容还是纯文本
|
|
|
+ $contentType = self::detectContentType($content);
|
|
|
|
|
|
- return $content;
|
|
|
+ // 3. 根据内容类型采取不同的处理策略
|
|
|
+ switch ($contentType) {
|
|
|
+ case 'pure_math':
|
|
|
+ // 纯数学表达式,如 "4x^2 - 25y^2" 或 "f(x) = x^2 - 4x + 5"
|
|
|
+ return self::wrapPureMath($content);
|
|
|
+
|
|
|
+ case 'mixed_content':
|
|
|
+ // 混合内容,如 "已知函数 f(x) = x^2 - 4x + 5,求最小值"
|
|
|
+ return self::smartWrapMixedContent($content);
|
|
|
+
|
|
|
+ case 'plain_text':
|
|
|
+ default:
|
|
|
+ // 纯文本,不需要处理
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检测内容类型
|
|
|
+ * 优化:加入中文检测,避免包裹包含中文的混合内容
|
|
|
+ */
|
|
|
+ private static function detectContentType(string $content): string
|
|
|
+ {
|
|
|
+ // 检查是否有定界符
|
|
|
+ if (self::hasDelimiters($content)) {
|
|
|
+ return 'delimited';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含数学特征
|
|
|
+ $hasMathFeatures = self::containsMathFeatures($content);
|
|
|
+
|
|
|
+ // 如果不包含数学特征,返回纯文本
|
|
|
+ if (!$hasMathFeatures) {
|
|
|
+ return 'plain_text';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含中文字符
|
|
|
+ if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $content)) {
|
|
|
+ // 包含中文 + 数学特征 = 混合内容,需要智能提取数学部分
|
|
|
+ return 'mixed_content';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含长文本(超过一定长度的字母组合)
|
|
|
+ $hasLongText = preg_match('/[a-zA-Z]{8,}/', $content);
|
|
|
+
|
|
|
+ if ($hasLongText) {
|
|
|
+ // 包含长文本,可能是混合内容,但不包裹(保守策略)
|
|
|
+ return 'plain_text';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是纯数学还是混合内容
|
|
|
+ // 混合内容:同时包含数学特征和普通英文单词
|
|
|
+ $hasPlainText = preg_match('/\b[a-zA-Z]{3,7}\b/', $content) &&
|
|
|
+ !preg_match('/^[a-zA-Z0-9\+\-\*\/\=\s\.\^\(\)\_\{\}]+$/', $content);
|
|
|
+
|
|
|
+ if ($hasPlainText) {
|
|
|
+ return 'mixed_content';
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'pure_math';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 包裹纯数学表达式
|
|
|
+ * 优化:只添加定界符,不修改内容本身
|
|
|
+ */
|
|
|
+ private static function wrapPureMath(string $content): string
|
|
|
+ {
|
|
|
+ // 已经是纯数学格式,直接用 $ 包裹
|
|
|
+ return '$' . $content . '$';
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -144,67 +151,50 @@ class MathFormulaProcessor
|
|
|
|
|
|
/**
|
|
|
* 智能识别并包裹富文本中的数学公式
|
|
|
+ * 支持:函数定义、导数表达式、LaTeX命令、数学运算
|
|
|
*/
|
|
|
private static function smartWrapMixedContent(string $content): string
|
|
|
{
|
|
|
- // 正则策略:匹配 HTML 标签 OR 数学公式候选
|
|
|
- // 捕获组 1: HTML 标签 (忽略)
|
|
|
- // 捕获组 2: 已有的定界符 (忽略)
|
|
|
- // 捕获组 3: 数学公式 (处理)
|
|
|
-
|
|
|
+ // 匹配策略:只匹配明确的数学表达式,避免误判
|
|
|
$tagPattern = '<[^>]+>';
|
|
|
-
|
|
|
- // 匹配已有的定界符
|
|
|
$existingDelimiterPattern = '(?:\$\$[\s\S]*?\$\$|\$[\s\S]*?\$|\\\\\([\s\S]*?\\\\\)|\\\\\[[\s\S]*?\\\\\])';
|
|
|
-
|
|
|
- // 数学公式特征:
|
|
|
- // 1. 函数定义: f(x) = ...
|
|
|
- // 2. 等式/不等式: ... = ..., ... > ..., ... < ...
|
|
|
- // 3. 包含 LaTeX 命令: \sqrt, \frac 等
|
|
|
- // 4. 包含上标/下标: x^2, a_n
|
|
|
-
|
|
|
- // 匹配函数定义或等式 (例如 f(x) = 2x^2 + 1)
|
|
|
- // 必须包含 = 或 > 或 <,且周围有类数学字符
|
|
|
- $equationPattern = '(?<![\w\\\\])(?:[a-zA-Z]\([a-zA-Z0-9,]+\)|[a-zA-Z0-9\^_\{\}]+)\s*[=<>]\s*[\w\s\+\-\*\/\^\.\(\)\{\}\\\\]+(?=\s|$|<|[.,;])';
|
|
|
-
|
|
|
- // 匹配显式 LaTeX 命令 (例如 \sqrt{...})
|
|
|
- $latexPattern = '\\\\[a-zA-Z]+(?:\{[^\}]*\})?';
|
|
|
-
|
|
|
- // 匹配简单的代数项 (例如 x^2, a_n) - 需谨慎,避免匹配普通单词
|
|
|
- $algebraPattern = '(?<![\w\\\\])[a-zA-Z0-9]+\^[\w\{]+';
|
|
|
-
|
|
|
- // 匹配多项式/复杂表达式 (例如 4x^2 - 25y^2, 2x \times 2 + ...)
|
|
|
- // 特征:包含变量、数字、运算符 (+, -, *, /)、LaTeX命令、上标/下标
|
|
|
- // 必须包含至少一个运算符,且长度适中
|
|
|
- $polynomialPattern = '(?<![\w\\\\])(?:[a-zA-Z0-9\.]+(?:[\^_\{\}][a-zA-Z0-9\.\{\}]+)?|\\\\[a-zA-Z]+(?:\{[^\}]*\})?)(?:\s*[\+\-\*\/]\s*(?:[a-zA-Z0-9\.]+(?:[\^_\{\}][a-zA-Z0-9\.\{\}]+)?|\\\\[a-zA-Z]+(?:\{[^\}]*\})?))+';
|
|
|
-
|
|
|
- $pattern = "/($tagPattern)|($existingDelimiterPattern)|($equationPattern|$polynomialPattern|$latexPattern|$algebraPattern)/u";
|
|
|
+
|
|
|
+ // 数学公式模式(按优先级排列)
|
|
|
+ $patterns = [
|
|
|
+ // 1. 函数定义: f(x) = 2x^3 - 3x^2 + 4x - 5
|
|
|
+ "[a-zA-Z]'?\\([a-zA-Z0-9,\\s]+\\)\\s*=\\s*[a-zA-Z0-9\\+\\-\\*\\/\\^\\s\\.\\(\\)\\_\\{\\}]+",
|
|
|
+ // 2. 导数/函数调用: f'(1), g(5), sin(x)
|
|
|
+ "[a-zA-Z]+'?\\([a-zA-Z0-9\\+\\-\\*\\/\\^\\s\\.]+\\)",
|
|
|
+ // 3. LaTeX 命令: \frac{1}{2}
|
|
|
+ "\\\\[a-zA-Z]+\\{[^}]*\\}(?:\\{[^}]*\\})?",
|
|
|
+ // 4. 数学表达式: x^2 + y^2, 2x - 3
|
|
|
+ "[a-zA-Z0-9]+[\\^_][a-zA-Z0-9\\{\\}]+(?:\\s*[\\+\\-\\*\\/]\\s*[a-zA-Z0-9\\^_\\{\\}\\.]+)*",
|
|
|
+ ];
|
|
|
+
|
|
|
+ $mathPattern = '(?:' . implode('|', $patterns) . ')';
|
|
|
+ $pattern = "/($tagPattern)|($existingDelimiterPattern)|($mathPattern)/u";
|
|
|
|
|
|
return preg_replace_callback($pattern, function ($matches) {
|
|
|
- // 如果是 HTML 标签 (组1),原样返回
|
|
|
+ // HTML 标签,原样返回
|
|
|
if (!empty($matches[1])) {
|
|
|
return $matches[1];
|
|
|
}
|
|
|
-
|
|
|
- // 如果是已有的定界符 (组2),原样返回
|
|
|
+
|
|
|
+ // 已有的定界符,原样返回
|
|
|
if (!empty($matches[2])) {
|
|
|
return $matches[2];
|
|
|
}
|
|
|
-
|
|
|
- // 如果是数学公式 (组3)
|
|
|
+
|
|
|
+ // 数学公式,添加 $ 包裹
|
|
|
if (!empty($matches[3])) {
|
|
|
- $math = $matches[3];
|
|
|
- // 再次检查是否已经被包裹 (虽然外层逻辑应该处理了,但为了安全)
|
|
|
+ $math = trim($matches[3]);
|
|
|
+ // 再次检查是否已经包裹
|
|
|
if (str_contains($math, '$')) {
|
|
|
return $math;
|
|
|
}
|
|
|
- // 排除纯数字或普通单词误判
|
|
|
- if (preg_match('/^[a-zA-Z0-9\s]+$/', $math)) {
|
|
|
- return $math;
|
|
|
- }
|
|
|
return '$' . $math . '$';
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return $matches[0];
|
|
|
}, $content);
|
|
|
}
|
|
|
@@ -237,26 +227,42 @@ class MathFormulaProcessor
|
|
|
|
|
|
/**
|
|
|
* 检测数学特征
|
|
|
+ * 优化:更精确的检测,减少误判
|
|
|
*/
|
|
|
private static function containsMathFeatures(string $content): bool
|
|
|
{
|
|
|
// 1. 检查是否有 LaTeX 命令
|
|
|
- if (strpos($content, '\\') !== false) {
|
|
|
+ if (preg_match('/\\\\[a-zA-Z]+\{?/', $content)) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // 2. 检查数学符号
|
|
|
- $symbols = ['+', '-', '*', '/', '=', '<', '>', '^', '_', '{', '}'];
|
|
|
- foreach ($symbols as $symbol) {
|
|
|
- if (strpos($content, $symbol) !== false) {
|
|
|
- // 排除普通文本中的符号(如连字符),这里做一个简单的宽容判断
|
|
|
- // 如果有数字紧随其后,或者是特定组合
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // 2. 检查函数定义或等式(如 f(x) =, g(x) =)
|
|
|
+ // 必须是:字母+括号+等号+数学内容
|
|
|
+ if (preg_match('/[a-zA-Z]\([a-zA-Z0-9,\s]+\)\s*=\s*[a-zA-Z0-9\+\-\*\/\^\.\(\)\s\\\\_\{]+/', $content)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 检查纯数学表达式(只包含数字、变量、运算符、括号)
|
|
|
+ // 严格的数学表达式:必须包含字母和运算符,且没有中文字符
|
|
|
+ if (preg_match('/^[a-zA-Z0-9\+\-\*\/\=\s\.\^\(\)\_\{\}]+$/', $content) &&
|
|
|
+ preg_match('/[a-zA-Z]/', $content) &&
|
|
|
+ preg_match('/[\+\-\*\/\=\^]/', $content)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 检查包含变量的数学表达式(带约束)
|
|
|
+ // 必须有明确的运算符连接,且周围是数学内容
|
|
|
+ if (preg_match('/[a-zA-Z0-9\.\^\_\{\}]\s*[\+\-\*\/]\s*[a-zA-Z0-9\.\^\_\{\}\(\)]/', $content)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 检查分数形式(如 \frac{}{})
|
|
|
+ if (preg_match('/\\\\frac\{/', $content)) {
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- // 3. 检查数字和字母的组合 (如 2x, x^2)
|
|
|
- if (preg_match('/[a-zA-Z]\d|\d[a-zA-Z]/', $content)) {
|
|
|
+ // 6. 检查上标或下标(仅当与数字/字母组合时)
|
|
|
+ if (preg_match('/[a-zA-Z0-9]\s*[\^_]\s*[a-zA-Z0-9]/', $content)) {
|
|
|
return true;
|
|
|
}
|
|
|
|