385 lines
11 KiB
Python
385 lines
11 KiB
Python
|
|
"""
|
||
|
|
data/download.py — Download text corpora from HuggingFace datasets.
|
||
|
|
|
||
|
|
Default sources (no HF token required):
|
||
|
|
1. wikimedia/wikipedia 20231101.ko (Korean Wikipedia, ~600MB text)
|
||
|
|
2. wikimedia/wikipedia 20231101.en (English Wikipedia, streamed/sampled)
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
# Korean + English Wikipedia (default)
|
||
|
|
python data/download.py
|
||
|
|
|
||
|
|
# Korean only
|
||
|
|
python data/download.py --langs ko
|
||
|
|
|
||
|
|
# Custom sample sizes
|
||
|
|
python data/download.py --langs ko en --ko_max 2000000 --en_max 500000
|
||
|
|
|
||
|
|
# Custom dataset
|
||
|
|
python data/download.py --dataset roneneldan/TinyStories --split train --text_col story
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from datasets import load_dataset
|
||
|
|
from tqdm import tqdm
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Text cleaning
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
def clean_text(text: str) -> str:
|
||
|
|
"""Minimal text cleaning: strip whitespace, collapse excessive newlines."""
|
||
|
|
text = text.strip()
|
||
|
|
# Collapse 3+ consecutive newlines to exactly 2
|
||
|
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
||
|
|
return text
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Core download helpers
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
def _open_shard(output_dir: Path, prefix: str, shard_idx: int):
|
||
|
|
"""Return an open file handle for a new shard."""
|
||
|
|
shard_path = output_dir / f"{prefix}_{shard_idx:04d}.txt"
|
||
|
|
return open(shard_path, "w", encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def download_wikipedia(
|
||
|
|
lang: str,
|
||
|
|
output_dir: Path,
|
||
|
|
max_articles: int,
|
||
|
|
shard_size: int,
|
||
|
|
) -> dict:
|
||
|
|
"""
|
||
|
|
Stream one Wikipedia language dump and write sharded plain-text files.
|
||
|
|
|
||
|
|
Returns a stats dict with keys: articles, chars, tokens_est, files.
|
||
|
|
"""
|
||
|
|
dataset_name = "wikimedia/wikipedia"
|
||
|
|
config = f"20231101.{lang}"
|
||
|
|
prefix = f"{lang}_wiki"
|
||
|
|
|
||
|
|
print(f"\n[{lang}] Loading {dataset_name} / {config} …")
|
||
|
|
|
||
|
|
try:
|
||
|
|
ds = load_dataset(
|
||
|
|
dataset_name,
|
||
|
|
config,
|
||
|
|
split="train",
|
||
|
|
streaming=True,
|
||
|
|
trust_remote_code=True,
|
||
|
|
)
|
||
|
|
except Exception as exc:
|
||
|
|
print(f" WARNING: Failed to load {dataset_name}/{config}: {exc}", file=sys.stderr)
|
||
|
|
return {"articles": 0, "chars": 0, "tokens_est": 0, "files": 0}
|
||
|
|
|
||
|
|
count = 0
|
||
|
|
total_chars = 0
|
||
|
|
shard_idx = 0
|
||
|
|
shard_count = 0 # articles written to the current shard
|
||
|
|
|
||
|
|
shard_fh = _open_shard(output_dir, prefix, shard_idx)
|
||
|
|
files = 1
|
||
|
|
|
||
|
|
try:
|
||
|
|
iterator = tqdm(ds, desc=f" {lang}", unit="art", dynamic_ncols=True)
|
||
|
|
for example in iterator:
|
||
|
|
text = example.get("text", "")
|
||
|
|
text = clean_text(text)
|
||
|
|
|
||
|
|
if len(text) < 200:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Rotate shard if needed
|
||
|
|
if shard_count > 0 and shard_count % shard_size == 0:
|
||
|
|
shard_fh.close()
|
||
|
|
shard_idx += 1
|
||
|
|
shard_fh = _open_shard(output_dir, prefix, shard_idx)
|
||
|
|
files += 1
|
||
|
|
|
||
|
|
if shard_count == 0:
|
||
|
|
shard_fh.write(text)
|
||
|
|
else:
|
||
|
|
shard_fh.write("\n\n" + text)
|
||
|
|
|
||
|
|
shard_count += 1
|
||
|
|
count += 1
|
||
|
|
total_chars += len(text)
|
||
|
|
|
||
|
|
# Progress print every 10,000 articles
|
||
|
|
if count % 10_000 == 0:
|
||
|
|
tqdm.write(f" {lang}: {count:,} articles, {total_chars / 1e6:.1f}M chars")
|
||
|
|
|
||
|
|
if max_articles and count >= max_articles:
|
||
|
|
break
|
||
|
|
|
||
|
|
except Exception as exc:
|
||
|
|
print(f"\n WARNING: Stream interrupted for {lang}: {exc}", file=sys.stderr)
|
||
|
|
finally:
|
||
|
|
shard_fh.close()
|
||
|
|
|
||
|
|
tokens_est = total_chars // 4
|
||
|
|
|
||
|
|
print(
|
||
|
|
f"\n [{lang}] Done — "
|
||
|
|
f"{count:,} articles, "
|
||
|
|
f"{total_chars / 1e6:.1f}M chars, "
|
||
|
|
f"~{tokens_est / 1e6:.1f}M tokens (est.), "
|
||
|
|
f"{files} shard file(s)"
|
||
|
|
)
|
||
|
|
|
||
|
|
return {"articles": count, "chars": total_chars, "tokens_est": tokens_est, "files": files}
|
||
|
|
|
||
|
|
|
||
|
|
def download_custom_dataset(
|
||
|
|
dataset_name: str,
|
||
|
|
output_dir: Path,
|
||
|
|
subset: str | None,
|
||
|
|
split: str,
|
||
|
|
text_col: str,
|
||
|
|
shard_size: int,
|
||
|
|
max_rows: int = 0,
|
||
|
|
) -> dict:
|
||
|
|
"""
|
||
|
|
Download an arbitrary HuggingFace dataset and write sharded plain-text files.
|
||
|
|
|
||
|
|
Returns a stats dict with keys: articles, chars, tokens_est, files.
|
||
|
|
"""
|
||
|
|
load_kwargs: dict = dict(split=split, streaming=True, trust_remote_code=True)
|
||
|
|
if subset:
|
||
|
|
load_kwargs["name"] = subset
|
||
|
|
|
||
|
|
print(f"\n[custom] Loading {dataset_name}" + (f" / {subset}" if subset else "") + f" …")
|
||
|
|
|
||
|
|
try:
|
||
|
|
ds = load_dataset(dataset_name, **load_kwargs)
|
||
|
|
except Exception as exc:
|
||
|
|
print(f" WARNING: Failed to load {dataset_name}: {exc}", file=sys.stderr)
|
||
|
|
return {"articles": 0, "chars": 0, "tokens_est": 0, "files": 0}
|
||
|
|
|
||
|
|
# Build a filesystem-safe prefix from the dataset name
|
||
|
|
safe_name = re.sub(r"[^A-Za-z0-9_-]", "_", dataset_name)
|
||
|
|
prefix = f"{safe_name}_{split}"
|
||
|
|
|
||
|
|
count = 0
|
||
|
|
total_chars = 0
|
||
|
|
shard_idx = 0
|
||
|
|
shard_count = 0
|
||
|
|
files = 1
|
||
|
|
|
||
|
|
shard_fh = _open_shard(output_dir, prefix, shard_idx)
|
||
|
|
|
||
|
|
try:
|
||
|
|
iterator = tqdm(ds, desc=" custom", unit="row", dynamic_ncols=True)
|
||
|
|
for example in iterator:
|
||
|
|
text = example.get(text_col, "")
|
||
|
|
if not isinstance(text, str):
|
||
|
|
text = str(text)
|
||
|
|
text = clean_text(text)
|
||
|
|
|
||
|
|
if len(text) < 1:
|
||
|
|
continue
|
||
|
|
|
||
|
|
if shard_count > 0 and shard_count % shard_size == 0:
|
||
|
|
shard_fh.close()
|
||
|
|
shard_idx += 1
|
||
|
|
shard_fh = _open_shard(output_dir, prefix, shard_idx)
|
||
|
|
files += 1
|
||
|
|
|
||
|
|
if shard_count == 0:
|
||
|
|
shard_fh.write(text)
|
||
|
|
else:
|
||
|
|
shard_fh.write("\n\n" + text)
|
||
|
|
|
||
|
|
shard_count += 1
|
||
|
|
count += 1
|
||
|
|
total_chars += len(text)
|
||
|
|
|
||
|
|
if count % 10_000 == 0:
|
||
|
|
tqdm.write(f" custom: {count:,} rows, {total_chars / 1e6:.1f}M chars")
|
||
|
|
|
||
|
|
if max_rows > 0 and count >= max_rows:
|
||
|
|
break
|
||
|
|
|
||
|
|
except Exception as exc:
|
||
|
|
print(f"\n WARNING: Stream interrupted: {exc}", file=sys.stderr)
|
||
|
|
finally:
|
||
|
|
shard_fh.close()
|
||
|
|
|
||
|
|
tokens_est = total_chars // 4
|
||
|
|
|
||
|
|
print(
|
||
|
|
f"\n [custom] Done — "
|
||
|
|
f"{count:,} rows, "
|
||
|
|
f"{total_chars / 1e6:.1f}M chars, "
|
||
|
|
f"~{tokens_est / 1e6:.1f}M tokens (est.), "
|
||
|
|
f"{files} shard file(s)"
|
||
|
|
)
|
||
|
|
|
||
|
|
return {"articles": count, "chars": total_chars, "tokens_est": tokens_est, "files": files}
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# CLI
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
def parse_args() -> argparse.Namespace:
|
||
|
|
parser = argparse.ArgumentParser(
|
||
|
|
description="Download text corpora from HuggingFace datasets.",
|
||
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--output_dir",
|
||
|
|
type=Path,
|
||
|
|
default=Path("data/raw"),
|
||
|
|
help="Directory where sharded .txt files are written.",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--langs",
|
||
|
|
nargs="+",
|
||
|
|
default=["ko", "en"],
|
||
|
|
metavar="LANG",
|
||
|
|
help="Wikipedia language codes to download.",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--ko_max",
|
||
|
|
type=int,
|
||
|
|
default=0,
|
||
|
|
help="Max Korean Wikipedia articles (0 = all).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--en_max",
|
||
|
|
type=int,
|
||
|
|
default=300_000,
|
||
|
|
help="Max English Wikipedia articles (0 = all).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--shard_size",
|
||
|
|
type=int,
|
||
|
|
default=100_000,
|
||
|
|
help="Number of articles per shard file.",
|
||
|
|
)
|
||
|
|
# Custom dataset overrides
|
||
|
|
parser.add_argument(
|
||
|
|
"--dataset",
|
||
|
|
type=str,
|
||
|
|
default=None,
|
||
|
|
help="Override: HuggingFace dataset name (e.g. roneneldan/TinyStories).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--subset",
|
||
|
|
type=str,
|
||
|
|
default=None,
|
||
|
|
help="Dataset subset / config name (used with --dataset).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--split",
|
||
|
|
type=str,
|
||
|
|
default="train",
|
||
|
|
help="Dataset split to download (used with --dataset).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--text_col",
|
||
|
|
type=str,
|
||
|
|
default="text",
|
||
|
|
help="Column name containing the text (used with --dataset).",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--max_rows",
|
||
|
|
type=int,
|
||
|
|
default=0,
|
||
|
|
help="Max rows to download from --dataset (0 = unlimited).",
|
||
|
|
)
|
||
|
|
return parser.parse_args()
|
||
|
|
|
||
|
|
|
||
|
|
def _lang_max(lang: str, args: argparse.Namespace) -> int:
|
||
|
|
"""Return the max-articles limit for a given Wikipedia language code."""
|
||
|
|
mapping = {
|
||
|
|
"ko": args.ko_max,
|
||
|
|
"en": args.en_max,
|
||
|
|
}
|
||
|
|
return mapping.get(lang, 0)
|
||
|
|
|
||
|
|
|
||
|
|
def print_summary(all_stats: dict[str, dict]) -> None:
|
||
|
|
"""Print a final summary table for all downloaded sources."""
|
||
|
|
print("\n" + "=" * 70)
|
||
|
|
print(f"{'Source':<20} {'Articles':>12} {'Chars (M)':>12} {'Tokens est.(M)':>16} {'Files':>6}")
|
||
|
|
print("-" * 70)
|
||
|
|
totals: dict = {"articles": 0, "chars": 0, "tokens_est": 0, "files": 0}
|
||
|
|
for name, stats in all_stats.items():
|
||
|
|
print(
|
||
|
|
f"{name:<20} "
|
||
|
|
f"{stats['articles']:>12,} "
|
||
|
|
f"{stats['chars'] / 1e6:>12.1f} "
|
||
|
|
f"{stats['tokens_est'] / 1e6:>16.1f} "
|
||
|
|
f"{stats['files']:>6}"
|
||
|
|
)
|
||
|
|
for key in totals:
|
||
|
|
totals[key] += stats[key]
|
||
|
|
print("-" * 70)
|
||
|
|
print(
|
||
|
|
f"{'TOTAL':<20} "
|
||
|
|
f"{totals['articles']:>12,} "
|
||
|
|
f"{totals['chars'] / 1e6:>12.1f} "
|
||
|
|
f"{totals['tokens_est'] / 1e6:>16.1f} "
|
||
|
|
f"{totals['files']:>6}"
|
||
|
|
)
|
||
|
|
print("=" * 70)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
args = parse_args()
|
||
|
|
|
||
|
|
output_dir: Path = args.output_dir
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
print(f"Output directory: {output_dir.resolve()}")
|
||
|
|
|
||
|
|
all_stats: dict[str, dict] = {}
|
||
|
|
|
||
|
|
if args.dataset is not None:
|
||
|
|
# Custom dataset mode — ignore --langs
|
||
|
|
stats = download_custom_dataset(
|
||
|
|
dataset_name=args.dataset,
|
||
|
|
output_dir=output_dir,
|
||
|
|
subset=args.subset,
|
||
|
|
split=args.split,
|
||
|
|
text_col=args.text_col,
|
||
|
|
shard_size=args.shard_size,
|
||
|
|
max_rows=args.max_rows,
|
||
|
|
)
|
||
|
|
all_stats[args.dataset] = stats
|
||
|
|
else:
|
||
|
|
# Wikipedia mode
|
||
|
|
for lang in args.langs:
|
||
|
|
max_articles = _lang_max(lang, args)
|
||
|
|
try:
|
||
|
|
stats = download_wikipedia(
|
||
|
|
lang=lang,
|
||
|
|
output_dir=output_dir,
|
||
|
|
max_articles=max_articles,
|
||
|
|
shard_size=args.shard_size,
|
||
|
|
)
|
||
|
|
except Exception as exc:
|
||
|
|
print(
|
||
|
|
f"\n WARNING: Unexpected error for lang={lang}: {exc}",
|
||
|
|
file=sys.stderr,
|
||
|
|
)
|
||
|
|
stats = {"articles": 0, "chars": 0, "tokens_est": 0, "files": 0}
|
||
|
|
all_stats[f"{lang}_wiki"] = stats
|
||
|
|
|
||
|
|
print_summary(all_stats)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|