From ead7b78343781071cc8999c2a8fe93b36bc19b93 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Thu, 12 Mar 2026 21:52:21 +0400 Subject: [PATCH 01/20] Fix(cargo): pined quick-xml version --- parser/Cargo.lock | 6 +++--- parser/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/parser/Cargo.lock b/parser/Cargo.lock index 7509547..0a85ef0 100644 --- a/parser/Cargo.lock +++ b/parser/Cargo.lock @@ -1374,7 +1374,7 @@ dependencies = [ "mime", "pdf-extract", "pyo3", - "quick-xml 0.39.2", + "quick-xml 0.38.4", "rayon", "rustypptx", "tesseract", @@ -1625,9 +1625,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 6569198..270db2e 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -22,7 +22,7 @@ mime = "0.3.17" docx-rs = "0.4.19" rustypptx = "0.2.0" zip = "8.1.0" -quick-xml = "0.39.2" +quick-xml = "0.38.4" # NOTE: Для парсинга pdf pdf-extract = "0.10.0" From 1a662bd071e6068512b52bed7fb929e1625c5d63 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Thu, 12 Mar 2026 21:59:00 +0400 Subject: [PATCH 02/20] Feat(cargo): add calamine crate --- parser/Cargo.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++++ parser/Cargo.toml | 1 + 2 files changed, 73 insertions(+) diff --git a/parser/Cargo.lock b/parser/Cargo.lock index 0a85ef0..b3ab4d4 100644 --- a/parser/Cargo.lock +++ b/parser/Cargo.lock @@ -146,6 +146,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "atoi_simd" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad17c7c205c2c28b527b9845eeb91cf1b4d008b438f98ce0e628227a822758e" +dependencies = [ + "debug_unsafe", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -332,6 +341,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "calamine" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ae05a4e39297eecf9a994210d27501318c37a9318201f8e11050add82bb6f0" +dependencies = [ + "atoi_simd", + "byteorder", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml 0.39.2", + "serde", + "zip 7.2.0", +] + [[package]] name = "cbc" version = "0.1.2" @@ -446,6 +472,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -553,6 +588,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + [[package]] name = "deflate64" version = "0.1.10" @@ -723,6 +764,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + [[package]] name = "fax" version = "0.2.6" @@ -1368,6 +1415,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" name = "parser" version = "0.1.0" dependencies = [ + "calamine", "docx-rs", "image", "infer", @@ -1632,6 +1680,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quote" version = "1.0.44" @@ -2600,6 +2658,20 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + [[package]] name = "zip" version = "8.1.0" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 270db2e..b1da89b 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -21,6 +21,7 @@ mime = "0.3.17" # NOTE: Для парсинга форматов офиса docx-rs = "0.4.19" rustypptx = "0.2.0" +calamine = "0.34.0" zip = "8.1.0" quick-xml = "0.38.4" From c578c0fc81ff5acc36500cc1a0642c7165936902 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Fri, 13 Mar 2026 13:20:59 +0400 Subject: [PATCH 03/20] Chore(assets-for-tests): update Book.xlsx --- parser/assets/Book.xlsx | Bin 8605 -> 11442 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/parser/assets/Book.xlsx b/parser/assets/Book.xlsx index 09b5344b9f1efe51949412162eefebba0382a8fc..bdf7bc0096022dd4d7b5504260024491cf19cfb5 100644 GIT binary patch literal 11442 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 J{I%W%_&=H*(hdLs From 47ab94640f6566ec561adc202b2d028f4181a7ca Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Fri, 13 Mar 2026 13:30:09 +0400 Subject: [PATCH 04/20] Chore(parsers): add type Bytes for MS office format parsers --- parser/src/parsers/docx.rs | 33 +++++++++++++++++++-------------- parser/src/parsers/pptx.rs | 12 +++++++----- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/parser/src/parsers/docx.rs b/parser/src/parsers/docx.rs index 34fc72d..4754892 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -1,6 +1,8 @@ -//! Парсинг docx файлов, а так же и тех которые zip, но по факту docx. +//! Модуль для парсинга docx файлов. //! -//! Для парсинга используется crate-ы docx_rs и zip +//! Текст извлекается как из обычных docx, так zip, но расширение у них docx +//! (имеются в виду MIME типы). +//! Для парсинга используется crate-ы docx_rs и zip. use crate::{ errors::ParserError, @@ -20,13 +22,15 @@ type Result = std::result::Result; type Id = String; type Target = String; type ImgNumber = u32; -type ImagesInfo = HashMap<(u32, ImgNumber), Vec>; +type Bytes = u8; +type ImagesInfo = HashMap<(u32, ImgNumber), Vec>; +/// FIX: дописать doc комментарии на каждое поле парсера pub(crate) struct DocxParser { /// HashMap, где хранятся id картинок и текст извлеченный из них pub images: HashMap, pub img_info: ImagesInfo, - temp_img_info: HashMap>, + temp_img_info: HashMap>, cur_img_ind: ImgNumber, } @@ -56,7 +60,7 @@ impl DocxParser { /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - [`ParserError::XmlError`] - ошибка во время парсинга конфигурационного файла docx /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - pub(crate) fn get_from_docx(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + pub(crate) fn get_from_docx(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let dox = read_docx(data)?; // Вытаскиваем все картинки let images_bytes = self.extract_images_from_docx(data)?; @@ -93,13 +97,13 @@ impl DocxParser { /// - `data` - слайс байтов данных docx файла /// /// # Returns - /// - Ok([`HashMap>`]) - возвращает имя словарь (id файла, байты файла) + /// - Ok([`HashMap>`]) - возвращает имя словарь (id файла, байты файла) /// - Err([`ParserError`]) - ошибка во время парсинга картинки /// /// # Errors /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - fn extract_text_from_images(&mut self, images: HashMap>) -> Result<()> { + fn extract_text_from_images(&mut self, images: HashMap>) -> Result<()> { self.temp_img_info = images.clone(); self.images = images .into_par_iter() @@ -114,13 +118,13 @@ impl DocxParser { /// - `data` - слайс байтов данных docx файла /// /// # Returns - /// - Ok([`HashMap>`]) - возвращает имя словарь (id файла, байты файла) + /// - Ok([`HashMap>`]) - возвращает имя словарь (id файла, байты файла) /// - Err([`ParserError`]) - ошибка во время парсинга файла /// /// # Errors /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - [`ParserError::XmlError`] - ошибка во время парсинга конфигурационного файла docx - fn extract_images_from_docx(&self, data: &[u8]) -> Result>> { + fn extract_images_from_docx(&self, data: &[Bytes]) -> Result>> { let reader = Cursor::new(data); let mut archive = ZipArchive::new(reader)?; @@ -149,7 +153,7 @@ impl DocxParser { /// - [`ParserError::ZipError`] - ошибка парсинга docx как zip /// - [`ParserError::XmlError`] - ошибка парсинга конфигурационного файла docx /// - [`ParserError::XmlAttrError`] - ошибка работы с аттрибутами в xml - fn find_images_info(archive: &mut ZipArchive>) -> Result> { + fn find_images_info(archive: &mut ZipArchive>) -> Result> { let mut rels_file = archive.by_name("word/_rels/document.xml.rels")?; let mut rels = Vec::new(); rels_file.read_to_end(&mut rels)?; @@ -163,12 +167,12 @@ impl DocxParser { /// - `images_info` - словарь из пар пути до файла и id файла /// /// # Returns - /// - Ok([`HashMap>`]) - возвращает словарь (id файла, байты файла) + /// - Ok([`HashMap>`]) - возвращает словарь (id файла, байты файла) /// - Err([`ParserError::ZipError`]) - ошибка во время парсинга файла fn extract_images( - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, images_info: HashMap, - ) -> Result>> { + ) -> Result>> { let mut images_with_id = HashMap::new(); for ind in 0..archive.len() { @@ -312,9 +316,10 @@ mod tests { use zip::ZipArchive; type Result = std::result::Result; + type Bytes = u8; /// Считывает данные из файла ввиде byte vec - fn read_data_from_file(file_name: &str) -> Result> { + fn read_data_from_file(file_name: &str) -> Result> { Ok(std::fs::read(file_name)?) } diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 7745dc7..0c4eda8 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -1,6 +1,6 @@ -//! Парсинг pptx файлов +//! Модуль для парсинга pptx файлов. //! -//! Для парсинга используется crate rustypptx +//! Для парсинга используется crate rustypptx. use std::collections::HashMap; @@ -11,7 +11,8 @@ use crate::{errors::ParserError, parsers::image::get_from_image}; type Result = std::result::Result; type SlideIndex = u32; type ImgOnSlideNum = u32; -type ImagesInfo = HashMap<(SlideIndex, ImgOnSlideNum), Vec>; +type Bytes = u8; +type ImagesInfo = HashMap<(SlideIndex, ImgOnSlideNum), Vec>; pub(crate) struct PptxParser { /// HashMap для сопоставления байтов картинки с её местом в тексте слайда @@ -43,7 +44,7 @@ impl PptxParser { /// - [`ParserError::PptxError`] - ошибка во время парсинга pptx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - pub(crate) fn get_from_pptx(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + pub(crate) fn get_from_pptx(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let pptx_doc = rustypptx::parse_pptx_bytes(data)?; let mut result_text = String::new(); @@ -123,10 +124,11 @@ impl PptxParser { mod tests { use crate::{errors::ParserError, parsers::pptx::PptxParser}; + type Bytes = u8; type Result = std::result::Result; /// Считывает данные из файла ввиде byte vec - fn read_data_from_file(file_name: &str) -> Result> { + fn read_data_from_file(file_name: &str) -> Result> { Ok(std::fs::read(file_name)?) } From fc6266349bae139de1e593c643a0195bafcb5f2b Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Fri, 13 Mar 2026 13:34:14 +0400 Subject: [PATCH 05/20] Feat(errors): add convert from calamine error to ParserError --- parser/src/errors.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/parser/src/errors.rs b/parser/src/errors.rs index bb0a2e9..9ebed6c 100644 --- a/parser/src/errors.rs +++ b/parser/src/errors.rs @@ -61,6 +61,12 @@ pub enum ParserError { #[error("Docx error: {0}")] PptxError(#[from] rustypptx::PptxError), + /// Ошибка чтения xlsx + /// + /// Ошибки библиотеки calamine для работы с xlsx + #[error("Docx error: {0}")] + XlsxError(#[from] calamine::XlsxError), + /// Ошибка tesseract::InitializeError #[error("Tesseract init error: {0}")] TesseractInitError(#[from] tesseract::InitializeError), From 4c81321c8b59563d77e3397970bcc8e0af1fd20b Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Fri, 13 Mar 2026 13:35:36 +0400 Subject: [PATCH 06/20] Chore(parsers): init xlsx --- parser/src/parsers/mod.rs | 1 + parser/src/parsers/xlsx.rs | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 parser/src/parsers/xlsx.rs diff --git a/parser/src/parsers/mod.rs b/parser/src/parsers/mod.rs index af76190..aa572c6 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -4,5 +4,6 @@ pub(crate) mod image; pub(crate) mod pdf; pub(crate) mod pptx; pub(crate) mod text; +pub(crate) mod xlsx; mod xml; diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs new file mode 100644 index 0000000..875eb61 --- /dev/null +++ b/parser/src/parsers/xlsx.rs @@ -0,0 +1,3 @@ +//! Модуль для парсинга xlsx файлов. +//! +//! Для парсинга используется crate-ы calamine и zip From ed349306c902cbb983bea6450f21c132e753de06 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 18:41:08 +0400 Subject: [PATCH 07/20] Feat(parsers): add MSOfficeParser trait --- parser/src/parsers/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/parser/src/parsers/mod.rs b/parser/src/parsers/mod.rs index aa572c6..b7b7361 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -1,4 +1,8 @@ //! Модуль для реализации парсеров + +use std::collections::HashMap; + +use crate::errors::ParserError; pub(crate) mod docx; pub(crate) mod image; pub(crate) mod pdf; @@ -7,3 +11,13 @@ pub(crate) mod text; pub(crate) mod xlsx; mod xml; + +type Result = std::result::Result; +type ImgNum = u32; +type Bytes = u8; +type ImagesInfo = HashMap<(u32, ImgNum), Vec>; + +/// Trait для парсеров MS office с извлечением текста и извлечением текста с изображений +pub(crate) trait MSOfficParser { + fn get_text(self, data: &[Bytes]) -> Result<(String, ImagesInfo)>; +} From fa012d87fd609e87f82fda7c882a96d616c82489 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 18:41:47 +0400 Subject: [PATCH 08/20] Feat(pptx): impl MSOfficeParser trait --- parser/src/match_parsers.rs | 4 ++-- parser/src/parsers/pptx.rs | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/parser/src/match_parsers.rs b/parser/src/match_parsers.rs index 964a8d8..6d35d02 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -11,7 +11,7 @@ use crate::{ APPLICATION_XLS, APPLICATION_XLSX, }, errors::ParserError, - parsers::{docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text}, + parsers::{MSOfficParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text}, }; type Result = std::result::Result; @@ -45,7 +45,7 @@ pub fn get_text(file_name: &str) -> Result<(String, ImagesInfo)> { Some(mime) if mime == APPLICATION_XLSX => todo!(), Some(mime) if mime == APPLICATION_PPTX => { let pptx_parser = pptx::PptxParser::new(); - pptx_parser.get_from_pptx(&file_data) + pptx_parser.get_text(&file_data) } Some(mime) if mime == APPLICATION_PDF => Ok((get_from_pdf(&file_data)?, HashMap::new())), Some(mime) if mime.type_() == TEXT => Ok((get_from_text(&file_data)?, HashMap::new())), diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 0c4eda8..e9da3d4 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -6,7 +6,10 @@ use std::collections::HashMap; use rayon::prelude::*; -use crate::{errors::ParserError, parsers::image::get_from_image}; +use crate::{ + errors::ParserError, + parsers::{MSOfficParser, image::get_from_image}, +}; type Result = std::result::Result; type SlideIndex = u32; @@ -21,15 +24,7 @@ pub(crate) struct PptxParser { pub slides_text: Vec, } -impl PptxParser { - /// Создает новый [`PptxParser`]. - pub(crate) fn new() -> Self { - Self { - slides_img_info: HashMap::new(), - slides_text: Vec::new(), - } - } - +impl MSOfficParser for PptxParser { /// Извлекает текстовые данные и текст из картинок /// /// # Arguments @@ -44,7 +39,7 @@ impl PptxParser { /// - [`ParserError::PptxError`] - ошибка во время парсинга pptx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - pub(crate) fn get_from_pptx(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { + fn get_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let pptx_doc = rustypptx::parse_pptx_bytes(data)?; let mut result_text = String::new(); @@ -57,6 +52,16 @@ impl PptxParser { Ok((result_text, self.slides_img_info)) } +} + +impl PptxParser { + /// Создает новый [`PptxParser`]. + pub(crate) fn new() -> Self { + Self { + slides_img_info: HashMap::new(), + slides_text: Vec::new(), + } + } /// Заполняет текущий парсер данными из pptx файла для дальнейшей обработки /// (текст и картинки со слайдов) @@ -122,7 +127,7 @@ impl PptxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::pptx::PptxParser}; + use crate::{errors::ParserError, parsers::{MSOfficParser, pptx::PptxParser}}; type Bytes = u8; type Result = std::result::Result; @@ -135,7 +140,7 @@ mod tests { fn extract_text_from_pptx(extract_file: &str, check_file: &str) -> Result<()> { let data = read_data_from_file(extract_file)?; let pars = PptxParser::new(); - let (res, _) = pars.get_from_pptx(&data)?; + let (res, _) = pars.get_text(&data)?; assert_eq!( res.trim(), From 4ffec68a46cbcf698b8164d424cc964212cbab48 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 18:43:37 +0400 Subject: [PATCH 09/20] Feat(docx): impl MSOfficeParser trait --- parser/src/match_parsers.rs | 6 ++++-- parser/src/parsers/docx.rs | 32 +++++++++++++++++--------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/parser/src/match_parsers.rs b/parser/src/match_parsers.rs index 6d35d02..c54c578 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -11,7 +11,9 @@ use crate::{ APPLICATION_XLS, APPLICATION_XLSX, }, errors::ParserError, - parsers::{MSOfficParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text}, + parsers::{ + MSOfficParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text, + }, }; type Result = std::result::Result; @@ -40,7 +42,7 @@ pub fn get_text(file_name: &str) -> Result<(String, ImagesInfo)> { || (mime == APPLICATION_DOCX_ZIP && file_name.ends_with(".docx")) => { let docx_parser = docx::DocxParser::new(); - docx_parser.get_from_docx(&file_data) + docx_parser.get_text(&file_data) } Some(mime) if mime == APPLICATION_XLSX => todo!(), Some(mime) if mime == APPLICATION_PPTX => { diff --git a/parser/src/parsers/docx.rs b/parser/src/parsers/docx.rs index 4754892..2dd865c 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -6,7 +6,7 @@ use crate::{ errors::ParserError, - parsers::{image::get_from_image, xml::get_info_from_xml_rels}, + parsers::{MSOfficParser, image::get_from_image, xml::get_info_from_xml_rels}, }; use rayon::prelude::*; @@ -34,17 +34,7 @@ pub(crate) struct DocxParser { cur_img_ind: ImgNumber, } -impl DocxParser { - /// Создает новый [`DocxParser`]. - pub(crate) fn new() -> Self { - Self { - images: HashMap::new(), - img_info: HashMap::new(), - temp_img_info: HashMap::new(), - cur_img_ind: 0, - } - } - +impl MSOfficParser for DocxParser { /// Извлекает текстовые данные из параграфов, таблиц и из картинок из docx файлов /// /// # Arguments @@ -60,7 +50,7 @@ impl DocxParser { /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - [`ParserError::XmlError`] - ошибка во время парсинга конфигурационного файла docx /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - pub(crate) fn get_from_docx(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { + fn get_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let dox = read_docx(data)?; // Вытаскиваем все картинки let images_bytes = self.extract_images_from_docx(data)?; @@ -90,6 +80,18 @@ impl DocxParser { self.img_info, )) } +} + +impl DocxParser { + /// Создает новый [`DocxParser`]. + pub(crate) fn new() -> Self { + Self { + images: HashMap::new(), + img_info: HashMap::new(), + temp_img_info: HashMap::new(), + cur_img_ind: 0, + } + } /// Проходится по всем парам /// @@ -311,7 +313,7 @@ impl DocxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::docx::DocxParser}; + use crate::{errors::ParserError, parsers::{MSOfficParser, docx::DocxParser}}; use std::io::Cursor; use zip::ZipArchive; @@ -369,7 +371,7 @@ mod tests { fn extract_text_from_docx(extract_file: &str, check_file: &str) -> Result<()> { let data = read_data_from_file(extract_file)?; let pars = DocxParser::new(); - let (res, _) = pars.get_from_docx(&data)?; + let (res, _) = pars.get_text(&data)?; assert_eq!( res.trim(), From 49c60bdb1688171227f2da249ef72602b4400a73 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 19:32:52 +0400 Subject: [PATCH 10/20] Feat(xlsx): init XlsxParser struct --- parser/src/parsers/xlsx.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 875eb61..05a2ae4 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -1,3 +1,25 @@ //! Модуль для парсинга xlsx файлов. //! //! Для парсинга используется crate-ы calamine и zip + +use std::{collections::HashMap, io::Cursor}; + +use calamine::{Reader, Xlsx}; +use rayon::prelude::*; + +use crate::{ + errors::ParserError, + parsers::{MSOfficParser, image::get_from_image}, +}; + +type Result = std::result::Result; +type SheetIndex = u32; +type ImgOnSheetNum = u32; +type ImagesInfo = HashMap<(SheetIndex, ImgOnSheetNum), Vec>; + +pub(crate) struct XlsxParser { + /// HashMap для сопоставления байтов картинки с нужным sheet + pub sheet_img_info: ImagesInfo, + /// Текст sheet + pub sheet_text: Vec, +} From 5ddabcb55a66a14af4e5c6364dd62a39b220ec4d Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 19:34:21 +0400 Subject: [PATCH 11/20] Feat(xlsx): add method `new` --- parser/src/parsers/xlsx.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 05a2ae4..75ad5b1 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -23,3 +23,12 @@ pub(crate) struct XlsxParser { /// Текст sheet pub sheet_text: Vec, } +impl XlsxParser { + /// Создает новый [`XlsxParser`]. + pub(crate) fn new() -> Self { + Self { + sheet_img_info: HashMap::new(), + sheet_text: Vec::new(), + } + } +} From 3e75daa51ba765a8195c7922e25bebbc0209de84 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 19:36:08 +0400 Subject: [PATCH 12/20] Feat(xlsx): impl MSOfficeParser with get_text method Only text extraction --- parser/src/parsers/xlsx.rs | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 75ad5b1..698f34a 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -23,6 +23,55 @@ pub(crate) struct XlsxParser { /// Текст sheet pub sheet_text: Vec, } + +impl MSOfficParser for XlsxParser { + /// Извлекает текстовые данные и текст из картинок + /// + /// # Arguments + /// - `mut `[`self`] - сам парсер (забирает владение над парсером) + /// - `data` - слайс байтов данных из файла + /// + /// # Returns + /// - Ok([`String`]) - возвращает текст + /// - Err([`ParserError`]) - ошибка во время парсинга xlsx файла + /// + /// # Errors + /// - [`ParserError::XlsxError`] - ошибка во время парсинга xlsx + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки + fn get_text(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + let cursor = Cursor::new(data); + + let mut excel = Xlsx::new(cursor)?; + + let sheet_names = excel.sheet_names().clone(); + + for name in sheet_names { + if let Ok(range) = excel.worksheet_range(&name) { + let mut cur_sheet_text = String::new(); + cur_sheet_text.push_str("\n*** Sheet: "); + cur_sheet_text.push_str(&name); + cur_sheet_text.push_str(" ***\n"); + cur_sheet_text.push_str( + &range + .rows() + .map(|row| { + row.iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(",") + }) + .collect::>() + .join("\n"), + ); + self.sheet_text.push(cur_sheet_text); + } + } + + Ok((self.sheet_text.join("\n"), self.sheet_img_info)) + } +} + impl XlsxParser { /// Создает новый [`XlsxParser`]. pub(crate) fn new() -> Self { From adfc791d2b86720b524d12af8c6c2dee2bcdd447 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 19:37:10 +0400 Subject: [PATCH 13/20] Fix(parsers): MSOFFIcePaser name typo --- parser/src/match_parsers.rs | 8 ++++++-- parser/src/parsers/docx.rs | 6 +++--- parser/src/parsers/mod.rs | 2 +- parser/src/parsers/pptx.rs | 6 +++--- parser/src/parsers/xlsx.rs | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/parser/src/match_parsers.rs b/parser/src/match_parsers.rs index c54c578..a933897 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -12,7 +12,8 @@ use crate::{ }, errors::ParserError, parsers::{ - MSOfficParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text, + MSOfficeParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text, + xlsx, }, }; @@ -44,7 +45,10 @@ pub fn get_text(file_name: &str) -> Result<(String, ImagesInfo)> { let docx_parser = docx::DocxParser::new(); docx_parser.get_text(&file_data) } - Some(mime) if mime == APPLICATION_XLSX => todo!(), + Some(mime) if mime == APPLICATION_XLSX => { + let xlsx_parser = xlsx::XlsxParser::new(); + xlsx_parser.get_text(&file_data) + } Some(mime) if mime == APPLICATION_PPTX => { let pptx_parser = pptx::PptxParser::new(); pptx_parser.get_text(&file_data) diff --git a/parser/src/parsers/docx.rs b/parser/src/parsers/docx.rs index 2dd865c..846ba68 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -6,7 +6,7 @@ use crate::{ errors::ParserError, - parsers::{MSOfficParser, image::get_from_image, xml::get_info_from_xml_rels}, + parsers::{MSOfficeParser, image::get_from_image, xml::get_info_from_xml_rels}, }; use rayon::prelude::*; @@ -34,7 +34,7 @@ pub(crate) struct DocxParser { cur_img_ind: ImgNumber, } -impl MSOfficParser for DocxParser { +impl MSOfficeParser for DocxParser { /// Извлекает текстовые данные из параграфов, таблиц и из картинок из docx файлов /// /// # Arguments @@ -313,7 +313,7 @@ impl DocxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::{MSOfficParser, docx::DocxParser}}; + use crate::{errors::ParserError, parsers::{MSOfficeParser, docx::DocxParser}}; use std::io::Cursor; use zip::ZipArchive; diff --git a/parser/src/parsers/mod.rs b/parser/src/parsers/mod.rs index b7b7361..6f1698a 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -18,6 +18,6 @@ type Bytes = u8; type ImagesInfo = HashMap<(u32, ImgNum), Vec>; /// Trait для парсеров MS office с извлечением текста и извлечением текста с изображений -pub(crate) trait MSOfficParser { +pub(crate) trait MSOfficeParser { fn get_text(self, data: &[Bytes]) -> Result<(String, ImagesInfo)>; } diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index e9da3d4..115cedb 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -8,7 +8,7 @@ use rayon::prelude::*; use crate::{ errors::ParserError, - parsers::{MSOfficParser, image::get_from_image}, + parsers::{MSOfficeParser, image::get_from_image}, }; type Result = std::result::Result; @@ -24,7 +24,7 @@ pub(crate) struct PptxParser { pub slides_text: Vec, } -impl MSOfficParser for PptxParser { +impl MSOfficeParser for PptxParser { /// Извлекает текстовые данные и текст из картинок /// /// # Arguments @@ -127,7 +127,7 @@ impl PptxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::{MSOfficParser, pptx::PptxParser}}; + use crate::{errors::ParserError, parsers::{MSOfficeParser, pptx::PptxParser}}; type Bytes = u8; type Result = std::result::Result; diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 698f34a..2a7a853 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -9,7 +9,7 @@ use rayon::prelude::*; use crate::{ errors::ParserError, - parsers::{MSOfficParser, image::get_from_image}, + parsers::{MSOfficeParser, image::get_from_image}, }; type Result = std::result::Result; @@ -24,7 +24,7 @@ pub(crate) struct XlsxParser { pub sheet_text: Vec, } -impl MSOfficParser for XlsxParser { +impl MSOfficeParser for XlsxParser { /// Извлекает текстовые данные и текст из картинок /// /// # Arguments From 8b8c9e6cf7a20a1012e880d9896ea6b77ce4701b Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 19:44:22 +0400 Subject: [PATCH 14/20] Chore(parser): rename all get_text fn names to extract_text --- parser/src/lib.rs | 6 +++--- parser/src/match_parsers.rs | 16 ++++++++-------- parser/src/parsers/docx.rs | 8 ++++---- parser/src/parsers/image.rs | 16 +++++++++------- parser/src/parsers/mod.rs | 2 +- parser/src/parsers/pdf.rs | 10 ++++++---- parser/src/parsers/pptx.rs | 8 ++++---- parser/src/parsers/text.rs | 10 ++++++---- parser/src/parsers/xlsx.rs | 4 ++-- 9 files changed, 43 insertions(+), 37 deletions(-) diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 9340605..3894bfa 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -17,8 +17,8 @@ mod parser { /// Парсинг текста `from` файла по `path` #[pyo3::pyfunction] - pub fn get_text(from_path: &str) -> PyResult<(String, ImagesInfo)> { - Ok(crate::match_parsers::get_text(from_path)?) + pub fn extract_text(from_path: &str) -> PyResult<(String, ImagesInfo)> { + Ok(crate::match_parsers::extract_text(from_path)?) } /// Конвертер старых Microsoft office форматов в новые @@ -34,7 +34,7 @@ mod parser { /// Функция реализации python модуля, добавляющая в него функции #[pymodule] fn docs_parser(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(parser::get_text, m)?)?; + m.add_function(wrap_pyfunction!(parser::extract_text, m)?)?; m.add_function(wrap_pyfunction!(parser::convert_to_new_format, m)?)?; Ok(()) } diff --git a/parser/src/match_parsers.rs b/parser/src/match_parsers.rs index a933897..2e8221d 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -12,7 +12,7 @@ use crate::{ }, errors::ParserError, parsers::{ - MSOfficeParser, docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text, + MSOfficeParser, docx, image::extract_text_from_image, pdf::extract_text_from_pdf, pptx, text::extract_from_text, xlsx, }, }; @@ -35,7 +35,7 @@ static INFER: LazyLock = LazyLock::new(Infer::new); /// # Errors /// - [`ParserError::InvalidFormat`] - тип файла не поддерживается/не определен /// - Остальные варианты [`ParserError`], если ошибка во время парсинга файла -pub fn get_text(file_name: &str) -> Result<(String, ImagesInfo)> { +pub fn extract_text(file_name: &str) -> Result<(String, ImagesInfo)> { let file_data = read_data_from_file(file_name)?; match define_mime_type(&file_data) { Some(mime) @@ -43,19 +43,19 @@ pub fn get_text(file_name: &str) -> Result<(String, ImagesInfo)> { || (mime == APPLICATION_DOCX_ZIP && file_name.ends_with(".docx")) => { let docx_parser = docx::DocxParser::new(); - docx_parser.get_text(&file_data) + docx_parser.extract_text(&file_data) } Some(mime) if mime == APPLICATION_XLSX => { let xlsx_parser = xlsx::XlsxParser::new(); - xlsx_parser.get_text(&file_data) + xlsx_parser.extract_text(&file_data) } Some(mime) if mime == APPLICATION_PPTX => { let pptx_parser = pptx::PptxParser::new(); - pptx_parser.get_text(&file_data) + pptx_parser.extract_text(&file_data) } - Some(mime) if mime == APPLICATION_PDF => Ok((get_from_pdf(&file_data)?, HashMap::new())), - Some(mime) if mime.type_() == TEXT => Ok((get_from_text(&file_data)?, HashMap::new())), - Some(mime) if mime.type_() == IMAGE => Ok((get_from_image(&file_data)?, HashMap::new())), + Some(mime) if mime == APPLICATION_PDF => Ok((extract_text_from_pdf(&file_data)?, HashMap::new())), + Some(mime) if mime.type_() == TEXT => Ok((extract_from_text(&file_data)?, HashMap::new())), + Some(mime) if mime.type_() == IMAGE => Ok((extract_text_from_image(&file_data)?, HashMap::new())), Some(mime) if is_converted_mime_type(&mime) => Err(ParserError::InvalidFormat(format!( "Не поддерживается данный тип файла {mime}, но его вы можете конвертировать \ в поддерживаемый формат через отдельный метод конвертации" diff --git a/parser/src/parsers/docx.rs b/parser/src/parsers/docx.rs index 846ba68..d88ff1d 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -6,7 +6,7 @@ use crate::{ errors::ParserError, - parsers::{MSOfficeParser, image::get_from_image, xml::get_info_from_xml_rels}, + parsers::{MSOfficeParser, image::extract_text_from_image, xml::get_info_from_xml_rels}, }; use rayon::prelude::*; @@ -50,7 +50,7 @@ impl MSOfficeParser for DocxParser { /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - [`ParserError::XmlError`] - ошибка во время парсинга конфигурационного файла docx /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - fn get_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { + fn extract_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let dox = read_docx(data)?; // Вытаскиваем все картинки let images_bytes = self.extract_images_from_docx(data)?; @@ -109,7 +109,7 @@ impl DocxParser { self.temp_img_info = images.clone(); self.images = images .into_par_iter() - .map(|(id, data)| Ok((id, get_from_image(&data)?))) + .map(|(id, data)| Ok((id, extract_text_from_image(&data)?))) .collect::>>()?; Ok(()) } @@ -371,7 +371,7 @@ mod tests { fn extract_text_from_docx(extract_file: &str, check_file: &str) -> Result<()> { let data = read_data_from_file(extract_file)?; let pars = DocxParser::new(); - let (res, _) = pars.get_text(&data)?; + let (res, _) = pars.extract_text(&data)?; assert_eq!( res.trim(), diff --git a/parser/src/parsers/image.rs b/parser/src/parsers/image.rs index 340a03c..5ec5ac3 100644 --- a/parser/src/parsers/image.rs +++ b/parser/src/parsers/image.rs @@ -8,6 +8,7 @@ use std::io::Cursor; use tesseract::Tesseract; type Result = std::result::Result; +type Bytes = u8; /// Парсит байты картинки и извлекает из них текст используя OCR /// @@ -24,7 +25,7 @@ type Result = std::result::Result; /// # Errors /// - [`ParserError::ImageError`] - ошибка во время обработки картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки -pub(crate) fn get_from_image(data: &[u8]) -> Result { +pub(crate) fn extract_text_from_image(data: &[Bytes]) -> Result { let valid_data = match match_parsers::define_mime_type(data) { Some(mime) if is_correct_img_mime(&mime) => data, _ => &convert_to_png(data)?, @@ -39,7 +40,7 @@ fn is_correct_img_mime(mime: &Mime) -> bool { } /// Попытка конвертировать байты катинки в png для дальнейшего парсинга -fn convert_to_png(data: &[u8]) -> Result> { +fn convert_to_png(data: &[Bytes]) -> Result> { let img = image::load_from_memory(data)?; let mut buf = Cursor::new(Vec::new()); img.write_to(&mut buf, image::ImageFormat::Png)?; @@ -55,7 +56,7 @@ fn convert_to_png(data: &[u8]) -> Result> { /// # Returns /// - Ok([`String`]) - извлеченный текст /// - Err([`ParserError`]) - если при работе с Tesseract возникает ошибка -fn parse_with_tesseract(data: &[u8]) -> Result { +fn parse_with_tesseract(data: &[Bytes]) -> Result { // Инициализируем Tesseract с Английским и Русским языками let tes = Tesseract::new(None, Some("eng+rus"))?; @@ -64,19 +65,20 @@ fn parse_with_tesseract(data: &[u8]) -> Result { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::image::get_from_image}; + use crate::{errors::ParserError, parsers::image::extract_text_from_image}; type Result = std::result::Result; + type Bytes = u8; /// Считывает данные из файла ввиде byte vec - fn read_data_from_file(file_name: &str) -> Result> { + fn read_data_from_file(file_name: &str) -> Result> { Ok(std::fs::read(file_name)?) } #[test] fn extract_from_image_en() -> Result<()> { let data = read_data_from_file("assets/text_from_img_en.png")?; - let res = get_from_image(&data)?; + let res = extract_text_from_image(&data)?; assert_eq!( res.trim(), @@ -91,7 +93,7 @@ mod tests { #[test] fn extract_from_image_ru() -> Result<()> { let data = read_data_from_file("assets/text_from_img_ru.png")?; - let res = get_from_image(&data)?; + let res = extract_text_from_image(&data)?; assert_eq!( res.trim(), diff --git a/parser/src/parsers/mod.rs b/parser/src/parsers/mod.rs index 6f1698a..58fb98e 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -19,5 +19,5 @@ type ImagesInfo = HashMap<(u32, ImgNum), Vec>; /// Trait для парсеров MS office с извлечением текста и извлечением текста с изображений pub(crate) trait MSOfficeParser { - fn get_text(self, data: &[Bytes]) -> Result<(String, ImagesInfo)>; + fn extract_text(self, data: &[Bytes]) -> Result<(String, ImagesInfo)>; } diff --git a/parser/src/parsers/pdf.rs b/parser/src/parsers/pdf.rs index 6088f18..336ddd8 100644 --- a/parser/src/parsers/pdf.rs +++ b/parser/src/parsers/pdf.rs @@ -7,6 +7,7 @@ use pdf_extract::extract_text_from_mem; use crate::errors::ParserError; type Result = std::result::Result; + type Bytes = u8; /// Извлекает текстовые данные из pdf /// @@ -16,25 +17,26 @@ type Result = std::result::Result; /// # Returns /// - Ok([`String`]) - возвращает текст /// - Err([`ParserError::PdfError`]) - ошибка во время парсинга pdf файла -pub(crate) fn get_from_pdf(data: &[u8]) -> Result { +pub(crate) fn extract_text_from_pdf(data: &[Bytes]) -> Result { Ok(extract_text_from_mem(data)?) } #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::pdf::get_from_pdf}; + use crate::{errors::ParserError, parsers::pdf::extract_text_from_pdf}; type Result = std::result::Result; + type Bytes = u8; /// Считывает данные из файла ввиде byte vec - fn read_data_from_file(file_name: &str) -> Result> { + fn read_data_from_file(file_name: &str) -> Result> { Ok(std::fs::read(file_name)?) } #[test] fn extract_from_pdf_file() -> Result<()> { let data = read_data_from_file("assets/main.pdf")?; - let res = get_from_pdf(&data)?; + let res = extract_text_from_pdf(&data)?; assert_eq!( res, diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 115cedb..c6bcbdb 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -8,7 +8,7 @@ use rayon::prelude::*; use crate::{ errors::ParserError, - parsers::{MSOfficeParser, image::get_from_image}, + parsers::{MSOfficeParser, image::extract_text_from_image}, }; type Result = std::result::Result; @@ -39,7 +39,7 @@ impl MSOfficeParser for PptxParser { /// - [`ParserError::PptxError`] - ошибка во время парсинга pptx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - fn get_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { + fn extract_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let pptx_doc = rustypptx::parse_pptx_bytes(data)?; let mut result_text = String::new(); @@ -114,7 +114,7 @@ impl PptxParser { "\n/********slide = {ind}; img_num = {img_num}********/\n" )); - res_slide_text.push_str(&get_from_image(data)?); + res_slide_text.push_str(&extract_text_from_image(data)?); res_slide_text .push_str("\n/**************************************************/\n"); } @@ -140,7 +140,7 @@ mod tests { fn extract_text_from_pptx(extract_file: &str, check_file: &str) -> Result<()> { let data = read_data_from_file(extract_file)?; let pars = PptxParser::new(); - let (res, _) = pars.get_text(&data)?; + let (res, _) = pars.extract_text(&data)?; assert_eq!( res.trim(), diff --git a/parser/src/parsers/text.rs b/parser/src/parsers/text.rs index c214fe3..fd3ac43 100644 --- a/parser/src/parsers/text.rs +++ b/parser/src/parsers/text.rs @@ -4,6 +4,7 @@ use crate::errors::ParserError; type Result = std::result::Result; +type Bytes = u8; /// Парсит байты текстового файла в текст /// @@ -13,25 +14,26 @@ type Result = std::result::Result; /// # Returns /// - Ok([`String`]) - возвращает текст /// - Err([`ParserError::FromUTF8Error`]) - ошибка во время парсинга байтов текстового файла -pub(crate) fn get_from_text(data: &[u8]) -> Result { +pub(crate) fn extract_from_text(data: &[Bytes]) -> Result { Ok(String::from_utf8(data.to_vec())?) } #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::text::get_from_text}; + use crate::{errors::ParserError, parsers::text::extract_from_text}; type Result = std::result::Result; + type Bytes = u8; /// Считывает данные из файла ввиде byte vec - fn read_data_from_file(file_name: &str) -> Result> { + fn read_data_from_file(file_name: &str) -> Result> { Ok(std::fs::read(file_name)?) } #[test] fn extract_from_txt_file() -> Result<()> { let data = read_data_from_file("assets/main.typ")?; - let res = get_from_text(&data)?; + let res = extract_from_text(&data)?; assert_eq!( res, String::from_utf8(read_data_from_file( diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 2a7a853..af3ef4e 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -9,7 +9,7 @@ use rayon::prelude::*; use crate::{ errors::ParserError, - parsers::{MSOfficeParser, image::get_from_image}, + parsers::{MSOfficeParser, image::extract_text_from_image}, }; type Result = std::result::Result; @@ -39,7 +39,7 @@ impl MSOfficeParser for XlsxParser { /// - [`ParserError::XlsxError`] - ошибка во время парсинга xlsx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - fn get_text(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + fn extract_text(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { let cursor = Cursor::new(data); let mut excel = Xlsx::new(cursor)?; From 3ae72bea7a0a47fa631d02a2f19259ed1445bbd6 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 20:49:21 +0400 Subject: [PATCH 15/20] Feat(errors): add std::fmt error in ParserError --- parser/src/errors.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/parser/src/errors.rs b/parser/src/errors.rs index 9ebed6c..57c4c39 100644 --- a/parser/src/errors.rs +++ b/parser/src/errors.rs @@ -17,6 +17,10 @@ pub enum ParserError { #[error("IO error: {0}")] IoError(#[from] io::Error), + /// Ошибка записи в буффер + #[error("Fmt error: {0}")] + FmtError(#[from] std::fmt::Error), + /// Ошибка парсинга utf-8 из байтов текстового файла #[error("From utf-8 error: {0}")] FromUTF8Error(#[from] std::string::FromUtf8Error), From 40a52539fed66f7ae386f14b41f2e9948d9dd2ac Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 20:51:22 +0400 Subject: [PATCH 16/20] Feat(xlsx): separate and rework extract text from sheets --- parser/src/parsers/xlsx.rs | 71 ++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index af3ef4e..fe653f1 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -2,7 +2,7 @@ //! //! Для парсинга используется crate-ы calamine и zip -use std::{collections::HashMap, io::Cursor}; +use std::{collections::HashMap, fmt::Write, io::Cursor}; use calamine::{Reader, Xlsx}; use rayon::prelude::*; @@ -38,35 +38,17 @@ impl MSOfficeParser for XlsxParser { /// # Errors /// - [`ParserError::XlsxError`] - ошибка во время парсинга xlsx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - [`ParserError::FmtError`] - ошибка во время записи в буффер + /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки fn extract_text(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { let cursor = Cursor::new(data); - let mut excel = Xlsx::new(cursor)?; + let excel = Xlsx::new(cursor)?; + let sheet_names = excel.sheet_names(); - let sheet_names = excel.sheet_names().clone(); - - for name in sheet_names { - if let Ok(range) = excel.worksheet_range(&name) { - let mut cur_sheet_text = String::new(); - cur_sheet_text.push_str("\n*** Sheet: "); - cur_sheet_text.push_str(&name); - cur_sheet_text.push_str(" ***\n"); - cur_sheet_text.push_str( - &range - .rows() - .map(|row| { - row.iter() - .map(std::string::ToString::to_string) - .collect::>() - .join(",") - }) - .collect::>() - .join("\n"), - ); - self.sheet_text.push(cur_sheet_text); - } - } + // чтение текста с страниц + self.read_sheets(excel, sheet_names)?; Ok((self.sheet_text.join("\n"), self.sheet_img_info)) } @@ -80,4 +62,43 @@ impl XlsxParser { sheet_text: Vec::new(), } } + + /// Читает текст со всех страниц + /// + /// # Arguments + /// - `excel` - документ + /// - `sheet_names` - названия страниц + /// + /// # Errors + /// - [`ParserError::XlsxError`] - ошибка во время парсинга xlsx + /// - [`ParserError::FmtError`] - ошибка во время записи в буффер + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки + fn read_sheets( + &mut self, + mut excel: Xlsx>, + sheet_names: Vec, + ) -> Result<()> { + for name in sheet_names { + if let Ok(range) = excel.worksheet_range(&name) { + let mut cur_sheet_text = String::new(); + cur_sheet_text.push_str("\n/*** Sheet: "); + cur_sheet_text.push_str(&name); + cur_sheet_text.push_str(" ***/\n"); + + // чтение текста из ячеек + range.rows().try_for_each(|row| -> Result<()> { + row.iter().enumerate().try_for_each(|(index, cell)| { + if index > 0 { + cur_sheet_text.push_str(", "); + } + write!(cur_sheet_text, "{cell}") + })?; + Ok(()) + })?; + + self.sheet_text.push(cur_sheet_text); + } + } + Ok(()) + } } From fcd57f39af0e6a3d0fde9f161248bff39938ccc0 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 22:20:33 +0400 Subject: [PATCH 17/20] Feat(xlsx): extract images and text from them --- parser/src/parsers/xlsx.rs | 73 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index fe653f1..1cbbbac 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -6,12 +6,14 @@ use std::{collections::HashMap, fmt::Write, io::Cursor}; use calamine::{Reader, Xlsx}; use rayon::prelude::*; +use zip::ZipArchive; use crate::{ errors::ParserError, parsers::{MSOfficeParser, image::extract_text_from_image}, }; +type Bytes = u8; type Result = std::result::Result; type SheetIndex = u32; type ImgOnSheetNum = u32; @@ -41,7 +43,7 @@ impl MSOfficeParser for XlsxParser { /// - [`ParserError::FmtError`] - ошибка во время записи в буффер /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - fn extract_text(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + fn extract_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let cursor = Cursor::new(data); let excel = Xlsx::new(cursor)?; @@ -50,7 +52,17 @@ impl MSOfficeParser for XlsxParser { // чтение текста с страниц self.read_sheets(excel, sheet_names)?; - Ok((self.sheet_text.join("\n"), self.sheet_img_info)) + // Вытаскиваем все картинки и парсим из них текст + let text_from_images = self.extract_images_from_xlsx(data)?; + + Ok(( + format!( + "{}\n{}", + self.sheet_text.join("\n").trim(), + text_from_images + ), + self.sheet_img_info, + )) } } @@ -81,7 +93,7 @@ impl XlsxParser { for name in sheet_names { if let Ok(range) = excel.worksheet_range(&name) { let mut cur_sheet_text = String::new(); - cur_sheet_text.push_str("\n/*** Sheet: "); + cur_sheet_text.push_str("/*** Sheet: "); cur_sheet_text.push_str(&name); cur_sheet_text.push_str(" ***/\n"); @@ -93,6 +105,7 @@ impl XlsxParser { } write!(cur_sheet_text, "{cell}") })?; + cur_sheet_text.push('\n'); Ok(()) })?; @@ -101,4 +114,58 @@ impl XlsxParser { } Ok(()) } + + /// Извлекает все картинки из xlsx и парсит их + /// + /// # Arguments + /// - `data` - слайс байтов данных xlsx файла + /// + /// # Returns + /// - Ok([`String`]) - возвращает текст со всех картинок + /// - Err([`ParserError`]) - ошибка во время парсинга xlsx файла + /// + /// # Errors + /// - [`ParserError::ZipError`] - ошибка во время парсинга xlsx как zip + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + fn extract_images_from_xlsx(&mut self, data: &[Bytes]) -> Result { + let reader = Cursor::new(data); + let mut archive = ZipArchive::new(reader)?; + + // Находим все картинки + let mut images_data: Vec> = Vec::new(); + for ind in 0..archive.len() { + let mut file = archive.by_index(ind)?; + let path = file.name(); + + if path.starts_with("xl/media/") { + let mut buf = Vec::new(); + std::io::copy(&mut file, &mut buf)?; + images_data.push(buf); + } + } + + // Извлекам текст из картинок + let extracted_data = images_data + .par_iter() + .enumerate() + .map(|(img_num, img_data)| { + let text = extract_text_from_image(img_data)?; + Ok((img_num, img_data, text)) + }) + .collect::>>()?; + + // Сохраняем данные о картинке и тексте + let mut text_from_images = String::new(); + for (img_num, img_data, text) in extracted_data { + text_from_images.push_str("\n/************* Image = "); + text_from_images.push_str(&img_num.to_string()); + text_from_images.push_str(" *************/\n"); + text_from_images.push_str(&text); + text_from_images.push_str("\n/*************************************/\n"); + self.sheet_img_info + .insert((0, img_num as u32), img_data.to_owned()); + } + + Ok(text_from_images) + } } From 31d0e99d623dbc26c3ba1daf358d50f3f200f907 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 22:20:59 +0400 Subject: [PATCH 18/20] Chore(app): add usage xlsx example --- app/main.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index b093b21..aa3fc16 100644 --- a/app/main.py +++ b/app/main.py @@ -1,16 +1,17 @@ import docs_parser # NOTE: все эти точно работают и работают хорошо -# (doc_p, _) = docs_parser.get_text("parser/assets/text_and_tables.docx") -# (doc_p, _) = docs_parser.get_text("parser/assets/text_and_tables.docx") -# (doc_p, _) = docs_parser.get_text("parser/assets/some_text.docx") -# (doc_p, _) = docs_parser.get_text("parser/assets/text_tables_png.docx") -# (doc_p, _) = docs_parser.get_text("parser/assets/text_from_img.png") -# (doc_p, _) = docs_parser.get_text("parser/assets/main.typ") -# (doc_p, _) = docs_parser.get_text("parser/assets/main.pdf") -# (doc_p, _) = docs_parser.get_text("parser/assets/too_many_png.docx") -# (doc_p, _) = docs_parser.get_text("parser/assets/Presentation.pptx") -# print(doc_p) +# (doc_p, _) = docs_parser.extract_text("parser/assets/text_and_tables.docx") +# (doc_p, _) = docs_parser.extract_text("parser/assets/text_and_tables.docx") +# (doc_p, _) = docs_parser.extract_text("parser/assets/some_text.docx") +# (doc_p, _) = docs_parser.extract_text("parser/assets/text_tables_png.docx") +# (doc_p, _) = docs_parser.extract_text("parser/assets/text_from_img.png") +# (doc_p, _) = docs_parser.extract_text("parser/assets/main.typ") +# (doc_p, _) = docs_parser.extract_text("parser/assets/main.pdf") +# (doc_p, _) = docs_parser.extract_text("parser/assets/too_many_png.docx") +# (doc_p, _) = docs_parser.extract_text("parser/assets/Presentation.pptx") +(doc_p, _) = docs_parser.extract_text("parser/assets/Book.xlsx") +print(doc_p) # docs_parser.convert_to_new_format("parser/assets/old_docs.doc", "parser/assets/tests_results") # docs_parser.convert_to_new_format("parser/assets/old_pres.ppt", "parser/assets/tests_results") # docs_parser.convert_to_new_format("parser/assets/old_exel.xls", "parser/assets/tests_results") From ead20195fa57c2118ccd6c33adcc86fa45b7ffc4 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 22:31:19 +0400 Subject: [PATCH 19/20] Feat(xlsx): add extract text from xlsx test --- .../tests_results/extract_text_from_xlsx.txt | 16 ++++++++++ parser/src/parsers/xlsx.rs | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 parser/assets/tests_results/extract_text_from_xlsx.txt diff --git a/parser/assets/tests_results/extract_text_from_xlsx.txt b/parser/assets/tests_results/extract_text_from_xlsx.txt new file mode 100644 index 0000000..87af416 --- /dev/null +++ b/parser/assets/tests_results/extract_text_from_xlsx.txt @@ -0,0 +1,16 @@ +/*** Sheet: Лист1 ***/ +Имя, Номер +Вася, 1 +Петя, 3 +Ваня, 2 +Тема, 4 +Егор, 6 +Саша, 5 + +/*** Sheet: Sheet2 ***/ +Страница 2 +some text + +/************* Image = 0 *************/ +МЯУ=191919 +/*************************************/ diff --git a/parser/src/parsers/xlsx.rs b/parser/src/parsers/xlsx.rs index 1cbbbac..760fe92 100644 --- a/parser/src/parsers/xlsx.rs +++ b/parser/src/parsers/xlsx.rs @@ -169,3 +169,35 @@ impl XlsxParser { Ok(text_from_images) } } + +#[cfg(test)] +mod test { + use crate::{ + errors::ParserError, + parsers::{MSOfficeParser, xlsx::XlsxParser}, + }; + + type Bytes = u8; + type Result = std::result::Result; + + /// Считывает данные из файла ввиде byte vec + fn read_data_from_file(file_name: &str) -> Result> { + Ok(std::fs::read(file_name)?) + } + + #[test] + fn extract_text_from_xlsx() -> Result<()> { + let data = read_data_from_file("assets/Book.xlsx")?; + let pars = XlsxParser::new(); + let (res, _) = pars.extract_text(&data)?; + + assert_eq!( + res.trim(), + String::from_utf8(read_data_from_file( + "assets/tests_results/extract_text_from_xlsx.txt" + )?)? + .trim() + ); + Ok(()) + } +} From aea6c65ee51cc36cde3934cdcd3fcf871024c595 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 14 Mar 2026 22:43:43 +0400 Subject: [PATCH 20/20] Feat(docs_parser.pyi): update info for python lsp --- parser/docs_parser.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/docs_parser.pyi b/parser/docs_parser.pyi index 588db5a..bdd6c4e 100644 --- a/parser/docs_parser.pyi +++ b/parser/docs_parser.pyi @@ -1,2 +1,2 @@ -def get_text(from_path: str) -> tuple[str, dict[tuple[int, int], bytes]]: ... +def extract_text(from_path: str) -> tuple[str, dict[tuple[int, int], bytes]]: ... def convert_to_new_format(old_file_path: str, new_path: str): ...