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 默认密钥已清空
This commit is contained in:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
"""
Chemistry detection utilities for identifying chemical equations and contexts.
This module provides functions to detect chemical reactions, conditions, and
other chemistry-specific patterns in mathematical equations.
"""
from typing import Optional, Dict
from lxml import etree as ET
# OMML namespace constant
OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math"
def contains_chinese_characters(text: str) -> bool:
"""Check if text contains Chinese characters.
Args:
text: Text to check
Returns:
True if text contains at least one Chinese character
Example:
>>> contains_chinese_characters('加热')
True
>>> contains_chinese_characters('heat')
False
"""
if not text:
return False
for char in text:
if '\u4e00' <= char <= '\u9fff': # Chinese character range
return True
return False
def is_typical_reaction_condition_text(text: str) -> bool:
"""Check if text looks like a typical chemical reaction condition.
Args:
text: Text to check
Returns:
True if text contains typical chemical reaction condition terms
Example:
>>> is_typical_reaction_condition_text('加热')
True
>>> is_typical_reaction_condition_text('heat')
True
>>> is_typical_reaction_condition_text('abc')
False
"""
if not text:
return False
# Common chemical reaction conditions
typical_conditions = {
# Chinese conditions
"电解", "加热", "催化剂", "高温", "高压", "光照", "加压", "蒸馏", "熔融", "溶解", "酸化", "碱化",
"氧化", "还原", "分解", "合成", "中和", "干燥", "过滤", "沉淀", "结晶", "升华", "煅烧",
# English conditions
"heat", "catalyst", "pressure", "light", "oxidation", "reduction", "electrolysis",
"distillation", "crystallization", "precipitation", "filtration", "drying",
# Symbols and abbreviations
"Δ", "hv", "H+", "OH-", "cat", "pt", "ni", "pd"
}
text_lower = text.lower().strip()
return any(condition.lower() in text_lower for condition in typical_conditions)
def is_chemical_reaction_arrow(eqArr_node: ET._Element) -> Optional[Dict[str, str]]:
"""Detect if an eqArr represents a chemical reaction arrow with conditions.
Args:
eqArr_node: OMML eqArr element
Returns:
Dictionary with chemical reaction info if detected, None otherwise.
Structure: {
'above': str, # Text above the arrow (e.g., '电解')
'below': str, # Text below the arrow (e.g., '加热', can be empty)
'type': str # 'chemical_reaction'
}
Example:
>>> # Assuming eqArr_node is an OMML element with reaction conditions
>>> result = is_chemical_reaction_arrow(eqArr_node)
>>> result['above']
'电解'
"""
# Find all equation array elements
e_elements = eqArr_node.findall(f"{{{OMML_NS}}}e")
# Must have exactly 2 elements for chemical reaction conditions
if len(e_elements) != 2:
return None
# Extract text from first element (above the arrow)
above_text = ""
first_elem = e_elements[0]
# Extract text from all t elements in the first element, avoiding duplication
# Use a set to track unique text content
text_parts = set()
for t_elem in first_elem.findall(f".//{{{OMML_NS}}}t"):
if t_elem.text and t_elem.text.strip():
text_parts.add(t_elem.text.strip())
# Join unique text parts
above_text = "".join(sorted(text_parts))
# Extract text from second element (below the arrow)
below_text = ""
second_elem = e_elements[1]
# Extract text from all t elements in the second element, avoiding duplication
text_parts_below = set()
for t_elem in second_elem.findall(f".//{{{OMML_NS}}}t"):
if t_elem.text and t_elem.text.strip():
text_parts_below.add(t_elem.text.strip())
# Join unique text parts
below_text = "".join(sorted(text_parts_below))
# Validate that this looks like a chemical reaction pattern
if above_text.strip():
# Check if contains chemical condition patterns (Chinese text or typical reaction words)
if (contains_chinese_characters(above_text) or
is_typical_reaction_condition_text(above_text) or
contains_chinese_characters(below_text) or
is_typical_reaction_condition_text(below_text)):
return {
'above': above_text.strip(),
'below': below_text.strip(),
'type': 'chemical_reaction'
}
return None
def detect_chemistry_context(equation_text: str) -> bool:
"""Detect if equation suggests chemistry context - Conservative approach.
Args:
equation_text: The full equation text to analyze
Returns:
True if chemistry context is detected
Example:
>>> detect_chemistry_context('H2O + pH = 7')
True
>>> detect_chemistry_context('sin x + cos x')
False
"""
if not equation_text:
return False
# Check if we're in a set theory context - if so, don't treat letters as chemical elements
if has_set_theory_symbols(equation_text):
return False
# Check if we're in a mathematical context (trigonometry, calculus, etc.)
if has_mathematical_functions(equation_text):
return False
# Conservative chemistry detection: require multiple indicators, not just chemical elements
chemistry_indicators = ['pH', 'pOH', 'mol', 'atm', 'bar', 'Torr', 'eq', 'aq', 'sol',
'↑', '↓', '→', '⇋', '⇌', 'xlongequal', 'rightarrow']
# Only return True if we have actual chemistry indicators in the equation text
return any(indicator in equation_text for indicator in chemistry_indicators)
def has_mathematical_functions(equation_text: str) -> bool:
"""Check if the equation contains mathematical functions.
Args:
equation_text: The equation text to analyze
Returns:
True if mathematical functions are detected
Example:
>>> has_mathematical_functions('sin x + cos x')
True
>>> has_mathematical_functions('H2O')
False
"""
if not equation_text:
return False
# Common mathematical functions that indicate math context, not chemistry
math_functions = ['sin', 'cos', 'tan', 'log', 'ln', 'exp', 'sqrt', 'lim', 'max', 'min',
'arcsin', 'arccos', 'arctan', 'sinh', 'cosh', 'tanh']
return any(func in equation_text for func in math_functions)
def has_set_theory_symbols(equation_text: str) -> bool:
"""Check if the equation contains set theory symbols.
Args:
equation_text: The equation text to analyze
Returns:
True if set theory symbols are detected
Example:
>>> has_set_theory_symbols('A ∩ B ⊂ C')
True
>>> has_set_theory_symbols('H2O')
False
"""
if not equation_text:
return False
set_symbols = ['⊂', '⊃', '⊆', '⊇', '⊄', '⊅', '⊈', '⊉', '⊊', '⊋',
'∈', '∉', '∋', '∌', '∪', '∩', '∖', '∁']
return any(symbol in equation_text for symbol in set_symbols)
def detect_physics_context(equation_text: str) -> bool:
"""Detect if equation suggests physics context.
Args:
equation_text: The equation text to analyze
Returns:
True if physics context is detected
Example:
>>> detect_physics_context('F = ma, where m is in kg')
True
>>> detect_physics_context('x + y = z')
False
"""
if not equation_text:
return False
# Check if we're in a mathematical context (trigonometry, calculus, etc.)
if has_mathematical_functions(equation_text):
return False
# Common physics constants and units - look for these in the equation context
physics_indicators = [
'kg', 'm', 's', 'A', 'K', 'mol', 'cd', # SI base units
'Hz', 'N', 'Pa', 'J', 'W', 'C', 'V', 'F', 'Ω', 'S', 'Wb', 'T', 'H', # Derived units
'eV', 'cal', 'Bar', 'L', # Common non-SI
# Note: removed single-letter constants like 'c', 'h', 'k', 'G', 'R', 'NA'
# These are too common in math contexts and cause false positives
]
# Only return True if we have actual physics indicators in the equation text
return any(indicator in equation_text for indicator in physics_indicators)
def is_physics_constant(content: str) -> bool:
"""Check if content is a known physics constant.
Args:
content: String to check
Returns:
True if content represents a known physics constant
Example:
>>> is_physics_constant('c')
True
>>> is_physics_constant('x')
False
"""
constants = {
'c', # speed of light
'h', # Planck constant
'hbar', # reduced Planck constant
'k', # Boltzmann constant
'kB', # Boltzmann constant
'G', # gravitational constant
'R', # gas constant
'NA', # Avogadro number
'e', # elementary charge
'me', # electron mass
'mp', # proton mass
'mn', # neutron mass
'mu', # atomic mass unit
'epsilon0', # vacuum permittivity
'mu0', # vacuum permeability
'alpha', # fine structure constant
'sigma', # Stefan-Boltzmann constant
}
return content in constants