Extract public data from Facebook post, reel, video, photo, story and profile URLs. No login, no API key, no browser.
Get the likes, comments, shares, views, text, images, video URL and author of any public Facebook post or reel β and the name, bio, followers, category and profile picture of any public profile or page β with one function call.
A lightweight alternative to the Facebook Graph API when all you need is public data: no app review, no access token, no rate-limit dashboard.
pip install facebook-posts-detailsThat is all you need β pip pulls the dependencies automatically.
import facebook_posts_details as fb
post = fb.post_data("https://www.facebook.com/reel/941309212326403")
post["likes_count"] # 104378
post["comments_count"] # 292
post["post_text"] # 'ππβ¦..'
post["video_url"] # 'https://video-del3-1.xx.fbcdn.net/...'profile = fb.profile_data("https://www.facebook.com/zuck")
profile["name"] # 'Mark Zuckerberg'
profile["followers"] # 121000000
profile["bio"] # 'Bringing the world closer together.'Let the URL pick the right one for you:
data = fb.scrape(url)| Call | Returns |
|---|---|
fb.post_data(url) |
a post, reel, video, photo or story |
fb.profile_data(url) |
a profile or page |
fb.profile_about(url) |
every "About" tab, merged into one dict |
fb.scrape(url) |
whichever of the above fits the URL |
All of them return None if the page could not be read, so one bad URL never
stops a batch:
for url in urls:
data = fb.scrape(url)
if data is None:
continue
...url = ("https://www.facebook.com/middlestumpcric/posts/"
"pfbid02UgzMPp1nyS1gzQt9RNBG1AaiqJ1eZWAVSbt5rHkTw7jod5JLNCAD8USh1wkxVYGPl")
post = fb.post_data(url, with_profile=True)
post["user"]["name"] # 'Middle Stump Cricket'
post["user"]["followers"] # 1400000Costs one extra request. Without the flag, post["user"] is {}.
Not every author publishes every field β many personal profiles hide their
follower count, in which case post["user"]["followers"] is None while
post["user"]["name"] still works.
Every dict has a fixed set of keys. A field that could not be found is
None (or an empty list/dict), never missing β so post["likes_count"] never
raises a KeyError.
The examples below are illustrative and come from different posts: no single
post has every field. views_count appears on some videos and reels,
transcript only on videos that have one, video_url only where there is a
video.
| Key | Example |
|---|---|
url |
'https://www.facebook.com/reel/941309212326403' |
post_id |
'941309212326403' |
post_type |
'reel' β one of post, reel, video, photo, story |
post_text |
'ππβ¦..' |
publish_date |
1786517734 (unix seconds) |
publish_date_iso |
'2026-08-12T06:55:34+00:00' |
likes_count |
104378 |
comments_count |
292 |
share_count |
1000 |
views_count |
9400000 |
post_images |
[{"url": "https://...", "caption": "May be an image of ..."}] |
video_url |
'https://video-del3-1.xx.fbcdn.net/...' |
transcript |
'What does Rahul Gandhi do every...' |
name |
'Sucha Shooter' |
profile_url |
'https://www.facebook.com/sucha.shooter.965' |
profile_image |
'https://...' |
user |
{}, or the author's profile with with_profile=True |
| Key | Example |
|---|---|
url |
'https://www.facebook.com/zuck' |
profile_id |
'zuck' (username, or the numeric id) |
profile_id_number |
'4' |
name |
'Mark Zuckerberg' |
bio |
'Bringing the world closer together.' |
category_name |
'Public figure' |
gender |
'MALE' |
is_verified |
True |
followers |
121000000 |
followers_text |
'121M followers' (as displayed) |
following |
None |
following_text |
None |
profile_pic |
'https://...' |
cover_photo |
'https://...' |
about_section |
{"intro": {"website": [{"value": "x.com", "url": "https://..."}]}} |
photos |
['https://...', ...] |
Good to know:
- Counts are always
intorNoneβ never a string like"1.4M". The displayed form is kept infollowers_text/following_text. - Reels only publish a rounded share count, so
share_counton a reel is rounded the same way the page shows it (1000for "1K"). Likes and comments are exact. post_imagesentries are always{"url", "caption"}.captionisNonewhen Facebook has no real description for the image.
facebook-posts-details <url> # prints JSON
facebook-posts-details <url1> <url2> ... # several at once
facebook-posts-details <url> --summary # short readable output
facebook-posts-details <url> --with-profile # include the author's profile
facebook-posts-details <url> --proxy http://user:pass@host:port
facebook-posts-details <url> --kind profile # skip URL auto-detection
facebook-posts-details <url> --timeout 60 --retries 3
facebook-posts-details <url> -v # show progress on stderrJSON goes to stdout and messages go to stderr, so this works cleanly:
facebook-posts-details <url> | jq .likes_countExit status is 1 if any URL returned nothing.
No proxy is used unless you pass one. Proxies work the same way as in
requests, and are accepted by post_data, profile_data, profile_about,
scrape and the CLI (--proxy).
# one URL, used for both http and https
fb.post_data(url, proxy="http://user:pass@1.2.3.4:8080")
fb.post_data(url, proxy="socks5://1.2.3.4:1080")
# a scheme-less value is treated as http
fb.post_data(url, proxy="1.2.3.4:8080")
# or a requests-style mapping
fb.post_data(url, proxy={"http": "http://1.2.3.4:8080",
"https": "http://1.2.3.4:8080"})The normal flow β nothing to configure.
import facebook_posts_details as fb
url = "https://www.facebook.com/reel/941309212326403"
post = fb.post_data(url)
if post is None:
print("could not read that page")
else:
print(post["name"], "-", post["post_text"])
print(post["likes_count"], "likes,", post["comments_count"], "comments")
for image in post["post_images"]:
print(image["url"])Pass proxy= to the same call. Everything else is identical.
import facebook_posts_details as fb
PROXY = "http://user:pass@1.2.3.4:8080"
url = "https://www.facebook.com/reel/941309212326403"
post = fb.post_data(url, proxy=PROXY)
print(post["likes_count"], post["comments_count"])Rotation is up to you β just pass a different proxy per call.
import itertools
import facebook_posts_details as fb
PROXIES = [
"http://user:pass@1.2.3.4:8080",
"http://user:pass@5.6.7.8:8080",
]
URLS = [
"https://www.facebook.com/reel/941309212326403",
"https://www.facebook.com/zuck",
]
pool = itertools.cycle(PROXIES)
for url in URLS:
data = fb.scrape(url, proxy=next(pool))
if data is None:
print("failed:", url)
continue
print(data.get("name"), data.get("likes_count") or data.get("followers"))To run the same loop without proxies, drop the argument:
data = fb.scrape(url)import json
import facebook_posts_details as fb
url = ("https://www.facebook.com/middlestumpcric/posts/"
"pfbid02UgzMPp1nyS1gzQt9RNBG1AaiqJ1eZWAVSbt5rHkTw7jod5JLNCAD8USh1wkxVYGPl")
post = fb.post_data(
url,
with_profile=True, # also fetch the author's profile
proxy=None, # or "http://user:pass@host:port"
timeout=60,
retries=3,
)
with open("post.json", "w", encoding="utf-8") as f:
json.dump(post, f, indent=2, ensure_ascii=False)
print(post["name"], "->", post["user"]["followers"], "followers")
# Middle Stump Cricket -> 1400000 followers# without
facebook-posts-details "https://www.facebook.com/reel/941309212326403" --summary
# with
facebook-posts-details "https://www.facebook.com/reel/941309212326403" \
--proxy http://user:pass@1.2.3.4:8080 --summaryfb.post_data(url, timeout=60, retries=3)Or change the defaults for every call:
fb.config.DEFAULT_TIMEOUT = 60 # default 30 seconds
fb.config.DEFAULT_RETRIES = 3 # default 2 attemptsNothing is printed to stdout. Failures are logged and the call returns None.
To see why something failed:
import logging
logging.basicConfig(level=logging.DEBUG)Exceptions are only raised for programming mistakes, e.g. fb.InvalidURL if you
pass a kind that does not exist.
- Python 3.8 or newer
tls-client3β installed automatically bypip
Reads only publicly accessible pages, exactly as a logged-out browser would. You are responsible for complying with Facebook's Terms of Service and any law that applies to you. Provided as-is under the MIT licence.