twitter-x-scraping · git:20260527.4abeec6 · 2026-05-27 · sha256 1ffaf36daabb826d
twitter-x-scraping git:20260527.4abeec6A
Immutable. This exact content never changes and is served at /api/v1/blob/1ffaf36daabb826d.
---
name: twitter-x-scraping
description: Scrape Twitter/X timelines, lists, or profiles via Nitter-compatible mirror HTML without the official API. Use for Twitter/X scraping, Nitter or xcancel.com scraping, tweet collection, cursor pagination, anti-bot challenge handling, and scrape-to-summary pipelines.
---
# Twitter/X Scraping
Scrape public Twitter/X timeline HTML through Nitter-compatible mirrors when the task needs public tweets and does not require the official X API.
## Scope
Use this skill for:
- Profile timelines, such as `/swyx`
- Public X list timelines, such as `/i/lists/<listId>`
- Tweet collection, filtering, raw JSON persistence, and downstream summaries
- Debugging Nitter-compatible markup, cursors, timestamps, stats, media, and anti-bot handling
Do not use this as a drop-in replacement for `GET /2/users/:id/following`.
Public Nitter-compatible mirrors are not a reliable source for a user's following or follower graph. Upstream Nitter profile tabs cover timelines such as tweets, replies, media, and search, but not a stable `/<handle>/following` or `/<handle>/followers` account-list route. Some mirrors may expose list members at `/i/lists/<listId>/members`, but that is an X List membership surface, not the accounts a user follows.
If the workflow needs a following graph, prefer one of:
- Official X API following endpoints
- A user-supplied export or maintained account list
- A known X List ID whose members are acceptable as the source set
- A self-hosted/custom fetcher with explicit support for the account graph
## Default Approach
Treat this as mirror-HTML scraping, not direct `x.com` automation.
Preferred stack:
- Fetch pages with HTTP first, using `fetch`, `axios`, `curl`, or equivalent.
- Parse server-rendered HTML with Cheerio, BeautifulSoup, DOMParser, or equivalent.
- Fall back to one persistent headed browser session only when the mirror returns an anti-bot challenge.
- Save raw scrape JSON before any summarization, enrichment, or Notion/database writes.
Known mirror patterns:
- Profile timeline: `/<handle>`
- List timeline: `/i/lists/<listId>`
- List members, when supported by the mirror: `/i/lists/<listId>/members`
- Pagination: append `?cursor=<cursor>`
- Common fallback mirror: `https://xcancel.com` when `https://nitter.net` is unavailable
Preserve list IDs and tweet IDs as strings. They can exceed safe integer precision.
## Scraping Workflow
1. Resolve the target:
- Numeric input usually means `list`.
- `@handle` or non-numeric input usually means `profile`.
- Allow an explicit `type` override.
- Treat `following` and `followers` targets as unsupported unless the implementation has a verified, current source for that specific graph.
2. Resolve the time window:
- Default to the last 24 hours.
- Accept ISO timestamps and relative offsets like `3d`, `12h`, or `90m`.
- Include tweets at or after `start`.
- Skip tweets newer than `end`.
3. Fetch each page:
- Use browser-like headers: `User-Agent`, `Accept`, `Accept-Language`, and `Accept-Encoding`.
- Retry `429`, `502`, and `503` with exponential backoff.
- Honor `Retry-After` and `X-Rate-Limit-Reset` headers when present.
- Cap retries and page count.
- Log the fetched URL, status, byte length, and whether a challenge marker was present.
4. Parse tweets from each page:
- Load HTML into a DOM parser.
- Select `.timeline .timeline-item` and filter for items containing `.tweet-body`.
- Extract tweet ID and source URL from `a.tweet-link[href]` using `/status/(\\d+)/`.
- Extract timestamp from `.tweet-date a[title]`; normalize bullets/dots before `Date` parsing.
- Extract text from `.tweet-content.media-body`.
- Extract author from `.tweet-header a.username[title]` and `.tweet-header a.fullname[title]`.
- Extract stats from `.tweet-stats .tweet-stat` by icon class: comment, retweet, quote, heart, and play.
- Extract media from `.attachments`, `.gallery-video`, and card links.
- Extract retweets from `.retweet-header`.
- Extract quoted tweets from `.quote`.
5. Stop pagination:
- Read the next cursor from `div.show-more a[href]`, falling back to any `a[href*="?cursor="]`.
- Stop if there is no next cursor.
- Stop once the newest relevant non-retweet on a page is older than `start`.
- Do not let old retweets alone force an early stop; they can appear out of chronological order.
6. Persist output:
- Sort collected tweets by timestamp.
- Save `tweets` plus `diagnostics` to JSON before downstream work.
- Include `baseUrl`, page count, byte count, tweet count, effective window, earliest/latest timestamps, duration, and partial/error flags.
## Anti-Bot Handling
Detect mirror challenge pages by checking the status code and HTML for strings such as:
- `Verifying your request`
- `Verifying your browser`
- `Making sure you're not a bot`
- `Anubis`
When detected, switch the rest of the run to one persistent headed browser session.
Browser fallback guidance:
- Use real system Chrome when available.
- Use headed mode; headless often fails these challenges.
- Reuse the same browser/page for all later pages.
- Wait for `.timeline-item` or for the challenge marker to disappear.
- Close or release the browser session at the end of the scrape.
Do not relaunch a browser per page. It is slower, noisier, and more likely to trigger blocking.
Do not solve CAPTCHAs or bypass explicit browser safety barriers. Proof-of-work pages that complete automatically in a normal browser are acceptable; interactive CAPTCHA or account-login walls are a blocker.
## Failure Behavior
Scrapers should preserve useful partial work.
- If page 1 fails, throw a clear error because there is no data to save.
- If a later page fails, return collected tweets with `diagnostics.wasPartial = true` and `diagnostics.errorMessage`.
- Save partial scrape JSON and allow downstream summarization or reprocessing.
- Support a resume mode that accepts raw scrape JSON and skips scraping.
- Reject summary JSON as resume input unless it contains raw tweets.
## Debugging Checklist
When a scrape breaks, narrow to the most likely one or two causes before changing code:
- Mirror is down, rate-limiting, or has changed markup.
- Anti-bot challenge was served and HTTP-only fetching parsed the challenge page.
- Cursor extraction failed, causing only the first page to be scraped.
- Timestamp parsing failed due to locale or title format changes.
- Date-stop logic stopped early because retweets or pinned content distorted ordering.
- Selector drift caused zero tweet items or empty text/stats.
- CLI args were misparsed, especially `--json`, extra `--`, `--type`, or shell-expanded globs.
- The requested route is not supported by Nitter-compatible mirrors, especially `/<handle>/following` and `/<handle>/followers`.
Add validation logs before implementing a fix:
- Effective target type, identifier, base URL, start, and end
- Fetched URL, status, byte length, and challenge marker
- Number of timeline items and tweet-body items per page
- Date ranges for all tweets, non-retweets, and kept tweets
- Next cursor presence, without printing the full cursor unless needed
- Partial status and last error
## Output Model
Use a stable tweet shape:
```ts
interface Tweet {
id: string;
url: string;
user: {
username: string;
displayName: string;
profileUrl?: string;
avatarUrl?: string;
};
timestamp: string;
timestampMs: number;
text: string;
attachments: Array<{
type: 'image' | 'video' | 'card' | 'unknown';
previewUrl: string;
fullUrl?: string;
alt?: string;
}>;
stats: {
replies: number;
retweets: number;
quotes: number;
likes: number;
plays?: number;
};
retweetedBy?: string;
quoted?: unknown;
isThread?: boolean;
}
```
When producing summaries, rewrite Nitter mirror links to `x.com` or `twitter.com`, but keep raw scrape JSON faithful to the source URL and base mirror used.
## Implementation Notes
- Keep scrape-only behavior independent from LLM summarization credentials.
- Add mock HTML mode for repeatable tests using `page1.html`, `page2.html`, and similar fixtures.
- Put a `maxPages` safety cap on every live scrape.
- Add a character budget only as a downstream or pipeline stop condition; do not silently discard raw scraped tweets unless the user requested it.
- Be polite with delays and retry cadence. Public mirrors are fragile community infrastructure.
- Check target site terms and local policy before scraping at scale.
## Current Mirror Notes
As of 2026-05-27, checks against public mirrors showed:
- `https://xcancel.com/swyx` can render a profile timeline after the browser completes proof-of-work.
- `https://xcancel.com/swyx/following` and `https://xcancel.com/swyx/followers` render `Page not found` after proof-of-work.
- HTTP-only fetches to xcancel commonly return `503` verification pages.
- Some Nitter mirrors return empty bodies or Anubis/proof-of-work pages.
Treat these observations as drift-prone diagnostics, not permanent guarantees. Recheck live mirror behavior before relying on a route.