A WhatsApp export parser usually breaks because the timestamp at the start of each message is not one universal format. Brackets, separators, date order, seconds, AM/PM position, numeral script and invisible direction marks can all change with the phone and locale. A parser that recognizes only DD/MM/YY, HH:MM - Name: text may appear correct in development and then merge an entire German, US, Dutch or Arabic export into one message.
Why one WhatsApp timestamp regex fails
The first parser I wrote had the usual shape: split the file into lines, run one regex against each line, and treat anything that matched as a new message. It worked against the sample in front of me. That was the problem. The sample came from one phone with one regional setting.
This pattern is a reasonable first experiment for an Android export using a slash date and an English AM/PM marker:
const header = /^(\d{1,2}\/\d{1,2}\/\d{2,4}),\s+(\d{1,2}:\d{2})\s+(AM|PM)\s+-\s+([^:]+):\s+(.*)$/i;
It matches:
3/15/24, 2:30 PM - Researcher: Hello
It does not match any of these valid shapes from the public fixture set:
[2024/07/09, 08:01:49] Researcher: Morning
15.03.2024, 14:30 - Researcher: Guten Tag
[15-03-2024, 14:30:01] Researcher: Hoi
[15/3/24 下午 2:30:45] Researcher: 你好
2026-01-13, 12:22 PM - Researcher: Hello

When a header fails to match, the common continuation-line rule appends it to the preceding message. That makes the bug quiet. Instead of throwing an error, the parser can report one enormous message with thousands of lines. A count check is therefore as important as the regex itself.
The exported file is plain text, but its structure depends on the exporting environment. The academic ChatDashboard parsing description reports the same underlying issue: operating system, language, date settings and time settings affect the exported log structure. WhatsApp's official export instructions explain how to create the file; they do not promise one developer-facing timestamp schema.
The timestamp families a parser meets
The 2026.07 benchmark groups 14 layouts by structural family. The point is not that 14 is a permanent total. It is that a production parser needs explicit coverage and fixtures rather than an assumption that one example represents the format.

- Family
- iOS year-first slash
- Example
- [2024/07/09, 08:01:49] Name: text
- Structural difference
- Brackets, seconds, year first, no dash
- Family
- iOS slash + AM/PM
- Example
- [3/15/24, 2:30:45 PM] Name: text
- Structural difference
- Month/day ambiguity and optional seconds
- Family
- Android slash
- Example
- 3/15/24, 2:30 PM - Name: text
- Structural difference
- No brackets and a dash before sender
- Family
- European dotted
- Example
- 15.03.2024, 14:30 - Name: text
- Structural difference
- Dots, day first and 24-hour time
- Family
- Dutch dashed
- Example
- 15-03-2024 14:30 - Name: text
- Structural difference
- Dash date and no comma
- Family
- ISO year-first
- Example
- 2026-01-13, 12:22 PM - Name: text
- Structural difference
- Year first with dash separators
- Family
- CJK marker
- Example
- [15/3/24 下午 2:30:45] Name: text
- Structural difference
- Day-period marker appears before time
| Family | Example | Structural difference |
|---|---|---|
| iOS year-first slash | [2024/07/09, 08:01:49] Name: text | Brackets, seconds, year first, no dash |
| iOS slash + AM/PM | [3/15/24, 2:30:45 PM] Name: text | Month/day ambiguity and optional seconds |
| Android slash | 3/15/24, 2:30 PM - Name: text | No brackets and a dash before sender |
| European dotted | 15.03.2024, 14:30 - Name: text | Dots, day first and 24-hour time |
| Dutch dashed | 15-03-2024 14:30 - Name: text | Dash date and no comma |
| ISO year-first | 2026-01-13, 12:22 PM - Name: text | Year first with dash separators |
| CJK marker | [15/3/24 下午 2:30:45] Name: text | Day-period marker appears before time |
Some families differ only in punctuation. Others change the meaning of the numeric fields. Bracket detection alone is not enough: iOS can emit bracketed slash, dotted and dash-date forms, while non-bracketed Android-style forms can use slash, dash or year-first dates.
This is also why a permissive expression such as ^(.+?), (.+?) - (.+?): (.*)$ is not a dependable fix. It accepts more lines, but it pushes all the difficult work into later guesses and can mistake message text for a header. A parser should be tolerant about known equivalent syntax and strict about the boundary that declares a new message.
Normalize before matching
Regular expressions are easier to reason about after equivalent characters have one representation. The input can contain characters that look familiar on screen but are not the ASCII characters in your pattern.
The published parser performs a normalization pass before trying timestamp families:
- Remove carriage returns and selected directional control marks around headers.
- Convert narrow no-break and non-breaking spaces to ordinary spaces.
- Map localized AM/PM markers to
AMorPM. - Translate Arabic-Indic, Persian, Devanagari and Thai numerals to ASCII digits.
- Normalize punctuation such as the Arabic comma where it appears in the header.

