| """Download Amazon Reviews 2023 (Clothing_Shoes_and_Jewelry). |
| |
| 銆愰噸瑕佸彉鏇淬€戝畬鍏ㄦ姏寮?HuggingFace 鏁版嵁闆?API,鏀逛负鐩存帴浠?McAuley Lab 瀹樻柟 |
| HTTPS 鏈嶅姟鍣ㄦ祦寮忔媺鍙栧師濮?jsonl.gz 鏂囦欢銆傚師鍥? |
| 1. 鏂扮増 datasets 宸插簾寮?loading script, load_dataset(..., trust_remote_code=True) |
| 鎶?"Dataset scripts are no longer supported"銆?2. McAuley-Lab/Amazon-Reviews-2023 浠撳簱鐨?main 鍒嗘敮鍙湁鑴氭湰鍜?raw 鐩綍, |
| parquet 鏂囦欢鍒嗘暎鍦?PR 鍒嗘敮(refs/pr/10)涓?涓?Clothing 绫荤洰骞朵笉瀹屾暣, |
| 渚濊禆 HF 璧板緱闈炲父鏇叉姌涓斿鏄撹俯鍧戙€?3. 瀹樻柟鏂囨。 (amazon-reviews-2023.github.io) 缁欑殑鐩撮摼鏈€绋? |
| https://mcauleylab.ucsd.edu/public_datasets/data/amazon_2023/... |
| 涓€浠?jsonl.gz,娴佸紡 GET + gunzip + readline 涓変欢濂楀嵆鍙€? 瀵?sample 妯″紡灏ゅ叾鍙嬪ソ鈥斺€斾笅澶?N 鏉″氨鎺愭柇杩炴帴,涓嶇敤涓嬪畬鏁翠釜鏂囦欢銆?4. UCSD 鏈嶅姟鍣ㄥ湪鍥藉唴璁块棶閫氬父姣?huggingface.co 绋冲畾 (HF 缁忓父 SNI 闃绘柇)銆? |
| 銆?026-05 淇銆?- pyarrow 鍐?parquet 鏃朵細鎸夊垪鎺ㄦ柇绫诲瀷, Amazon meta 鐨?price 瀛楁鏄? "鍙兘鏁板瓧 / 鍙兘 '鈥? (em-dash) / 鍙兘 'None' / 鍙兘绌? 鐨勮剰娣峰悎瀛楁, |
| pyarrow 璇曞浘鎺ㄦ垚 double 鏃堕亣鍒?'鈥? 鐩存帴 ArrowInvalid銆? 瑙e喅: 鍐?parquet 鍓嶇敤 _clean_meta_for_parquet 鎶?price 寮鸿浆 float銆?- 鍚屾椂澧炲姞 jsonl 鍏滃簳鐩? meta 鎷夊畬鍏堝瓨涓€浠藉師濮?jsonl, 鍐嶅啓 parquet銆? 杩欐牱涓囦竴 parquet 鍙堝嚭鍒殑闂, 涓嬫鑳界绾ф仮澶? 涓嶅繀閲嶆柊鐖?25 鍒嗛挓銆?- reviews parquet 濡傛灉宸茬粡瀛樺湪, 璺宠繃 reviews 涓嬭浇闃舵, 鐩存帴澶嶇敤銆?""" |
| import gzip |
| import json |
| import logging |
| import os |
| from collections import defaultdict |
| from typing import Iterable, Iterator |
|
|
| import pandas as pd |
| import requests |
| from tqdm import tqdm |
|
|
| from . import config as cfg |
|
|
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| _BASE = "https://mcauleylab.ucsd.edu/public_datasets/data/amazon_2023/raw" |
| _REVIEW_URL_TMPL = _BASE + "/review_categories/{cat}.jsonl.gz" |
| _META_URL_TMPL = _BASE + "/meta_categories/meta_{cat}.jsonl.gz" |
|
|
|
|
| def _stream_jsonl_gz(url: str) -> Iterator[dict]: |
| """Stream a gzipped JSONL file and yield parsed rows. |
| |
| The function intentionally avoids downloading the full file to disk, so |
| sample mode can stop early after enough valid records have been collected. |
| """ |
| logger.info("GET %s", url) |
| headers = {"User-Agent": "Mozilla/5.0 (acsa_clothing downloader)"} |
| with requests.get(url, stream=True, headers=headers, timeout=(15, 60)) as r: |
| r.raise_for_status() |
| r.raw.decode_content = False |
| with gzip.GzipFile(fileobj=r.raw) as gz: |
| for raw_line in gz: |
| if not raw_line: |
| continue |
| try: |
| yield json.loads(raw_line) |
| except json.JSONDecodeError as e: |
| logger.warning("Skipping one invalid JSON line: %s", e) |
| continue |
|
|
| def _records_to_df(records: Iterable[dict]) -> pd.DataFrame: |
| return pd.DataFrame(list(records)) |
|
|
|
|
| def _clean_meta_for_parquet(df: pd.DataFrame) -> pd.DataFrame: |
| """鎶?meta 涓細璁?pyarrow 鎺ㄦ柇绫诲瀷澶辫触鐨勮剰鍊兼竻娲楁帀銆? |
| Amazon 鍏冩暟鎹噷 price 鏄?"鍙兘鏁板瓧 / 鍙兘 '鈥? / 鍙兘 'None' / 鍙兘绌? 鐨勬贩鍚堝瓧娈? |
| pandas 榛樿鎺ㄦ垚 object, pyarrow 钀?parquet 鏃跺嵈璇曞浘鎺ㄦ垚 double, |
| 閬囧埌 '鈥? (em-dash) 杩欑闈炴暟瀛楀瓧绗︿覆灏辨姏 ArrowInvalid銆? 鎵€浠ヨ繖閲屾墜鍔ㄦ妸 price 寮哄埗杞垚 float64, 鏃犳硶瑙f瀽鐨勪竴寰嬪彉 NaN銆? """ |
| if "price" in df.columns: |
| df["price"] = pd.to_numeric(df["price"], errors="coerce") |
| return df |
|
|
|
|
| def _backup_jsonl(records: list, parquet_path) -> str: |
| """鍦ㄥ啓 parquet 鍓嶅厛钀戒竴浠藉師濮?jsonl 鍏滃簳銆? |
| 璺緞灏辨槸 parquet 鍚屽悕鎹㈠悗缂€銆傚嵆浣垮悗缁?to_parquet 澶辫触涔熶笉蹇呴噸鐖€? """ |
| raw_jsonl_path = os.path.splitext(str(parquet_path))[0] + ".jsonl" |
| os.makedirs(os.path.dirname(raw_jsonl_path) or ".", exist_ok=True) |
| with open(raw_jsonl_path, "w", encoding="utf-8") as f: |
| for row in records: |
| f.write(json.dumps(row, ensure_ascii=False) + "\n") |
| logger.info("Backup jsonl saved to %s (%d rows)", raw_jsonl_path, len(records)) |
| return raw_jsonl_path |
|
|
|
|
| def download_sample(n_reviews: int = 30000, min_text_len: int = 20, |
| min_reviews_per_product: int = 2, |
| max_reviews_per_product: int = 5, |
| scan_multiplier: int = 5, |
| force: bool = False): |
| """Sample 妯″紡: 娴佸紡璇诲彇鍓?N 鏉″彲鐢ㄨ瘎璁?鍐嶆媺瀵瑰簲 meta銆? |
| 'Usable' 鎸? text 闈炵┖涓旈暱搴?>= min_text_len, 涓?parent_asin 闈炵┖銆? """ |
| review_url = _REVIEW_URL_TMPL.format(cat=cfg.CATEGORY) |
| meta_url = _META_URL_TMPL.format(cat=cfg.CATEGORY) |
|
|
| |
| |
| |
| if os.path.exists(cfg.RAW_REVIEWS_PATH) and not force: |
| logger.info("Reviews parquet 宸插瓨鍦? 澶嶇敤: %s", cfg.RAW_REVIEWS_PATH) |
| reviews_df = pd.read_parquet(cfg.RAW_REVIEWS_PATH) |
| seen_asins = set(reviews_df["parent_asin"].dropna().unique().tolist()) |
| logger.info("浠庡凡鏈?reviews 涓仮澶?%d 鏉? %d 涓?unique parent_asins", |
| len(reviews_df), len(seen_asins)) |
| else: |
| logger.info( |
| "Streaming reviews (grouped sample, target=%d, min_per_product=%d, max_per_product=%d, scan_multiplier=%d) from %s ...", |
| n_reviews, min_reviews_per_product, max_reviews_per_product, scan_multiplier, review_url, |
| ) |
| candidate_target = max(n_reviews, n_reviews * max(scan_multiplier, 1)) |
| candidates = [] |
| pbar = tqdm(total=candidate_target, desc="review candidates") |
| for row in _stream_jsonl_gz(review_url): |
| text = (row.get("text") or "").strip() |
| if len(text) < min_text_len: |
| continue |
| if not row.get("parent_asin"): |
| continue |
| candidates.append(row) |
| pbar.update(1) |
| if len(candidates) >= candidate_target: |
| break |
| pbar.close() |
|
|
| by_asin = defaultdict(list) |
| for row in candidates: |
| by_asin[row["parent_asin"]].append(row) |
|
|
| eligible = {asin: rows for asin, rows in by_asin.items() if len(rows) >= min_reviews_per_product} |
| kept = [] |
| for asin, rows in sorted(eligible.items(), key=lambda x: len(x[1]), reverse=True): |
| for row in rows[:max_reviews_per_product]: |
| kept.append(row) |
| if len(kept) >= n_reviews: |
| break |
| if len(kept) >= n_reviews: |
| break |
|
|
| if len(kept) < n_reviews: |
| used = {id(row) for row in kept} |
| for row in candidates: |
| if id(row) in used: |
| continue |
| kept.append(row) |
| if len(kept) >= n_reviews: |
| break |
|
|
| reviews_df = _records_to_df(kept[:n_reviews]) |
| seen_asins = set(reviews_df["parent_asin"].dropna().unique().tolist()) |
| per_product = reviews_df.groupby("parent_asin").size() |
| logger.info( |
| "Got %d reviews, %d unique parent_asins, avg_reviews_per_product=%.2f, products_with_%d_plus_reviews=%d.", |
| len(reviews_df), len(seen_asins), len(reviews_df) / max(len(seen_asins), 1), |
| min_reviews_per_product, int((per_product >= min_reviews_per_product).sum()), |
| ) |
| reviews_df.to_parquet(cfg.RAW_REVIEWS_PATH, index=False) |
|
|
| |
| |
| |
| |
| meta_kept = [] |
| remaining = set(seen_asins) |
| pbar = tqdm(total=len(seen_asins), desc="meta") |
| for row in _stream_jsonl_gz(meta_url): |
| asin = row.get("parent_asin") |
| if asin in remaining: |
| meta_kept.append(row) |
| remaining.discard(asin) |
| pbar.update(1) |
| if not remaining: |
| break |
| pbar.close() |
|
|
| |
| _backup_jsonl(meta_kept, cfg.RAW_META_PATH) |
|
|
| |
| meta_df = _records_to_df(meta_kept) |
| logger.info( |
| "Got %d meta entries (matched %.1f%% of review asins).", |
| len(meta_df), |
| 100 * len(meta_df) / max(len(seen_asins), 1), |
| ) |
| meta_df = _clean_meta_for_parquet(meta_df) |
| meta_df.to_parquet(cfg.RAW_META_PATH, index=False) |
| return reviews_df, meta_df |
|
|
|
|
| def download_full(): |
| """Download the full review and metadata tables. Very large; use with care.""" |
| review_url = _REVIEW_URL_TMPL.format(cat=cfg.CATEGORY) |
| meta_url = _META_URL_TMPL.format(cat=cfg.CATEGORY) |
|
|
| logger.info("Downloading FULL reviews from %s ...", review_url) |
| reviews = [] |
| for row in tqdm(_stream_jsonl_gz(review_url), desc="reviews"): |
| reviews.append(row) |
| reviews_df = _records_to_df(reviews) |
| logger.info("Reviews: %d rows", len(reviews_df)) |
| reviews_df.to_parquet(cfg.RAW_REVIEWS_PATH, index=False) |
|
|
| logger.info("Downloading FULL meta from %s ...", meta_url) |
| metas = [] |
| for row in tqdm(_stream_jsonl_gz(meta_url), desc="meta"): |
| metas.append(row) |
|
|
| |
| _backup_jsonl(metas, cfg.RAW_META_PATH) |
|
|
| meta_df = _records_to_df(metas) |
| logger.info("Meta: %d rows", len(meta_df)) |
| meta_df = _clean_meta_for_parquet(meta_df) |
| meta_df.to_parquet(cfg.RAW_META_PATH, index=False) |
| return reviews_df, meta_df |
|
|
|
|
|
|
|
|
|
|