| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- <?php
- namespace App\Services;
- class MathFormulaProcessor
- {
- /**
- * 处理数学公式,确保有正确的 LaTeX 标记
- *
- * 优化策略:最小化干预,只修复真正需要修复的问题
- * 1. 检查是否已有正确的 LaTeX 标记,如有则直接返回
- * 2. 只在检测到明显错误时才进行修复
- * 3. 优先保护正确的数学表达式不被破坏
- */
- public static function processFormulas(string $content): string
- {
- if (empty($content)) {
- return $content;
- }
- // 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);
- // 1. 如果已有正确的定界符,只清理内部 HTML,不做其他修改
- if (self::hasDelimiters($content)) {
- return self::cleanInsideDelimiters($content);
- }
- // 2. 检测内容类型:纯数学、混合内容还是纯文本
- $contentType = self::detectContentType($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 . '$';
- }
- /**
- * 清理定界符内部的 HTML 标签
- */
- private static function cleanInsideDelimiters(string $content): string
- {
- // 修复:使用更精确的正则表达式,避免模式冲突
- // 先处理 $$...$$ 模式,然后处理单个 $...$ 模式(但确保不被$$包含)
- // 1. 处理 $$...$$ 显示公式
- $content = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($matches) {
- $cleanContent = strip_tags($matches[1]);
- $cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
- $cleanContent = trim($cleanContent);
- return '$$' . $cleanContent . '$$';
- }, $content);
- // 2. 处理 \(...\) 行内公式
- $content = preg_replace_callback('/\\\\\(([\s\S]*?)\\\\\)/', function ($matches) {
- $cleanContent = strip_tags($matches[1]);
- $cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
- $cleanContent = trim($cleanContent);
- return '\\(' . $cleanContent . '\\)';
- }, $content);
- // 3. 处理 \[...\] 显示公式
- $content = preg_replace_callback('/\\\\\[([\s\S]*?)\\\\\]/', function ($matches) {
- $cleanContent = strip_tags($matches[1]);
- $cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
- $cleanContent = trim($cleanContent);
- return '\\[' . $cleanContent . '\\]';
- }, $content);
- // 4. 最后处理 $...$ 行内公式(避免与$$冲突)
- $content = preg_replace_callback('/(?<!\$)\$([^$\n]+?)\$(?!\$)/', function ($matches) {
- $cleanContent = strip_tags($matches[1]);
- $cleanContent = html_entity_decode($cleanContent, ENT_QUOTES, 'UTF-8');
- $cleanContent = trim($cleanContent);
- return '$' . $cleanContent . '$';
- }, $content);
- return $content;
- }
- /**
- * 智能识别并包裹富文本中的数学公式
- * 支持:函数定义、导数表达式、LaTeX命令、数学运算
- */
- private static function smartWrapMixedContent(string $content): string
- {
- // 匹配策略:只匹配明确的数学表达式,避免误判
- $tagPattern = '<[^>]+>';
- $existingDelimiterPattern = '(?:\$\$[\s\S]*?\$\$|\$[\s\S]*?\$|\\\\\([\s\S]*?\\\\\)|\\\\\[[\s\S]*?\\\\\])';
- // 数学公式模式(按优先级排列)
- $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 标签,原样返回
- if (!empty($matches[1])) {
- return $matches[1];
- }
- // 已有的定界符,原样返回
- if (!empty($matches[2])) {
- return $matches[2];
- }
- // 数学公式,添加 $ 包裹
- if (!empty($matches[3])) {
- $math = trim($matches[3]);
- // 再次检查是否已经包裹
- if (str_contains($math, '$')) {
- return $math;
- }
- return '$' . $math . '$';
- }
- return $matches[0];
- }, $content);
- }
- /**
- * 检查是否已有定界符
- */
- private static function hasDelimiters(string $content): bool
- {
- $content = trim($content);
- // 检查 $$...$$
- if (str_starts_with($content, '$$') && str_ends_with($content, '$$')) {
- return true;
- }
- // 检查 $...$
- if (str_starts_with($content, '$') && str_ends_with($content, '$')) {
- return true;
- }
- // 检查 \[...\]
- if (str_starts_with($content, '\\[') && str_ends_with($content, '\\]')) {
- return true;
- }
- // 检查 \(...\)
- if (str_starts_with($content, '\\(') && str_ends_with($content, '\\)')) {
- return true;
- }
- return false;
- }
- /**
- * 检测数学特征
- * 优化:更精确的检测,减少误判
- */
- private static function containsMathFeatures(string $content): bool
- {
- // 1. 检查是否有 LaTeX 命令
- if (preg_match('/\\\\[a-zA-Z]+\{?/', $content)) {
- 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;
- }
- // 6. 检查上标或下标(仅当与数字/字母组合时)
- if (preg_match('/[a-zA-Z0-9]\s*[\^_]\s*[a-zA-Z0-9]/', $content)) {
- return true;
- }
- return false;
- }
- /**
- * 批量处理
- */
- public static function processArray(array $data, array $fieldsToProcess): array
- {
- foreach ($data as $key => &$value) {
- if (in_array($key, $fieldsToProcess) && is_string($value)) {
- $value = self::processFormulas($value);
- } elseif (is_array($value)) {
- $value = self::processArray($value, $fieldsToProcess);
- }
- }
- return $data;
- }
- /**
- * 处理题目数据
- */
- public static function processQuestionData(array $question): array
- {
- $fieldsToProcess = [
- 'stem', 'content', 'question_text', 'answer',
- 'correct_answer', 'student_answer', 'explanation',
- 'solution', 'question_content'
- ];
- return self::processArray($question, $fieldsToProcess);
- }
- /**
- * 修复被污染的数学公式(包含重复的转义字符)
- */
- private static function fixCorruptedFormulas(string $content): string
- {
- // 简化的修复策略,只处理明确的问题
- // 1. 将超过2个连续的$符号减少为2个
- $content = preg_replace('/\${3,}/', '$$', $content);
- // 2. 修复$$B . - \frac{1}{2}$$ 这种格式,在选项前加空格
- $content = preg_replace('/\$\$([A-Z])\s*\.\s*/', '$$ $1. ', $content);
- // 3. 修复不完整的frac命令:\frac{1}{2} -> \frac{1}{2}
- $content = preg_replace('/\\\\frac\\\\({[^}]+)([^}]*)\\\\/', '\\\\frac$1}{$2}', $content);
- // 4. 移除孤立的反斜杠(在非LaTeX命令前的)
- $content = preg_replace('/\\\\(?![a-zA-Z{])/', '', $content);
- return $content;
- }
- }
|