需要自备已授权的 X/Twitter 账号(auth_token 与 ct0),否则无法抓取;默认使用 curl-cffi 模拟浏览器 TLS 指纹,出问题可设 TWS_HTTP_BACKEND=httpx 切回 httpx;提前跳出异步生成器时需用 contextlib.aclosing 关闭,否则账号锁不会及时释放
twscrape
vladkensMIT活跃维护用 Python 库和命令行抓取 X/Twitter 推文,支持多账号轮换和内置限流处理,解决大规模采集时账号被封或触发频率限制的问题。
Python library and CLI for X/Twitter scraping with multi-account rotation and built-in rate-limit handling.
用账号池异步抓取 X/Twitter 搜索与 GraphQL 数据的 Python 库和 CLI
用 Python 库和命令行抓取 X/Twitter 推文,支持多账号轮换和内置限流处理,解决大规模采集时账号被封或触发频率限制的问题。
核心能力
- 多账号轮换,遇到限流自动切换账号
- 会话持久化到 SQLite,支持按账号配置代理
- 提供 async/await API,可并发运行多个抓取任务
- 返回 SNScrape 兼容模型或原始 API 响应
- 支持 Cookie 导入账号,可选 IMAP 自动取邮箱验证码
容易踩的坑
- v0.21.0最新正式版2026-11-06 · 2026-11-06 18:57 UTC
Breaking Changes
- Failed requests and unparsable users now raise
AbortReqError,ApiError,XClIdParseErrororUserParseErrorinstead of returningNoneor ending iteration early;Noneand empty results now mean X has no data (#347, by @Flaburgan) tweet_details,user_about,community_infoandparse_trendnow raiseParseErrorwhen the requested object can't be parsed, instead of returningNone;UserParseErroris now its subclass, anduser_aboutreturnsNonefor an unavailable user instead of an empty object (#348, #364)- Made curl-cffi a required dependency and the default HTTP backend, and removed the
twscrape[curl]extra; setTWS_HTTP_BACKEND=httpxto switch back (#363) - With the default backend,
Response.urlis astr,Response.headersandResponse.requestare curl-cffi objects,Response.requestcan beNone, andAccount.user_agentis not sent (#363)
Features
- Added
tweet.articlefor X Articles, with the full article content intweet_details(#323, by @softrebel) - Added parsing for message me, Periscope broadcast, live event, app, promo video and promo image conversation cards, and polls with video (#333, #335, by @Flaburgan)
- Added
reactivateCLI command andAccountsPool.reactivateto mark accounts with a session active again (#334, by @Flaburgan) - Added
retweetedTweetIdandquotedTweetIdwith their string versions toTweet, set even when the referenced tweet is unavailable (#361, by @Flaburgan) - Added media metadata to photos, videos and gifs: IDs, alt text, size, short and expanded URLs, and the source tweet and user (#370, by @Flaburgan)
Fixes
- Fixed quoted tweets appearing as standalone timeline items (#338, by @Matthew-Selvam)
- Fixed items yielded twice when they appear on several pages of a paginated call (#332, by @Flaburgan)
- Fixed tweets being dropped or user profiles being overwritten when X sends an author without a full profile; such tweets now get a placeholder
tweet.user(#344, #345, #352, #353, by @Flaburgan) - Fixed requests failing when X requires GraphQL features twscrape doesn't send yet; they are now enabled and the request is retried (#339, by @niyamvora)
- Fixed
user_by_idbeing blocked by Cloudflare, by using curl-cffi by default (#337, #363) - Fixed requests stopping instead of moving to the next account on a 429 or a rate limit error without rate limit headers (#351, #364)
- Fixed a suspended account stopping the whole request; it's now marked inactive and the next account is used (#211, #364)
- Fixed
trendsfailing for the news, trending and entertainment tabs (#371, by @Many0therFunctions) - Fixed the curl-cffi backend sending auth cookies to other hosts, resolving
socks5://proxy hosts locally, and retrying timeouts (#363)
Improvements
- Retried X timeouts (
DeadlineExceeded) a few times before raising (#347, by @Flaburgan) - Reported Cloudflare challenge pages with a clear
XClIdParseError(#338, by @Matthew-Selvam) - Exited with code 1 without a traceback on failed CLI requests
Maintenance
- Updated GraphQL operation IDs for current X API compatibility
Full Changelog: https://github.com/vladkens/twscrape/compare/v0.20.1...v0.21.0
- Failed requests and unparsable users now raise
- v0.20.12026-08-25 · 2026-08-25 16:21 UTC
查看更新说明
Features
- Restored user lookup by numeric ID through the API and CLI (#326, #329, by @Flaburgan)
Fixes
- Fixed X client transaction ID generation for builds using 16-character chunk hashes (#327, #328, by @Flaburgan)
Maintenance
- Updated GraphQL operation IDs for current X API compatibility
Full Changelog: https://github.com/vladkens/twscrape/compare/v0.20.0...v0.20.1
- v0.20.02026-08-07 · 2026-08-07 00:29 UTC
查看更新说明
Features
- Added support for refreshing existing account sessions with
add_cookie - Added parsing for image-based poll cards
Fixes
- Fixed parsing of current user profile fields
- Fixed cookie-based account status reporting
- Fixed account username queries for usernames containing special characters
- Fixed pagination loops caused by repeated cursors or pages
- Fixed tweet author parsing when responses omit the users map
- Fixed load-shedding retries to avoid unnecessary account rotation
- Fixed API error throttling dropping response data
- Limited network error retries per account before applying backoff and rotating accounts (#325, by @Flaburgan)
- Fixed X client transaction ID cookies being sent outside
x.com - Replaced process termination with
GqlFeaturesOutdatedErrorwhen GraphQL features become outdated (#324, by @Flaburgan)
Maintenance
- Updated GraphQL operation IDs for current X API compatibility
Full Changelog: https://github.com/vladkens/twscrape/compare/v0.19.2...v0.20.0
- Added support for refreshing existing account sessions with
- v0.19.22026-07-21 · 2026-07-21 21:19 UTC
查看更新说明
Features
- Added
wait_timeoutandwait_intervaloptions to bound and tune account wait polling (#317, by @Flaburgan)
Fixes
- Fixed X client transaction ID generation to use each account's effective proxy (#319, by @kn1l)
- Fixed X client transaction ID asset requests to use account cookies (#321, by @jonsuh)
- Fixed X client transaction ID failures incorrectly disabling valid accounts
Maintenance
- Updated GraphQL operation IDs for current X API compatibility
Full Changelog: https://github.com/vladkens/twscrape/compare/v0.19.1...v0.19.2
- Added
- v0.19.12026-06-26 · 2026-06-26 14:54 UTC
查看更新说明
Maintenance
- Updated GraphQL operation IDs for current X API compatibility
Full Changelog: https://github.com/vladkens/twscrape/compare/v0.19.0...v0.19.1
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
twscrape
twscrape is an async Python library and CLI for X/Twitter Search and GraphQL endpoints. It runs on your own account pool, keeps sessions in SQLite, rotates accounts when an endpoint is rate-limited, and returns either parsed SNScrape-style models or raw API responses.
Install
pip install twscrape
twscrape uses curl-cffi by default, with a browser-like TLS fingerprint that Cloudflare requires on some operations, for example user_by_id. If something breaks, TWS_HTTP_BACKEND=httpx switches back to httpx.
Features
- Search and GraphQL X/Twitter API methods
- Async/await API for running multiple scrapers concurrently
- Login flow with optional email verification code retrieval
- Cookie-based account setup
- Saved account sessions and per-account proxies
- Raw Twitter API responses and parsed SNScrape-compatible models
- Automatic account switching across rate-limited operations
Sponsor
Start With Cookies
twscrape requires authorized X/Twitter accounts. The most stable setup is to add an account from browser cookies containing auth_token and ct0. The recommended way to export them from your current browser profile is unjar:
unjar x.com -f header | twscrape add_cookie my_account
twscrape accounts
twscrape search "from:xdevelopers lang:en" --limit=20
my_account is a local identifier; twscrape does not verify that it matches the X username stored in the cookies. Run the same command again to replace its saved session while preserving credentials, statistics, locks, and proxy settings.
Alternatively, let the CLI prompt securely for cookies copied from x.com -> DevTools (F12) -> Application -> Cookies:
twscrape add_cookie my_account
Cookie accounts that include auth_token and ct0 are activated immediately; no login_accounts step is needed.
Ready-to-use cookie accounts are available from this provider. Proxy users can bring their own proxies or use Proxy-Seller. These are referral links.
X/Twitter's Terms of Service discourage using multiple accounts. Use this project responsibly and at your own discretion.
Python API
import asyncio
from twscrape import API, gather
async def main():
api = API() # or API("accounts.db")
# Add once; the session is stored in the account database.
await api.pool.add_account_cookies("my_account", "auth_token=xxx; ct0=yyy")
user = await api.user_by_login("xdevelopers")
print(user.id, user.username, user.followersCount)
tweets = await gather(api.search("from:xdevelopers lang:en", limit=20))
for tweet in tweets:
print(tweet.id, tweet.user.username, tweet.rawContent)
if __name__ == "__main__":
asyncio.run(main())
gather() is a convenience helper. You can stream results directly:
async for tweet in api.search("open source lang:en", limit=100):
print(tweet.id, tweet.rawContent)
Configure what happens when no account is immediately available:
api = API(raise_when_no_account=True, wait_timeout=30, wait_interval=1)
wait_timeout limits how long to wait for a locked account, wait_interval controls how often the pool checks again, and raise_when_no_account raises NoAccountError instead of ending the operation. By default, twscrape waits indefinitely while active accounts are locked.
X sometimes sends a tweet without its author's profile. The tweet is still returned, with a placeholder tweet.user (the same goes for retweeted and quoted tweets): only id is known, plus username and sometimes displayname when the referencing tweet names the author. Its counts are 0, created is the Unix epoch and profileImageUrl is empty, which never happens for a real profile, so check tweet.user.profileImageUrl == "" before storing user stats.
Search defaults to the Latest tab. Pass kv={"product": "Top"} or kv={"product": "Media"} to use another search product:
tweets = await gather(api.search("python", limit=20, kv={"product": "Top"}))
Every parsed method has a _raw version for the original response wrapper:
async for rep in api.search_raw("from:xdevelopers", limit=20):
print(rep.status_code, rep.json())
When breaking out of an async generator early, close it with contextlib.aclosing so the account lock is released promptly:
from contextlib import aclosing
async with aclosing(api.search("elon musk")) as gen:
async for tweet in gen:
if tweet.id < 200:
break
API Surface
Search:
await gather(api.search("elon musk", limit=20)) # list[Tweet]
await gather(api.search("elon musk", limit=20, kv={"product": "Top"})) # Top tab
await gather(api.search_user("openai", limit=20)) # list[User]
await gather(api.search_trend("python", limit=20)) # list[Trend]
Tweets:
tweet_id = 20
await api.tweet_details(tweet_id) # Tweet
await gather(api.tweet_replies(tweet_id, limit=20)) # list[Tweet]
await gather(api.tweet_thread(tweet_id, limit=20)) # list[Tweet]
await gather(api.retweeters(tweet_id, limit=20)) # list[User]
await gather(api.bookmarks(limit=20)) # list[Tweet]
Users and timelines:
user_login = "xdevelopers"
user_id = 2244994945
await api.user_by_id(user_id) # User
await api.user_by_login(user_login) # User
await api.user_about(user_login) # AccountAbout
await gather(api.following(user_id, limit=20)) # list[User]
await gather(api.followers(user_id, limit=20)) # list[User]
await gather(api.verified_followers(user_id, limit=20)) # list[User]
await gather(api.subscriptions(user_id, limit=20)) # list[User]
await gather(api.user_tweets(user_id, limit=20)) # list[Tweet]
await gather(api.user_tweets_and_replies(user_id, limit=20)) # list[Tweet]
await gather(api.user_media(user_id, limit=20)) # list[Tweet]
Lists:
list_id = 123456789
await gather(api.list_timeline(list_id, limit=20)) # list[Tweet]
await gather(api.list_members(list_id, limit=20)) # list[User]
Communities:
community_id = 1501272736215322629
await api.community_info(community_id) # Community
await gather(api.community_members(community_id, limit=20)) # list[User]
await gather(api.community_moderators(community_id, limit=20)) # list[User]
await gather(api.community_tweets(community_id, limit=20)) # list[Tweet]
Trends:
await gather(api.trends("news")) # list[Trend]
await gather(api.trends("sport")) # list[Trend]
await gather(api.trends("entertainment")) # list[Trend]
await gather(api.trends("VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA=")) # list[Trend]
Parsed Tweet, User, Community, and trend objects can be converted with .dict() or .json().
Errors
A failed request raises instead of looking like an empty result:
AbortReqErrorwhen twscrape gives up on a request, for example on an HTML block page, includingGqlFeaturesOutdatedErrorwhen X requires GraphQL features twscrape can't enableApiError, its subclass, when X answers with errors and no data; a busy or timed-out X (LoadShed,DeadlineExceeded) is retried a few times firstXClIdParseErrorwhen the X client transaction ID can't be generatedParseErrorwhentweet_details(),user_about()orcommunity_info()gets the requested object but twscrape can't parse it; its subclassUserParseErrorforuser_by_id()anduser_by_login()
A paginated method raises on the page that failed, after yielding the earlier pages, and skips single items it can't parse. None from tweet_details(), user_by_id(), user_by_login(), user_about() or community_info() means X has no such object (or no account was available and raise_when_no_account is off). In the CLI, a failed request exits with code 1 without a traceback.
CLI
twscrape
twscrape search --help
Commands:
twscrape search "QUERY" --limit=20
twscrape tweet_details TWEET_ID
twscrape tweet_replies TWEET_ID --limit=20
twscrape tweet_thread TWEET_ID --limit=20
twscrape retweeters TWEET_ID --limit=20
twscrape user_by_id USER_ID
twscrape user_by_login USERNAME
twscrape user_about USERNAME
twscrape user_media USER_ID --limit=20
twscrape following USER_ID --limit=20
twscrape followers USER_ID --limit=20
twscrape verified_followers USER_ID --limit=20
twscrape subscriptions USER_ID --limit=20
twscrape user_tweets USER_ID --limit=20
twscrape user_tweets_and_replies USER_ID --limit=20
twscrape list_timeline LIST_ID --limit=20
twscrape list_members LIST_ID --limit=20
twscrape community_info COMMUNITY_ID
twscrape community_members COMMUNITY_ID --limit=20
twscrape community_moderators COMMUNITY_ID --limit=20
twscrape community_tweets COMMUNITY_ID --limit=20
twscrape trends sport
CLI output is JSON Lines: one document per line.
twscrape search "elon musk lang:es" --limit=20 > tweets.jsonl
twscrape search "elon musk lang:es" --limit=20 --raw
Use a separate account database when you need isolated account pools:
twscrape --db research.db search "python lang:en" --limit=100
Accounts
Add username/password accounts from a file:
twscrape add_accounts ./accounts.txt username:password:email:email_password
twscrape login_accounts
twscrape login_accounts starts the login flow for each inactive account. If X asks for email verification and email_password is available, twscrape tries to read the code through IMAP and saves the resulting cookies for later use.
line_format describes how each line is split. Supported tokens:
username- requiredpassword- requiredemail- requiredemail_password- used to fetch email verification codes through IMAPcookies- cookie string, JSON, base64, or another format accepted by the parser_- skip column
Example account file:
username:password:email:email password:user_agent:cookies
Command:
twscrape add_accounts ./accounts.txt username:password:email:email_password:_:cookies
If IMAP is unavailable, enter verification codes manually:
twscrape login_accounts --manual
twscrape relogin user1 user2 --manual
twscrape relogin_failed --manual
Inspect and maintain the pool:
twscrape accounts
twscrape stats
twscrape relogin user1 user2
twscrape relogin_failed
twscrape reactivate user1 user2
twscrape reset_locks
twscrape delete_inactive
twscrape del_accounts user1 user2
twscrape accounts prints the current account state:
username logged_in active last_used total_req error_msg
user1 True True 2023-05-20 03:20:40 100 None
user2 True True 2023-05-20 03:25:45 120 None
user3 False False None 120 Login error
Limits
limit is the target number of parsed objects, not a page size. X/Twitter controls page size per endpoint, so a call can return fewer or more objects than requested.
Rate limits are tracked per account and per endpoint. When an account is limited for one operation, twscrape locks it for that operation until the reset time and tries another active account.
user_tweets and user_tweets_and_replies are limited by X/Twitter to about 3200 tweets.
Proxy
Use a proxy per account, per API instance, or for CLI commands:
await api.pool.add_account(
"user1",
"pass1",
"[email protected]",
"email_pass1",
proxy="http://login:[email protected]:8080",
)
api = API(proxy="http://login:[email protected]:8080")
doc = await api.user_by_login("xdevelopers")
TWS_PROXY=socks5://user:[email protected]:1080 twscrape user_by_login xdevelopers
Proxy priority:
api.proxyTWS_PROXY- account proxy
Do not set api.proxy or TWS_PROXY when you want per-account proxies to be used.
Environment
TWS_PROXY- global proxy for all accountsTWS_WAIT_EMAIL_CODE- email verification timeout in seconds, default30TWS_RAISE_WHEN_NO_ACCOUNT- raiseNoAccountErrorinstead of waiting; acceptsfalse,0,true,1TWS_HTTP_BACKEND-curl(default) orhttpxTWS_LOG_LEVEL- logger level, defaultINFOTWS_TELEMETRY=0- disable anonymous telemetry
Telemetry
twscrape collects anonymous, aggregated telemetry about used GraphQL operation names and the selected HTTP backend. It does not collect usernames, cookies, proxies, queries, request URLs, or response bodies. Disable it with TWS_TELEMETRY=0.
See Also
- twitter-advanced-search - guide on search filters
- TweeterPy - another X client
- twitter-api-client - implementation of Twitter's v1, v2, and GraphQL APIs
Contributing
Pull requests are welcome, including ones made with agents: point yours at agents.md.