Summary
- rsrppのメイン関数である
parse()
を解説
GiHub -> https://github.com/akitenkrad/rsrpp
crates.io -> https://crates.io/crates/rsrpp
ToDo
-
pdftotextで論文から単語単位のテキストと位置情報を取得する (
Word
,Line
,Block
,Page
) -
テキストの属性 (本文, タイトル, 脚注, etc.) を判定する
-
テキストが含まれるエリアを抽出する
- 2段組みを扱えるようにする
- セクションのタイトルを識別する
-
テキストが含まれるエリアを抽出する
-
図表に含まれるテキストを除外する
-
表を除外する
- PDFを画像に変換
- 画像処理で表の位置を特定
-
表を除外する
今日のファイル
rsrpp
├── Cargo.toml
├── rsrpp
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── parser
│ ├── mod.rs <-- today
│ ├── structs.rs
│ └── tests.rs
└── rsrpp-cli
├── Cargo.toml
└── src
└── main.rs
前回までのあらすじ
前回までで学術論文からテキストを抽出するためのパーツは全て揃ったので,今回はPDFをパースする関数本体を実装します.
parse()
関数
1. HTMLのパース
parse()
関数の本体は,pdf2html()
でPDFをHTMLに構造化したテキストからpage
> block
> line
> word
を順に取り出していくループがメインになります.
表領域の抽出はpage
単位で実行するので,該当するPDFのページを画像に変換したものから表を抽出しPage
構造体に格納しています.
表の抽出にはなかなか苦労させられたので,この数行には思い入れがあります.
let fig_path = config.pdf_figures.get(&page_number).unwrap();
extract_tables(
fig_path,
&mut _page.tables,
_page.width as i32,
_page.height as i32,
);
抽出した表領域はline
のループにおいて,line
が表に含まれるテキストであるかどうかを判定するために使用されます.
また,表の矩形にline
の矩形が含まれているかどうかを判定するために,第3回で実装した Coordinate::is_contained_in()
を使用しています.
for table in _page.tables.iter() {
let line_coord = Coordinate::from_object(_line.x, _line.y, _line.width, _line.height);
if line_coord.is_contained_in(&table) {
continue 'line_iter;
}
}
2. テキストエリア外のblock
の除外
HTMLをパースして基本的なPDF構造を把握したら,脚注などテキストエリア外のblock
を除外します.
なお,HTMLのパースとテキストエリアの算出の処理を分離している理由ですが,テキストエリアを算出するためには,すべてのページにわたるblock
の情報が必要になり,HTMLのパースとテキストエリアの抽出を並列に実行することができないためです.
ここでPDFの段組の判定も同時に行います.
let text_area = get_text_area(&pages);
表領域の処理のときはCoordiante::is_contained_in()
を使いましたが,テキストエリアの判定ではblock
の一部がエリア外にはみ出しているケースも存在するので,iou
を用いて "大体テキストエリア内に存在するblock
"を残すようにしています.
if (iou - 0.0).abs() < 1e-6 {
remove_indices.push(i);
} else if !section_titles.contains(&block_text.to_lowercase())
&& (block.width / width < 0.3 && block.lines.len() < 4)
{
remove_indices.push(i);
}
その後,adjust_columns()
を実行し,2段組PDFの場合のblock
の順序を調整しています.
adjst_columns(&mut pages, config);
3. セクション情報の抽出
最後に,論文のセクションのテキストを抽出してBlock
にセクションの情報を付与しています.
以上で,PDFからテキスト抽出した結果をVec::<Page>
に格納することができました.
あとは,第9回で実装した通り,Page
をSection
に変換してJSONにするなり,Page
のままさらにPDFの情報の解析を行うなりできます.
let mut current_section = "Abstract".to_string();
let mut page_number = 1;
let title_regex = regex::Regex::new(r"\d+\.").unwrap();
for page in pages.iter_mut() {
for block in page.blocks.iter_mut() {
for line in block.lines.iter_mut() {
let text = line.get_text();
let text = title_regex.replace(&text, "").trim().to_string();
if config.sections.iter().any(|(pg, section)| {
text.to_lowercase() == *section.to_lowercase() && pg == &page_number
}) {
current_section = text;
}
block.section = current_section.clone();
}
}
page_number += 1;
}
プログラム全体
parse()
の全体です.
pub async fn parse(path_or_url: &str, config: &mut ParserConfig) -> Result<Vec<Page>> {
let html = pdf2html(path_or_url, config).await?;
// ① HTMLのパース
let mut pages = Vec::new();
let page_selector = scraper::Selector::parse("page").unwrap();
let _pages = html.select(&page_selector);
for (_page_number, page) in _pages.enumerate() {
let page_number = (_page_number + 1) as PageNumber;
let page_width = page.value().attr("width").unwrap().parse::<f32>().unwrap();
let page_height = page.value().attr("height").unwrap().parse::<f32>().unwrap();
let mut _page = Page::new(page_width, page_height, page_number);
// extract tables
let fig_path = config.pdf_figures.get(&page_number).unwrap();
extract_tables(
fig_path,
&mut _page.tables,
_page.width as i32,
_page.height as i32,
);
let block_selector = scraper::Selector::parse("block").unwrap();
let _blocks = page.select(&block_selector);
for block in _blocks {
let block_xmin = block.value().attr("xmin").unwrap().parse::<f32>().unwrap();
let block_ymin = block.value().attr("ymin").unwrap().parse::<f32>().unwrap();
let block_xmax = block.value().attr("xmax").unwrap().parse::<f32>().unwrap();
let block_ymax = block.value().attr("ymax").unwrap().parse::<f32>().unwrap();
let mut _block = Block::new(
block_xmin,
block_ymin,
block_xmax - block_xmin,
block_ymax - block_ymin,
);
let line_selector = scraper::Selector::parse("line").unwrap();
let _lines = block.select(&line_selector);
'line_iter: for line in _lines {
let line_xmin = line.value().attr("xmin").unwrap().parse::<f32>().unwrap();
let line_ymin = line.value().attr("ymin").unwrap().parse::<f32>().unwrap();
let line_xmax = line.value().attr("xmax").unwrap().parse::<f32>().unwrap();
let line_ymax = line.value().attr("ymax").unwrap().parse::<f32>().unwrap();
let mut _line = Line::new(
line_xmin,
line_ymin,
line_xmax - line_xmin,
line_ymax - line_ymin,
);
for table in _page.tables.iter() {
let line_coord =
Coordinate::from_object(_line.x, _line.y, _line.width, _line.height);
if line_coord.is_contained_in(&table) {
continue 'line_iter;
}
}
let word_selector = scraper::Selector::parse("word").unwrap();
let _words = line.select(&word_selector);
for word in _words {
let word_xmin = word.value().attr("xmin").unwrap().parse::<f32>().unwrap();
let word_ymin = word.value().attr("ymin").unwrap().parse::<f32>().unwrap();
let word_xmax = word.value().attr("xmax").unwrap().parse::<f32>().unwrap();
let word_ymax = word.value().attr("ymax").unwrap().parse::<f32>().unwrap();
let text = word.text().collect::<String>();
_line.add_word(
text.clone(),
word_xmin,
word_ymin,
word_xmax - word_xmin,
word_ymax - word_ymin,
);
}
if _line.get_text().trim().len() > 0 {
_block.lines.push(_line);
}
}
if _block.lines.len() > 0 {
_page.blocks.push(_block);
}
}
if _page.blocks.len() > 0 {
pages.push(_page);
}
}
// ② テキストエリア外のBlockの除外
let section_titles =
config.sections.iter().map(|(_, section)| section.to_lowercase()).collect::<Vec<String>>();
let text_area = get_text_area(&pages);
let title_index_regex = regex::Regex::new(r"\d+\.").unwrap();
for page in pages.iter_mut() {
let mut remove_indices: Vec<usize> = Vec::new();
let width = if page.number_of_columns == 2 {
page.width / 2.2
} else {
page.width / 1.1
};
for (i, block) in page.blocks.iter_mut().enumerate() {
let block_coord = Coordinate::from_object(block.x, block.y, block.width, block.height);
let iou = text_area.iou(&block_coord);
let block_text = block.get_text();
let block_text = title_index_regex.replace(&block_text, "").trim().to_string();
if (iou - 0.0).abs() < 1e-6 {
remove_indices.push(i);
} else if !section_titles.contains(&block_text.to_lowercase())
&& (block.width / width < 0.3 && block.lines.len() < 4)
{
remove_indices.push(i);
}
}
for i in remove_indices.iter().rev() {
page.blocks.remove(*i);
}
}
adjst_columns(&mut pages, config);
// ③ セクション情報の抽出
let mut current_section = "Abstract".to_string();
let mut page_number = 1;
let title_regex = regex::Regex::new(r"\d+\.").unwrap();
for page in pages.iter_mut() {
for block in page.blocks.iter_mut() {
for line in block.lines.iter_mut() {
let text = line.get_text();
let text = title_regex.replace(&text, "").trim().to_string();
if config.sections.iter().any(|(pg, section)| {
text.to_lowercase() == *section.to_lowercase() && pg == &page_number
}) {
current_section = text;
}
block.section = current_section.clone();
}
}
page_number += 1;
}
return Ok(pages);
}
次回
以上で,rsrpp
が完成しました.
ただし,このままではPDFをパースするために毎回プログラムを書かなくてはいけないので,次回はこれをcliで利用できるようにコマンドラインツールを整備します.