4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ユニークビジョン株式会社Advent Calendar 2022

Day 4

TwitterのDMで送られてきた住所と転記したスプレッドシートのチェック

Last updated at Posted at 2022-12-03

目的

TwitterのDMで住所を送ってもらって、それをスプレッドシートに転記をする業務があります。
どうしても転記には手作業の部分が入ってしまうので、きちんと転記できたかチェックする仕組みを作りました。

RustでSQLiteとCSVを使っています。これらを使う時のサンプルとして参考にしてください。

DMデータ

DMは複数行にわたって入力される可能性があります。

twitter_id message
123 こんにちは
123 住所は
123 東京都です
456 住所は千葉です

住所データ

上記データから住所データを作ります

twitter_id address
123 東京都
456 千葉県

この時住所データが正しいことをチェックするために、住所データに入っている文字がDMのメッセージにどれだけヒットするかで調べました。
例えば
123は住所は3文字でヒット数は3
456は住所は3文字でヒット数は2
になります。

また「1-2-3」と「1-2-3」が全くヒットしないのは厳しいのでユニコード正規化をすることでヒット数を上げました。

コード

DMデータ読み込み

CSVにあるDMデータを読み込んでtwitter_idからデータを取得できるようにSQLiteに保存するようにしました。
メッセージの順番は今回意味が無いんですが、なんとなく到着順になるように調整しました。

fn prepare(conn: &Connection) -> anyhow::Result<()> {
    conn.execute(
        "CREATE TABLE dm1 (
            twitter_id TEXT,
            message TEXT,
            line_no INT
        )",
        (),
    )?;

    conn.execute(
        "CREATE TABLE dm2 (
            twitter_id TEXT,
            message TEXT
        )",
        (),
    )?;
    Ok(())
}

fn read_dm(conn: &Connection) -> anyhow::Result<()> {
    let mut rdr = csv::Reader::from_path("dm.csv")?;
    let mut count = 0;
    for result in rdr.records() {
        let record = result?;
        let twitter_id = record.get(1).unwrap();
        let message = record.get(2).unwrap().to_owned();
        conn.execute(
            "INSERT INTO dm1 (twitter_id, message, line_no) VALUES (?1, ?2, ?3)",
            (twitter_id, message, count),
        )?;
        count += 1;
    }
    conn.execute(
        r#"
            INSERT INTO dm2 (twitter_id, message) 
            SELECT 
                account_twid, group_concat(message, ',') 
            FROM 
                (
                    SELECT
                        twitter_id
                        ,message
                    FROM
                        dm1
                    ORDER BY
                        twitter_id,
                        line_no
                ) 
            GROUP BY twitter_id
        "#,
        (),
    )?;
    Ok(())
}

fn get_message(conn: &Connection, twitter_id: &str) -> anyhow::Result<Option<String>> {
    let mut stmt = conn.prepare("SELECT message FROM dm2 WHERE twitter_id = ?1")?;
    let mut res = stmt.query(params![twitter_id])?;
    if let Some(row) = res.next()? {
        Ok(Some(row.get::<usize, String>(0)?))
    } else {
        Ok(None)
    }
}

住所データチェック

CSVで住所データを読み込んでチェックした結果をresult.csvに書き出しています。
正規化前と正規化後の文字数とヒット数を出しています。
また正規化後にマッチしなかった文字も出しています。

fn parse_contain(src: &str, target: &str) -> (i64, i64, Vec<String>) {
    let mut count = 0;
    let mut hit = 0;
    let mut unmatch = vec![];
    for ch in src.chars() {
        count += 1;
        if target.contains(ch) {
            hit += 1;
        } else {
            unmatch.push(ch.to_string());
        }
    }
    (count, hit, unmatch)
}

fn read_address(conn: &Connection, file_name: &str) -> anyhow::Result<()> {
    let mut rdr = Reader::from_path(file_name)?;
    let mut address_count = 0;
    let mut unmatch_count = 0;
    let mut min = 100.0;

    let mut wtr = WriterBuilder::new().from_path("result.csv")?;
    for result in rdr.records() {
        let record = result?;
        let twitter_id = record.get(0).unwrap();
        let message = get_message(conn, twitter_id)?.unwrap();
        let src = record.get(1).unwrap();
        let (count, hit, _unmatch) = parse_contain(&src, &message);
        let src2 = src.nfkc().collect::<String>();
        let message2 = message.nfkc().collect::<String>();
        let (count2, hit2, unmatch2) = parse_contain(&src2, &message2);
        if count != hit {
            let rate = (hit as f64) / (count as f64) * 100.0;
            if rate < min {
                min = rate;
            }
            unmatch_count += 1;
            if rate < 90.0 {
                println!("{}, {},{},{}", twitter_id, count, hit, rate);
            }
        }
        address_count += 1;
        wtr.write_record(&[twitter_id, &count.to_string(), &hit.to_string(), &count2.to_string(), &hit2.to_string(), &unmatch2.join("")])?;
    }
    println!("address count={}, unmatch={}, min={}", address_count, unmatch_count, min);
    wtr.flush()?;

    Ok(())
}

