#![allow(missing_docs)]
use std::{
path::{Path, PathBuf},
};
use quick_xml::Reader;
use quick_xml::events::Event;
pub struct SentiSentence {
pub id: String,
pub value: String,
pub text: String,
}
pub struct SentiComment {
pub id: String,
pub holder: String,
pub value: String,
pub sentences: Vec<SentiSentence>,
}
pub struct SentiDoc {
pub title: String,
pub product_type: String,
pub comments: Vec<SentiComment>,
}
fn collect_xml_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(collect_xml_files(&path));
} else if path.extension().map_or(false, |e| e == "xml") {
files.push(path);
}
}
}
files
}
fn parse_senti_docs(content: &str) -> Vec<SentiDoc> {
let mut docs: Vec<SentiDoc> = Vec::new();
let mut reader = Reader::from_str(content);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut in_product = false;
let mut current_doc: Option<SentiDoc> = None;
let mut in_comment = false;
let mut current_comment: Option<SentiComment> = None;
let mut in_sentence = false;
let mut current_sentence: Option<SentiSentence> = None;
let mut current_text = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
match name.as_str() {
"Product" => {
in_product = true;
let mut doc = SentiDoc {
title: String::new(),
product_type: String::new(),
comments: Vec::new(),
};
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let val = String::from_utf8_lossy(&attr.value).into_owned();
if key == b"Title" {
doc.title = val;
} else if key == b"Type" {
doc.product_type = val;
}
}
current_doc = Some(doc);
}
"Opinion" | "Criticism" if in_product => {
in_comment = true;
let mut comment = SentiComment {
id: String::new(),
holder: String::new(),
value: String::new(),
sentences: Vec::new(),
};
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let val = String::from_utf8_lossy(&attr.value).into_owned();
match key {
b"id" => comment.id = val,
b"holder" => comment.holder = val,
b"value" => comment.value = val,
_ => {}
}
}
current_comment = Some(comment);
}
"Sentence" if in_comment => {
in_sentence = true;
let mut sent = SentiSentence {
id: String::new(),
value: String::new(),
text: String::new(),
};
for attr in e.attributes().flatten() {
let key = attr.key.as_ref();
let val = String::from_utf8_lossy(&attr.value).into_owned();
match key {
b"id" => sent.id = val,
b"value" => sent.value = val,
_ => {}
}
}
current_sentence = Some(sent);
current_text.clear();
}
_ => {}
}
}
Ok(Event::End(e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
match name.as_str() {
"Product" => {
if let Some(doc) = current_doc.take() {
docs.push(doc);
}
in_product = false;
}
"Opinion" | "Criticism" if in_comment => {
if let (Some(ref mut doc), Some(comment)) =
(&mut current_doc, current_comment.take())
{
doc.comments.push(comment);
}
in_comment = false;
}
"Sentence" if in_sentence => {
if let Some(ref mut sent) = current_sentence {
sent.text = current_text.trim().to_string();
}
if let (Some(ref mut comment), Some(sent)) =
(&mut current_comment, current_sentence.take())
{
comment.sentences.push(sent);
}
in_sentence = false;
current_text.clear();
}
_ => {}
}
}
Ok(Event::Text(e)) if in_sentence => {
let text = e.unescape().unwrap_or_default().into_owned();
current_text.push_str(&text);
}
Ok(Event::CData(e)) if in_sentence => {
let text = String::from_utf8_lossy(e.as_ref()).into_owned();
current_text.push_str(&text);
}
Ok(Event::Eof) => break,
Err(e) => {
eprintln!("SentiPersReader: XML error: {}", e);
break;
}
_ => {}
}
buf.clear();
}
docs
}
pub struct SentiPersReader {
root: PathBuf,
}
impl SentiPersReader {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn docs(&self) -> Box<dyn Iterator<Item = SentiDoc> + '_> {
let files = collect_xml_files(&self.root);
let mut docs: Vec<SentiDoc> = Vec::new();
for file_path in &files {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
eprintln!("SentiPersReader: cannot read {:?}: {}", file_path, e);
continue;
}
};
docs.extend(parse_senti_docs(&content));
}
Box::new(docs.into_iter())
}
pub fn comments(&self) -> Box<dyn Iterator<Item = Vec<Vec<String>>> + '_> {
let docs: Vec<SentiDoc> = self.docs().collect();
let result: Vec<Vec<Vec<String>>> = docs
.into_iter()
.map(|doc| {
doc.comments
.into_iter()
.map(|c| c.sentences.into_iter().map(|s| s.text).collect())
.collect()
})
.collect();
Box::new(result.into_iter())
}
}