use std::collections::HashMap;
use std::io::Read;
use async_trait::async_trait;
use regex::Regex;
use crate::retrieval::loaders::{DocumentLoader, LoaderError};
use crate::vector_stores::Document;
pub struct DocxLoader {
path: String,
}
impl DocxLoader {
pub fn new(path: impl Into<String>) -> Self {
Self { path: path.into() }
}
fn extract_text_from_bytes(data: &[u8]) -> Result<String, LoaderError> {
let reader = std::io::Cursor::new(data);
let mut archive = zip::ZipArchive::new(reader).map_err(|e| {
LoaderError::Other(format!("DOCX 不是有效 ZIP: {}", e))
})?;
let mut xml_content = String::new();
let mut found = false;
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| {
LoaderError::Other(format!("读取 ZIP 条目失败: {}", e))
})?;
if file.name() == "word/document.xml" {
file.read_to_string(&mut xml_content).map_err(|e| {
LoaderError::Other(format!("读取 document.xml 失败: {}", e))
})?;
found = true;
break;
}
}
if !found {
return Err(LoaderError::Other(
"DOCX 中未找到 word/document.xml".to_string(),
));
}
Self::extract_text_from_xml(&xml_content)
}
fn extract_text_from_xml(xml: &str) -> Result<String, LoaderError> {
let re = Regex::new(r"<w:t[^>]*>(.*?)</w:t>").unwrap();
let para_re = Regex::new(r"</w:p>").unwrap();
let mut result = String::new();
let mut last_end = 0;
for cap in re.captures_iter(xml) {
let m = cap.get(1).unwrap();
let before = &xml[last_end..m.start()];
if para_re.is_match(before) && !result.is_empty() {
result.push('\n');
} else if !result.is_empty() {
}
result.push_str(m.as_str());
last_end = m.end();
}
Ok(result)
}
}
#[async_trait]
impl DocumentLoader for DocxLoader {
async fn load(&self) -> Result<Vec<Document>, LoaderError> {
let data = tokio::task::spawn_blocking({
let path = self.path.clone();
move || std::fs::read(&path)
})
.await
.map_err(|e| LoaderError::Other(format!("读取文件失败: {}", e)))?
.map_err(LoaderError::IoError)?;
let text = Self::extract_text_from_bytes(&data)?;
let mut metadata = HashMap::new();
metadata.insert("format".to_string(), "docx".to_string());
metadata.insert("source".to_string(), self.path.clone());
Ok(vec![Document {
content: text,
metadata,
id: None,
}])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_text_from_xml() {
let xml = r#"<?xml version="1.0"?>
<w:document>
<w:body>
<w:p><w:r><w:t>Hello</w:t></w:r><w:r><w:t> World</w:t></w:r></w:p>
<w:p><w:r><w:t>Second paragraph</w:t></w:r></w:p>
</w:body>
</w:document>"#;
let text = DocxLoader::extract_text_from_xml(xml).unwrap();
assert!(text.contains("Hello World"));
assert!(text.contains("Second paragraph"));
}
#[test]
fn test_extract_text_from_xml_empty() {
let xml = r#"<?xml version="1.0"?><w:document><w:body></w:body></w:document>"#;
let text = DocxLoader::extract_text_from_xml(xml).unwrap();
assert!(text.is_empty());
}
#[test]
fn test_extract_text_from_xml_with_xml_space() {
let xml = r#"<w:p><w:r><w:t xml:space="preserve"> spaced </w:t></w:r></w:p>"#;
let text = DocxLoader::extract_text_from_xml(xml).unwrap();
assert_eq!(text, " spaced ");
}
#[test]
fn test_new() {
let loader = DocxLoader::new("/path/to/file.docx");
assert_eq!(loader.path, "/path/to/file.docx");
}
}