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:
296
scripts/omml_mathml2latex/utils/chemistry.py
Normal file
296
scripts/omml_mathml2latex/utils/chemistry.py
Normal 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
|
||||
Reference in New Issue
Block a user