{"id":"facebook-groups-scrape-posts","name":"facebook-groups-scrape-posts","summary":"Facebookグループの投稿から、グループURL、並べ替え順、希望数が与えられると、post_id、パーマリンク、著者、タイムスタンプ、本文、本文、画像/動画、リアクション数、リアクションタイプ内訳、コメント数、シェア数などの構造化された投稿メタデータを返します。","body":"# Facebook Groups — Scrape Posts\n\n> Input: Facebook group URL + sort order + desired count → Output: post list with full metadata (JSON).\n\n## Language\n\nAll process output to user (progress updates, process notifications) follows the user's language.\n\n## Objective\n\nGiven a Facebook group URL, scrape N posts sorted by the specified order and return structured metadata for each post.\n\n## Prerequisites\n\n- Target group page is already open in the browser: `https://www.facebook.com/groups/{group_slug_or_id}`\n- Already logged into Facebook (user avatar, Messenger icon, and notification bell visible in the top-right corner)\n\n## Pre-execution Checks\n\n### 1. Tool Readiness\n\nIf browser-act has been confirmed available in the current session → skip this step.\n\nInvoke `browser-act` via Skill tool to load usage. If installation or configuration issues arise, follow its guidance to resolve then retry.\n\n### 2. Login Verification\n\nIf Facebook login status has been confirmed in the current session → skip this step.\n\nOtherwise, navigate to `https://www.facebook.com/` and verify login status programmatically:\n\n```\nbrowser-act navigate 'https://www.facebook.com/'\nbrowser-act wait stable --timeout 15000\nbrowser-act eval \"JSON.stringify({user_id: document.cookie.match(/c_user=(\\d+)/)?.[1] || '0', USER_ID: (()=>{try{return require('CurrentUserInitialData').USER_ID;}catch(e){return '0';}})()})\"\n```\n\nVerdict:\n- `user_id` is a non-empty numeric string (e.g., `\"61560817072276\"`), or `USER_ID !== \"0\"` → logged in, continue\n- `user_id === null` or `USER_ID === \"0\"` → not logged in; assist user: run `browser-act browser open {browser_id} https://www.facebook.com/login --headed` to open a headed window so the user can sign in manually (Stealth `normal` mode persists cookies — one login is reusable)\n\n**Facebook may clear `c_user` mid-session**: if GraphQL errors such as `field_exception` or `missing_required_variable_value` occur during execution, re-run this login check before assuming the script is broken.\n\nUser refuses or cannot log in → terminate execution. Facebook enforces strict restrictions on unauthenticated group access (login modal blocks pagination, feed returns partial data + `field_exception`); login is a hard prerequisite.\n\n## Capability Components\n\n> This Skill's operational boundary = what the user can manually do in their browser. It only reads data already displayed to the authenticated user, never bypassing authentication or access controls — equivalent to copy-pasting on the user's behalf. JS code is encapsulated in Python files under the `scripts/` directory, invoked via `eval \"$(python scripts/xxx.py {params})\"`. `$(...)` is bash syntax; use the bash tool for execution.\n\n### API: Scrape group posts (with auto-pagination)\n\nNavigate to the target group page first, then invoke the scrape script (it auto-resolves the numeric group ID from the current page):\n\n```\nbrowser-act navigate 'https://www.facebook.com/groups/{group_slug_or_id}'\nbrowser-act wait stable --timeout 20000\nbrowser-act eval \"$(python scripts/scrape-posts.py --sort CHRONOLOGICAL --count 20)\"\n```\n\nParameters:\n- `--sort`: Sort order, default `CHRONOLOGICAL`. See \"Enum Parameters\" below\n- `--count`: Desired number of posts, default `20`. Script auto-paginates until count is met or feed is exhausted\n- `--max-pages`: Pagination safety cap, default `100`\n- `--doc-id`: GraphQL persisted query `doc_id` for `GroupsCometFeedRegularStoriesPaginationQuery`, default `26577462205242925`. Update via this flag if Facebook rotates the version (see \"Known Limitations\")\n\nOutput example:\n```json\n{\n  \"ok\": true,\n  \"group_id\": \"2580640642080467\",\n  \"group_name\": \"Programmer Humor\",\n  \"sort\": \"CHRONOLOGICAL\",\n  \"total\": 20,\n  \"posts\": [\n    {\n      \"post_id\": \"4052937798184070\",\n      \"cache_id\": \"6790541484885792441\",\n      \"id\": \"UzpfSTEwMDA4ODY4MzIx...\",\n      \"permalink_url\": \"https://www.facebook.com/groups/programmerhumor/posts/4052937798184070/\",\n      \"creation_time\": 1772941518,\n      \"message\": \"Those were the days my friend ...\",\n      \"author\": {\n        \"id\": \"100088683215191\",\n        \"name\": \"Jeff Bramlett\",\n        \"profile_picture\": null,\n        \"url\": \"https://www.facebook.com/JeffieB56\"\n      },\n      \"group\": {\n        \"id\": \"2580640642080467\",\n        \"name\": \"Programmer Humor\",\n        \"url\": \"https://www.facebook.com/groups/programmerhumor/\"\n      },\n      \"reactions\": {\n        \"total\": 1,\n        \"total_formatted\": \"1\",\n        \"breakdown\": [\n          { \"name\": \"Haha\", \"reaction_id\": \"115940658764963\", \"count\": 1 }\n        ]\n      },\n      \"share_count\": 0,\n      \"share_count_formatted\": \"0\",\n      \"comment_count\": 0,\n      \"media\": [\n        {\n          \"__typename\": \"Photo\",\n          \"id\": \"938454962453936\",\n          \"photo_image\": \"https://scontent-...fbcdn.net/v/t39...jpg\"\n        }\n      ]\n    }\n  ],\n  \"diagnostics\": {\n    \"pages\": [\n      { \"pageIdx\": 0, \"httpStatus\": 200, \"edgeCount\": 4, \"err\": null, \"hasNext\": true }\n    ]\n  }\n}\n```\n\nVideo posts include additional fields in `media`: `playable_url` (mp4 direct link), `playable_url_hd`, and `thumbnail`.\n\n## Enum Parameters\n\n[AI] `--sort` sort order — Facebook accepts the following three values:\n\n- `TOP_POSTS` — most relevant (default web sort)\n- `CHRONOLOGICAL` — newest first (reverse chronological by post time)\n- `RECENT_ACTIVITY` — most recently active (reverse chronological by latest comment/reaction time)\n\nValues are fixed and validated by `argparse choices`; no runtime query needed.\n\n## Pagination\n\n**API Pagination**: handled automatically by the script.\n\n- Pagination parameter: `cursor` (embedded in GraphQL `variables`)\n- Type: opaque cursor (server-side state, base64-encoded)\n- Initial value: `null` (first request)\n- Next page value: `data.node.group_feed.page_info.end_cursor`\n- Each response returns 3 edges (FB streaming mode ignores client-provided `count`)\n- Termination: `has_next_page === false`, or `--count` / `--max-pages` limit reached\n\n## Success Criteria\n\n- `ok === true` and `total >= 1`\n- `posts[*].post_id` non-null rate = 100% (non-post units such as Section Headers are filtered out by the script)\n- `posts[*].permalink_url` and `posts[*].creation_time` non-null rate = 100%\n- When using `CHRONOLOGICAL` sort, `creation_time` is strictly monotonically decreasing\n\n## Known Limitations\n\n- **Public groups only**: private groups require membership; returns empty or permission error when not a member\n- **No comment body**: `comment_count` returns total count but the group feed GraphQL does not include `top_comments` content or authors. Facebook places comment data in a separate `CommentsRenderer` query triggered only when the user clicks \"Comments\" — fetching comment bodies requires additional per-`post_id` GraphQL requests (out of scope)\n- **`doc_id` rotates with Facebook frontend versions**: when the default `26577462205242925` expires (`PersistedQueryNotFound` or HTTP 404), retrieve a fresh one:\n  1. Open any group page while logged in\n  2. Scroll down to trigger a new batch of posts\n  3. `browser-act network requests --filter api/graphql --method POST`\n  4. Check `X-FB-Friendly-Name` header on each request; find `GroupsCometFeedRegularStoriesPaginationQuery`\n  5. Extract `doc_id` from that request's POST body and pass it via `--doc-id`\n- **`group_name` can be null**: parsed from page HTML via heuristic regex; prefer `posts[*].group.name` (more reliable)\n- **Localized count fields**: `reactions.total_formatted` and `share_count_formatted` format depends on Facebook's UI language (e.g., non-English Facebook UI may return locale-specific number abbreviations instead of `\"12K\"`)\n- **Rapid requests trigger temporary throttling**: paginating too fast or calling multiple groups concurrently may return empty responses or temporary bans. Serialize group requests with a 2–5 s sleep between each\n- **GraphQL `field_exception` / partial edges + errors**: almost always caused by session cookie being cleared. Check `c_user` cookie and `require('CurrentUserInitialData').USER_ID` — if `0` / `null`, return to \"Login Verification\" and re-login; do not attempt to extract data from error responses\n- **Author avatar often null**: `author.profile_picture` is frequently unloaded in the group feed default response (Facebook lazy-loads avatars); a separate query is required if avatars are needed\n\n## Execution Efficiency\n\n- **Batch orchestration**: for small counts, call each group directly; for large counts, write a bash script to loop serially — do not parallelize (prone to anti-scraping triggers). Test with a minimal sample before running the full batch. Add appropriate intervals per rate guidance in \"Known Limitations\" above\n- **Test before batch execution**: always test with 1–2 items to verify the script runs correctly before running the full batch\n- **Reduce redundant pre-operations**: when multiple steps share the same prerequisite state, complete them in batch under that state to avoid repeatedly re-establishing it\n- **Error resumption**: save results item by item during batch processing; resume from the breakpoint on failure rather than starting over\n\n## Experience Notes\n\nPath: `{working-directory}/browser-act-skill-forge-memories/facebook-groups-scrape-posts-facebook-groups-scrape-posts.memory.md` (working directory is determined by the Agent running the Skill, typically the project root or current working directory)\n\n**Before execution**: If the file exists, read it first — it records unexpected situations encountered during past executions (e.g., a strategy has become ineffective); adjust strategy order accordingly.\n\n**After execution**: If an unexpected situation is encountered (strategy became ineffective, page redesigned, anti-scraping upgraded, better path discovered), append a line:\n`{YYYY-MM-DD}: {what happened} → {conclusion}`\n\nNormal execution does not write to the file. Do not record which groups were used or how many posts were returned — those are task outputs, not experience.","author":"@browser-act","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/browser-act/skills/tree/main/solutions/social-listening/facebook-groups-scrape-posts","license":"MIT","category":"document","lang":"en","tokens":2382,"stars":0,"calls30d":2,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"scripts/scrape-posts.py","size":13417,"sha256":"53fa0508a1d830bdfdadb8ce775a7add3ce7e54b376e57fbe73f9ee37e89efff"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[{"code":"net.endpoints","kind":"exfiltration","excerpt":"scontent-...fbcdn.net, www.facebook.com","message":"bundled scripts reach 2 external host(s)","severity":"warn"}],"scannedAt":"2026-08-22","hasScripts":true,"networkEndpoints":["scontent-...fbcdn.net","www.facebook.com"]}}