~ / guides / How to Scrape Facebook: A Complete Guide

How to Scrape Facebook: A Complete Guide

NB
Noah Berg
Facebook data engineer · about the author
the short version
  • I sent facebook.com/Meta a request with a normal Chrome User-Agent and got HTTP 400 with a 1,542-byte error page. The same request with no User-Agent returned the full 472 KB public page, like count and all.
  • Logged-out public Pages carry real data in og: meta tags and an embedded __bbox JSON blob. I pulled 106,675,535 likes off Meta's Page from the markup with no login in June 2026.
  • The official Graph API returns clean JSON but caps you at 200 × users calls per hour and exposes only assets you manage. Meta deprecated the Groups API in April 2024, so there is no API route to group content at all.
  • For pages, groups, posts, comments, Marketplace, and emails at volume, a scraper API that takes a URL and returns parsed JSON removes the proxy, header-guessing, and markup-churn work.

I tried to scrape Facebook the obvious way first: one requests.get against facebook.com/Meta with a normal Chrome User-Agent. It came back HTTP 400 with a 1,542-byte error page before I parsed anything. Then I removed the User-Agent entirely and the same URL returned HTTP 200 and 472 KB of real public markup. That backwards result is the right place to start a guide on how to scrape data from Facebook, because Facebook’s defenses are not the ones most tutorials assume.

Below is exactly what I ran in June 2026, what Facebook returned, and the practical route for each surface: Pages, posts, groups, comments, Marketplace, and emails.

How do you scrape data from Facebook? The three routes

There are three ways to get data off Facebook, and picking the wrong one is where most projects stall. Here is how they compare before I get into code.

RouteAuthWhat it returnsGood forMain limit
Logged-out HTML scrapeNonePublic Page/post markup, og: tags, embedded JSONPublic Pages, posts, profilesIP blocks, markup churn
Official Graph APIApp token + reviewClean JSON for assets you manageYour Pages, your ads200 × users/hour, no public groups
Scraper APIAPI keyParsed JSON from any public URLGroups, Marketplace, comments, emails at scalePer-request cost

The Graph API gives the cleanest data, and it covers only assets you own or manage. The logged-out HTML route reaches public data the API will not hand you, and it carries the blocking problem I hit above. A scraper API takes a URL and returns parsed JSON, which is what most teams reach for once they need groups, Marketplace, or volume. I will walk all three, starting with the legal line, because that decides which surfaces are safe to touch.

Scraping public, logged-out Facebook data is broadly defensible in the US, and scraping while logged in is restricted by contract. Those are two separate situations and they get conflated constantly.

On January 23, 2024, US District Judge Edward Chen ruled in Meta Platforms v. Bright Data that Meta’s terms do not bar logged-off scraping of public data, because those terms bind a user who is actively logged into an account. His order states plainly that “the Facebook and Instagram Terms do not bar logged-off scraping of public data” (court docket, Eric Goldman’s analysis). Meta dropped the case the following month.

The flip side is Meta’s Automated Data Collection Terms, which prohibit automated access or collection while logged in to a Facebook account without prior written permission, and restrict what collected data may be used for. Personal data stays in scope of privacy law such as the GDPR no matter how you obtain it. That distinction has teeth: the Irish Data Protection Commission fined Meta 265 million euros in November 2022 after scraped profile data on 533 million users surfaced online, a penalty that matters a great deal once you start collecting emails or member lists.

I keep the full breakdown in is scraping Facebook legal. The working rule I use: stay logged out, take only public data, and treat anything personal as regulated.

How do you scrape a Facebook Page?

The fastest way to scrape a public Facebook Page is to request its URL logged out and parse the og: meta tags, which carry the Page name, description, like count, and image without any login. This is the route the Bright Data ruling speaks to.

Here is the test that surprised me. I hit the same Page several ways in June 2026:

RequestUser-AgentStatusBody sizeUsable data
GET facebook.com/MetaDesktop Chrome string4001,542 bytesNone (“Error” page)
GET facebook.com/MetaNone200472,532 bytesPage name, likes, JSON blob
GET facebook.com/MetaMozilla/5.0 (compatible; bot)200473,967 bytesSame public data
GET mbasic.facebook.com/MetaDesktop Chrome string4003,676 bytesNone

The lesson: a realistic desktop Chrome User-Agent was the one combination that got rejected, and removing the header returned the full page. Facebook’s edge makes a blocking decision on the whole request shape, so the header that looks most human is not reliably the one that returns data.

When the 200 came back, the public fields were sitting in the markup. This is the parse I ran, and the real values it produced. Note the like count comes back inside a localized description string (Meta’s Page served me Lithuanian text on this run), so I pull the digits with a locale-agnostic regex instead of splitting on a fixed separator:

