File size: 10,662 Bytes
422d4ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""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__)


# McAuley Lab 瀹樻柟鐩撮摼妯℃澘
_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)

    # ===== Reviews 闃舵 =====
    # 濡傛灉 reviews parquet 宸插瓨鍦?(涓婃璺戞垚鍔熶簡, 鏄?meta 闃舵宕╃殑),
    # 鐩存帴璇诲嚭鏉ュ鐢?seen_asins, 璺宠繃杩欐閲嶆柊涓嬭浇銆?
    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 闃舵 =====
    # 鍐嶆媺 meta,鍖归厤 parent_asin
    # 娉ㄦ剰: meta 娌℃湁淇濊瘉鎺掑簭,鐞嗚涓婅鎵畬鏁翠唤鎵嶈兘纭繚鍛戒腑鎵€鏈?asin銆?
    # 浣?meta 姣?reviews 灏忓緱澶?(Clothing meta 鍑犵櫨 MB 鍘嬬缉),瀹屾暣鎵竴閬嶅彲鎺ュ彈銆?    logger.info("Streaming meta to match parent_asins from %s ...", meta_url)
    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()

    # 1) 鍏堣惤 jsonl 鍏滃簳, 鍝€曚笅闈?to_parquet 鐐镐簡涔熶笉蹇呴噸鐖?
    _backup_jsonl(meta_kept, cfg.RAW_META_PATH)

    # 2) 鍐嶅仛娓呮礂 + 鍐?parquet
    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)

    # 钀?jsonl 鍏滃簳
    _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