-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathto_csv_progress.py
More file actions
101 lines (88 loc) · 3.33 KB
/
Copy pathto_csv_progress.py
File metadata and controls
101 lines (88 loc) · 3.33 KB
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
#!/usr/bin/env python3
import sys, os, re, csv, time, argparse
def human(n):
for unit in ["", "K", "M", "B"]:
if abs(n) < 1000:
return f"{n:.0f}{unit}"
n /= 1000.0
return f"{n:.1f}T"
def stream_lines_with_progress(path, encoding="cp1252", chunk_size=1024*1024):
"""
Read the input file in binary, decode in cp1252, normalize <br> -> \n,
and yield logical non-empty text lines. Yields (line, bytes_read_so_far).
Because cp1252 is single-byte, len(chunk) bytes read maps 1:1 to bytes consumed.
"""
br_re = re.compile(r"<br\s*/?>", re.IGNORECASE)
bytes_read = 0
with open(path, "rb") as fb:
carry = ""
while True:
chunk = fb.read(chunk_size)
if not chunk:
break
bytes_read += len(chunk)
text = chunk.decode(encoding, errors="strict")
carry += text
# Normalize breaks then split
carry = br_re.sub("\n", carry)
*lines, carry = carry.split("\n")
for ln in lines:
ln = ln.strip()
if ln:
yield ln, bytes_read
# Flush remainder
carry = carry.strip()
if carry:
yield carry, bytes_read
def main():
ap = argparse.ArgumentParser(description="Convert pipe+<br> export to RFC-4180 CSV with accurate progress.")
ap.add_argument("inp", help="Input text file (pipe-delimited, <br> line breaks, likely CP1252)")
ap.add_argument("out", help="Output CSV file (UTF-8)")
ap.add_argument("--encoding", default="cp1252", help="Source encoding (default: cp1252)")
ap.add_argument("--tick", type=int, default=100_000, help="Print progress every N rows (default: 100000)")
ap.add_argument("--chunk", type=int, default=1024*1024, help="Read chunk size in bytes (default: 1 MiB)")
args = ap.parse_args()
try:
total_bytes = os.path.getsize(args.inp)
except OSError:
total_bytes = None
start = time.time()
line_iter = stream_lines_with_progress(args.inp, encoding=args.encoding, chunk_size=args.chunk)
# Header
try:
header_line, bytes_read = next(line_iter)
except StopIteration:
print("Input appears empty after normalization.", file=sys.stderr)
sys.exit(1)
header = [c.strip() for c in header_line.split("|")]
with open(args.out, "w", newline="", encoding="utf-8") as csvfile:
w = csv.writer(csvfile)
w.writerow(header)
rows = 0
for ln, bytes_read in line_iter:
row = [c.strip() for c in ln.split("|")]
w.writerow(row)
rows += 1
if rows % args.tick == 0:
now = time.time()
elapsed = max(now - start, 1e-6)
rps = rows / elapsed
pct = ""
eta = ""
if total_bytes:
p = (bytes_read / total_bytes) * 100.0
pct = f" {p:5.1f}%"
if bytes_read > 0:
total_time = elapsed * (total_bytes / bytes_read)
eta_sec = max(total_time - elapsed, 0)
eta = f" ETA {int(eta_sec//3600):02d}:{int((eta_sec%3600)//60):02d}:{int(eta_sec%60):02d}"
print(f"[{time.strftime('%H:%M:%S')}] rows={human(rows)} rate={rps:,.0f}/s{pct}{eta}")
elapsed = time.time() - start
rate = (rows / elapsed) if elapsed > 0 else 0.0
print(f"Done. Wrote {rows:,} data rows (+ header) to {args.out} in {elapsed:,.1f}s, {rate:,.0f} rows/s.")
if __name__ == "__main__":
try:
main()
except UnicodeDecodeError as e:
print("Encoding error. Try --encoding cp1252 or latin-1.", file=sys.stderr)
raise