Here is a reduced JavaScript version of the digit step:
function translateDigits(text) {
return [...text].map((char) => {
const point = char.codePointAt(0);
if (point >= 0x0660 && point <= 0x0669) return String(point - 0x0660); // Arabic-Indic
if (point >= 0x06f0 && point <= 0x06f9) return String(point - 0x06f0); // Persian
if (point >= 0x0966 && point <= 0x096f) return String(point - 0x0966); // Devanagari
if (point >= 0x0e50 && point <= 0x0e59) return String(point - 0x0e50); // Thai
return char;
}).join('');
}
For example, the Arabic-Indic header ١٥/٠٣/٢٤, ١٤:٣٠ becomes 15/03/24, 14:30 before structural matching. The message body remains unchanged. Normalization should be scoped: rewriting the entire message body can damage the content you were asked to preserve.
Normalization reduces the number of patterns, but it does not remove date-order ambiguity. It makes symbols comparable; it does not tell you whether 03/04/24 means 3 April or March 4.
Use ordered format families
The parser uses an ordered list of sender and system-message patterns. More specific shapes are tested before more general shapes. A match returns the date string, time string, optional day-period marker, sender and body, plus a pattern type that controls date construction.
Conceptually, the loop looks like this:
for (const pattern of timestampPatterns) {
const senderMatch = line.match(pattern.senderRegex);
if (senderMatch) return parseSenderMessage(senderMatch, pattern.type);
const systemMatch = line.match(pattern.systemRegex);
if (systemMatch) return parseSystemMessage(systemMatch, pattern.type);
}
return null; // continuation text, preamble or unsupported header
Pattern order matters. A broad slash-date expression placed first can consume a more specific four-digit Brazilian form and apply the wrong month/day rule. A sender pattern should also run before a corresponding system pattern so the colon in Name: message is not swallowed as generic system text.
Keep timestamp recognition separate from message classification. Once the header boundary is known, the body can be classified as text, media, call, deleted or system using its own localized markers. Combining every possible body type into the timestamp regex makes the structural expression difficult to test and easy to break.
Treat ambiguous numeric dates honestly
15/03/24 is not ambiguous because 15 cannot be a month. 03/15/24 is equally clear. 03/04/24 is not. Without context, both 3 April and March 4 are valid interpretations.

The published parser uses structural context and a limited heuristic:
- If the first number is above 12, treat it as the day.
- If the second number is above 12, treat it as the day.
- If both are 12 or below and an English AM/PM marker is present, prefer the US month-first interpretation.
- Otherwise prefer day-first for the generic slash family.
This is useful, not infallible. The right production behavior is to retain the raw line, record the detected pattern and make ambiguous dates auditable. For evidence, research or compliance work, compare the result with the source device and surrounding messages. The parser should not turn a heuristic into a certainty claim.
A stronger application can detect locale from a sample of unambiguous dates across the file, then apply the inferred order to ambiguous dates. Even then, keep a warning when the sample provides no decisive values.
Parse messages as state, not isolated lines
A WhatsApp message can contain line breaks. Only its first line has a timestamp header. The following lines must be appended to the current message until another recognized header begins.
3/15/24, 2:30 PM - Alice: first line
second line
third line
3/15/24, 2:31 PM - Bob: next message

