webscraper / tools /text_stats.py
bluedragonDC
🚀 Deploy: Sniper MCP Forensic Scraper + Gradio
5ddaa4f
Raw
History Blame Contribute Delete
8.19 kB
# filename: text_stats.py
import string
import unicodedata
import tiktoken
import os
# --------------------
# CONFIG
# --------------------
# --------------------
# CONFIG
# --------------------
import sys
if len(sys.argv) > 1:
FILE_PATH = sys.argv[1]
else:
FILE_PATH = r"C:\Users\dcanb\.gemini\antigravity\scratch\wcs3\sessions\capture_20260123_1503_www_vap_org_tr"
N_TOP_BIGGEST = 3
M_TOP_SMALLEST = 3
# Filter settings
INCLUDE_TYPES = [".py", ".json", ".md", ".txt", ".js", ".jsonl"] # List of extensions like [".py", ".json"] or ["all"]
EXCLUDE_TYPES = [".pyc", ".png", ".jpg", ".bin"] # List of extensions to skip like [".json"]
MIN_FILE_SIZE = "10b" # e.g. "500b", "1kb", "0b" for no limit
MAX_FILE_SIZE = "100mb" # Skip files larger than this
# Exclude directories
EXCLUDE_DIRS = {".git", "__pycache__", ".idea", "node_modules", ".vscode", "capture", "captures", "logs", "data", "venv", "env", "global_knowledge"}
# --------------------
# HELPERS
# --------------------
def parse_size(size_str: str) -> int:
"""Parses strings like '1kb', '2mb', '500b' into bytes."""
size_str = size_str.lower().strip()
if not size_str or size_str == "0b":
return 0
units = {"kb": 1024, "mb": 1024**2, "gb": 1024**3, "b": 1}
for unit, factor in units.items():
if size_str.endswith(unit):
try:
number = float(size_str.replace(unit, "").strip())
return int(number * factor)
except ValueError:
pass
return 0
def format_bytes(b):
if b < 0: return "0.00 B"
for u in ["B", "KB", "MB", "GB"]:
if b < 1024: return f"{b:.2f} {u}"
b /= 1024
return f"{b:.2f} TB"
def fmt(n: float | int) -> str:
return f"{n:,.2f}".replace(",", ".") if isinstance(n, float) else f"{n:,}".replace(",", ".")
# --------------------
# READ FILE / DIRECTORY
# --------------------
text = ""
file_stats = [] # Store (name, size, tokens)
min_bytes = parse_size(MIN_FILE_SIZE)
if os.path.isfile(FILE_PATH):
print(f"Reading file: {FILE_PATH}")
ext = os.path.splitext(FILE_PATH)[1].lower()
size = os.path.getsize(FILE_PATH)
# Check filters
is_included = "all" in INCLUDE_TYPES or ext in [t.lower() for t in INCLUDE_TYPES]
is_excluded = ext in [t.lower() for t in EXCLUDE_TYPES]
is_big_enough = size >= min_bytes
if is_included and not is_excluded and is_big_enough:
with open(FILE_PATH, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
text = content
enc = tiktoken.get_encoding("cl100k_base")
tokens = len(enc.encode(content))
file_stats.append((os.path.basename(FILE_PATH), size, tokens))
elif os.path.isdir(FILE_PATH):
max_bytes = parse_size(MAX_FILE_SIZE)
print(f"Reading directory: {FILE_PATH}")
print(f"Filters: Include={INCLUDE_TYPES}, Exclude={EXCLUDE_TYPES}")
print(f"Size Constraints: Min={MIN_FILE_SIZE}, Max={MAX_FILE_SIZE}")
print(f"Ignoring Dirs: {EXCLUDE_DIRS}")
file_count = 0
enc = tiktoken.get_encoding("cl100k_base")
# Walk the directory
for root, dirs, files in os.walk(FILE_PATH):
# Skip common non-code/garbage directories
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
for file in files:
ext = os.path.splitext(file)[1].lower()
full_path = os.path.join(root, file)
try:
size = os.path.getsize(full_path)
# Filter Logic
is_included = "all" in INCLUDE_TYPES or ext in [t.lower() for t in INCLUDE_TYPES]
is_excluded = ext in [t.lower() for t in EXCLUDE_TYPES]
is_big_enough = size >= min_bytes
is_small_enough = (max_bytes == 0) or (size <= max_bytes)
if is_included and not is_excluded and is_big_enough and is_small_enough:
# Print progress for potentially slow files (e.g. > 100KB)
if size > 100 * 1024:
print(f"Processing large file: {file} ({format_bytes(size)})")
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
text += content + "\n" # Add newline between files
tokens = len(enc.encode(content))
file_stats.append((file, size, tokens))
file_count += 1
elif not is_small_enough:
print(f"Skipping too large: {file} ({format_bytes(size)})")
except Exception as e:
print(f"Skipping {file}: {e}")
print(f"Processed {file_count} files.")
else:
print(f"Error: Path not found - {FILE_PATH}")
exit()
lines = text.splitlines()
# --------------------
# BASIC STATS
# --------------------
char_count = len(text)
line_count = len(lines)
word_count = len(text.split())
# --------------------
# UNICODE STATS
# --------------------
letters = uppercase = lowercase = accented = 0
digits = punctuation = symbols = math_symbols = currency_symbols = emojis = whitespace = 0
punct_set = set(string.punctuation)
for ch in text:
cat = unicodedata.category(ch)
if ch.isalpha():
letters += 1
if ch.isupper():
uppercase += 1
elif ch.islower():
lowercase += 1
if "WITH" in unicodedata.name(ch, ""):
accented += 1
elif ch.isdigit():
digits += 1
elif ch in punct_set:
punctuation += 1
elif cat.startswith("S"):
symbols += 1
if cat == "Sm":
math_symbols += 1
elif cat == "Sc":
currency_symbols += 1
elif cat == "So":
emojis += 1
elif ch.isspace():
whitespace += 1
# --------------------
# LINE STATS
# --------------------
line_lengths = [len(line) for line in lines]
empty_lines = sum(1 for l in lines if l == "")
whitespace_only_lines = sum(1 for l in lines if l.strip() == "" and l != "")
longest_line = max(line_lengths) if lines else 0
avg_line_len = int(sum(line_lengths) / line_count) if line_count else 0
# --------------------
# TOKENIZATION
# --------------------
enc = tiktoken.get_encoding("cl100k_base")
input_tokens = len(enc.encode(text))
# --------------------
# OUTPUT
# --------------------
print("Stats")
print("-" * 60)
print("Lines:", fmt(line_count))
print(" Empty:", fmt(empty_lines))
print(" Whitespace-only:", fmt(whitespace_only_lines))
print(" Longest line:", fmt(longest_line))
print(" Avg line length:", fmt(avg_line_len))
print("-" * 60)
print("Words:", fmt(word_count))
print("Characters:", fmt(char_count))
print("Letters:", fmt(letters))
print(" Uppercase:", fmt(uppercase))
print(" Lowercase:", fmt(lowercase))
print(" Accented:", fmt(accented))
print("Digits:", fmt(digits))
print("Punctuation:", fmt(punctuation))
print("Symbols:", fmt(symbols))
print(" Math:", fmt(math_symbols))
print(" Currency:", fmt(currency_symbols))
print("Emojis:", fmt(emojis))
print("Whitespace chars:", fmt(whitespace))
print("-" * 60)
print("TikToken tokens (cl100k_base):", fmt(input_tokens))
# --------------------
# RANKED FILE STATS
# --------------------
def format_bytes(b):
for u in ["B", "KB", "MB"]:
if b < 1024: return f"{b:.2f} {u}"
b /= 1024
if file_stats:
# Sort by size
sorted_by_size = sorted(file_stats, key=lambda x: x[1], reverse=True)
print("\nTop Biggest Files (Size)")
print("-" * 40)
for name, size, tokens in sorted_by_size[:N_TOP_BIGGEST]:
print(f"{name:<25} | {format_bytes(size):>10} | {fmt(tokens):>8} tokens")
print(f"\nTop Smallest Files (Size)")
print("-" * 40)
for name, size, tokens in sorted_by_size[-M_TOP_SMALLEST:]:
# Reverse the smallest list if you want it ascending
pass
# Let's do it properly:
smallest = sorted(file_stats, key=lambda x: x[1])
for name, size, tokens in smallest[:M_TOP_SMALLEST]:
print(f"{name:<25} | {format_bytes(size):>10} | {fmt(tokens):>8} tokens")