File size: 6,710 Bytes
7b46abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455976a
7b46abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""下载维基百科中文数据 + 分词 + 生成 LAL 训练 .bin 文件.

用法:
  python3 scripts/prepare_wiki_data.py [--n_articles 10000] [--out data/wiki_bpe.bin]

输出格式: LALT 二进制 (与 large_bpe_v3.bin 兼容)
  [magic: 4 bytes = "LALT"]
  [n_samples: int32]
  [n_vocab: int32]
  then n_samples records:
    [n_tokens: int32]
    [token_ids: int32 * n_tokens]
"""
import os
import sys
import struct
import subprocess
import argparse
import time

# === 固化配置 ===
TOKENIZER_MODEL = "tokenizer/chinese_bpe.model"
WIKI_DUMP_URL = "https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles-multistream.xml.bz2"
DEFAULT_OUT = "data/wiki_bpe.bin"
DEFAULT_N_ARTICLES = 10000  # 先取 1 万篇, 约 500-1000 万 token


def log(msg):
    print(f"[WIKI] {msg}", flush=True)


def download_wiki_dump(local_path, max_articles=None):
    """下载维基百科 dump (流式解压 + 解析, 避免下载整个 2GB+ bz2)."""
    import bz2
    import xml.etree.ElementTree as ET
    import urllib.request
    
    log(f"下载+解析维基百科 (最多 {max_articles or '全部'} 篇)...")
    
    articles = []
    in_text = False
    in_title = False
    current_title = ""
    current_text = ""
    article_count = 0
    
    # 流式下载 + bz2 解压
    req = urllib.request.Request(WIKI_DUMP_URL, headers={"User-Agent": "LAL-Data-Prep/1.0"})
    with urllib.request.urlopen(req) as resp:
        with bz2.open(resp, "rt", encoding="utf-8") as f:
            for line in f:
                if "<title>" in line:
                    start = line.index("<title>") + 7
                    end = line.index("</title>")
                    current_title = line[start:end].strip()
                elif "<text" in line:
                    in_text = True
                    # 提取 text 标签内的内容
                    if ">" in line:
                        start = line.index(">") + 1
                        current_text = line[start:]
                    else:
                        current_text = ""
                elif "</text>" in line:
                    in_text = False
                    end = line.index("</text>")
                    current_text += line[:end]
                    
                    # 过滤: 跳过重定向、空页面
                    if current_text and not current_text.startswith("#REDIRECT"):
                        # 清理 wiki 标记 (简单版)
                        text = clean_wiki_text(current_text)
                        if len(text) > 100:  # 太短的文章跳过
                            articles.append(text)
                            article_count += 1
                            if article_count % 1000 == 0:
                                log(f"  已收集 {article_count} 篇文章")
                    
                    current_text = ""
                    
                    if max_articles and article_count >= max_articles:
                        break
            
            # 处理最后一篇
            if in_text and current_text:
                text = clean_wiki_text(current_text)
                if len(text) > 100:
                    articles.append(text)
                    article_count += 1
    
    log(f"共收集 {len(articles)} 篇文章")
    return articles


def clean_wiki_text(text):
    """简单清理 wiki 标记."""
    import re
    # 去掉 wiki 模板 {{...}}
    text = re.sub(r'\{\{[^}]*\}\}', '', text)
    # 去掉 wiki 链接 [[...]]
    text = re.sub(r'\[\[([^|\]]*\|)?([^\]]*)\]\]', r'\2', text)
    # 去掉 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 去掉 wiki 标题标记 ==
    text = re.sub(r'^=+\s*([^=]+)\s*=+$', r'\1', text, flags=re.MULTILINE)
    # 去掉引用
    text = re.sub(r'<ref[^>]*>.*?</ref>', '', text, flags=re.DOTALL)
    text = re.sub(r'<ref[^>]*/>', '', text)
    # 去掉多余空行
    text = re.sub(r'\n{3,}', '\n\n', text)
    return text.strip()


def tokenize_with_bpe(texts, tokenizer_model):
    """用 sentencepiece BPE 分词."""
    try:
        import sentencepiece as spm
    except ImportError:
        log("安装 sentencepiece...")
        subprocess.run([sys.executable, "-m", "pip", "install", "-q", "sentencepiece"], check=True)
        import sentencepiece as spm
    
    sp = spm.SentencePieceProcessor()
    sp.Load(tokenizer_model)
    
    all_samples = []
    total_tokens = 0
    
    for i, text in enumerate(texts):
        # 分词 (每篇文章作为一个 sample)
        tokens = sp.EncodeAsIds(text)
        if len(tokens) > 10:  # 太短的跳过
            all_samples.append(tokens)
            total_tokens += len(tokens)
        
        if (i + 1) % 1000 == 0:
            log(f"  分词 {i+1}/{len(texts)} 篇, 总 token {total_tokens}")
    
    log(f"分词完成: {len(all_samples)} samples, {total_tokens} tokens ({total_tokens/10000:.1f}万)")
    return all_samples


def write_lalt_bin(samples, out_path, n_vocab=32768):
    """写 LALT 二进制格式."""
    log(f"写入 {out_path}...")
    with open(out_path, "wb") as f:
        # header
        f.write(b"LALT")
        f.write(struct.pack("<i", len(samples)))
        f.write(struct.pack("<i", n_vocab))
        
        # samples
        for tokens in samples:
            f.write(struct.pack("<i", len(tokens)))
            for tok in tokens:
                f.write(struct.pack("<i", tok))
    
    size = os.path.getsize(out_path)
    log(f"完成: {out_path} ({size / 1024 / 1024:.1f} MB)")


def main():
    parser = argparse.ArgumentParser(description="准备维基百科训练数据")
    parser.add_argument("--n_articles", type=int, default=DEFAULT_N_ARTICLES,
                        help=f"文章数 (默认 {DEFAULT_N_ARTICLES})")
    parser.add_argument("--out", type=str, default=DEFAULT_OUT,
                        help=f"输出路径 (默认 {DEFAULT_OUT})")
    args = parser.parse_args()
    
    # 检查 tokenizer
    if not os.path.exists(TOKENIZER_MODEL):
        log(f"[!] tokenizer 不存在: {TOKENIZER_MODEL}")
        sys.exit(1)
    
    # 确保输出目录存在
    os.makedirs(os.path.dirname(args.out), exist_ok=True)
    
    start = time.time()
    
    # 1. 下载 + 解析维基百科
    articles = download_wiki_dump(args.out + ".tmp", max_articles=args.n_articles)
    
    # 2. 分词
    samples = tokenize_with_bpe(articles, TOKENIZER_MODEL)
    
    # 3. 写 .bin
    write_lalt_bin(samples, args.out)
    
    elapsed = time.time() - start
    log(f"总计耗时 {elapsed:.0f}s")
    log(f"数据文件: {args.out}")
    log(f"替换训练数据: cp {args.out} data/large_bpe_v3.bin")


if __name__ == "__main__":
    main()