Rustで学術論文からテキストを抽出する #9 - JSON変換

:::note
## Summary
- Rustでは構造体からJSONへの変換はserdeで一撃

GiHub -> https://github.com/akitenkrad/rsrpp
crates.io -> https://crates.io/crates/rsrpp

## ToDo
- [x] pdftotextで論文から単語単位のテキストと位置情報を取得する (`Word`,`Line`,`Block`,`Page`)
- [x] テキストの属性 (本文, タイトル, 脚注, etc.) を判定する
    - [x] テキストが含まれるエリアを抽出する
        - [x] 2段組みを扱えるようにする
    - [x] セクションのタイトルを識別する
- [x] 図表に含まれるテキストを除外する
    - [x] 表を除外する
        - [x] PDFを画像に変換
        - [x] 画像処理で表の位置を特定
    - [x] SVG画像，数式を除外する
- [ ] 抽出したテキストをJSONで出力できるようにする
  
## 今日のファイル
```text
rsrpp
├── Cargo.toml
├── rsrpp
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       └── parser
│           ├── mod.rs <-- today
│           ├── structs.rs <-- today
│           └── tests.rs
└── rsrpp-cli
    ├── Cargo.toml
    └── src
        └── main.rs
```
:::

## 前回までのあらすじ

[前回：Rustで学術論文からテキストを抽出する #8](https://qiita.com/akitenkrad/items/b09446fecca9db045bf9)

前回まででPDFのパースがほぼ完了しました．
今回はパースした結果をJSONに変換する部分を実装します．

```mermaid
graph LR
    A[要件定義]

    subgraph Popplerの観察
    B[Popperの観察]
    B1[pdftotext]
    B2[pdfinfo]
    B3[pdftohtml]
    B4[pdftocairo]
    end

    subgraph テキストの属性判定
    C[テキストの属性判定]
    C1[テキストエリアの抽出]
    end

    subgraph 図表の処理
    D[図表の処理]
    subgraph 表の検出

    D1[表の検出]
    end
    end
 
    subgraph 今日はここ
    E[JSON変換]
    end
    
    A-->B
    B1-->C
    B3-->C
    C1-->D
    D-->E
    B4-->D1
```

## 構造体 → JSON変換
Rustでは[serde](https://crates.io/crates/serde)というクレートを使うことで，構造体を簡単にシリアライズ・デシリアライズできます．そして，[serde_json](https://crates.io/crates/serde-json)によって簡単にJSONテキストを得ることができるので，正直今回はあまり解説することがありません．
全てはクレートの優秀さが原因です．

PDFの構造を扱うための構造体としては`Page`を実装していましたが，あちらをそのままJSONに落とすのは少々手間なので，JSON用の構造体を別途実装します．

下記の`Section`はその名の通り，論文の各セクションのタイトルとテキストを保持します．
`index`はセクションの順番を格納しています．

JSON変換するにあたっては，特に追加の実装は必要なく，`#[derive(Serialize, Deserialize)]`を指定しておくだけでOKです．

```rust: rsrpp > rsrpp > src > parser > structs.rs
use serde::{Deserialize, Serialize}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Section {
    pub index: i8,
    pub title: String,
    pub contents: Vec<String>,
}
```

PDFをパースした結果は`Vec<Page>`として得られるので，それを受け取って`Vec<Section>`を返すコンストラクタを実装しておきます．
実装内容についてはプログラムを読んで貰えば分かるとおり，セクションごとの`HashMap`を用意して該当するテキストを集約しているだけです．
ただし，`HashMap`では追加した順番が保存されないため，そのままではセクションの順序が壊れてしまいます．
そこで，セクションごとのインデックスを別途保存することで最終的にセクションの順番をソートできるようにしています．

```rust: rsrpp > rsrpp > src > parser > structs.rs
impl Section {
    pub fn from_pages(pages: &Vec<Page>) -> Vec<Section> {
        let mut section_indices: HashMap<String, i8> = HashMap::new();
        let mut section_map: HashMap<String, Vec<String>> = HashMap::new();
        for page in pages {
            for block in &page.blocks {
                let keys = section_map.keys().cloned().collect::<Vec<String>>();
                let text_block = block.get_text();
                if keys.contains(&block.section) {
                    let content = section_map.get_mut(&block.section).unwrap();
                    content.push(text_block);
                } else {
                    section_map.insert(block.section.clone(), vec![text_block]);
                    section_indices.insert(block.section.clone(), section_indices.len() as i8);
                }
            }
        }
        let mut sections = Vec::new();
        for (title, contents) in section_map {
            sections.push(Section {
                index: section_indices.get(&title).unwrap().clone(),
                title: title,
                contents: contents,
            });
        }
        sections.sort_by(|a, b| a.index.cmp(&b.index));
        return sections;
    }
}
```

`Page`を`Section`に変換してしまえば，あとは`serde_json`を使って以下のように簡単にJSONのテキストを得ることができます．

```rust
let sections = Section::from_pages(&pages); // Vec<Section>
let json = serde_json::to_string(&sections).unwrap(); // String
```

## 次回

次回はこれまで紹介してこなかったUtil系の処理やテストたちを紹介しようと思います．

[次回：Rustで学術論文からテキストを抽出する #10](https://qiita.com/akitenkrad/items/57104dfe83905dede6de)