Pull to refresh

Shiva — Open Source проект на Rust для парсинга и генерации документов любого типа

Level of difficultyMedium
Reading time3 min
Views9.5K

Идея проекта возникла у меня во время работы над проектом поисковика документов. Существует такая библиотека, как Apache Tika, написанная на Java, которая умеет парсить документы различных типов. Чтобы мой поисковик работал, он должен уметь извлекать текст из документов разных типов (PDF, DOC, XLS, HTML, XML, JSON и т. д.). Сам поисковик я писал на Rust. Но, к сожалению, в мире Rust нет библиотеки, которая умела бы парсить документы всех типов.

Shiva Library
Shiva Library

По этой причине пришлось использовать Apache Tika и вызывать её из моего Rust-кода. Какие недостатки такого решения?

1. Необходимо устанавливать Java на каждом компьютере, где будет запускаться мой поисковик.

2. Очень высокие требования к RAM. Apache Tika использует очень много памяти. Из-за того что в Java есть сборщик мусора, который работает не очень эффективно, приходится выделять очень много памяти для JVM.

Я тогда не стал писать библиотеку на Rust, которая умеет парсить документы всех типов, поскольку работодатель не был готов оплатить несколько лет моей разработки. Но идея осталась в голове. И вот, я решился на этот шаг и начал писать свою библиотеку на Rust.

На самом деле, задача по созданию такой библиотеки достаточно интересна. Нужно разработать хорошую архитектуру ядра библиотеки, чтобы впоследствии легко было добавлять новые парсеры и генераторы для различных типов документов. Я выбрал подход, основанный на использовании Common Document Model (CDM). То есть код любого парсера должен преобразовывать документ в CDM, а код любого генератора должен преобразовывать CDM в документ.

В модуле core я заложил следующие структуры:

pub struct Document {
    pub elements: Vec<Box<dyn Element>>,
    pub page_width: f32,
    pub page_height: f32,
    pub left_page_indent: f32,
    pub right_page_indent: f32,
    pub top_page_indent: f32,
    pub bottom_page_indent: f32,
    pub page_header: Vec<Box<dyn Element>>,
    pub page_footer: Vec<Box<dyn Element>>,
}

trait Element  {
   fn as_any(&self) -> &dyn Any; 

   fn as_any_mut(&mut self) -> &mut dyn Any;

   fn element_type(&self) -> ElementType;
}

pub enum ElementType {
   Text,
   Paragraph,
   Image,
   Hyperlink,
   Header,
   Table,
   TableHeader,
   TableRow,
   TableCell,
   List,
   ListItem,
   PageBreak,
   TableOfContents,
}

Поскольку в Rust отсутствует Reflection, я решил использовать Any. Таким образом, я могу хранить в одном векторе различные типы элементов документа. При необходимости я могу приводить их к нужному типу через downcast_ref и downcast_mut. Для этого в трейт Element я добавил методы для всех типов элементов (например, paragraph_as_ref, paragraph_as_mut и т.д.).

    fn paragraph_as_ref(&self) -> anyhow::Result<&ParagraphElement> {
        Ok(self
            .as_any()
            .downcast_ref::<ParagraphElement>()
            .ok_or(CastingError::Common)?)
    }

    fn paragraph_as_mut(&mut self) -> anyhow::Result<&mut ParagraphElement> {
        Ok(self
            .as_any_mut()
            .downcast_mut::<ParagraphElement>()
            .ok_or(CastingError::Common)?)
    }

Для того, чтобы добавить новый тип документа, достаточно реализовать трейт TransformerTrait:

pub trait TransformerTrait {
    fn parse(document: &Bytes, images: &HashMap<String, Bytes>) -> anyhow::Result<Document>;
    fn generate(document: &Document) -> anyhow::Result<(Bytes, HashMap<String, Bytes>)>;
}

На текущей момент реализованы следующие парсеры и генераторы:

  1. Plain text

  2. Markdown

  3. HTML

  4. PDF

Что бы подключить мою библиотеку в свой проект нужно добавить в Cargo.toml следующую строку:

[dependencies]
shiva = "0.1.14"

Пример использования библиотеки:

fn main() {
    let input_vec = std::fs::read("input.html").unwrap();
    let input_bytes = bytes::Bytes::from(input_vec);
    let document = shiva::html::Transformer::parse(&input_bytes, &HashMap::new()).unwrap();
    let output_bytes = shiva::markdown::Transformer::generate(&document, &HashMap::new()).unwrap();
    std::fs::write("out.md", output_bytes).unwrap();
}

Текущий статус проекта — это MVP (Minimum Viable Product). Я планирую добавить поддержку всех типов документов, которые поддерживает Apache Tika, в течение следующих нескольких лет. А в ближайшее время допишу более глубокую поддержку PDF-документов, так как PDF — это самый популярный тип документов. Также добавлю режим работы в виде веб-сервиса, чтобы можно было использовать мою библиотеку через REST API, а не только через CLI.

Исходный код проекта находится на GitHub. Если у вас есть желание помочь мне в разработке, то пишите мне на почту.

Tags:
Hubs:
+23
Comments31

Articles