import requests, re, html

# Logged out. No User-Agent header on purpose: in my June 2026 test,
# adding a desktop Chrome UA returned HTTP 400 here, no UA returned 200.
r = requests.get("https://www.facebook.com/Meta", timeout=20)
print(r.status_code)  # -> 200

def og(prop):
    m = re.search(rf'<meta property="{prop}" content="(.*?)"\s*/?>', r.text)
    return html.unescape(m.group(1)) if m else None

print(og("og:title"))            # -> Meta
desc = og("og:description") or ""
# First grouped number in the description is the like count, in any locale.
m = re.search(r"([\d][\d.,   ]{4,})", desc)
like_count = "".join(re.findall(r"\d", m.group(1))) if m else None
print(like_count)                # -> 106675535
print(bool(og("og:image")))      # -> True
print("__bbox" in r.text)        # -> True

That returned Meta, a like count of 106,675,535, a usable image URL, and confirmed the page embeds a __bbox JSON blob with deeper structured data, all from one logged-out request. Facebook obfuscates and rotates the __bbox keys, so that deeper path breaks often.

This is why the HTML route, while real, is fragile. The markup is minified, the JSON keys change, the like count arrives in whatever language Facebook decides to serve, and the IP gets throttled if you loop it. The production version is to send the same Page URL to a scraper API and get back stable parsed fields:

import os, requests

CHOCO = os.environ["CHOCO_API_KEY"]
resp = requests.get(
    "https://chocodata.com/api/v1/facebook/page",
    params={"url": "https://www.facebook.com/Meta", "api_key": CHOCO},
    timeout=60,
)
page = resp.json()
print(page["name"], page["likes"], page["followers"])

Same target, no header guessing, no locale parsing, no __bbox reverse-engineering. I dig into the Facebook Page Scraper API separately, and you can get an API key to run the call above.

What about Selenium and BeautifulSoup?

Selenium and BeautifulSoup are the pair most older Facebook tutorials reach for, and they still work for surfaces that need a rendered DOM. The split of labor is simple: Selenium drives a real Chrome session to render the JavaScript and scroll the feed, then you hand the page source to BeautifulSoup to extract the fields. I confirmed the boundary in June 2026: when I parsed the logged-out facebook.com/Meta shell with BeautifulSoup, soup.find("meta", property="og:title") returned Meta, but soup.find_all("span") returned zero span tags, because the public shell ships its content inside the __bbox JSON and leaves the server-rendered markup nearly empty. To reach the span-level post text the way those guides do, you need Selenium to render the page first.

A minimal Selenium scaffold for Facebook Pages looks like this. It logs in with an email and password, scrolls to load posts, and collects them into an all_posts list before BeautifulSoup parses each card:

from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import time

