mirror of
https://github.com/mii443/akaza.git
synced 2025-08-22 14:55:31 +00:00
refactor vibrato_annotate.rs
This commit is contained in:
@ -9,6 +9,8 @@ use crate::subcmd::structured_perceptron::learn_structured_perceptron;
|
||||
use crate::subcmd::vibrato_annotate::annotate_wikipedia;
|
||||
|
||||
mod subcmd;
|
||||
mod tokenizer;
|
||||
mod wikipedia;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(
|
||||
|
@ -1,137 +1,17 @@
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use crate::tokenizer::vibrato::VibratoTokenizer;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use kelp::{kata2hira, ConvOption};
|
||||
use log::info;
|
||||
use regex::Regex;
|
||||
use vibrato::{Dictionary, Tokenizer};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub struct VibtaroRunner {
|
||||
tokenizer: Tokenizer,
|
||||
pub alnum_pattern: Regex,
|
||||
pub yomigana_pattern: Regex,
|
||||
}
|
||||
|
||||
impl VibtaroRunner {
|
||||
pub fn new() -> anyhow::Result<VibtaroRunner> {
|
||||
let dict = Dictionary::read(File::open("work/mecab/ipadic-mecab-2_7_0/system.dic")?)?;
|
||||
let dict = dict
|
||||
.reset_user_lexicon_from_reader(Some(File::open(
|
||||
"jawiki-kana-kanji-dict/mecab-userdic.csv",
|
||||
)?))
|
||||
.with_context(|| "Opening userdic")?;
|
||||
|
||||
let tokenizer = vibrato::Tokenizer::new(dict);
|
||||
|
||||
// 英数/記号のみの行を無視するための正規表現。
|
||||
// 75||19||colspan=2|-||1||0||76||19
|
||||
let alnum_pattern = Regex::new("^[a-zA-Z0-9|=-]+")?;
|
||||
|
||||
// 上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
// → 上級個人情報保護士は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
let yomigana_pattern = Regex::new(r#"[(\(][\u3041-\u309F、]+[))]"#)?;
|
||||
|
||||
Ok(VibtaroRunner {
|
||||
tokenizer,
|
||||
alnum_pattern,
|
||||
yomigana_pattern,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_file(&self, ifname: &Path, ofname: &Path) -> anyhow::Result<()> {
|
||||
let file = File::open(ifname)?;
|
||||
let mut buf = String::new();
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.starts_with('<') {
|
||||
// <doc id="3697757" url="https://ja.wikipedia.org/wiki?curid=3697757"
|
||||
// title="New Sunrise">
|
||||
// のような、タグから始まる行を無視する。
|
||||
continue;
|
||||
}
|
||||
if line.is_empty() {
|
||||
// 空行を無視する
|
||||
continue;
|
||||
}
|
||||
if self.alnum_pattern.is_match(line) {
|
||||
// 英数字のみの行は無視する
|
||||
continue;
|
||||
}
|
||||
let line = self.remove_yomigana(line);
|
||||
|
||||
buf += self.annotate(line.as_str())?.as_str();
|
||||
}
|
||||
let mut ofile = File::create(ofname)?;
|
||||
ofile.write_all(buf.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_yomigana(&self, src: &str) -> String {
|
||||
self.yomigana_pattern.replace_all(src, "").to_string()
|
||||
}
|
||||
|
||||
/// Vibrato を利用してファイルをアノテーションします。
|
||||
pub fn annotate(&self, src: &str) -> anyhow::Result<String> {
|
||||
let mut worker = self.tokenizer.new_worker();
|
||||
|
||||
worker.reset_sentence(src);
|
||||
worker.tokenize();
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
// TODO 連結処理的なことが必要ならする。
|
||||
// Vibrato/mecab の場合、接尾辞などが細かく分かれることは少ないが、
|
||||
// 一方で、助詞/助動詞などが細かくわかれがち。
|
||||
for i in 0..worker.num_tokens() {
|
||||
let token = worker.token(i);
|
||||
let feature: Vec<&str> = token.feature().split(',').collect();
|
||||
// if feature.len() <= 7 {
|
||||
// println!("Too few features: {}/{}", token.surface(), token.feature())
|
||||
// }
|
||||
|
||||
// let hinshi = feature[0];
|
||||
let yomi = if feature.len() > 7 {
|
||||
feature[7]
|
||||
} else {
|
||||
// 読みがな不明なもの。固有名詞など。
|
||||
// サンデフィヨルド・フォトバル/名詞,固有名詞,組織,*,*,*,*
|
||||
token.surface()
|
||||
};
|
||||
let yomi = kata2hira(yomi, ConvOption::default());
|
||||
buf += format!("{}/{} ", token.surface(), yomi).as_str();
|
||||
// println!("{}/{}/{}", token.surface(), hinshi, yomi);
|
||||
}
|
||||
|
||||
Ok(buf.trim().to_string() + "\n")
|
||||
}
|
||||
}
|
||||
use crate::wikipedia::wikipedia_extracted::ExtractedWikipediaProcessor;
|
||||
|
||||
pub fn annotate_wikipedia() -> anyhow::Result<()> {
|
||||
let output_dir = Path::new("work/mecab/wikipedia-annotated/");
|
||||
let runner = VibtaroRunner::new()?;
|
||||
let runner = VibratoTokenizer::new()?;
|
||||
|
||||
// TODO parallel processing
|
||||
for src_file in WalkDir::new("work/extracted")
|
||||
.into_iter()
|
||||
.filter_map(|file| file.ok())
|
||||
.filter(|file| file.metadata().unwrap().is_file())
|
||||
{
|
||||
let src_path = src_file.path();
|
||||
let dirname = src_path.parent().unwrap().file_name().unwrap();
|
||||
fs::create_dir_all(output_dir.join(dirname))?;
|
||||
let output_file = output_dir.join(dirname).join(src_path.file_name().unwrap());
|
||||
info!(
|
||||
"{} => {}",
|
||||
src_file.path().display(),
|
||||
output_file.as_path().display()
|
||||
);
|
||||
runner.process_file(src_file.path(), &output_file)?;
|
||||
}
|
||||
let processor = ExtractedWikipediaProcessor::new()?;
|
||||
processor.process_files(
|
||||
Path::new("work/extracted"),
|
||||
Path::new("work/mecab/wikipedia-annotated/"),
|
||||
|line| runner.annotate(line),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -142,21 +22,17 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test() -> anyhow::Result<()> {
|
||||
let runner = VibtaroRunner::new()?;
|
||||
runner.annotate("私の名前は中野です。")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_wikipedia() -> anyhow::Result<()> {
|
||||
let runner = VibtaroRunner::new()?;
|
||||
let runner = VibratoTokenizer::new()?;
|
||||
let processor = ExtractedWikipediaProcessor::new()?;
|
||||
|
||||
let fname = "work/extracted/BE/wiki_02";
|
||||
fs::create_dir_all("work/mecab/wikipedia-annotated/BE/")?;
|
||||
runner.process_file(
|
||||
processor.process_file(
|
||||
Path::new(fname),
|
||||
Path::new("work/mecab/wikipedia-annotated/BE/wiki_02"),
|
||||
&mut (|line| runner.annotate(line)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -168,15 +44,4 @@ mod tests {
|
||||
annotate_wikipedia()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_yomigana() -> anyhow::Result<()> {
|
||||
// 上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
// → 上級個人情報保護士は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
let runner = VibtaroRunner::new()?;
|
||||
let got =
|
||||
runner.remove_yomigana("上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は");
|
||||
assert_eq!(got, "上級個人情報保護士は");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
1
akaza-data/src/tokenizer/mod.rs
Normal file
1
akaza-data/src/tokenizer/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod vibrato;
|
70
akaza-data/src/tokenizer/vibrato.rs
Normal file
70
akaza-data/src/tokenizer/vibrato.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use anyhow::Context;
|
||||
use kelp::{kata2hira, ConvOption};
|
||||
use std::fs::File;
|
||||
use vibrato::{Dictionary, Tokenizer};
|
||||
|
||||
pub struct VibratoTokenizer {
|
||||
tokenizer: Tokenizer,
|
||||
}
|
||||
|
||||
impl VibratoTokenizer {
|
||||
pub fn new() -> anyhow::Result<VibratoTokenizer> {
|
||||
let dict = Dictionary::read(File::open("work/mecab/ipadic-mecab-2_7_0/system.dic")?)?;
|
||||
let dict = dict
|
||||
.reset_user_lexicon_from_reader(Some(File::open(
|
||||
"jawiki-kana-kanji-dict/mecab-userdic.csv",
|
||||
)?))
|
||||
.with_context(|| "Opening userdic")?;
|
||||
|
||||
let tokenizer = vibrato::Tokenizer::new(dict);
|
||||
|
||||
Ok(VibratoTokenizer { tokenizer })
|
||||
}
|
||||
|
||||
/// Vibrato を利用してファイルをアノテーションします。
|
||||
pub fn annotate(&self, src: &str) -> anyhow::Result<String> {
|
||||
let mut worker = self.tokenizer.new_worker();
|
||||
|
||||
worker.reset_sentence(src);
|
||||
worker.tokenize();
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
// TODO 連結処理的なことが必要ならする。
|
||||
// Vibrato/mecab の場合、接尾辞などが細かく分かれることは少ないが、
|
||||
// 一方で、助詞/助動詞などが細かくわかれがち。
|
||||
for i in 0..worker.num_tokens() {
|
||||
let token = worker.token(i);
|
||||
let feature: Vec<&str> = token.feature().split(',').collect();
|
||||
// if feature.len() <= 7 {
|
||||
// println!("Too few features: {}/{}", token.surface(), token.feature())
|
||||
// }
|
||||
|
||||
// let hinshi = feature[0];
|
||||
let yomi = if feature.len() > 7 {
|
||||
feature[7]
|
||||
} else {
|
||||
// 読みがな不明なもの。固有名詞など。
|
||||
// サンデフィヨルド・フォトバル/名詞,固有名詞,組織,*,*,*,*
|
||||
token.surface()
|
||||
};
|
||||
let yomi = kata2hira(yomi, ConvOption::default());
|
||||
buf += format!("{}/{} ", token.surface(), yomi).as_str();
|
||||
// println!("{}/{}/{}", token.surface(), hinshi, yomi);
|
||||
}
|
||||
|
||||
Ok(buf.trim().to_string() + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test() -> anyhow::Result<()> {
|
||||
let runner = VibratoTokenizer::new()?;
|
||||
runner.annotate("私の名前は中野です。")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
1
akaza-data/src/wikipedia/mod.rs
Normal file
1
akaza-data/src/wikipedia/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod wikipedia_extracted;
|
120
akaza-data/src/wikipedia/wikipedia_extracted.rs
Normal file
120
akaza-data/src/wikipedia/wikipedia_extracted.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use log::info;
|
||||
use regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// wikiextractor で処理したデータを取り扱うための処理
|
||||
pub struct ExtractedWikipediaProcessor {
|
||||
alnum_pattern: Regex,
|
||||
yomigana_pattern: Regex,
|
||||
}
|
||||
|
||||
impl ExtractedWikipediaProcessor {
|
||||
pub fn new() -> anyhow::Result<ExtractedWikipediaProcessor> {
|
||||
// 英数/記号のみの行を無視するための正規表現。
|
||||
// 75||19||colspan=2|-||1||0||76||19
|
||||
let alnum_pattern = Regex::new("^[a-zA-Z0-9|=-]+")?;
|
||||
|
||||
// 上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
// → 上級個人情報保護士は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
let yomigana_pattern = Regex::new(r#"[(\(][\u3041-\u309F、]+[))]"#)?;
|
||||
|
||||
Ok(ExtractedWikipediaProcessor {
|
||||
alnum_pattern,
|
||||
yomigana_pattern,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_file<F>(
|
||||
&self,
|
||||
ifname: &Path,
|
||||
ofname: &Path,
|
||||
annotate: &mut F,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
F: FnMut(&str) -> anyhow::Result<String>,
|
||||
{
|
||||
let file = File::open(ifname)?;
|
||||
let mut buf = String::new();
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.starts_with('<') {
|
||||
// <doc id="3697757" url="https://ja.wikipedia.org/wiki?curid=3697757"
|
||||
// title="New Sunrise">
|
||||
// のような、タグから始まる行を無視する。
|
||||
continue;
|
||||
}
|
||||
if line.is_empty() {
|
||||
// 空行を無視する
|
||||
continue;
|
||||
}
|
||||
if self.alnum_pattern.is_match(line) {
|
||||
// 英数字のみの行は無視する
|
||||
continue;
|
||||
}
|
||||
let line = self.remove_yomigana(line);
|
||||
|
||||
buf += annotate(line.as_str()).with_context(|| line)?.as_str();
|
||||
}
|
||||
let mut ofile = File::create(ofname)?;
|
||||
ofile.write_all(buf.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_yomigana(&self, src: &str) -> String {
|
||||
self.yomigana_pattern.replace_all(src, "").to_string()
|
||||
}
|
||||
|
||||
/// ファイルを処理します。
|
||||
/// シリアルに処理すると遅いので、パラレルに処理します(する予定)
|
||||
pub fn process_files<F>(
|
||||
&self,
|
||||
src_dir: &Path,
|
||||
dst_dir: &Path,
|
||||
mut annotate: F,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
F: FnMut(&str) -> anyhow::Result<String>,
|
||||
{
|
||||
// TODO parallel processing
|
||||
for src_file in WalkDir::new(src_dir)
|
||||
.into_iter()
|
||||
.filter_map(|file| file.ok())
|
||||
.filter(|file| file.metadata().unwrap().is_file())
|
||||
{
|
||||
let src_path = src_file.path();
|
||||
let dirname = src_path.parent().unwrap().file_name().unwrap();
|
||||
fs::create_dir_all(dst_dir.join(dirname))?;
|
||||
let output_file = dst_dir.join(dirname).join(src_path.file_name().unwrap());
|
||||
info!(
|
||||
"{} => {}",
|
||||
src_file.path().display(),
|
||||
output_file.as_path().display()
|
||||
);
|
||||
self.process_file(src_file.path(), &output_file, &mut annotate)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_remove_yomigana() -> anyhow::Result<()> {
|
||||
// 上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
// → 上級個人情報保護士は、財団法人全日本情報学習振興協会が設けている民間資格の称号。
|
||||
let runner = ExtractedWikipediaProcessor::new()?;
|
||||
let got =
|
||||
runner.remove_yomigana("上級個人情報保護士(じょうきゅうこじんじょうほうほごし)は");
|
||||
assert_eq!(got, "上級個人情報保護士は");
|
||||
Ok(())
|
||||
}
|
||||
}
|
20
docs/preproc/mecab.md
Normal file
20
docs/preproc/mecab.md
Normal file
@ -0,0 +1,20 @@
|
||||
# mecab (Vibrato)の出力に対する前処理
|
||||
|
||||
MeCab の文節処理は、かな漢字変換に使うと考えると細かすぎる。
|
||||
|
||||
例えば以下のようになる。
|
||||
|
||||
```
|
||||
養子となっている
|
||||
養子 名詞,一般,*,*,*,*,養子,ヨウシ,ヨーシ
|
||||
と 助詞,格助詞,一般,*,*,*,と,ト,ト
|
||||
なっ 動詞,自立,*,*,五段・ラ行,連用タ接続,なる,ナッ,ナッ
|
||||
て 助詞,接続助詞,*,*,*,*,て,テ,テ
|
||||
いる 動詞,非自立,*,*,一段,基本形,いる,イル,イル
|
||||
EOS
|
||||
```
|
||||
|
||||
これは形態素解析機としては妥当なのだけれど、かな漢字変換器として考えた場合には少しこまかくちぎりすぎである。
|
||||
よって、これを少し、調整していく。
|
||||
|
||||
具体的には、「養子/ようし と/と なって/なって いる/いる」ぐらいになっていると自然かなぁ。
|
Reference in New Issue
Block a user