【VRChat】Boothでの購入履歴。
Boothで合計何円使ったかの計算してくれるスクリプトは有名ですよね!
今回思ったのは……
Boothは多くのECサイトと同様に、購入履歴を複数のページに分割して表示しています。そのため購入した商品をメモしていないと再DLする時に、探すのがすごく大変になってしまうこともが多々あります。
全てが単一のページであれば探しやすいのかな?と思い、静的なHTMLで全履歴を出力するコードを書いてみました。
こんな感じででますよ~!
仲良く使ってね。
※スクレイピングで取得するため、サーバーに負荷をかけないように連続で投げたりはしないように注意を払ってください。各ページ取得に1秒のディレイは設定済みです。
Python or TypeScript……で書くのが一番楽ですが敢えてRust……で……!(チョットツカレタ……)
use reqwest;
use scraper::{Html, Selector};
use serde::Serialize;
use std::error::Error;
use tokio;
use html_escape;
#[derive(Debug, Serialize, Clone)]
struct PurchaseItem {
name: String,
date: String,
price: String,
thumbnail_url: String,
product_url: String,
}
async fn fetch_booth_history_page(client: &reqwest::Client, session_id: &str, page: u32) -> Result<Vec<PurchaseItem>, Box<dyn Error>> {
let url = format!("https://accounts.booth.pm/orders?page={}", page);
let res = client
.get(&url)
.header("Cookie", format!("_plaza_session_nktz7u={}", session_id))
.send()
.await?
.text()
.await?;
let document = Html::parse_document(&res);
let item_selector = Selector::parse(".sheet.sheet--p250.sheet--outline0").unwrap();
let mut purchases = Vec::new();
for item in document.select(&item_selector) {
let name = item.select(&Selector::parse(".u-tpg-caption1.u-text-gray-500").unwrap()).next()
.map(|n| n.text().collect::<String>().trim().to_string())
.unwrap_or_default();
let date = item.select(&Selector::parse(".u-tpg-caption2.u-text-gray-500").unwrap()).next()
.map(|d| d.text().collect::<String>().trim().to_string()
.replace("注文日時: ", ""))
.unwrap_or_default();
let price = String::new();
let thumbnail_url = item.select(&Selector::parse("img").unwrap()).next()
.and_then(|img| img.value().attr("src"))
.unwrap_or_default()
.to_string();
let product_url = item.select(&Selector::parse("a").unwrap()).next()
.and_then(|a| a.value().attr("href"))
.unwrap_or_default()
.to_string();
purchases.push(PurchaseItem { name, date, price, thumbnail_url, product_url });
}
Ok(purchases)
}
async fn fetch_all_booth_history(session_id: &str) -> Result<Vec<PurchaseItem>, Box<dyn Error>> {
let client = reqwest::Client::new();
let mut all_purchases = Vec::new();
let mut page = 1;
loop {
let purchases = fetch_booth_history_page(&client, session_id, page).await?;
if purchases.is_empty() {
break;
}
all_purchases.extend(purchases);
page += 1;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
Ok(all_purchases)
}
fn generate_html(purchases: &[PurchaseItem]) -> String {
let mut html = String::from(r#"
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boothの全購入履歴</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; background-color: #f4f4f4; }
h1 { color: #333; }
ul { list-style-type: none; padding: 0; }
li { background-color: #fff; margin-bottom: 20px; padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); display: flex; align-items: center; }
.item-image { width: 100px; height: 100px; object-fit: cover; margin-right: 15px; border-radius: 5px; }
.item-details { flex-grow: 1; }
.item-name { font-weight: bold; color: #0066cc; text-decoration: none; }
.item-name:hover { text-decoration: underline; }
.purchase-date { color: #666; font-size: 0.9em; }
.price { color: #009900; font-weight: bold; }
#csvButton {
display: block;
margin: 20px auto;
padding: 10px 20px;
font-size: 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#csvButton:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>Boothの全購入履歴</h1>
<ul id="purchaseList">
"#);
for item in purchases {
html.push_str(&format!(r#"
<li data-name="{}" data-date="{}" data-url="{}" data-price="{}">
<img src="{}" alt="{}" class="item-image">
<div class="item-details">
<a href="{}" class="item-name" target="_blank">{}</a><br>
<span class="purchase-date">購入日: {}</span><br>
<!-- <span class="price">価格: {}</span> -->
</div>
</li>
"#,
html_escape::encode_text(&item.name),
html_escape::encode_text(&item.date),
html_escape::encode_text(&item.product_url),
html_escape::encode_text(&item.price),
item.thumbnail_url,
html_escape::encode_text(&item.name),
item.product_url,
html_escape::encode_text(&item.name),
item.date,
item.price));
}
html.push_str(r#"
</ul>
</body>
</html>
"#);
html
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let session_id = "SESSION_ID";
println!("Fetching purchase history from BOOTH...");
let purchases = fetch_all_booth_history(session_id).await?;
println!("Total purchases fetched: {}", purchases.len());
println!("Generating HTML...");
let html = generate_html(&purchases);
println!("Saving HTML file...");
std::fs::write("boothBuyHistory.html", html)?;
println!("Purchase history with CSV export functionality has been saved to booth_history_with_csv_export.html");
Ok(())
}
使い方。
Rustを下記のサイトからダウンロードしてインストールします。
ターミナルで
cargo bootBuyHistory
cd bootBuyHistory
mian.rsに上記のコードをコピーします。
Cargo.tomを以下の内容に変更します。
[package]
name = "boothByHistory"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.11", features = ["cookies"] }
tokio = { version = "1", features = ["full"] }
scraper = "0.12"
serde = { version = "1.0", features = ["derive"] }
html-escape = "0.2.13"
Boothにログインして
main関数の「let session_id = "SESSION_ID";」のSESSION_IDの部分にBoothにログインしたときのセッションIDを貼り付けます。
セッションIDの確認方法は「https://accounts.booth.pm/orders」へアクセス、「F12」キーを押し開発者ツールを表示、アプリケーション→Cookie→「https://accounts.booth.pm/」→「_plaza_session_nktz7u」の値です。
以下のコマンドで実行します。
cargo run
実行が完了したら「BoothBuyHistory.html」という静的なHTMLが
そこに全購入履歴が1ページに表示されるため、検索等がしやすいのかな?と思います。
今回はRustが書きたくて、こんなに使いにくいものが出来てしまいましたが、そのうち……Chromeの拡張機能としてでも作ろうとか……と思います。
やることがどんどん増えていきますね。寝てる場合じゃなーい。
それでは良きVRChatライフを~✨