class FacebookPagesScraper:
    def __init__(self, email, password):
        self.driver = webdriver.Chrome()
        self.driver.get("https://www.facebook.com/login")
        self.driver.find_element(By.ID, "email").send_keys(email)
        self.driver.find_element(By.ID, "pass").send_keys(password)
        self.driver.find_element(By.NAME, "login").click()
        time.sleep(5)

    def scrape(self, page_url, scrolls=5):
        self.driver.get(page_url)
        all_posts = []
        for _ in range(scrolls):
            self.driver.execute_script(
                "window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(3)  # let lazy-loaded posts render
        soup = BeautifulSoup(self.driver.page_source, "html.parser")
        for card in soup.find_all("div", {"role": "article"}):
            text = card.get_text(" ", strip=True)
            if text:
                all_posts.append(text)
        return all_posts

The reason I do not lean on this in production is maintenance. A logged-in Selenium session against Facebook Pages is exactly what Meta’s Automated Data Collection Terms restrict, the By.ID and div[role=article] selectors break whenever Facebook reships its markup, and a single Chrome instance does not rotate IPs, so the account and the datacenter address both get flagged fast. Hosted actors like Apify wrap this same Selenium pattern, and a scraper API skips the browser entirely. The pure-Python variants, including requests-only parsing and other languages, sit in how to scrape Facebook with Python.

How do you scrape Facebook posts and comments?

Public posts and comments come back from the post URL the same way Page data does, and the per-comment fields sit buried in the embedded JSON, so this is where a parser earns its keep. The naive route gives you the post body and counts. The threaded comment tree is the hard part by hand.

For your own Page’s posts, the Graph API is the precise tool. A token with the right Page permissions returns posts and comment edges as JSON:

import requests

# Page access token for a Page you manage. v23.0 was current in June 2026;
# Meta deprecates older versions on a rolling schedule (v20.0 retires Sep 24, 2026).
TOKEN = "EAAB..."
PAGE_ID = "20531316728"  # the Facebook (Meta) Page id
r = requests.get(
    f"https://graph.facebook.com/v23.0/{PAGE_ID}/posts",
    params={"fields": "message,created_time,comments.limit(5){message,from}",
            "access_token": TOKEN},
    timeout=30,
)
for post in r.json().get("data", []):
    print(post.get("created_time"), "-", (post.get("message") or "")[:60])

That works only for Pages you administer. For arbitrary public posts and their comments, the Graph API will not help, and hand-parsing the comment JSON is brittle. A scraper API endpoint takes the post URL and returns the comment list already structured:

import os, requests

CHOCO = os.environ["CHOCO_API_KEY"]
resp = requests.get(
    "https://chocodata.com/api/v1/facebook/post",
    params={"url": "https://www.facebook.com/Meta/posts/PFBID...",
            "api_key": CHOCO},
    timeout=60,
)
post = resp.json()
print(post["text"], "-", post["comment_count"], "comments")
for c in post["comments"]:
    print(c["author"], ":", c["text"][:50])

I keep deeper notes in the Facebook Post Scraper API and Facebook Comment Scraper API writeups. The Python-specific patterns, including Selenium for the lazy-loaded comment feed, live in how to scrape Facebook with Python.

How do you scrape Facebook groups and group emails?

Group posts and member details are only collectable from public groups, and emails specifically are only available where a member or Page chose to publish them. There is no Meta API that returns a group’s member list or email roster. Meta deprecated the Facebook Groups API in April 2024, so no group content is retrievable through the official API at all, which is the single most common misconception I see.

Two hard constraints to set expectations:

You wantAvailable via official API?Public-data route exists?
Public group postsNo (Groups API gone since April 2024)Yes (parse the public group URL)
Private group contentNoNo (logged-in only, restricted by terms)
Member email addressesNoOnly where publicly displayed
Page contact emailLimitedYes (public Page “About” section)

The public route does work. A logged-out GET of a public group URL returned HTTP 200 and about 310 KB of markup for me in June 2026, so the feed is there to parse. The catch is that the post list is lazy-loaded and the markup churns, so the workable production route is the group URL through a scraper that handles the scroll and the parsing:

import os, requests

CHOCO = os.environ["CHOCO_API_KEY"]
resp = requests.get(
    "https://chocodata.com/api/v1/facebook/group",
    params={"url": "https://www.facebook.com/groups/PythonProgrammers",
            "api_key": CHOCO},
    timeout=60,
)
for post in resp.json()["posts"]:
    print(post["author"], "-", post["text"][:60])

On the email question, scrape only what a Page or profile has chosen to display publicly, such as a business Page’s contact section. Harvesting hidden personal emails is where you cross into GDPR and US CFAA territory, and the Irish DPC fine above shows the regulators act on it, so I treat displayed business contact details as the only safe target. The Facebook Group Scraper API and Facebook Email Scraper API cover the field shapes, and the legal line sits in is scraping Facebook legal.

How do you scrape Facebook Marketplace and Ads?

Marketplace listings and the Ad Library are two surfaces where the official paths are narrow, so the public-URL route does most of the work. They behave very differently from Pages.

Marketplace has no public API. Listings render behind heavy client-side JavaScript and location gating, so a plain requests call returns almost nothing useful. You either drive a headless browser yourself or send the listing or search URL to a scraper API that runs the browser for you:

import os, requests

CHOCO = os.environ["CHOCO_API_KEY"]
resp = requests.get(
    "https://chocodata.com/api/v1/facebook/marketplace",
    params={"url": "https://www.facebook.com/marketplace/nyc/search?query=bicycle",
            "api_key": CHOCO},
    timeout=90,
)
for item in resp.json()["listings"]:
    print(item["title"], "-", item["price"], "-", item["location"])

Facebook Ads do have an official path: the Ad Library API. It is free, and the friction is real. You verify your identity with a government ID and confirm your location before access is granted, long-lived tokens expire around every 60 days, and the standard tier allows roughly 200 calls per hour (apidog’s API walkthrough documents the verification flow). The API also only covers ads Meta classifies as social-issue, electoral, or political in most regions, plus housing, employment, and credit ads in the US, so general commercial ad coverage is patchy. For broader ad data, the public Ad Library URL through a scraper sidesteps the verification and 60-day token churn:

import os, requests

CHOCO = os.environ["CHOCO_API_KEY"]
resp = requests.get(
    "https://chocodata.com/api/v1/facebook/ads",
    params={"url": "https://www.facebook.com/ads/library/?q=nike&active_status=all",
            "api_key": CHOCO},
    timeout=90,
)
for ad in resp.json()["ads"]:
    print(ad["advertiser"], "-", ad["text"][:60])

The endpoint details sit in the Facebook Marketplace Scraper API and Facebook Ads Scraper API writeups.

When should you use the official Graph API instead?

Use the Graph API when the data belongs to an asset you own or manage, because it returns clean JSON and stays well inside Meta’s terms. It is the right tool for your own Pages and your own ad account, and the wrong tool for arbitrary public data.

The ceiling is documented. Meta’s rate-limiting page sets the platform limit at:

Calls within one hour = 200 * Number of Users

where “Number of Users” is roughly your app’s daily active users. New apps sit in a development tier with tighter limits until they pass App Review, the Marketing API runs on its own separate Business Use Case budget, and Meta ships a new API version roughly twice a year (v25.0 was current as of early 2026). Here is how the official route stacks against scraping for the jobs people actually have:

TaskGraph APILogged-out scrape / scraper API
Read your own Page’s postsYes, clean JSONWorks, less precise
Read a competitor’s public PageNoYes
Pull public group postsNo (API removed)Yes
Marketplace listingsNoYes (headless required)
Political/social adsYes (verified)Yes
Other users’ emailsNoOnly if publicly shown

The pattern is consistent: the API is precise for your own assets, and it goes silent the moment you ask for someone else’s public data. That gap is the reason scraping sits alongside the API as a permanent second tool.

How do you scrape Facebook at scale without managing proxies?

A scraper API removes the blocking and parsing work by accepting a Facebook URL and returning parsed JSON, with proxy rotation, the headless browser, and retries handled server-side. You send one request and get structured data, with no 400 to debug and no __bbox keys to reverse-engineer.

The request shape is the same one I used in every example above:

curl "https://chocodata.com/api/v1/facebook/page?url=https://www.facebook.com/Meta&api_key=$CHOCO_API_KEY"

For a one-off pull of a few public Pages, the logged-out HTML route is fine, and I showed it works. The moment you need groups, Marketplace, threaded comments, or steady daily collection, the maintenance load adds up: residential proxies, header tuning, locale-aware parsing, markup churn, and the 400-versus-200 surprise. At that point the per-request cost of a managed API is usually cheaper than your own time. I compare the managed options in the best Facebook scrapers of 2026, and you can grab an API key to run the calls in this guide against a live target.

Before you collect anything personal at volume, settle the legal question first. I walk through it in is scraping Facebook legal.

FAQ

How do I scrape data from Facebook without logging in?

Request the public Page or post URL while logged out and parse the HTML. In my June 2026 test, a logged-out GET of facebook.com/Meta returned HTTP 200, the Page name, the like count, and an embedded __bbox JSON blob in the markup. A January 2024 federal ruling found that logged-off scraping of public Facebook data is not barred by Meta's terms. Logged-in scraping is restricted by Meta's Automated Data Collection Terms.

Can I scrape Facebook with the official Graph API?

Yes, for data tied to assets you own or manage, such as your own Pages and your ad account. The Graph API returns structured JSON and is rate limited to 200 × number of users calls per hour per app, per Meta's rate-limiting docs. It does not expose arbitrary public groups, Marketplace listings, or other users' contact details, and the Groups API was deprecated in April 2024.

How do I scrape emails from Facebook groups?

Emails are only collectable where a member or Page has chosen to display them publicly, for example in a business Page's contact section. There is no Meta API that returns a group member email list, and the Groups API itself was shut off in April 2024. You parse the public surface where the address appears. Scraping hidden personal contact data raises GDPR and CFAA exposure, which I cover in my guide on whether scraping Facebook is legal.

Why did my Facebook request return HTTP 400 instead of the page?

In my June 2026 tests a desktop Chrome User-Agent triggered a 400 error page of about 1,542 bytes, while sending no User-Agent returned the full public page with HTTP 200. Facebook's edge keys part of its bot handling on the header combination, so the User-Agent that looks most realistic was the one combination that got rejected here.

Is there a rate limit on scraping Facebook?

There is no published limit for logged-out HTML requests, but Facebook throttles and blocks by IP reputation and request volume, so datacenter IPs get cut off quickly. The official Graph API has a documented ceiling of 200 × users calls per hour, and the Ad Library API allows roughly 200 calls per hour per token.

NB
Noah Berg
I've built Facebook data pipelines for years. On facebookscraperapi.com I run Facebook scraping methods against live pages and publish what actually holds up.