LatexCleanerService.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. <?php
  2. namespace App\Services;
  3. /**
  4. * LaTeX 清理服务
  5. * 专门用于清理 OCR 识别返回的 LaTeX 公式中的常见错误
  6. * 在数据存入数据库之前进行预处理
  7. */
  8. class LatexCleanerService
  9. {
  10. /**
  11. * 清理 LaTeX 文本
  12. *
  13. * @param string $latex 原始 LaTeX 文本
  14. * @return string 清理后的 LaTeX 文本
  15. */
  16. public function clean(string $latex): string
  17. {
  18. if (empty($latex)) {
  19. return $latex;
  20. }
  21. // 1. 基础清理
  22. $latex = $this->basicCleanup($latex);
  23. // 2. 空格规范化
  24. $latex = $this->normalizeWhitespace($latex);
  25. // 3. 清理错误的定界符
  26. $latex = $this->cleanDelimiters($latex);
  27. // 4. 修复常见的 LaTeX 命令
  28. $latex = $this->fixCommonCommands($latex);
  29. // 5. 清理括号匹配问题
  30. $latex = $this->fixBraces($latex);
  31. return trim($latex);
  32. }
  33. /**
  34. * 基础清理
  35. */
  36. protected function basicCleanup(string $latex): string
  37. {
  38. // 递归解码 HTML 实体
  39. $decoded = html_entity_decode($latex, ENT_QUOTES, 'UTF-8');
  40. while ($decoded !== $latex) {
  41. $latex = $decoded;
  42. $decoded = html_entity_decode($latex, ENT_QUOTES, 'UTF-8');
  43. }
  44. // 移除 HTML 标签
  45. $latex = strip_tags($latex);
  46. return $latex;
  47. }
  48. /**
  49. * 空格规范化 - 处理 OCR 常见的空格问题
  50. */
  51. protected function normalizeWhitespace(string $latex): string
  52. {
  53. // 1. 移除 LaTeX 命令后的空格: \frac { -> \frac{
  54. $latex = preg_replace('/\\\\([a-zA-Z]+)\s+\{/', '\\\\$1{', $latex);
  55. // 2. 移除花括号内的前导和尾随空格: { 1 } -> {1}
  56. $latex = preg_replace('/\{\s+/', '{', $latex);
  57. $latex = preg_replace('/\s+\}/', '}', $latex);
  58. // 2.1 移除闭合花括号后紧跟开放花括号之间的空格: } { -> }{
  59. $latex = preg_replace('/\}\s+\{/', '}{', $latex);
  60. // 3. 移除上标/下标符号周围的空格: x ^ { a } -> x^{a}
  61. $latex = preg_replace('/\s*\^\s*\{\s*/', '^{', $latex);
  62. $latex = preg_replace('/\s*_\s*\{\s*/', '_{', $latex);
  63. // 4. 移除 \left 和 \right 后的空格: \left ( -> \left(, \right ) -> \right)
  64. $latex = preg_replace('/\\\\(left|right)\s+/', '\\\\$1', $latex);
  65. // 4.1 特殊处理 \right 和 ) 之间的空格
  66. $latex = preg_replace('/\\\\right\s+\)/', '\\right)', $latex);
  67. $latex = preg_replace('/\\\\left\s+\(/', '\\left(', $latex);
  68. // 5. 移除括号内侧的空格: ( x ) -> (x)
  69. $latex = preg_replace('/\(\s+/', '(', $latex);
  70. $latex = preg_replace('/\s+\)/', ')', $latex);
  71. // 6. 规范化多个连续空格为单个空格
  72. $latex = preg_replace('/\s+/', ' ', $latex);
  73. return $latex;
  74. }
  75. /**
  76. * 清理错误的定界符 - OCR 常见错误
  77. */
  78. protected function cleanDelimiters(string $latex): string
  79. {
  80. // 1. 移除花括号内的 $: {a$} -> {a}
  81. $latex = preg_replace('/\{([^}]*)\$+([^}]*)\}/', '{$1$2}', $latex);
  82. // 2. 移除末尾的多余 $$$
  83. $latex = preg_replace('/\$+\s*$/', '', $latex);
  84. // 3. 移除开头的多余 $$$
  85. $latex = preg_replace('/^\s*\$+/', '', $latex);
  86. // 4. 移除连续的 $$$ (3个或更多) -> $$
  87. $latex = preg_replace('/\$\$\$+/', '$$', $latex);
  88. // 5. 修复不匹配的定界符
  89. // 如果只有一个 $,可能是 OCR 错误,移除它
  90. $dollarCount = substr_count($latex, '$');
  91. if ($dollarCount === 1) {
  92. $latex = str_replace('$', '', $latex);
  93. }
  94. return $latex;
  95. }
  96. /**
  97. * 修复常见的 LaTeX 命令
  98. */
  99. protected function fixCommonCommands(string $latex): string
  100. {
  101. // 常见的 LaTeX 命令列表
  102. $commands = [
  103. 'frac', 'sqrt', 'sum', 'int', 'lim', 'prod',
  104. 'sin', 'cos', 'tan', 'log', 'ln', 'exp',
  105. 'alpha', 'beta', 'gamma', 'delta', 'theta', 'pi', 'sigma', 'omega',
  106. 'leq', 'geq', 'neq', 'approx', 'infty', 'partial',
  107. 'times', 'div', 'pm', 'mp', 'cdot',
  108. 'left', 'right', 'big', 'Big', 'bigg', 'Bigg'
  109. ];
  110. // 为缺少反斜杠的命令添加反斜杠
  111. foreach ($commands as $cmd) {
  112. // 匹配单词边界的命令(不是已经有反斜杠的)
  113. $pattern = '/(?<!\\\\)\b' . preg_quote($cmd, '/') . '\b/';
  114. $latex = preg_replace($pattern, '\\\\' . $cmd, $latex);
  115. }
  116. // 规范化反斜杠(处理多重转义)
  117. $latex = preg_replace('/\\\\+([a-zA-Z])/', '\\\\$1', $latex);
  118. return $latex;
  119. }
  120. /**
  121. * 修复括号匹配问题
  122. */
  123. protected function fixBraces(string $latex): string
  124. {
  125. // 统计花括号数量
  126. $openCount = substr_count($latex, '{');
  127. $closeCount = substr_count($latex, '}');
  128. // 如果不匹配,尝试修复
  129. if ($openCount > $closeCount) {
  130. // 缺少闭合括号,在末尾添加
  131. $latex .= str_repeat('}', $openCount - $closeCount);
  132. } elseif ($closeCount > $openCount) {
  133. // 多余的闭合括号,移除末尾的
  134. $diff = $closeCount - $openCount;
  135. for ($i = 0; $i < $diff; $i++) {
  136. $latex = preg_replace('/\}\s*$/', '', $latex, 1);
  137. }
  138. }
  139. return $latex;
  140. }
  141. /**
  142. * 批量清理文本数组
  143. *
  144. * @param array $texts 文本数组
  145. * @param array $keys 需要清理的键名
  146. * @return array 清理后的数组
  147. */
  148. public function cleanArray(array $texts, array $keys = ['content', 'question_text', 'student_answer', 'answer']): array
  149. {
  150. foreach ($texts as &$item) {
  151. if (is_array($item)) {
  152. foreach ($keys as $key) {
  153. if (isset($item[$key]) && is_string($item[$key])) {
  154. $item[$key] = $this->clean($item[$key]);
  155. }
  156. }
  157. }
  158. }
  159. return $texts;
  160. }
  161. /**
  162. * 验证清理后的 LaTeX 是否有效
  163. *
  164. * @param string $latex 清理后的 LaTeX
  165. * @return array ['valid' => bool, 'errors' => array]
  166. */
  167. public function validate(string $latex): array
  168. {
  169. $errors = [];
  170. // 检查括号匹配
  171. if (substr_count($latex, '{') !== substr_count($latex, '}')) {
  172. $errors[] = '花括号不匹配';
  173. }
  174. if (substr_count($latex, '(') !== substr_count($latex, ')')) {
  175. $errors[] = '圆括号不匹配';
  176. }
  177. if (substr_count($latex, '[') !== substr_count($latex, ']')) {
  178. $errors[] = '方括号不匹配';
  179. }
  180. // 检查定界符匹配
  181. $dollarCount = substr_count($latex, '$');
  182. if ($dollarCount % 2 !== 0) {
  183. $errors[] = '$ 定界符不匹配';
  184. }
  185. return [
  186. 'valid' => empty($errors),
  187. 'errors' => $errors
  188. ];
  189. }
  190. }