#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""极简 OLE2 + BIFF8 (.xls) 解析器：提取每个工作表的单元格文本，输出 TSV。
仅用标准库。用法: python3 xls_dump.py 文件.xls [关键词]
"""
import sys, struct, re

def read_ole_streams(path):
    data = open(path, 'rb').read()
    assert data[:8] == b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1', 'not OLE2'
    ssz = 1 << struct.unpack_from('<H', data, 0x1E)[0]
    mssz = 1 << struct.unpack_from('<H', data, 0x20)[0]
    n_fat = struct.unpack_from('<I', data, 0x2C)[0]
    dir_start = struct.unpack_from('<I', data, 0x30)[0]
    cutoff = struct.unpack_from('<I', data, 0x38)[0]
    mini_start = struct.unpack_from('<I', data, 0x3C)[0]
    n_mini = struct.unpack_from('<I', data, 0x40)[0]
    difat_start = struct.unpack_from('<I', data, 0x44)[0]
    n_difat = struct.unpack_from('<I', data, 0x48)[0]

    # DIFAT -> FAT 扇区列表
    fat_sectors = list(struct.unpack_from('<109I', data, 0x4C))
    sec = difat_start
    for _ in range(n_difat):
        if sec >= 0xFFFFFFFA: break
        off = 512 + sec * ssz
        vals = struct.unpack_from('<%dI' % (ssz // 4), data, off)
        fat_sectors.extend(vals[:-1])
        sec = vals[-1]
    fat_sectors = [s for s in fat_sectors[:n_fat] if s < 0xFFFFFFFA]

    fat = []
    for s in fat_sectors:
        off = 512 + s * ssz
        fat.extend(struct.unpack_from('<%dI' % (ssz // 4), data, off))

    def chain(start):
        out, s = [], start
        seen = set()
        while s < 0xFFFFFFFA and s not in seen:
            seen.add(s); out.append(s)
            s = fat[s] if s < len(fat) else 0xFFFFFFFE
        return out

    def read_chain(start, size=None):
        buf = b''.join(data[512 + s * ssz: 512 + (s + 1) * ssz] for s in chain(start))
        return buf[:size] if size else buf

    # 目录
    dir_data = read_chain(dir_start)
    entries = []
    for i in range(0, len(dir_data), 128):
        e = dir_data[i:i + 128]
        if len(e) < 128: break
        nlen = struct.unpack_from('<H', e, 64)[0]
        name = e[:max(0, nlen - 2)].decode('utf-16-le', 'ignore')
        typ = e[66]
        start = struct.unpack_from('<I', e, 116)[0]
        size = struct.unpack_from('<Q', e, 120)[0]
        entries.append((name, typ, start, size))

    # mini FAT / mini stream
    mini_fat = []
    if n_mini:
        md = read_chain(mini_start)
        mini_fat = list(struct.unpack_from('<%dI' % (len(md) // 4), md, 0))
    root = next((e for e in entries if e[1] == 5), None)
    mini_stream = read_chain(root[2], root[3]) if root and root[3] else b''

    def mini_chain(start):
        out, s, seen = [], start, set()
        while s < 0xFFFFFFFA and s not in seen:
            seen.add(s); out.append(s)
            s = mini_fat[s] if s < len(mini_fat) else 0xFFFFFFFE
        return out

    streams = {}
    for name, typ, start, size in entries:
        if typ != 2: continue
        if size < cutoff:
            buf = b''.join(mini_stream[s * mssz:(s + 1) * mssz] for s in mini_chain(start))
        else:
            buf = read_chain(start, size)
        streams[name] = buf[:size] if size else buf
    return streams

def biff_records(buf):
    i, n = 0, len(buf)
    while i + 4 <= n:
        typ, ln = struct.unpack_from('<HH', buf, i)
        rec = buf[i + 4: i + 4 + ln]
        yield typ, rec
        i += 4 + ln

def parse_sst(recs, start_idx):
    """合并 SST(0x00FC) 与其后的 CONTINUE(0x003C)。返回字符串列表和结束索引。"""
    typ, rec = recs[start_idx]
    total, unique = struct.unpack_from('<II', rec, 0)
    data = rec[8:]
    j = start_idx + 1
    while j < len(recs) and recs[j][0] == 0x003C:
        data += recs[j][1]; j += 1
    # 注意: 跨 CONTINUE 的字符串编码标志处理从简（多数 WPS 导出表格可正常解析）
    strings, p = [], 0
    for _ in range(unique):
        if p + 3 > len(data): break
        cch = struct.unpack_from('<H', data, p)[0]; p += 2
        flags = data[p]; p += 1
        rt = bool(flags & 0x08); ext = bool(flags & 0x04)
        cRun = 0; cbExt = 0
        if rt:
            cRun = struct.unpack_from('<H', data, p)[0]; p += 2
        if ext:
            cbExt = struct.unpack_from('<I', data, p)[0]; p += 4
        if flags & 0x01:
            s = data[p:p + cch * 2].decode('utf-16-le', 'ignore'); p += cch * 2
        else:
            s = data[p:p + cch].decode('latin-1', 'ignore'); p += cch
        p += 4 * cRun + cbExt
        strings.append(s)
    return strings, j

def parse_workbook(wb):
    recs = list(biff_records(wb))
    # SST
    sst = []
    for k, (typ, rec) in enumerate(recs):
        if typ == 0x00FC:
            sst, _ = parse_sst(recs, k); break
    # 工作表名与位置
    sheets = []
    for typ, rec in recs:
        if typ == 0x0085:
            pos = struct.unpack_from('<I', rec, 0)[0]
            cch = rec[6]
            flags = rec[7]
            if flags & 0x01:
                name = rec[8:8 + cch * 2].decode('utf-16-le', 'ignore')
            else:
                name = rec[8:8 + cch].decode('latin-1', 'ignore')
            sheets.append((name, pos))
    # 各 sheet 的单元格
    out = {}
    positions = sorted([p for _, p in sheets])
    for idx, (name, pos) in enumerate(sheets):
        end = positions[idx + 1] if idx + 1 < len(positions) else len(wb)
        cells = {}
        i = 0
        sub = list(biff_records(wb[pos:end]))
        pending_formula = None
        for typ, rec in sub:
            try:
                if typ == 0x00FD:  # LABELSST
                    r, c, xf, si = struct.unpack_from('<HHHI', rec, 0)
                    cells[(r, c)] = sst[si] if si < len(sst) else ''
                elif typ == 0x0204:  # LABEL
                    r, c, xf, cch = struct.unpack_from('<HHHH', rec, 0)
                    flags = rec[8]
                    if flags & 0x01:
                        cells[(r, c)] = rec[9:9 + cch * 2].decode('utf-16-le', 'ignore')
                    else:
                        cells[(r, c)] = rec[9:9 + cch].decode('latin-1', 'ignore')
                elif typ == 0x027E:  # RK
                    r, c, xf = struct.unpack_from('<HHH', rec, 0)
                    rk = struct.unpack_from('<i', rec, 6)[0]
                    if rk & 0x02:
                        val = rk >> 2
                    else:
                        val = struct.unpack('<d', struct.pack('<q', (rk & 0xFFFFFFFC) << 32))[0]
                    if rk & 0x01: val /= 100.0
                    cells[(r, c)] = ('%g' % val)
                elif typ == 0x0203:  # NUMBER
                    r, c, xf = struct.unpack_from('<HHH', rec, 0)
                    cells[(r, c)] = '%g' % struct.unpack_from('<d', rec, 6)[0]
                elif typ == 0x00BD:  # MULRK
                    r, c1 = struct.unpack_from('<HH', rec, 0)
                    n = (len(rec) - 6) // 6
                    for k in range(n):
                        rk = struct.unpack_from('<i', rec, 4 + k * 6 + 2)[0]
                        if rk & 0x02:
                            val = rk >> 2
                        else:
                            val = struct.unpack('<d', struct.pack('<q', (rk & 0xFFFFFFFC) << 32))[0]
                        if rk & 0x01: val /= 100.0
                        cells[(r, c1 + k)] = '%g' % val
                elif typ == 0x0006:  # FORMULA
                    r, c, xf = struct.unpack_from('<HHH', rec, 0)
                    if rec[12] == 0xFF and rec[13] == 0xFF:
                        pending_formula = (r, c)
                elif typ == 0x0207 and pending_formula:  # STRING (公式结果)
                    cch = struct.unpack_from('<H', rec, 0)[0]
                    flags = rec[2]
                    s = rec[3:3 + cch * 2].decode('utf-16-le', 'ignore') if flags & 1 else rec[3:3 + cch].decode('latin-1', 'ignore')
                    cells[pending_formula] = s; pending_formula = None
            except Exception:
                pass
        out[name] = cells
    return sheets, out

def main():
    path = sys.argv[1]
    kw = sys.argv[2] if len(sys.argv) > 2 else None
    streams = read_ole_streams(path)
    wb = streams.get('Workbook') or streams.get('Book')
    if not wb:
        print('未找到 Workbook 流，现有流：', list(streams)); return
    sheets, data = parse_workbook(wb)
    print('工作表:', [s[0] for s in sheets])
    for name, cells in data.items():
        if not cells: continue
        rows = sorted(set(r for r, _ in cells))
        print('\n===== 工作表: %s（%d 行）=====' % (name, len(rows)))
        for r in rows:
            cols = sorted(c for rr, c in cells if rr == r)
            def norm(v):
                v = (v or '').replace('\n', ' ')
                try:
                    f = float(v)
                    if abs(f) >= 100000 and abs(f - round(f)) < 1e-6:
                        return str(int(round(f)))
                except Exception:
                    pass
                return v
            line = '\t'.join(norm(cells.get((r, c), '')) for c in range(0, max(cols) + 1))
            if kw and kw not in line: continue
            print('R%-4d %s' % (r + 1, line))

if __name__ == '__main__':
    main()
