Download a Facebook page's posts, and work out which ones worked
Two jobs get conflated here. One is archiving — you want a copy of what a page published, because Facebook is not an archive and posts get edited and deleted. The other is analysis — you want to know which posts earned attention and why. Both start from the same export, and both are cheaper than people expect if you set them up properly.
What you actually get per post
One row per post, with the parts that matter for either job:
{
"postId": "1619357056226331",
"pageName": "NASA",
"time": "2026-09-02T04:52:09.000Z",
"text": "Red Moon over Louisiana\n\nOn the night of Aug. 27-28…",
"likes": 2275,
"comments": 86,
"shares": 169,
"isVideo": false,
"link": null,
"media": [ { "__typename": "Photo", "ocrText": "May be an image of the moon" } ],
"topComments": [ { "text": "Beautiful!", "likes": 12, "author": { "name": "…" } } ],
"url": "https://www.facebook.com/NASA/posts/pfbid02UWQesy…"
}
Two fields here are more useful than they look. ocrText is Facebook's own description of the image, which means you can analyse image posts as text without running anything over the pictures yourself. And topComments gives you the tone of the response, not just the volume — a post with 400 angry comments and one with 400 delighted ones look identical in the counts.
Archiving a page without paying for the same posts twice
This is the mistake that makes people abandon the idea. A naive daily job re-reads the whole timeline every morning, so you pay for a thousand posts to discover the three that are new.
Use a date filter instead. Set onlyPostsNewerThan to the last time you ran, and the run stops as soon as it reaches posts older than that, rather than reading to the end and discarding them:
{
"startUrls": [{ "url": "https://www.facebook.com/NASA" }],
"onlyPostsNewerThan": "2026-09-07",
"resultsLimit": 200
}
The filter costs nothing extra. That is worth saying plainly, because it is not how this is usually sold — Apify's own posts scraper charges a per-post surcharge for running with a date filter on, which is a charge for doing less work.
Finding out what actually works
Once you have a few hundred posts, the interesting question is which formats earn attention. Raw likes are the weakest signal — they cost a viewer nothing. A share costs reputation, and a comment costs effort, so weight them accordingly.
This groups a page's posts by format and ranks them:
import json, sys
from collections import defaultdict
posts = json.load(open(sys.argv[1]))
def kind(p):
if p.get("isVideo"): return "video"
if p.get("media"): return "photo"
if p.get("link"): return "link"
return "text"
def score(p):
# shares are the scarcest signal, comments next, likes cheapest
return (p.get("likes") or 0) + 3 * (p.get("comments") or 0) + 5 * (p.get("shares") or 0)
by_kind = defaultdict(list)
for p in posts:
by_kind[kind(p)].append(score(p))
print(f"{len(posts)} posts\n")
print("what format earns most")
for k, v in sorted(by_kind.items(), key=lambda kv: -sum(kv[1]) / len(kv[1])):
print(f" {k:<6} n={len(v):<4} median {sorted(v)[len(v)//2]:>7,} mean {sum(v)//len(v):>7,}")
print("\ntop posts")
for p in sorted(posts, key=score, reverse=True)[:5]:
text = " ".join((p.get("text") or "").split())[:58]
print(f" {score(p):>8,} {p.get('time','')[:10]} {kind(p):<6} {text}")
Output looks like this:
what format earns most
video n=2 median 670 mean 465
photo n=2 median 155 mean 130
text n=1 median 26 mean 26
link n=1 median 23 mean 23
Read the median, not the mean. One viral post drags an average up and tells you nothing about what happens on a normal Tuesday. If a page has twelve video posts and one of them did fifty times the rest, the mean says "make videos" and the median says "that one video was a fluke".
The pattern above — video first, plain links last — holds on most pages, and the reason is not mysterious: Facebook does not like sending people away. It is still worth measuring on the specific page you care about rather than taking it as given.
What people use this for
- Competitor monitoring. What a rival posts, how often, and which of it lands. Cheaper and more honest than any dashboard that reports on your own account only.
- Content archives. Keeping your own copy before posts are edited or deleted. Facebook is not an archive and does not pretend to be.
- Campaign records. Evidence of what ran and when, which is the sort of thing that matters exactly once, urgently, a year later.
- Feeding a model. A page's whole posting history is a compact, well-labelled corpus, and
ocrTextmeans the image posts come with captions already attached.
Getting it into a spreadsheet
Every run stores results as a dataset you can download as CSV or Excel from its Storage tab, or fetch through the API:
curl "https://api.apify.com/v2/datasets/$DATASET_ID/items?format=csv&fields=time,text,likes,comments,shares,url&token=$APIFY_TOKEN" -o posts.csv
The fields parameter is worth using. The full record has nested media and comment objects that flatten into a very wide sheet, and you rarely want all of it.
Questions people ask
Do I need a Facebook account?
No. These are posts a page shows to anyone, signed in or not.
How far back can I go?
As far as the page keeps publicly visible. Use onlyPostsOlderThan together with a limit to walk backwards through an archive in chunks.
Can I get posts from a personal profile or a group?
Public profiles, yes. Groups need a login and are out of scope.
What does it cost?
$2 per 1,000 posts, no start fee, no surcharge for filtering, platform usage and residential proxies included. You are billed only for posts that reach your dataset.
Try the Facebook Posts Scraper Or go deeper into the comments