| import numpy as np |
| import shutil |
| from .common import qd_tqdm as tqdm |
| import mmap |
| import time |
| from .common import ( |
| dict_update_path_value, |
| dict_get_path_value, |
| get_all_path, |
| load_from_yaml_str, |
| ) |
| import logging |
|
|
| |
| try: |
| from azfuse import File |
| except ImportError: |
| import types |
|
|
| File = types.SimpleNamespace() |
| File.open = open |
| File.get_file_size = lambda x: os.stat(x).st_size |
|
|
|
|
| import os |
| import os.path as op |
| from contextlib import contextmanager |
| import subprocess |
| import tempfile |
| import hashlib |
| from datasets.utils.filelock import FileLock |
| from urllib.parse import urlparse, urlunparse |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def concat_files(ins, out): |
| File.prepare(ins) |
| with File.open(out, "wb") as fp_out: |
| for i, f in enumerate(ins): |
| logging.info("concating {}/{} - {}".format(i, len(ins), f)) |
| with File.open(f, "rb") as fp_in: |
| shutil.copyfileobj(fp_in, fp_out, 1024 * 1024 * 10) |
|
|
|
|
| def concat_tsv_files(tsvs, out_tsv): |
| if len(tsvs) == 1 and tsvs[0] == out_tsv: |
| return |
| File.prepare(tsvs) |
| concat_files(tsvs, out_tsv) |
| sizes = [File.get_file_size(t) for t in tsvs] |
| sizes = np.cumsum(sizes) |
| sizes = [0] + sizes[:-1].tolist() |
|
|
| concate_lineidx_8b(sizes, tsvs, out_tsv) |
|
|
|
|
| def get_tmp_folder(): |
| folder = os.environ.get("GIT_TMP_FOLDER", "/tmp") |
| return folder |
|
|
|
|
| def parallel_map(func, all_task, num_worker=16): |
| if num_worker > 0: |
| from pathos.multiprocessing import ProcessingPool as Pool |
|
|
| with Pool(num_worker) as m: |
| result = m.map(func, all_task) |
| return result |
| else: |
| result = [] |
| for t in all_task: |
| result.append(func(t)) |
| return result |
|
|
|
|
| def ensure_remove_file(d): |
| if op.isfile(d) or op.islink(d): |
| try: |
| os.remove(d) |
| except: |
| pass |
|
|
|
|
| def concate_lineidx_8b(sizes, tsvs, out_tsv): |
| File.prepare(tsvs) |
| folder = get_tmp_folder() |
|
|
| def row_processor_8b(row): |
| offset, in_tsv, out_tsv = row |
| fbar = tqdm(unit_scale=True) |
| bulk_size = 1024 |
| with File.open(in_tsv, "rb") as fp: |
| with File.open(out_tsv, "wb") as fpout: |
| while True: |
| x = fp.read(8 * bulk_size) |
| fbar.update(len(x) // 8) |
| if x != b"": |
| import struct |
|
|
| fmt = "<{}q".format(len(x) // 8) |
| x = [i + offset for i in struct.unpack(fmt, x)] |
| fpout.write(b"".join([i.to_bytes(8, "little") for i in x])) |
| else: |
| break |
|
|
| all_info_8b = [(sizes[i], op.splitext(t)[0] + ".lineidx.8b") for i, t in enumerate(tsvs)] |
| File.prepare([in_tsv for _, in_tsv in all_info_8b]) |
| |
| all_info_8b = [(offset, in_tsv, "{}/{}".format(folder, in_tsv + ".lineidx.8b")) for offset, in_tsv in all_info_8b] |
| parallel_map(row_processor_8b, all_info_8b, 64) |
| concat_files([i[2] for i in all_info_8b], op.splitext(out_tsv)[0] + ".lineidx.8b") |
| for d in all_info_8b: |
| ensure_remove_file(d[2]) |
|
|
|
|
| def tsv_reader(tsv_file_name, sep="\t"): |
| with File.open(tsv_file_name, "r") as fp: |
| for i, line in enumerate(fp): |
| yield [x.strip() for x in line.split(sep)] |
|
|
|
|
| def load_from_yaml_file(file_name): |
| |
| with File.open(file_name, "r") as fp: |
| |
| data = load_from_yaml_str(fp) |
| while isinstance(data, dict) and "_base_" in data: |
| b = op.join(op.dirname(file_name), data["_base_"]) |
| result = load_from_yaml_file(b) |
| assert isinstance(result, dict) |
| del data["_base_"] |
| all_key = get_all_path(data, with_list=False) |
| for k in all_key: |
| v = dict_get_path_value(data, k) |
| dict_update_path_value(result, k, v) |
| data = result |
| return data |
|
|
|
|
| def read_to_character(fp, c): |
| result = [] |
| while True: |
| s = fp.read(32) |
| assert s != b"" and s != "" |
| if c in s: |
| result.append(s[: s.index(c)]) |
| break |
| else: |
| result.append(s) |
| return b"".join(result) |
|
|
|
|
| class TSVFile(object): |
| def __init__(self, tsv_file, cache_policy=None, open_func=None): |
| self.tsv_file = tsv_file |
| if "://" in tsv_file: |
| parsed_url = urlparse(tsv_file) |
| path = parsed_url.path |
| lineidx = op.splitext(path)[0] + ".lineidx" |
| self.lineidx = urlunparse(parsed_url._replace(path=lineidx)) |
| lineidx_8b = lineidx + ".8b" |
| self.lineidx_8b = urlunparse(parsed_url._replace(path=lineidx_8b)) |
| else: |
| self.lineidx = op.splitext(tsv_file)[0] + ".lineidx" |
| self.lineidx_8b = self.lineidx + ".8b" |
| self._fp = None |
| self._mfp = None |
| self._lineidx = None |
| self.fp8b = None |
| self.cache_policy = cache_policy |
| self.close_fp_after_read = False |
| if os.environ.get("QD_TSV_CLOSE_FP_AFTER_READ"): |
| self.close_fp_after_read = bool(os.environ["QD_TSV_CLOSE_FP_AFTER_READ"]) |
| self.use_mmap = False |
| if os.environ.get("QD_TSV_MMAP"): |
| self.use_mmap = int(os.environ["QD_TSV_MMAP"]) |
| |
| self.has_lineidx_8b = True |
| |
| |
| |
| self.pid = None |
| self.lineidx_8b_pid = None |
| self.open_once = False |
|
|
| self._len = None |
| self._tsv_file_size = None |
|
|
| self.open_func = File.open if open_func is None else open_func |
|
|
| |
| has_azcopy = subprocess.run(["azcopy"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode |
| self.has_azcopy = has_azcopy == 0 |
| if self.has_azcopy: |
| self.temp_dir = self._get_temp_dir(tsv_file) |
|
|
| def _get_temp_dir(self, fname): |
| with tempfile.NamedTemporaryFile() as fp: |
| base_temp_dir = os.path.dirname(fp.name) |
| hash_str = hashlib.md5(fname.encode()).hexdigest() |
| return os.path.join(base_temp_dir, "tsv_io-" + hash_str) |
|
|
| @property |
| def tsv_file_size(self): |
| if self._tsv_file_size is None: |
| self._tsv_file_size = File.get_file_size(self.tsv_file) |
| return self._tsv_file_size |
|
|
| def get_row_len(self, i): |
| start = self.get_offset(i) |
| if i < len(self) - 1: |
| end = self.get_offset(i + 1) |
| else: |
| end = self.tsv_file_size |
| return end - start |
|
|
| def get_row_offsets(self, i): |
| start = self.get_offset(i) |
| if i < len(self) - 1: |
| end = self.get_offset(i + 1) |
| else: |
| end = self.tsv_file_size |
| return start, end |
|
|
| def _is_file_open(self, file_path): |
| return ( |
| subprocess.run( |
| ["lsof", file_path], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| ).returncode |
| == 0 |
| ) |
|
|
| def _remove_unopened_file(self, file_path): |
| if self.temp_dir not in file_path: |
| return |
|
|
| logger.info("Try to remove file {}.".format(file_path)) |
|
|
| if self._is_file_open(file_path): |
| logger.info(f"{file_path} is still open.") |
| else: |
| logger.info(f"{file_path} is all closed. So we remove it.") |
|
|
| if op.exists(file_path): |
| os.remove(file_path) |
| logger.info(f"Successfully remove file {file_path}.") |
|
|
| lock_file = file_path + ".lock" |
| if op.exists(lock_file): |
| os.remove(lock_file) |
| logger.info(f"Successfully remove lock file {lock_file}.") |
|
|
| def close_fp(self): |
| if self._fp: |
| _fp_name = self._fp.name |
| self._fp.close() |
| self._fp = None |
| self._remove_unopened_file(_fp_name) |
| if self._mfp: |
| _mfp_name = self._mfp.name |
| self._mfp.close() |
| self._mfp = None |
| self._remove_unopened_file(_mfp_name) |
| if self.has_lineidx_8b and self.fp8b: |
| fp8b_name = self.fp8b.name |
| self.fp8b.close() |
| self.fp8b = None |
| self._remove_unopened_file(fp8b_name) |
| if op.exists(self.temp_dir): |
| if os.listdir(self.temp_dir) == 0: |
| logger.info(f"{self.temp_dir} is not empty. So we do not remove it.") |
| else: |
| logger.info(f"Successfully remove temp dir {self.temp_dir} for {self.tsv_file}") |
| shutil.rmtree(self.temp_dir, ignore_errors=True) |
|
|
| def release(self): |
| self.close_fp() |
| self._lineidx = None |
|
|
| def close(self): |
| |
| self.close_fp() |
|
|
| def __del__(self): |
| self.release() |
|
|
| def __str__(self): |
| return "TSVFile(tsv_file='{}')".format(self.tsv_file) |
|
|
| def __repr__(self): |
| return str(self) |
|
|
| def __iter__(self): |
| self._ensure_tsv_opened() |
| self.fp_seek(0) |
| if not self.use_mmap: |
| for line in self._fp: |
| result = [s.strip() for s in line.decode().split("\t")] |
| yield result |
| else: |
| while True: |
| line = self._mfp.readline() |
| if line == b"": |
| break |
| result = [s.strip() for s in line.decode().split("\t")] |
| yield result |
|
|
| def num_rows(self): |
| if self._len is None: |
| if self.has_lineidx_8b: |
| try: |
| self._len = File.get_file_size(self.lineidx_8b) // 8 |
| except FileNotFoundError: |
| with self.open(self.lineidx_8b, "rb") as fp: |
| self._len = fp.seek(0, os.SEEK_END) // 8 |
| else: |
| self._ensure_lineidx_loaded() |
| self._len = len(self._lineidx) |
| return self._len |
|
|
| def get_key(self, idx): |
| return self.seek_first_column(idx) |
|
|
| def get_current_column(self): |
| if self.use_mmap: |
| result = [s.strip() for s in self._mfp.readline().decode().split("\t")] |
| else: |
| result = [s.strip() for s in self._fp.readline().split("\t")] |
| return result |
|
|
| def get_current_column2(self, size): |
| if self.use_mmap: |
| result = [s.strip() for s in self._mfp.read(size).decode().split("\t")] |
| else: |
| result = [s.strip() for s in self._fp.read(size).decode().split("\t")] |
| return result |
|
|
| def fp_seek(self, pos): |
| if self.use_mmap: |
| self._mfp.seek(pos) |
| else: |
| self._fp.seek(pos) |
|
|
| def seek(self, idx): |
| self._ensure_tsv_opened() |
| pos, end = self.get_row_offsets(idx) |
| self.fp_seek(pos) |
| result = self.get_current_column2(end - pos) |
| if self.close_fp_after_read: |
| self.close_fp() |
| return result |
|
|
| def seek3(self, idx): |
| self._ensure_tsv_opened() |
| pos = self.get_offset(idx) |
| self.fp_seek(pos) |
| result = self.get_current_column() |
| if self.close_fp_after_read: |
| self.close_fp() |
| return result |
|
|
| def seek_first_column(self, idx): |
| self._ensure_tsv_opened() |
| pos = self.get_offset(idx) |
| self._fp.seek(pos) |
| return read_to_character(self._fp, b"\t").decode() |
|
|
| def seek_first_columns(self): |
| assert self.has_lineidx_8b |
| self._ensure_tsv_opened() |
| self.ensure_lineidx_8b_opened() |
| result = [] |
| for idx in range(len(self)): |
| self.fp8b.seek(idx * 8) |
| pos = int.from_bytes(self.fp8b.read(8), "little") |
| self._fp.seek(pos) |
| result.append(read_to_character(self._fp, b"\t").decode()) |
| return result |
|
|
| def _get_lock_file_name(self, fname): |
| path = urlparse(fname).path |
| name = op.basename(path) |
| return op.join(self.temp_dir, name), op.join(self.temp_dir, name + ".lock") |
|
|
| def open(self, fname, mode): |
| if "://" in fname and "blob.core.windows.net" in fname and self.has_azcopy: |
| if not op.isdir(self.temp_dir): |
| os.makedirs(self.temp_dir, exist_ok=True) |
|
|
| temp_file, lock_path = self._get_lock_file_name(fname) |
| with FileLock(lock_path): |
| try: |
| result = subprocess.run( |
| ["azcopy", "cp", fname, temp_file], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| ) |
| if result.returncode != 0: |
| raise ConnectionError(f"azcopy failed with return code {result.returncode}") |
| logger.info(f"Successfully azcopy {fname} to {temp_file}.") |
| return self.open_func(temp_file, mode) |
|
|
| except Exception as e: |
| logger.error(f"azcopy failed with exception {e}. Use regular xopen instead which can be slow.") |
| if op.isfile(temp_file): |
| os.remove(temp_file) |
| if op.isfile(lock_path): |
| os.remove(lock_path) |
|
|
| return self.open_func(fname, mode) |
|
|
| def ensure_lineidx_8b_opened(self): |
| if self.fp8b is None: |
| self.fp8b = self.open(self.lineidx_8b, "rb") |
| self.lineidx_8b_pid = os.getpid() |
| if self.lineidx_8b_pid != os.getpid(): |
| self.fp8b.close() |
| logging.info("re-open {} because the process id changed".format(self.lineidx_8b)) |
| self.fp8b = self.open(self.lineidx_8b, "rb") |
| self.lineidx_8b_pid = os.getpid() |
|
|
| def get_offset(self, idx): |
| |
| |
| |
| if self.has_lineidx_8b: |
| self.ensure_lineidx_8b_opened() |
| self.fp8b.seek(idx * 8) |
| ret = int.from_bytes(self.fp8b.read(8), "little") |
| return ret |
| else: |
| self._ensure_lineidx_loaded() |
| pos = self._lineidx[idx] |
| return pos |
|
|
| def __getitem__(self, index): |
| return self.seek(index) |
|
|
| def __len__(self): |
| return self.num_rows() |
|
|
| def _ensure_lineidx_loaded(self): |
| if self._lineidx is None: |
| with self.open(self.lineidx, "r") as fp: |
| self._lineidx = tuple([int(i.strip()) for i in fp.readlines()]) |
| logging.info("loaded {} from {}".format(len(self._lineidx), self.lineidx)) |
|
|
| def get_tsv_fp(self): |
| start = time.time() |
| fp = self.open(self.tsv_file, "rb") |
| if self.use_mmap: |
| mfp = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) |
| else: |
| mfp = fp |
| end = time.time() |
| if (end - start) > 10: |
| logging.info("too long ({}) to open {}".format(end - start, self.tsv_file)) |
| return mfp, fp |
|
|
| def _ensure_tsv_opened(self): |
| if self.cache_policy == "memory": |
| assert self._fp is not None |
| return |
|
|
| if self._fp is None: |
| self._mfp, self._fp = self.get_tsv_fp() |
| self.pid = os.getpid() |
|
|
| if self.pid != os.getpid(): |
| self._mfp.close() |
| self._fp.close() |
| logging.info("re-open {} because the process id changed".format(self.tsv_file)) |
| self._mfp, self._fp = self.get_tsv_fp() |
| self.pid = os.getpid() |
|
|
|
|
| def tsv_writer(values, tsv_file_name, sep="\t"): |
| tsv_lineidx_file = os.path.splitext(tsv_file_name)[0] + ".lineidx" |
| tsv_8b_file = tsv_lineidx_file + ".8b" |
| idx = 0 |
| sep = sep.encode() |
| with File.open(tsv_file_name, "wb") as fp, File.open(tsv_lineidx_file, "w") as fpidx, File.open( |
| tsv_8b_file, "wb" |
| ) as fp8b: |
| assert values is not None |
| for value in tqdm(values): |
| assert value is not None |
| value = map(lambda v: v if type(v) == bytes else str(v).encode(), value) |
| v = sep.join(value) + b"\n" |
| fp.write(v) |
| fpidx.write(str(idx) + "\n") |
| |
| |
| |
| fp8b.write(idx.to_bytes(8, "little")) |
| idx = idx + len(v) |
|
|
|
|
| |
| @contextmanager |
| def TSVWriter(tsv_file_name, sep="\t"): |
| _tsv_writer = _TSVWriter(tsv_file_name, sep) |
| with File.open(_tsv_writer.tsv_file_name, "wb") as fp, File.open( |
| _tsv_writer.tsv_lineidx_file, "w" |
| ) as fpidx, File.open(_tsv_writer.tsv_8b_file, "wb") as fp8b: |
| _tsv_writer.fp = fp |
| _tsv_writer.fpidx = fpidx |
| _tsv_writer.fp8b = fp8b |
|
|
| yield _tsv_writer |
|
|
|
|
| class _TSVWriter: |
| def __init__(self, tsv_file_name, sep="\t"): |
| self.tsv_file_name = tsv_file_name |
| self.tsv_lineidx_file = os.path.splitext(tsv_file_name)[0] + ".lineidx" |
| self.tsv_8b_file = self.tsv_lineidx_file + ".8b" |
| self.sep = sep.encode() |
| self.fp = None |
| self.fpidx = None |
| self.fp8b = None |
| self.idx = 0 |
|
|
| def write(self, value): |
| assert value is not None |
| value = map(lambda v: v if type(v) == bytes else str(v).encode(), value) |
| v = self.sep.join(value) + b"\n" |
| self.fp.write(v) |
| self.fpidx.write(str(self.idx) + "\n") |
| self.fp8b.write(self.idx.to_bytes(8, "little")) |
| self.idx = self.idx + len(v) |
|
|