A Twitter archive is a ZIP containing a viewer and account-data files; it is not necessarily a ready-made CSV. Post records commonly sit inside JavaScript assignment wrappers around JSON arrays. Inspect your own filenames and a sample record before choosing a parser.
This guide covers a personal account archive. A research dataset containing only post IDs is a different source: an ID identifies a record but does not supply its text or recover unavailable content. For requesting the ZIP, use the download guide. For turning post records into a table, use the Excel conversion workflow.
Identify the viewer, data and media
| Common item | Purpose | What to check |
|---|---|---|
Your archive.html | Browser-based entry point, when included. | Keep its companion folders in place. |
data/ | Account-data files, often wrapped JSON. | Actual names, categories and split parts. |
tweets.js or post-data parts | Post records used by some archive versions. | Wrapper name and whether records contain a nested tweet object. |
| Media and supporting assets | Images, videos or viewer resources. | A text record can exist even if an attachment is absent. |
This is an orientation, not a guaranteed manifest. X describes its download as account information in HTML and JSON formats; packaging can change. Preserve the source ZIP and inspect a duplicate. Do not rename extensions in the original to force a tool to accept it.
Recognize the wrapper before parsing
The following is a small illustrative record, not an export from a real account:
window.YTD.tweets.part0 = [
{
"tweet": {
"id_str": "1234567890123456789",
"created_at": "Mon Jan 15 10:15:00 +0000 2024",
"full_text": "Illustrative archive record, not a real post.",
"entities": {"hashtags": [], "urls": []}
}
}
];The prefix window.YTD.tweets.part0 = makes the whole file JavaScript rather than plain JSON. The array after the assignment is the JSON payload in this example. A parser should recognize the expected wrapper and decode the payload as data. It should not run the file with eval, import it as a program or paste it into a console.
Some archives split post records into multiple files. Inspect every relevant part and record the source filename. Processing one part and calling it the entire archive silently drops records.
Keep IDs exact and interpret fields cautiously
| Field in this example | Use | Important limit |
|---|---|---|
id_str | Stable record key as a string. | Do not convert long IDs into spreadsheet numbers. |
created_at | Source timestamp. | Retain its timezone and original value before transforming it. |
full_text | Post text present in this record. | May contain links, newlines or text that looks like a spreadsheet formula. |
entities | Structured links, hashtags or mentions when present. | Missing values are different from an empty measured result. |
| Optional count fields | A value supplied in that snapshot. | Not necessarily a current metric, complete metric or historical timeseries. |
X recommends handling IDs as strings because some languages lose precision with large integers. In Excel, preserve them as Text before import. Formatting an already rounded ID afterward cannot recover the missing digits.
Do not assume every engagement field is always zero, always complete, or equivalent to the live post's current count. In particular, missing impressions cannot be reconstructed from a post's text.
Parse a known wrapper without executing it
For the illustrative structure above, this Python example reads a local working copy and rejects an unexpected wrapper. Change the path to the actual file you inspected.
import json
import re
from pathlib import Path
text = Path("data/tweets.js").read_text(encoding="utf-8-sig")
match = re.fullmatch(
r"\s*window\.YTD\.tweets\.part\d+\s*=\s*(\[.*\])\s*;?\s*",
text, flags=re.DOTALL
)
if not match:
raise ValueError("Unexpected wrapper: inspect the file before converting")
records = json.loads(match.group(1))
if not isinstance(records, list):
raise ValueError("Expected an array of post records")
print(f"Parsed {len(records)} records without running JavaScript")This is deliberately limited to a tweets.partN assignment. If your archive uses another shape, stop and inspect it rather than weakening the check until arbitrary input passes. json.loads decodes JSON; it does not execute JavaScript.
After parsing, confirm that each record has the expected object structure before accessing fields. Do not discard records with missing IDs or dates without counting and documenting them. For large files, use tools designed for your available memory rather than opening a huge file in a browser tab.
Validate a small sample before analysis
- Compare the parsed record count with the files processed; include every relevant part.
- Spot-check an early post, a recent post and a reply against the local viewer where possible.
- Compare an exact ID character by character after any conversion.
- Check timestamps before grouping by day or hour. A UTC day is not necessarily the account owner's local day.
- Count missing fields separately from zero values.
A monthly count of records in your file can describe that file. It does not automatically describe every post ever published, audience reach or follower growth. Name the quantity you actually measured.
Keep the archive private and preserve uncertainty
Other data categories can include messages, account details and inferred interests. Do not upload the entire ZIP to a random converter just to read a post table. Share only the data needed for the task.
A snapshot can preserve data it contains; it cannot establish that absent content never existed. If the objective is deleted-post evidence, use the prior-backup and snapshot checklist. Keep source filenames, archive date and transformation notes with any derived workbook so someone else can understand its limits.