Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/core/cache.py
Quentin 3271fee5cc init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
2026-09-29 09:43:13 +08:00

291 lines
8.2 KiB
Python

"""
Caching utilities for performance optimization.
Provides singleton pattern for expensive-to-create objects like
XSL transformers, and LRU caching for frequently-used operations.
"""
import os
import threading
from typing import Dict, Optional
from lxml import etree as ET
from functools import lru_cache
class XSLTransformerCache:
"""
Thread-safe singleton cache for XSL transformers.
The OMML2MML.xsl file is expensive to parse (lxml XSLT compilation).
This cache ensures it's only parsed once and reused across all conversions.
Example:
>>> cache = XSLTransformerCache.get_instance()
>>> transformer = cache.get_transformer('path/to/OMML2MML.xsl')
>>> # Subsequent calls return cached transformer
>>> transformer2 = cache.get_transformer('path/to/OMML2MML.xsl')
>>> assert transformer is transformer2
"""
_instance: Optional['XSLTransformerCache'] = None
_lock = threading.Lock()
def __init__(self):
"""Initialize cache storage."""
self._transformers: Dict[str, ET.XSLT] = {}
self._cache_lock = threading.Lock()
@classmethod
def get_instance(cls) -> 'XSLTransformerCache':
"""
Get singleton instance of the cache.
Thread-safe singleton pattern using double-checked locking.
Returns:
Singleton XSLTransformerCache instance
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_transformer(self, xsl_path: str) -> ET.XSLT:
"""
Get cached XSL transformer or create and cache new one.
Args:
xsl_path: Path to XSL stylesheet file
Returns:
Compiled XSLT transformer
Raises:
FileNotFoundError: If XSL file doesn't exist
ET.XSLTParseError: If XSL file is malformed
Example:
>>> cache = XSLTransformerCache.get_instance()
>>> transformer = cache.get_transformer('OMML2MML.xsl')
>>> result = transformer(omml_doc)
"""
# Normalize path for consistent caching
xsl_path = os.path.abspath(xsl_path)
# Check cache (read lock)
if xsl_path in self._transformers:
return self._transformers[xsl_path]
# Not in cache - create with write lock
with self._cache_lock:
# Double-check after acquiring lock
if xsl_path in self._transformers:
return self._transformers[xsl_path]
# Validate file exists
if not os.path.exists(xsl_path):
raise FileNotFoundError(f"XSL file not found: {xsl_path}")
# Parse and compile XSL
try:
xsl_doc = ET.parse(xsl_path)
transformer = ET.XSLT(xsl_doc)
self._transformers[xsl_path] = transformer
return transformer
except (ET.XSLTParseError, ET.XMLSyntaxError) as e:
raise ET.XSLTParseError(f"Failed to parse XSL file {xsl_path}: {e}") from e
def clear_cache(self) -> None:
"""
Clear all cached transformers.
Useful for testing or when XSL files have been updated.
Example:
>>> cache = XSLTransformerCache.get_instance()
>>> cache.clear_cache()
"""
with self._cache_lock:
self._transformers.clear()
def get_cache_size(self) -> int:
"""
Get number of cached transformers.
Returns:
Number of cached XSL transformers
Example:
>>> cache = XSLTransformerCache.get_instance()
>>> size = cache.get_cache_size()
>>> print(f"Cached {size} transformers")
"""
return len(self._transformers)
class ConverterObjectPool:
"""
Object pool for visitor registries and strategies.
Maintains a pool of pre-initialized conversion objects to avoid
recreation overhead for each equation.
Thread-safe implementation using locks.
Example:
>>> pool = ConverterObjectPool(pool_size=4)
>>> obj = pool.acquire_converter('math', wrap_identifiers=False)
>>> # Use converter...
>>> pool.release_converter(obj)
"""
def __init__(self, pool_size: int = 4):
"""
Initialize object pool.
Args:
pool_size: Maximum number of pooled objects
"""
self.pool_size = pool_size
self._available = []
self._in_use = set()
self._lock = threading.Lock()
def acquire_converter(
self,
subject_mode: str = 'auto',
wrap_identifiers: bool = False,
):
"""
Acquire a converter from the pool.
If pool is exhausted, creates a new converter.
Args:
subject_mode: Subject formatting mode
wrap_identifiers: Wrap identifiers in \\mathrm{}
Returns:
Converter instance
Example:
>>> pool = ConverterObjectPool()
>>> converter = pool.acquire_converter('math')
"""
with self._lock:
# Try to reuse from pool
for i, (cached_mode, cached_wrap, obj) in enumerate(self._available):
if cached_mode == subject_mode and cached_wrap == wrap_identifiers:
self._available.pop(i)
self._in_use.add(id(obj))
return obj
# Pool miss - create new converter
from ..compat import LegacyMathMLToTeXConverter
converter = LegacyMathMLToTeXConverter(
wrap_identifiers_in_mathrm=wrap_identifiers,
subject_mode=subject_mode,
)
self._in_use.add(id(converter))
return converter
def release_converter(
self,
converter,
subject_mode: str = 'auto',
wrap_identifiers: bool = False,
) -> None:
"""
Release converter back to pool.
Args:
converter: Converter instance to release
subject_mode: Subject mode of the converter
wrap_identifiers: Wrap identifiers setting
Example:
>>> pool.release_converter(converter, 'math', False)
"""
with self._lock:
obj_id = id(converter)
if obj_id in self._in_use:
self._in_use.remove(obj_id)
# Only pool if under limit
if len(self._available) < self.pool_size:
self._available.append((subject_mode, wrap_identifiers, converter))
def clear_pool(self) -> None:
"""Clear the object pool."""
with self._lock:
self._available.clear()
self._in_use.clear()
# LRU cache for expensive pattern matching operations
@lru_cache(maxsize=256)
def cached_function_detection(text: str) -> Optional[str]:
"""
Cached function name detection for fragmented functions.
Args:
text: Text to check for function names
Returns:
Function name if detected, None otherwise
Example:
>>> cached_function_detection('sin')
'sin'
>>> cached_function_detection('xyz')
None
"""
from ..data import MATH_FUNCTIONS
text_lower = text.lower()
if text_lower in MATH_FUNCTIONS:
return text_lower
return None
@lru_cache(maxsize=128)
def cached_chemical_element_detection(symbol: str) -> bool:
"""
Cached chemical element symbol detection.
Args:
symbol: Symbol to check
Returns:
True if symbol is a chemical element
Example:
>>> cached_chemical_element_detection('Fe')
True
>>> cached_chemical_element_detection('Xy')
False
"""
from ..data import CHEMICAL_ELEMENTS
return symbol in CHEMICAL_ELEMENTS
def clear_all_caches() -> None:
"""
Clear all caches (XSL, LRU caches, object pools).
Useful for testing or when memory needs to be reclaimed.
Example:
>>> clear_all_caches()
"""
# Clear XSL cache
cache = XSLTransformerCache.get_instance()
cache.clear_cache()
# Clear LRU caches
cached_function_detection.cache_clear()
cached_chemical_element_detection.cache_clear()