Skip to content

Instantly share code, notes, and snippets.

@Kishlay-notabot
Created May 15, 2026 06:34
Show Gist options
  • Select an option

  • Save Kishlay-notabot/e608cf98ce017ba58fab08a6e7944071 to your computer and use it in GitHub Desktop.

Select an option

Save Kishlay-notabot/e608cf98ce017ba58fab08a6e7944071 to your computer and use it in GitHub Desktop.
Merge WhatsApp 'Message Yourself' exports into one sorted, deduplicated txt file.
```
#!/usr/bin/env python3
"""
Merge WhatsApp 'Message Yourself' exports into one sorted, deduplicated .txt.
Usage:
python merge_whatsapp.py <folder> [output.txt]
- Reads every .txt in <folder>.
- Parses messages (multi-line messages preserved as one entry).
- Deduplicates messages by exact text — duplicate files and overlapping date
ranges across exports both collapse automatically.
- Sorts oldest -> newest.
- Writes to output.txt (default: <folder>/merged.txt).
"""
import re
import sys
from datetime import datetime
from pathlib import Path
# Header of a new message: "20/01/2026, 6:30 pm - "
# Tolerates invisible unicode chars (LRM/RLM/BOM) WhatsApp sometimes inserts,
# and the narrow-no-break-space WhatsApp uses around am/pm (matched by \s).
MSG_START = re.compile(
r'^[\u200e\u200f\ufeff]*'
r'(\d{1,2}/\d{1,2}/\d{4}),\s+' # date
r'(\d{1,2}:\d{2})\s*' # time
r'(am|pm|AM|PM)\s+-\s', # period + " - "
)
def parse_timestamp(date: str, time: str, ampm: str) -> datetime:
return datetime.strptime(f"{date} {time} {ampm.upper()}", "%d/%m/%Y %I:%M %p")
def parse_messages(path: Path):
"""Return list of (timestamp, full_text) for every message in a chat export."""
out = []
buf_ts, buf_lines = None, []
with open(path, encoding='utf-8', errors='replace') as f:
for line in f:
line = line.rstrip('\n')
m = MSG_START.match(line)
if m:
if buf_ts is not None:
out.append((buf_ts, '\n'.join(buf_lines)))
buf_ts = parse_timestamp(m.group(1), m.group(2), m.group(3))
buf_lines = [line]
elif buf_ts is not None:
# continuation of a multi-line message
buf_lines.append(line)
# lines before the first message (encryption notice) are skipped
if buf_ts is not None:
out.append((buf_ts, '\n'.join(buf_lines)))
return out
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
folder = Path(sys.argv[1])
output = Path(sys.argv[2]) if len(sys.argv) > 2 else folder / 'merged.txt'
txts = sorted(p for p in folder.glob('*.txt') if p.resolve() != output.resolve())
if not txts:
print(f"no .txt files found in {folder}")
sys.exit(1)
# 1. parse every file
all_msgs = []
for p in txts:
msgs = parse_messages(p)
print(f" {p.name:40s} {len(msgs):>6} messages")
all_msgs.extend(msgs)
# 2. dedupe by exact message text
seen, deduped = set(), []
for ts, text in all_msgs:
if text not in seen:
seen.add(text)
deduped.append((ts, text))
# 3. sort oldest -> newest (stable)
deduped.sort(key=lambda x: x[0])
# 4. write
with open(output, 'w', encoding='utf-8') as f:
for _, text in deduped:
f.write(text + '\n')
print(f"\n{len(all_msgs)} parsed -> {len(deduped)} unique -> {output}")
if __name__ == '__main__':
main()
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment