""" preprocess_corpus.py — 语料预处理:文本 → TOK1 二进制格式 TOK1 格式规范: Magic: 4 bytes "TOK1" Version: 2 bytes (uint16 LE) = 1 Header: 4 bytes (uint32 LE) = tokenizer_vocab_size 4 bytes (uint32 LE) = max_seq_len 4 bytes (uint32 LE) = total_sample_count (占位,写完回填) Samples: [2 bytes seq_len (uint16 LE)] [seq_len * 4 bytes token_ids (uint32 LE each)] ... Footer: 4 bytes "END\0" 支持输入格式:txt, json, jsonl, csv, 目录递归 """ import os import sys import json import struct import argparse import logging from pathlib import Path from collections.abc import Iterator logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') logger = logging.getLogger(__name__) MAGIC = b'TOK1' VERSION = 1 FOOTER = b'END\x00' SPECIAL_TOKENS = {"": 0, "": 1, "": 2, "": 3} class SimpleBPETokenizer: """轻量级BPE分词器,兼容tokenizer_cn_013.json格式""" def __init__(self, vocab_path: str): with open(vocab_path, 'r', encoding='utf-8') as f: data = json.load(f) if isinstance(data, dict) and 'model' in data: self.vocab = data['model'].get('vocab', {}) self.merges = data['model'].get('merges', []) elif isinstance(data, dict) and 'vocab' in data: self.vocab = data['vocab'] self.merges = data.get('merges', []) else: self.vocab = data if isinstance(data, dict) else {} self.merges = [] self.id_to_token = {v: k for k, v in self.vocab.items()} self.vocab_size = len(self.vocab) self.merge_ranks = {} for i, merge in enumerate(self.merges): if isinstance(merge, list) and len(merge) == 2: pair = (merge[0], merge[1]) elif isinstance(merge, str) and ' ' in merge: parts = merge.split(' ', 1) pair = (parts[0], parts[1]) else: continue self.merge_ranks[pair] = i logger.info(f"词表加载: vocab_size={self.vocab_size}, merges={len(self.merge_ranks)}") def _tokenize_word(self, word: str) -> list: tokens = list(word) if len(word) > 0 else [] if not tokens: return tokens while len(tokens) >= 2: best_pair = None best_rank = float('inf') for i in range(len(tokens) - 1): pair = (tokens[i], tokens[i + 1]) rank = self.merge_ranks.get(pair, float('inf')) if rank < best_rank: best_rank = rank best_pair = pair best_pos = i if best_pair is None or best_rank == float('inf'): break tokens[best_pos] = best_pair[0] + best_pair[1] tokens.pop(best_pos + 1) return tokens def encode(self, text: str, max_seq_len: int = 128) -> list: if not text or not text.strip(): return [] text = text.strip() words = [] current = [] for ch in text: if '\u4e00' <= ch <= '\u9fff': if current: words.append(''.join(current)) current = [] words.append(ch) elif ch.isspace(): if current: words.append(''.join(current)) current = [] else: current.append(ch) if current: words.append(''.join(current)) token_ids = [SPECIAL_TOKENS.get("", 1)] for word in words: sub_tokens = self._tokenize_word(word) for st in sub_tokens: tid = self.vocab.get(st, SPECIAL_TOKENS.get("", 3)) token_ids.append(tid) if len(token_ids) >= max_seq_len - 1: break if len(token_ids) >= max_seq_len - 1: break token_ids.append(SPECIAL_TOKENS.get("", 2)) return token_ids[:max_seq_len] def read_texts_from_json(file_path: str) -> Iterator[str]: """流式读取JSON文件中的文本字段""" with open(file_path, 'r', encoding='utf-8') as f: brace_depth = 0 record = '' for line in f: for ch in line: if ch == '{': if brace_depth == 0: record = '' brace_depth += 1 elif ch == '}': brace_depth -= 1 if brace_depth == 0 and record: record += '}' for text in _extract_texts_from_record(record): yield text record = '' if brace_depth > 0: record += ch def _extract_texts_from_record(record: str) -> Iterator[str]: """从JSON record中提取文本字段""" for field in ['"text"', '"content"', '"title"', '"question"', '"answer"']: pos = record.find(field) if pos == -1: continue colon = record.find(':', pos + len(field)) if colon == -1: continue colon += 1 while colon < len(record) and record[colon] != '"': colon += 1 if colon >= len(record): continue colon += 1 end = colon while end < len(record) and record[end] != '"': if record[end] == '\\': end += 1 end += 1 text = record[colon:end] text = _unescape_json(text) if len(text) >= 10: yield text def _unescape_json(s: str) -> str: return s.replace('\\n', '\n').replace('\\t', '\t').replace('\\"', '"').replace('\\\\', '\\') def read_texts_from_jsonl(file_path: str) -> Iterator[str]: """逐行读取JSONL文件""" with open(file_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue try: obj = json.loads(line) except json.JSONDecodeError: for text in _extract_texts_from_record(line): yield text continue for field in ['text', 'content', 'title', 'question', 'answer']: if field in obj and isinstance(obj[field], str) and len(obj[field]) >= 10: yield obj[field] def read_texts_from_txt(file_path: str) -> Iterator[str]: """按段落读取纯文本文件""" with open(file_path, 'r', encoding='utf-8', errors='replace') as f: paragraph = '' for line in f: if line.strip() == '': if len(paragraph) >= 10: yield paragraph.strip() paragraph = '' else: paragraph += line if len(paragraph) > 10000: yield paragraph.strip() paragraph = '' if len(paragraph) >= 10: yield paragraph.strip() def read_texts_from_csv(file_path: str) -> Iterator[str]: """读取CSV,第一列作为文本""" with open(file_path, 'r', encoding='utf-8', errors='replace') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = line.split(',', 1) text = parts[0].strip().strip('"') if len(text) >= 10: yield text def read_texts(file_path: str) -> Iterator[str]: """根据扩展名自动分派读取器""" ext = Path(file_path).suffix.lower() if ext == '.json': return read_texts_from_json(file_path) elif ext == '.jsonl': return read_texts_from_jsonl(file_path) elif ext == '.txt': return read_texts_from_txt(file_path) elif ext in ('.csv', '.tsv'): return read_texts_from_csv(file_path) else: logger.warning(f"跳过不支持的格式: {file_path}") return iter([]) def read_texts_recursive(path: str) -> Iterator[tuple]: """递归遍历目录,返回 (文件路径, 文本) 对""" p = Path(path) if p.is_file(): for text in read_texts(str(p)): yield (str(p), text) elif p.is_dir(): files = sorted(p.rglob('*')) for f in files: if not f.is_file(): continue ext = f.suffix.lower() if ext in ('.json', '.jsonl', '.txt', '.csv', '.tsv'): logger.info(f"读取: {f}") try: for text in read_texts(str(f)): yield (str(f), text) except Exception as e: logger.warning(f"跳过 {f}: {e}") else: raise ValueError(f"路径不存在: {path}") def write_tok1(output_path: str, tokenizer: SimpleBPETokenizer, text_iter: Iterator[tuple], max_seq_len: int = 128, max_samples: int = 0, min_tokens: int = 4): """将文本流写入TOK1二进制格式""" total_written = 0 total_skipped = 0 with open(output_path, 'wb') as f: f.write(MAGIC) f.write(struct.pack(' 0 and total_written >= max_samples: logger.info(f"达到采样上限 {max_samples},停止") break f.write(FOOTER) f.seek(count_offset) f.write(struct.pack('= chunk_size: out.close() logger.info(f"分片 {chunk_idx} 完成: {count} 条") chunk_idx += 1 count = 0 out = None if out: out.close() logger.info(f"分片 {chunk_idx} 完成: {count} 条") logger.info(f"拆分完成: {chunk_idx + 1} 个分片") def verify_tok1(file_path: str, max_show: int = 5): """验证TOK1文件格式""" with open(file_path, 'rb') as f: magic = f.read(4) assert magic == MAGIC, f"Magic不匹配: {magic}" version = struct.unpack('