0
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?

Rustのお遊び帳

Posted at

経緯

上記のような箇所で最近ぼちぼちGo言語を扱うことが増えてきました。
Goの案件というより、単純に雑務を自動化するのに勝手が良いからです。そのあたりは既に他の方が書かれた記事があるのでそちらを参照していただくほうが良いでしょう。Don't reinvent the wheelというわけです。

nitpickingかもしれませんが、一つだけ合意できなかったのはビルドが驚くほど速いというところです。もちろんそれもGo開発の主眼のひとつではあるのですが、(参考)雑務自動化スクリプトの範疇であれば大した複雑さ・規模でもないので、そもそもビルド時間が気になるほど長くなることはないのでは…?という気はします。

感想の掛け合いだし、私のコードが小さすぎるだけ、コーディング始めたのが最近過ぎてCPUに困ったことが無いだけ、など色々な可能性が既に見えています。だから何と言いたいわけじゃないです。

といった具合で概ね困っていないのですが、やはりCPU intensive なものになると Go にはどうしても陰りが見え始めてしまいます。また shared memory の状態で並列処理をすると当然 write skew であったりとかのバグを踏みやすく、 Rust 教に入信し fearless concurrency の恩恵を享受したいというわけです。

つまりなんだ

Rust の練習のために前投稿したやつの劣化版書きました。いぇい!ということですね。

SEO 的にもちゃんとここでも処理内容をいいなさい

登記所備付地図データというものがCKANから取得できます。CKANのAPIから当情報の入った zip ファイルのURL一覧が取得できます。
なのでAPIを使用して zip への URL を取得、このURLを使って zip ファイルを取得しメモリ上に保存、再帰を使って中のXMLまで行ってから、今回は、標準出力に書き出しましたという内容ですね。

※以下の要領で zip の中に zip があるので再帰をしています。

top.zip
├───0123456789.zip
│   └───0123456789.xml
…

なるほど、で、コードは

zip 解凍部分は CPU intensive なわけですが、どうやってtokio::spawnじゃなくて普通のOSスレッドのほうに乗せるか難しいですねぇ( Vec<u8> としてレスポンスボディを持ってきたら tokio 側はおしまい、channel にポインタ乗せてさっさとOSスレッド側に渡す…とかすればよいのでしょうか)
それ以前に私が初心者すぎてIO周りの定石( Read+Seek するには Cursor を使えばいいんだ!など)を知らず色々躓いたのでそれのメモとして今回は残し、今後は上記を改善しようと思います。

use std::io::Cursor;
use std::io::Read;
use std::vec;
use reqwest::header::USER_AGENT;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use serde_json::Value;
use reqwest;
use tokio::join;
use tokio::sync::mpsc;
use std::sync::LazyLock;
use zip;

//The Client holds a connection pool internally, so it is advised that you create one and reuse it.
//You do not have to wrap the Client in an Rc or Arc to reuse it, because it already uses an Arc internally.
//★その意味では LazyLock じゃなくて LazyCell でもよいのかも? https://doc.rust-lang.org/std/cell/struct.LazyCell.html
//まあ一度しかやらない処理なので差があったとして(処理時間のほとんどはIOなので)有意な差には多分ならないけど、それ言ったらなんでRust使ってるのとも()
static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(||{
    let policy = reqwest::redirect::Policy::custom(|attempt| {
        println!("redirecting to {}...", attempt.url());
        attempt.follow()
    });
    reqwest::Client::builder().redirect(policy).build().unwrap()
});
static FAKE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1";
static CKAN_ENDPOINT:&str = "https://www.geospatial.jp/ckan/api/3/action/package_search?q=(tags:%E6%B3%95%E5%8B%99%E7%9C%81%20AND%20tags:%E5%9C%B0%E5%9B%B3%E6%83%85%E5%A0%B1)&rows=9";

#[tokio::main]
async fn main() {
    let (send_chan, mut receive_chan) = mpsc::channel::<String>(32);
    let u2c = url_to_chan(send_chan);
    let dwu = download_with_url(&mut receive_chan);


    join!(u2c, dwu);
    
}

async fn download_with_url(chan : &mut tokio::sync::mpsc::Receiver<String>)->(){
    //後でよんどく https://rust-unofficial.github.io/patterns/idioms/option-iter.html
    for url in chan.recv().await {
        let response = reqwest::Client::new()
                              .get(&url)
                              .header(USER_AGENT, FAKE_USER_AGENT)
                              .send()
                              .await
                              .unwrap();
        

        println!("{}", &url);
        println!("content length {}", response.content_length().unwrap());
        println!("content type {}", response.headers().get("Content-Type").unwrap().to_str().unwrap());


        let body = response.bytes()
                              .await
                              .unwrap();
        let cursor = Cursor::new(&body);
        let _ = recursive_unzip(cursor, ".xml");

    }
}

