Files
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

297 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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