Alfalfa

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.

Updated September 2026.

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

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