fn recursive_unzip<ReadAt: std::io::Read+std::io::Seek> (cursor:ReadAt, target_extension:&str){
    let mut archive = zip::ZipArchive::new(cursor).unwrap();

    for index in 0 .. archive.len() {
        let mut file = archive.by_index(index).unwrap();
        if !file.is_file() {
            continue
        };
        if file.name().ends_with(".zip"){
            let mut buf = vec!();
            let _ = file.read_to_end(&mut buf);
            let inner_cursor = Cursor::new(&buf);
            let _ = recursive_unzip(inner_cursor, target_extension);
            println!("file name! {}",file.name());
        }
        if file.name().ends_with(target_extension){
            let mut buf = vec!();
            let _ = file.read_to_end(&mut buf);
            
            //https://stackoverflow.com/questions/42240663/how-to-read-stdioread-from-a-vec-or-slice
            let _ = std::io::copy(&mut &buf[..], &mut std::io::stdout().lock());
        }
    }
}
async fn url_to_chan(send_chan: tokio::sync::mpsc::Sender<String>)->(){
    for url in get_urls_for_zips().await.unwrap(){
        send_chan.send(url).await.unwrap();  
    }
    drop(send_chan)
}

async fn get_urls_for_zips() -> std::result::Result<impl Iterator<Item=String>, Box<dyn std::error::Error>> {
    let body = CLIENT
        .get(CKAN_ENDPOINT)
        .header(USER_AGENT, FAKE_USER_AGENT)
        .send()
        .await?
        .text()
        .await?;

    let root: Root = serde_json::from_str(&body)?;
    let i = root.result.results
    .into_iter()
    .filter(|result| result.title.ends_with("登記所備付地図データ"))
    .flat_map(|result| result.resources)
    .filter(|resource| resource.format.to_lowercase() == "zip")
    .map(|resource| resource.url);

    return Ok(i)
}



#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
    pub help: String,
    pub success: bool,
    pub result: Result,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Result {
    pub count: i64,
    pub facets: Facets,
    pub results: Vec<Result2>,
    pub sort: String,
    #[serde(rename = "search_facets")]
    pub search_facets: SearchFacets,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Facets {}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Result2 {
    pub area: String,
    pub author: String,
    #[serde(rename = "author_email")]
    pub author_email: String,
    pub charge: String,
    #[serde(rename = "creator_user_id")]
    pub creator_user_id: String,
    pub id: String,
    pub isopen: bool,
    #[serde(rename = "license_id")]
    pub license_id: String,
    #[serde(rename = "license_title")]
    pub license_title: String,
    pub maintainer: String,
    #[serde(rename = "maintainer_email")]
    pub maintainer_email: String,
    #[serde(rename = "metadata_created")]
    pub metadata_created: String,
    #[serde(rename = "metadata_modified")]
    pub metadata_modified: String,
    pub name: String,
    pub notes: String,
    #[serde(rename = "num_resources")]
    pub num_resources: i64,
    #[serde(rename = "num_tags")]
    pub num_tags: i64,
    pub organization: Organization,
    #[serde(rename = "owner_org")]
    pub owner_org: String,
    pub private: bool,
    #[serde(rename = "registerd_date")]
    pub registerd_date: String,
    pub spatial: String,
    pub state: String,
    pub title: String,
    #[serde(rename = "type")]
    pub type_field: String,
    pub url: Option<String>,
    pub version: Option<String>,
    pub extras: Vec<Extra>,
    pub resources: Vec<Resource>,
    pub tags: Vec<Tag>,
    pub groups: Vec<Value>,
    #[serde(rename = "relationships_as_subject")]
    pub relationships_as_subject: Vec<Value>,
    #[serde(rename = "relationships_as_object")]
    pub relationships_as_object: Vec<Value>,
    pub emergency: Option<String>,
    pub fee: Option<String>,
    #[serde(rename = "license_agreement")]
    pub license_agreement: Option<String>,
    pub quality: Option<String>,
    pub restriction: Option<String>,
    #[serde(rename = "thumbnail_url")]
    pub thumbnail_url: Option<String>,
    #[serde(rename = "license_url")]
    pub license_url: Option<String>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Organization {
    pub id: String,
    pub name: String,
    pub title: String,
    #[serde(rename = "type")]
    pub type_field: String,
    pub description: String,
    #[serde(rename = "image_url")]
    pub image_url: String,
    pub created: String,
    #[serde(rename = "is_organization")]
    pub is_organization: bool,
    #[serde(rename = "approval_status")]
    pub approval_status: String,
    pub state: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Extra {
    pub key: String,
    pub value: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
    #[serde(rename = "cache_last_updated")]
    pub cache_last_updated: Value,
    #[serde(rename = "cache_url")]
    pub cache_url: Value,
    pub created: String,
    pub description: Option<String>,
    pub format: String,
    pub hash: String,
    pub id: String,
    #[serde(rename = "last_modified")]
    pub last_modified: Option<String>,
    #[serde(rename = "metadata_modified")]
    pub metadata_modified: String,
    pub mimetype: Option<String>,
    #[serde(rename = "mimetype_inner")]
    pub mimetype_inner: Value,
    pub name: String,
    #[serde(rename = "package_id")]
    pub package_id: String,
    pub position: i64,
    #[serde(rename = "resource_type")]
    pub resource_type: Value,
    pub size: Option<i64>,
    pub state: String,
    pub url: String,
    #[serde(rename = "url_type")]
    pub url_type: Option<String>,
    #[serde(rename = "datastore_active")]
    pub datastore_active: Option<bool>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tag {
    #[serde(rename = "display_name")]
    pub display_name: String,
    pub id: String,
    pub name: String,
    pub state: String,
    #[serde(rename = "vocabulary_id")]
    pub vocabulary_id: Value,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchFacets {}

0
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
0
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?