Article General April 29, 2026 5 min read

Python Script to Extract Contact and Names from WhatsApp Contact Page Screenshots

Calder Pike

Technical Writer

April 29, 2026
5 min read

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

Your email address will not be published. Required fields are marked *