A minimal state machine is clearer than splitting on a regex:
const messages = [];
let current = null;
for (const line of normalizedText.split('\n')) {
const header = recognizeHeader(line);
if (header) {
if (current) messages.push(current);
current = header;
} else if (current) {
current.text += `\n${line}`;
}
}
if (current) messages.push(current);
This state also gives you a place to handle preamble text, blank lines and unsupported headers deliberately. The first unmatched line should not create a phantom message. An unmatched line after a valid message is continuation text. A run of thousands of unmatched lines is a diagnostic signal that header recognition probably failed.
System messages need a separate path because they have a timestamp but no sender/body colon. Participant joins, group changes and encryption notices are not sender-attributed chat messages, yet dropping them changes message counts and chronology. The benchmark includes one system case, one multiline case and participant extraction across two senders for that reason.
How the 24-case benchmark works
The WhatsApp Export Parser Benchmark 2026 is intentionally small enough to inspect. It contains no customer messages and no private chats. Every fixture is synthetic and declares its input plus expected structured fields.

The suite currently contains:
- 14 timestamp cases covering bracketed and non-bracketed layouts, slash/dot/dash separators, year-first dates, seconds, and day-period placement.
- 4 normalization cases using Arabic-Indic, Persian, Devanagari and Thai numerals.
- 6 behavior cases for multiline messages, system messages, media markers, deleted messages, call markers and participant extraction.
Both runners read the same fixture JSON. They assert message count and, when applicable, participants, sender, text, type, year, month, day, hour and minute. A failure produces a non-zero exit status, which makes the benchmark usable in CI.
{
"id": "timestamp-android-slash",
"input": "3/15/24, 2:30 PM - Researcher: Hello",
"expected": {
"messageCount": 1,
"sender": "Researcher",
"year": 2024,
"month": 3,
"day": 15,
"hour": 14,
"type": "text"
}
}
Node.js 0.1.1 and Python 0.1.1 each passed 24 of 24 published cases. Read that result narrowly. It does not prove support for every device, app version, locale, malformed ZIP or future WhatsApp change. It proves that the two published implementations satisfy the explicit expectations in this public fixture set. You can download the fixture JSON, case results CSV and full summary.
A safer implementation shape
If you need a parser rather than a finished PDF, the open-source package exposes the same approach in Node and Python. The source, runners and release artifacts are available in the whatsapp-chat-to-pdf GitHub repository.
Node.js:
npm install whatsapp-chat-to-pdf
import fs from 'node:fs';
import { parseText } from 'whatsapp-chat-to-pdf';
const input = fs.readFileSync('_chat.txt', 'utf8');
const chat = parseText(input);
console.log(chat.messageCount);
console.log(chat.participants);
console.log(chat.messages[0]);
Python:
pip install whatsapp-chat-to-pdf
from whatsapp_chat_to_pdf import parse_file
chat = parse_file("WhatsApp Chat with Alice.zip")
print(chat.message_count)
print(chat.participants)
print(chat.messages[0])
The implementation sequence is the transferable part:
- Find the real chat text inside a TXT or ZIP, ignoring macOS
__MACOSXand._resource-fork entries. - Normalize structural Unicode variants without rewriting the message body unnecessarily.
- Recognize an ordered timestamp family and retain the raw input line.
- Build the timestamp with an explicit rule for that family.
- Stitch continuation lines into the current message.
- Classify the message body separately.
- Extract participants from non-system senders.
- Assert fields against synthetic fixtures in every supported runtime.
If you are inspecting the export before writing code, the extract WhatsApp chat data guide explains the fields inside _chat.txt. The export chat meaning guide explains what the ZIP is and is not. For a formatted media-rich result rather than a parser library, the WhatsApp to PDF guide covers the hosted workflow.
Production checklist and limitations

