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") diff --git a/parser/Cargo.lock b/parser/Cargo.lock index 7509547..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,13 +1415,14 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" name = "parser" version = "0.1.0" dependencies = [ + "calamine", "docx-rs", "image", "infer", "mime", "pdf-extract", "pyo3", - "quick-xml 0.39.2", + "quick-xml 0.38.4", "rayon", "rustypptx", "tesseract", @@ -1623,12 +1671,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +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", ] @@ -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 6569198..b1da89b 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -21,8 +21,9 @@ 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.39.2" +quick-xml = "0.38.4" # NOTE: Для парсинга pdf pdf-extract = "0.10.0" diff --git a/parser/assets/Book.xlsx b/parser/assets/Book.xlsx index 09b5344..bdf7bc0 100644 Binary files a/parser/assets/Book.xlsx and b/parser/assets/Book.xlsx differ 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/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): ... diff --git a/parser/src/errors.rs b/parser/src/errors.rs index bb0a2e9..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), @@ -61,6 +65,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), 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 964a8d8..2e8221d 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -11,7 +11,10 @@ use crate::{ APPLICATION_XLS, APPLICATION_XLSX, }, errors::ParserError, - parsers::{docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text}, + parsers::{ + MSOfficeParser, docx, image::extract_text_from_image, pdf::extract_text_from_pdf, pptx, text::extract_from_text, + xlsx, + }, }; type Result = std::result::Result; @@ -32,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) @@ -40,16 +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_from_docx(&file_data) + docx_parser.extract_text(&file_data) + } + Some(mime) if mime == APPLICATION_XLSX => { + let xlsx_parser = xlsx::XlsxParser::new(); + xlsx_parser.extract_text(&file_data) } - 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.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 34fc72d..d88ff1d 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -1,10 +1,12 @@ -//! Парсинг docx файлов, а так же и тех которые zip, но по факту docx. +//! Модуль для парсинга docx файлов. //! -//! Для парсинга используется crate-ы docx_rs и zip +//! Текст извлекается как из обычных docx, так zip, но расширение у них docx +//! (имеются в виду MIME типы). +//! Для парсинга используется crate-ы docx_rs и zip. use crate::{ errors::ParserError, - parsers::{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::*; @@ -20,27 +22,19 @@ 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, } -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 MSOfficeParser for DocxParser { /// Извлекает текстовые данные из параграфов, таблиц и из картинок из docx файлов /// /// # Arguments @@ -56,7 +50,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)> { + fn extract_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { let dox = read_docx(data)?; // Вытаскиваем все картинки let images_bytes = self.extract_images_from_docx(data)?; @@ -86,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, + } + } /// Проходится по всем парам /// @@ -93,17 +99,17 @@ 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() - .map(|(id, data)| Ok((id, get_from_image(&data)?))) + .map(|(id, data)| Ok((id, extract_text_from_image(&data)?))) .collect::>>()?; Ok(()) } @@ -114,13 +120,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 +155,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 +169,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() { @@ -307,14 +313,15 @@ impl DocxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::docx::DocxParser}; + use crate::{errors::ParserError, parsers::{MSOfficeParser, docx::DocxParser}}; use std::io::Cursor; 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)?) } @@ -364,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.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 af76190..58fb98e 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -1,8 +1,23 @@ //! Модуль для реализации парсеров + +use std::collections::HashMap; + +use crate::errors::ParserError; pub(crate) mod docx; pub(crate) mod image; pub(crate) mod pdf; pub(crate) mod pptx; 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 MSOfficeParser { + 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 7745dc7..c6bcbdb 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -1,17 +1,21 @@ -//! Парсинг pptx файлов +//! Модуль для парсинга pptx файлов. //! -//! Для парсинга используется crate rustypptx +//! Для парсинга используется crate rustypptx. use std::collections::HashMap; use rayon::prelude::*; -use crate::{errors::ParserError, parsers::image::get_from_image}; +use crate::{ + errors::ParserError, + parsers::{MSOfficeParser, image::extract_text_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 для сопоставления байтов картинки с её местом в тексте слайда @@ -20,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 MSOfficeParser for PptxParser { /// Извлекает текстовые данные и текст из картинок /// /// # Arguments @@ -43,7 +39,7 @@ impl PptxParser { /// - [`ParserError::PptxError`] - ошибка во время парсинга pptx /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки - pub(crate) fn get_from_pptx(mut self, data: &[u8]) -> 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(); @@ -56,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 файла для дальнейшей обработки /// (текст и картинки со слайдов) @@ -108,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"); } @@ -121,19 +127,20 @@ impl PptxParser { #[cfg(test)] mod tests { - use crate::{errors::ParserError, parsers::pptx::PptxParser}; + use crate::{errors::ParserError, parsers::{MSOfficeParser, 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)?) } 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.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 new file mode 100644 index 0000000..760fe92 --- /dev/null +++ b/parser/src/parsers/xlsx.rs @@ -0,0 +1,203 @@ +//! Модуль для парсинга xlsx файлов. +//! +//! Для парсинга используется crate-ы calamine и zip + +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; +type ImagesInfo = HashMap<(SheetIndex, ImgOnSheetNum), Vec>; + +pub(crate) struct XlsxParser { + /// HashMap для сопоставления байтов картинки с нужным sheet + pub sheet_img_info: ImagesInfo, + /// Текст sheet + pub sheet_text: Vec, +} + +impl MSOfficeParser for XlsxParser { + /// Извлекает текстовые данные и текст из картинок + /// + /// # Arguments + /// - `mut `[`self`] - сам парсер (забирает владение над парсером) + /// - `data` - слайс байтов данных из файла + /// + /// # Returns + /// - Ok([`String`]) - возвращает текст + /// - Err([`ParserError`]) - ошибка во время парсинга xlsx файла + /// + /// # Errors + /// - [`ParserError::XlsxError`] - ошибка во время парсинга xlsx + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - [`ParserError::FmtError`] - ошибка во время записи в буффер + /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки + fn extract_text(mut self, data: &[Bytes]) -> Result<(String, ImagesInfo)> { + let cursor = Cursor::new(data); + + let excel = Xlsx::new(cursor)?; + let sheet_names = excel.sheet_names(); + + // чтение текста с страниц + self.read_sheets(excel, sheet_names)?; + + // Вытаскиваем все картинки и парсим из них текст + 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, + )) + } +} + +impl XlsxParser { + /// Создает новый [`XlsxParser`]. + pub(crate) fn new() -> Self { + Self { + sheet_img_info: HashMap::new(), + 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("/*** 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}") + })?; + cur_sheet_text.push('\n'); + Ok(()) + })?; + + self.sheet_text.push(cur_sheet_text); + } + } + 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) + } +} + +#[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(()) + } +}