最後に全体

Cargo.toml
[package]
name = "sqlite_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rusqlite = { version = "0.28.0", features = ["bundled"] }
anyhow = "1.0.66"
csv = "1.1.6"
unicode-normalization = "0.1.22"
main.rs
use rusqlite::{Connection, params};
use csv::{Reader, WriterBuilder};
use unicode_normalization::UnicodeNormalization;

fn prepare(conn: &Connection) -> anyhow::Result<()> {
    conn.execute(
        "CREATE TABLE dm1 (
            twitter_id TEXT,
            message TEXT,
            line_no INT
        )",
        (),
    )?;

    conn.execute(
        "CREATE TABLE dm2 (
            twitter_id TEXT,
            message TEXT
        )",
        (),
    )?;
    Ok(())
}

fn read_dm(conn: &Connection) -> anyhow::Result<()> {
    let mut rdr = csv::Reader::from_path("dm.csv")?;
    let mut count = 0;
    for result in rdr.records() {
        let record = result?;
        let twitter_id = record.get(1).unwrap();
        let message = record.get(2).unwrap().to_owned();
        conn.execute(
            "INSERT INTO dm1 (twitter_id, message, line_no) VALUES (?1, ?2, ?3)",
            (twitter_id, message, count),
        )?;
        count += 1;
    }
    conn.execute(
        r#"
            INSERT INTO dm2 (twitter_id, message) 
            SELECT 
                twitter_id, group_concat(message, ',') 
            FROM 
                (
                    SELECT
                        twitter_id
                        ,message
                    FROM
                        dm1
                    ORDER BY
                        twitter_id,
                        line_no
                ) 
            GROUP BY twitter_id
        "#,
        (),
    )?;
    Ok(())
}

fn get_message(conn: &Connection, twitter_id: &str) -> anyhow::Result<Option<String>> {
    let mut stmt = conn.prepare("SELECT message FROM dm2 WHERE twitter_id = ?1")?;
    let mut res = stmt.query(params![twitter_id])?;
    if let Some(row) = res.next()? {
        Ok(Some(row.get::<usize, String>(0)?))
    } else {
        Ok(None)
    }
}

fn parse_contain(src: &str, target: &str) -> (i64, i64, Vec<String>) {
    let mut count = 0;
    let mut hit = 0;
    let mut unmatch = vec![];
    for ch in src.chars() {
        count += 1;
        if target.contains(ch) {
            hit += 1;
        } else {
            unmatch.push(ch.to_string());
        }
    }
    (count, hit, unmatch)
}

fn read_address(conn: &Connection, file_name: &str) -> anyhow::Result<()> {
    let mut rdr = Reader::from_path(file_name)?;
    let mut address_count = 0;
    let mut unmatch_count = 0;
    let mut min = 100.0;

    let mut wtr = WriterBuilder::new().from_path("result.csv")?;
    for result in rdr.records() {
        let record = result?;
        let twitter_id = record.get(0).unwrap();
        let message = get_message(conn, twitter_id)?.unwrap();
        /*
        let mut src = String::new();
        for n in 3..9 {
            src.push_str(record.get(n).unwrap());
        }
        let src = record.get(1).unwrap();
         */
        let src = record.get(1).unwrap().replace("\n", "");
        let (count, hit, _unmatch) = parse_contain(&src, &message);
        let src2 = src.nfkc().collect::<String>();
        let message2 = message.nfkc().collect::<String>();
        let (count2, hit2, unmatch2) = parse_contain(&src2, &message2);
        if count != hit {
            let rate = (hit as f64) / (count as f64) * 100.0;
            if rate < min {
                min = rate;
            }
            unmatch_count += 1;
            if rate < 90.0 {
                println!("{}, {},{},{}", twitter_id, count, hit, rate);
            }
        }
        address_count += 1;
        wtr.write_record(&[twitter_id, &count.to_string(), &hit.to_string(), &count2.to_string(), &hit2.to_string(), &unmatch2.join("")])?;
    }
    println!("address count={}, unmatch={}, min={}", address_count, unmatch_count, min);
    wtr.flush()?;

    Ok(())
}

fn main() -> anyhow::Result<()> {
    let conn = Connection::open_in_memory()?;
    prepare(&conn)?;
    read_dm(&conn)?;
    read_address(&conn, "address.csv")?;
    Ok(())
}
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?