mql5-tick-compression/tests/data/parquet_mini.py
310 라인
9.6 KiB
Python
#!/usr/bin/env python3
"""Minimal pure-Python Parquet reader (flat schemas, PLAIN/dictionary pages,
SNAPPY/GZIP/uncompressed). Enough to extract tick columns from public datasets.
"""
import struct
import sys
import zlib
from array import array
# ---------------------------------------------------------------- thrift compact
class TReader:
def __init__(self, buf, pos=0):
self.b = buf
self.p = pos
def byte(self):
v = self.b[self.p]
self.p += 1
return v
def varint(self):
shift = 0
result = 0
while True:
c = self.b[self.p]
self.p += 1
result |= (c & 0x7F) << shift
if not (c & 0x80):
return result
shift += 7
def zigzag(self):
n = self.varint()
return (n >> 1) ^ -(n & 1)
def read_value(self, t):
if t == 1:
return True
if t == 2:
return False
if t == 3:
v = self.byte()
return v - 256 if v > 127 else v
if t in (4, 5, 6):
return self.zigzag()
if t == 7:
v = struct.unpack_from('<d', self.b, self.p)[0]
self.p += 8
return v
if t == 8:
n = self.varint()
v = bytes(self.b[self.p:self.p + n])
self.p += n
return v
if t in (9, 10):
h = self.byte()
size = h >> 4
et = h & 15
if size == 15:
size = self.varint()
out = []
for _ in range(size):
if et in (1, 2):
out.append(self.byte() == 1)
else:
out.append(self.read_value(et))
return out
if t == 11:
size = self.varint()
out = {}
if size:
kv = self.byte()
for _ in range(size):
k = self.read_value(kv >> 4)
out[k] = self.read_value(kv & 15)
return out
if t == 12:
return self.read_struct()
raise ValueError('bad thrift type %d' % t)
def read_struct(self):
out = {}
fid = 0
while True:
h = self.byte()
if h == 0:
return out
t = h & 15
d = h >> 4
if d == 0:
fid = self.zigzag()
else:
fid += d
out[fid] = self.read_value(t)
# ---------------------------------------------------------------- snappy
def snappy_decompress(src):
r = TReader(src)
n = r.varint()
out = bytearray()
p = r.p
L = len(src)
while p < L:
tag = src[p]
p += 1
kind = tag & 3
if kind == 0:
ln = tag >> 2
if ln >= 60:
nb = ln - 59
ln = int.from_bytes(src[p:p + nb], 'little')
p += nb
ln += 1
out += src[p:p + ln]
p += ln
continue
if kind == 1:
ln = ((tag >> 2) & 7) + 4
off = ((tag >> 5) << 8) | src[p]
p += 1
elif kind == 2:
ln = (tag >> 2) + 1
off = src[p] | (src[p + 1] << 8)
p += 2
else:
ln = (tag >> 2) + 1
off = int.from_bytes(src[p:p + 4], 'little')
p += 4
start = len(out) - off
if off >= ln:
out += out[start:start + ln]
else:
while ln > 0:
chunk = out[start:start + min(off, ln)]
out += chunk
ln -= len(chunk)
if len(out) != n:
raise ValueError('snappy length mismatch')
return bytes(out)
_zstd = None
def zstd_decompress(data, usize):
"""ZSTD pages are decoded with the system libzstd (or $LIBZSTD) via ctypes."""
global _zstd
import ctypes
import ctypes.util
import os
if _zstd is None:
lib = os.environ.get('LIBZSTD') or ctypes.util.find_library('zstd')
if not lib:
raise RuntimeError('libzstd not found: install it or set LIBZSTD=/path/to/libzstd.so')
_zstd = ctypes.CDLL(lib)
_zstd.ZSTD_decompress.restype = ctypes.c_size_t
_zstd.ZSTD_decompress.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t]
_zstd.ZSTD_isError.restype = ctypes.c_uint
_zstd.ZSTD_isError.argtypes = [ctypes.c_size_t]
dst = ctypes.create_string_buffer(usize)
src = bytes(data)
r = _zstd.ZSTD_decompress(dst, usize, src, len(src))
if _zstd.ZSTD_isError(r) or r != usize:
raise ValueError('zstd failure')
return dst.raw
def decompress(codec, data, usize):
if codec == 6:
return zstd_decompress(data, usize)
if codec == 0:
return data
if codec == 1:
return snappy_decompress(data)
if codec == 2:
return zlib.decompress(data, 16 + zlib.MAX_WBITS)
raise ValueError('unsupported codec %d' % codec)
# ---------------------------------------------------------------- rle hybrid
def rle_hybrid(buf, pos, end, width, count):
out = []
r = TReader(buf, pos)
mask = (1 << width) - 1
vbytes = (width + 7) // 8
while len(out) < count and r.p < end:
h = r.varint()
if h & 1:
groups = h >> 1
nb = groups * width
chunk = int.from_bytes(buf[r.p:r.p + nb], 'little')
r.p += nb
if width == 0:
out.extend([0] * (groups * 8))
else:
for i in range(groups * 8):
out.append((chunk >> (i * width)) & mask)
else:
run = h >> 1
v = int.from_bytes(buf[r.p:r.p + vbytes], 'little') if vbytes else 0
r.p += vbytes
out.extend([v] * run)
return out[:count]
PTYPE_FMT = {1: ('i', 4), 2: ('q', 8), 4: ('f', 4), 5: ('d', 8)}
def plain_values(ptype, buf, pos, count):
code, size = PTYPE_FMT[ptype]
a = array(code)
a.frombytes(bytes(buf[pos:pos + count * size]))
if sys.byteorder != 'little':
a.byteswap()
return a
def read_parquet(path, wanted=None):
with open(path, 'rb') as f:
data = f.read()
if data[:4] != b'PAR1' or data[-4:] != b'PAR1':
raise ValueError('not parquet')
mlen = struct.unpack_from('<I', data, len(data) - 8)[0]
meta = TReader(data, len(data) - 8 - mlen).read_struct()
schema = meta[2]
leaves = []
for el in schema[1:]:
if el.get(5):
raise ValueError('nested schema not supported')
leaves.append((el[4].decode(), el.get(1), el.get(3, 0), el.get(6), el.get(10)))
cols = {name: [] for name, *_ in leaves}
for rg in meta[4]:
for ci, cc in enumerate(rg[1]):
name, ptype, rep, conv, logical = leaves[ci]
if wanted and name not in wanted:
continue
md = cc[3]
codec = md[4]
nvals = md[5]
start = md.get(11) or md[9]
if md.get(11) is not None and md[11] < md[9]:
start = md[11]
pos = start
dictionary = None
got = 0
while got < nvals:
r = TReader(data, pos)
ph = r.read_struct()
pos = r.p
ptype_page = ph[1]
usize = ph[2]
csize = ph[3]
raw = data[pos:pos + csize]
pos += csize
if ptype_page == 2: # dictionary
page = decompress(codec, raw, usize)
dictionary = plain_values(ptype, page, 0, ph[7][1])
continue
if ptype_page == 0:
page = decompress(codec, raw, usize)
dh = ph[5]
n = dh[1]
enc = dh[2]
p = 0
ndef = n
if rep == 1: # optional -> def levels
ln = struct.unpack_from('<I', page, 0)[0]
lv = rle_hybrid(page, 4, 4 + ln, 1, n)
p = 4 + ln
ndef = sum(lv)
if ndef != n:
raise ValueError('nulls not supported')
elif ptype_page == 3:
dh = ph[8]
n = dh[1]
if dh.get(2, 0):
raise ValueError('nulls not supported')
enc = dh[4]
dl = dh.get(5, 0)
rl = dh.get(6, 0)
lev = raw[:dl + rl]
body = raw[dl + rl:]
if dh.get(7, True):
body = decompress(codec, body, usize - dl - rl)
page = body
p = 0
ndef = n
else:
continue
if enc == 0:
vals = plain_values(ptype, page, p, ndef)
elif enc in (2, 8):
width = page[p]
idx = rle_hybrid(page, p + 1, len(page), width, ndef)
d = dictionary
vals = [d[i] for i in idx]
else:
raise ValueError('unsupported encoding %d' % enc)
cols[name].extend(vals)
got += n
return cols, leaves, meta[3]
if __name__ == '__main__':
cols, leaves, nrows = read_parquet(sys.argv[1])
print('rows', nrows)
for name, ptype, rep, conv, logical in leaves:
c = cols[name]
print(name, ptype, rep, conv, logical, len(c), list(c[:5]))