Setup
pip install opencv-python pytesseract numpy
Install Tesseract binary:
Ubuntu/Debian:
sudo apt install tesseract-ocr
macOS:
brew install tesseract
Windows: https://github.com/UB-Mannheim/tesseract/wiki
python extract_whatsapp_contacts.py screenshot.png
Single image → print to terminal
Whole folder → save CSV
python extract_whatsapp_contacts.py --folder ./screenshots/ --output contacts.csv
Save as JSON
python extract_whatsapp_contacts.py *.png --output contacts.json
How it works:
- OpenCV loads the image, converts to grayscale, upscales if needed, then applies adaptive thresholding to sharpen text against varying backgrounds
- Tesseract (PSM 6 — uniform text block mode) runs OCR on the cleaned image
- A regex scans each line for phone number patterns (international formats, dashes, brackets, spaces)
- The line just before a phone number is taken as the contact name; UI chrome like “Search”, “Chats”, timestamps etc. is filtered out
- Results print as a table or export to CSV/JSON
Full Python script
#!/usr/bin/env python3
"""
WhatsApp Contact Extractor
Extracts contact names and phone numbers from WhatsApp contact page screenshots
using Tesseract OCR and OpenCV.
Usage:
python extract_whatsapp_contacts.py image1.png image2.jpg
python extract_whatsapp_contacts.py --folder ./screenshots/
python extract_whatsapp_contacts.py image.png --output contacts.csv
Requirements:
pip install opencv-python pytesseract pillow
Install Tesseract binary:
- Ubuntu/Debian: sudo apt install tesseract-ocr
- macOS: brew install tesseract
- Windows: https://github.com/UB-Mannheim/tesseract/wiki
"""
import argparse
import csv
import json
import re
import sys
from pathlib import Path
try:
import cv2
except ImportError:
print("ERROR: 'opencv-python' not found. Run: pip install opencv-python")
sys.exit(1)
try:
import pytesseract
except ImportError:
print("ERROR: 'pytesseract' not found. Run: pip install pytesseract")
sys.exit(1)
import numpy as np
SUPPORTED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
# Regex patterns for phone numbers
PHONE_PATTERNS = [
r"\+?\d[\d\s\-().]{6,18}\d",
r"(\d{2,4})\s*\d{3,4}[\s-]?\d{3,4}",
r"\d{4}[\s-]\d{7}",
r"\+\d{1,3}\s\d{2,4}\s\d{3,4}\s\d{3,4}",
]
PHONE_RE = re.compile("|".join(PHONE_PATTERNS))
# UI chrome to ignore
UI_NOISE = re.compile(
r"^(whatsapp|contacts|search|new contact|groups|calls|chats|settings|"
r"status|messages?|all|unread|favorites?|ok|cancel|done|edit|add|"
r"broadcast|starred|archived|\d{1,2}:\d{2}|am|pm|online|last seen|"
r"typing…|muted?|\W+)$",
re.IGNORECASE,
)
def preprocess(img: np.ndarray) -> np.ndarray:
"""Grayscale + upscale + adaptive threshold for best OCR accuracy."""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
if w < 1000:
scale = 1000 / w
gray = cv2.resize(gray, None, fx=scale, fy=scale,
interpolation=cv2.INTER_CUBIC)
binary = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
31, 11,
)
return binary
def run_ocr(img: np.ndarray) -> str:
"""Run Tesseract with settings tuned for UI text."""
config = "--oem 3 --psm 6"
return pytesseract.image_to_string(img, config=config)
def parse_contacts(text: str) -> list[dict]:
"""
Parse OCR text into (name, phone) pairs.
- Lines containing a phone number are treated as phone lines.
- The preceding non-noise line is used as the contact name.
"""
lines = [l.strip() for l in text.splitlines() if l.strip()]
contacts = []
i = 0
while i < len(lines):
line = lines[i]
phone_match = PHONE_RE.search(line)
if phone_match:
phone = phone_match.group().strip()
name_part = line[:phone_match.start()].strip()
if not name_part and i > 0:
prev = lines[i - 1]
if not PHONE_RE.search(prev) and not UI_NOISE.match(prev) and len(prev) > 1:
name_part = prev
contacts.append({"name": name_part, "phone": phone})
i += 1
# Fallback: no phone numbers found — return names only
if not contacts:
for line in lines:
if not UI_NOISE.match(line) and len(line) > 2:
contacts.append({"name": line, "phone": ""})
return contacts
def process_image(image_path: Path) -> list[dict]:
"""Full pipeline for one screenshot."""
print(f"Processing: {image_path.name}")
img = cv2.imread(str(image_path))
if img is None:
print(f" ✗ Could not read image.")
return []
processed = preprocess(img)
text = run_ocr(processed)
contacts = parse_contacts(text)
for c in contacts:
c["source_file"] = image_path.name
print(f" ✓ Found {len(contacts)} contact(s)")
return contacts
def collect_images(inputs: list[str], folder: str | None) -> list[Path]:
paths = []
if folder:
d = Path(folder)
if not d.is_dir():
print(f"ERROR: '{folder}' is not a directory.")
sys.exit(1)
paths += sorted(p for p in d.iterdir()
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS)
for inp in inputs:
p = Path(inp)
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS:
paths.append(p)
else:
print(f"WARNING: '{inp}' skipped (not found or unsupported).")
seen, unique = set(), []
for p in paths:
r = p.resolve()
if r not in seen:
seen.add(r)
unique.append(p)
return unique
def print_table(contacts: list[dict]) -> None:
if not contacts:
print("\nNo contacts found.")
return
w_name = max((len(c["name"]) for c in contacts), default=4)
w_phone = max((len(c["phone"]) for c in contacts), default=5)
w_src = max((len(c["source_file"]) for c in contacts), default=11)
w_name, w_phone, w_src = max(w_name, 4), max(w_phone, 5), max(w_src, 11)
sep = "-" * (w_name + w_phone + w_src + 6)
print(f"\n{sep}")
print(f"{'Name':<{w_name}} {'Phone':<{w_phone}} {'Source File':<{w_src}}")
print(sep)
for c in contacts:
print(f"{c['name']:<{w_name}} {c['phone']:<{w_phone}} {c['source_file']:<{w_src}}")
print(sep)
print(f"Total: {len(contacts)} contact(s)\n")
def save_output(contacts: list[dict], output: str) -> None:
out = Path(output)
if out.suffix.lower() == ".json":
out.write_text(json.dumps(contacts, ensure_ascii=False, indent=2), encoding="utf-8")
else:
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "phone", "source_file"])
writer.writeheader()
writer.writerows(contacts)
print(f"\n✅ Saved {len(contacts)} contact(s) to: {out}")
def main():
parser = argparse.ArgumentParser(
description="Extract contact names & phone numbers from WhatsApp screenshots."
)
parser.add_argument("images", nargs="*", help="Screenshot image files.")
parser.add_argument("--folder", "-f", help="Folder of screenshots to process.")
parser.add_argument("--output", "-o",
help="Output file (.csv or .json). Default: print to terminal.")
args = parser.parse_args()
if not args.images and not args.folder:
parser.error("Provide at least one image or use --folder.")
images = collect_images(args.images, args.folder)
if not images:
print("No supported images found.")
sys.exit(1)
print(f"\n🔍 Processing {len(images)} screenshot(s)...\n")
all_contacts = []
for img_path in images:
all_contacts.extend(process_image(img_path))
if args.output:
save_output(all_contacts, args.output)
else:
print_table(all_contacts)
if __name__ == "__main__":
main()
Leave a Reply