├── requirements.txt ├── MANIFEST.in ├── fbpagefeed ├── __init__.py ├── _version.py ├── cli.py └── feed.py ├── .gitignore ├── LICENSE.txt ├── setup.py ├── test └── test_api_consistency.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | fbiter 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.md *.rst 2 | -------------------------------------------------------------------------------- /fbpagefeed/__init__.py: -------------------------------------------------------------------------------- 1 | from .feed import get 2 | -------------------------------------------------------------------------------- /fbpagefeed/_version.py: -------------------------------------------------------------------------------- 1 | VERSION_TUPLE = (0, 0, 1) 2 | __version__ = ".".join(map(str, VERSION_TUPLE)) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Python ===##### 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jeremy Singer-Vine 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | from setuptools import setup, find_packages 3 | import subprocess 4 | 5 | NAME = "fbpagefeed" 6 | HERE = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | version_ns = {} 9 | with open(os.path.join(HERE, NAME, '_version.py')) as f: 10 | exec(f.read(), {}, version_ns) 11 | 12 | with open(os.path.join(HERE, 'requirements.txt')) as f: 13 | requirements = f.read().strip().split("\n") 14 | 15 | setup( 16 | name = NAME, 17 | description = "A library and command-line tool for fetching Facebook Pages' published posts.", 18 | version = version_ns['__version__'], 19 | packages = find_packages(exclude=["test",]), 20 | tests_require = [ "nose" ] + requirements, 21 | install_requires = requirements, 22 | entry_points = { 23 | "console_scripts": [ 24 | "fbpagefeed = fbpagefeed.cli:main" 25 | ] 26 | }, 27 | author = "Jeremy Singer-Vine", 28 | author_email = "jsvine@gmail.com", 29 | license = "MIT", 30 | keywords = "facebook api", 31 | url = "https://github.com/jsvine/fbpagefeed", 32 | ) 33 | -------------------------------------------------------------------------------- /test/test_api_consistency.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import fbpagefeed 3 | from fbpagefeed.cli import get_input 4 | import sys, os 5 | 6 | access_token = get_input("FB_ACCESS_TOKEN") 7 | 8 | class QualityTest(unittest.TestCase): 9 | def test_missing_healthranger_post(self): 10 | feed = fbpagefeed.get( 11 | account_id=35590531315, 12 | access_token=access_token, 13 | extra_params={ 14 | "until": "2012-09-18T15:00:00+0000" 15 | }, 16 | max_results=1 17 | ) 18 | post = list(feed)[0] 19 | assert post["id"] == "35590531315_455891227789720" 20 | 21 | def test_reactions(self): 22 | feed = fbpagefeed.get( 23 | account_id=35590531315, 24 | access_token=access_token, 25 | extra_params={ 26 | "until": "2017-01-01" 27 | }, 28 | max_results=1 29 | ) 30 | post = list(feed)[0] 31 | assert post["reactions"] > 0 32 | assert post["reactions"] == sum([ 33 | post["reactions_like"], 34 | post["reactions_love"], 35 | post["reactions_wow"], 36 | post["reactions_haha"], 37 | post["reactions_sad"], 38 | post["reactions_angry"], 39 | ]) 40 | 41 | if __name__ == '__main__': 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /fbpagefeed/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, os 3 | import getpass 4 | import json 5 | import csv 6 | import argparse 7 | from . import feed 8 | 9 | def get_input(key, secret=True): 10 | stream = getpass.getpass if secret else input 11 | return os.environ.get(key) or stream(key + ": ") 12 | 13 | CSV_HEADERS = [ 14 | "id", 15 | "type", 16 | "created_time", 17 | "message", 18 | "link", 19 | "shares", 20 | "comments", 21 | "reactions", 22 | "reactions_like", 23 | "reactions_love", 24 | "reactions_wow", 25 | "reactions_haha", 26 | "reactions_sad", 27 | "reactions_angry", 28 | ] 29 | 30 | def parse_args(): 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument("account_id") 33 | parser.add_argument("--access-token", help="Facebook API access token. Alternatively, you can set the FB_ACCESS_TOKEN environment variable in your terminal.") 34 | parser.add_argument("--api-version", default=feed.DEFAULT_API_VERSION) 35 | parsed, unknown = parser.parse_known_args() 36 | for arg in unknown: 37 | if arg.startswith(("-", "--")): 38 | parser.add_argument(arg) 39 | args = parser.parse_args() 40 | return args 41 | 42 | def write_csv(results): 43 | writer = csv.DictWriter(sys.stdout, fieldnames=CSV_HEADERS) 44 | writer.writeheader() 45 | for r in results: 46 | writer.writerow(r) 47 | 48 | def main(): 49 | args = vars(parse_args()) 50 | account_id = args.pop("account_id") 51 | access_token = args.pop("access_token") 52 | if access_token is None: 53 | access_token = get_input("FB_ACCESS_TOKEN") 54 | results = feed.get(account_id, access_token, extra_params=args) 55 | write_csv(results) 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /fbpagefeed/feed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import fbiter 3 | import requests 4 | import sys 5 | import time 6 | import json 7 | import itertools 8 | 9 | DEFAULT_API_VERSION = "v2.9" 10 | DEFAULT_MAX_RESULTS = None 11 | 12 | def get(account_id, access_token, 13 | api_version=DEFAULT_API_VERSION, 14 | extra_params={}, 15 | max_results=DEFAULT_MAX_RESULTS): 16 | 17 | path = "{}/{}/posts".format(api_version, account_id) 18 | 19 | if max_results is not None: 20 | limit = max_results 21 | elif extra_params.get("limit") is not None: 22 | limit = extra_params.get("limit") 23 | else: 24 | limit = 100 25 | 26 | params = { 27 | "access_token": access_token, 28 | "limit": limit, 29 | "fields": ",".join([ 30 | "type", 31 | "created_time", 32 | "message", 33 | "link", 34 | "shares", 35 | "comments.limit(0).summary(true)", 36 | "reactions.limit(0).summary(true)", 37 | "reactions.type(LIKE).limit(0).summary(total_count).as(reactions_like)", 38 | "reactions.type(LOVE).limit(0).summary(total_count).as(reactions_love)", 39 | "reactions.type(WOW).limit(0).summary(total_count).as(reactions_wow)", 40 | "reactions.type(HAHA).limit(0).summary(total_count).as(reactions_haha)", 41 | "reactions.type(SAD).limit(0).summary(total_count).as(reactions_sad)", 42 | "reactions.type(ANGRY).limit(0).summary(total_count).as(reactions_angry)", 43 | ]) 44 | } 45 | params.update(extra_params) 46 | endpoint = fbiter.Endpoint(path, params) 47 | results = endpoint.iter_results(max_results=max_results) 48 | 49 | for r in results: 50 | r["shares"] = r.get("shares", {}).get("count") 51 | r["comments"] = r.get("comments", {}).get("summary", {}).get("total_count") 52 | for key, value in r.items(): 53 | if type(value) == dict: 54 | if "summary" in value: 55 | if "total_count" in value["summary"]: 56 | r[key] = value["summary"]["total_count"] 57 | yield r 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/pypi/v/fbpagefeed.svg)](https://pypi.python.org/pypi/fbpagefeed) [![License](https://img.shields.io/pypi/l/fbpagefeed.svg)](https://pypi.python.org/pypi/fbpagefeed) 2 | 3 | # fbpagefeed 4 | 5 | A simple library/CLI for getting all posts from a given Facebook Page. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | pip install fbpagefeed 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Command-line usage 16 | 17 | Installing `fbpagefeed` gives you the `fbpagefeed` command-line tool. Use it like so: 18 | 19 | ```sh 20 | fbpagefeed {facebook-page-id} [--my-param x] [--my-other-param y] > my-output.csv 21 | ``` 22 | 23 | ... where the keyword params correspond to the optional parameters in Facebook's Graph API. (See "Default params" below.) For example: 24 | 25 | ```sh 26 | fbpagefeed 91414372270 --since 2017-03-01 --until 2017-03-02 > my-output.csv 27 | ``` 28 | 29 | You can also use the page's spelled-out ID: 30 | 31 | ```sh 32 | fbpagefeed BuzzFeedNews --since 2017-03-01 --until 2017-03-02 > my-output.csv 33 | ``` 34 | 35 | You can supply your FB access token one of three ways: 36 | 37 | - Passing it to the `--access_token` flag 38 | - Setting it as the `FB_ACCESS_TOKEN` environment variable 39 | - Waiting for `fbpagefeed` to prompt you to enter it 40 | 41 | 42 | ### Library usage 43 | 44 | Example: 45 | 46 | ```python 47 | import fbpagefeed 48 | import os 49 | 50 | feed = fbpagefeed.get( 51 | "BuzzFeedNews", 52 | os.environ["FB_ACCESS_TOKEN"], 53 | extra_params={ 54 | "since": "2017-05-01", 55 | "until": "2017-05-02" 56 | } 57 | ) 58 | 59 | for post in feed: 60 | print(post) 61 | ``` 62 | 63 | ### Default params 64 | 65 | By default, `fbpagefeed` passes these parameters to the Facebook API: 66 | 67 | ```python 68 | { 69 | "limit": 100, 70 | "fields": ",".join([ 71 | "type", 72 | "created_time", 73 | "message", 74 | "link", 75 | "shares", 76 | "comments.limit(0).summary(true)", 77 | "reactions.limit(0).summary(true)", 78 | "reactions.type(LIKE).limit(0).summary(total_count).as(reactions_like)", 79 | "reactions.type(LOVE).limit(0).summary(total_count).as(reactions_love)", 80 | "reactions.type(WOW).limit(0).summary(total_count).as(reactions_wow)", 81 | "reactions.type(HAHA).limit(0).summary(total_count).as(reactions_haha)", 82 | "reactions.type(SAD).limit(0).summary(total_count).as(reactions_sad)", 83 | "reactions.type(ANGRY).limit(0).summary(total_count).as(reactions_angry)", 84 | ]) 85 | } 86 | ``` 87 | --------------------------------------------------------------------------------