Before trusting a WhatsApp export parser, check these properties:
- Format coverage is named. “Supports WhatsApp” is too broad. Publish the layouts or fixtures you actually test.
- Normalization is tested independently. A format test written only with ASCII digits will not catch localized-numeral failures.
- Raw input is retained. When a timestamp is ambiguous or classification looks wrong, the original line is needed for review.
- Counts are plausible. Zero, one or an unexpectedly small number of messages should trigger a diagnostic rather than a confident empty output.
- Multiline content survives. A continuation line should stay with its message and preserve its newline.
- System messages are explicit. Do not attribute group events to the previous participant.
- ZIP selection is defensive. Ignore resource forks and prefer the recognized chat filename over the first
.txtentry. - Failures are visible. Record unsupported header samples using synthetic or safely redacted examples; never upload a private conversation to a public issue.
- Claims stay bounded. Passing a fixture suite is not proof of export completeness, authorship, authenticity or legal admissibility.
The current benchmark does not measure speed, memory consumption, PDF fidelity, attachment matching or voice transcription. It does not statistically sample all WhatsApp versions. Those are separate tests. A useful next contribution is a new synthetic fixture representing a genuinely different header shape, accompanied by the expected parsed fields and a source description that does not expose private chat content.
Download the fixtures, run both public benchmark commands, then add a synthetic case shaped like the export you expect to receive. A parser is reliable to the extent that its assumptions are visible and repeatedly tested.
FAQ
What format is a WhatsApp exported chat?
WhatsApp exports one conversation as a plain-text chat file, optionally inside a ZIP with media. Message headers commonly contain a localized timestamp followed by a sender and message body, but punctuation, ordering and day-period markers differ across device and locale. Use WhatsApp's official Export Chat flow to create the file; treat the parser schema as something your own fixtures must verify.
Can one regex parse every WhatsApp export?
Not safely. One broad regex can accept many header shapes, but it cannot by itself resolve date order, localized digits, day-period markers, system events and multiline continuation. Use normalization plus ordered structural families, then construct dates according to the matched family and validate the result with field-level fixtures.
How do I parse multiline WhatsApp messages?
Read the export as a stateful stream. A line with a recognized timestamp header starts a new message. A non-header line after a message is appended to that current message with its newline preserved. Flush the current message when the next header appears and again at end of file.
Why does a parser swap the day and month?
Purely numeric slash dates are ambiguous when both values are 12 or below. Use unambiguous dates elsewhere in the file, structural markers and device context to infer the order, but retain the raw line and surface the assumption. For sensitive work, verify the interpreted date against the source device and surrounding conversation.
Is the 24 out of 24 benchmark result universal?
No. It means Node.js 0.1.1 and Python 0.1.1 passed the 24 synthetic fixtures published in benchmark version 2026.07. It does not cover every WhatsApp release, locale or damaged export and it makes no claim about authorship, authenticity, completeness or admissibility.
Key takeaways
- A WhatsApp export parser needs multiple explicit timestamp families because brackets, separators, date order, seconds and day-period position vary.
- Normalize structural Unicode variants and four tested numeral scripts before matching; do not rewrite the preserved message body unnecessarily.
- Treat numeric day/month ambiguity as a visible assumption, not a fact inferred from one line.
- Parse with state so multiline messages and system events remain separate and message counts stay meaningful.
- The public benchmark reports 24 of 24 for two runtimes only against its 24 synthetic cases; fixtures and limitations are downloadable.
- Production reliability comes from named assumptions, raw-line retention, diagnostics and regression fixtures rather than one permissive regex.

