├── LICENSE ├── README.md ├── igql ├── __init__.py ├── constants.py ├── exceptions.py ├── hashtag.py ├── igql.py ├── location.py ├── media.py ├── user.py └── utils.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Fatih Kılıç 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # End of support 2 | According to this issue [#1](https://github.com/FKLC/IGQL/issues/1) [Instaloader](https://github.com/instaloader/instaloader) is already doing what this library does so why reinvent the wheel while it exists also I'm so sorry about wasting your time using this library instead of Instaloader it is much better than IGQL. @ogencoglu actually created issue about it but I think when I checked the source of [Instaloader](https://github.com/instaloader/instaloader) I accidentally typed something else instead of `graphql`. 3 | 4 | # InstagramGraphQL Unofficial API 5 | Unofficial Instagram GraphQL API to collet data without authentication. 6 | 7 | ### Features 8 | * Search for people, hashtags and locations 9 | * Get media data 10 | * Get hashtag data 11 | * Get location data 12 | * Get all comments 13 | * Get all likes 14 | * Get specific user posts 15 | * With sessionid supplied you can get data from private accounts 16 | * There is a lot of cool data returned by GraphQL. For example `accessibility_caption` which you can train your image classifier through it 17 | 18 | ###### NOTE: This is basically a API to collet data not for uploading or interacting with media. If you want more advanced IG library you should check [LevPasha's Instagram-API-python](https://github.com/LevPasha/Instagram-API-python) package. 19 | 20 | ### Getting all media of a user 21 | ```python 22 | from igql import InstagramGraphQL 23 | 24 | 25 | igql_api = InstagramGraphQL() 26 | 27 | user = igql_api.get_user('instagram') 28 | for media in user.timeline(): 29 | print(media) 30 | ``` 31 | 32 | ## Installation 33 | Library is avaible on PyPi so just run 34 | 35 | ``` 36 | pip install igql 37 | ``` 38 | 39 | 40 | # To learn more check [wiki page](https://github.com/FKLC/IGQL/wiki). 41 | -------------------------------------------------------------------------------- /igql/__init__.py: -------------------------------------------------------------------------------- 1 | from .igql import InstagramGraphQL 2 | -------------------------------------------------------------------------------- /igql/constants.py: -------------------------------------------------------------------------------- 1 | # URLs 2 | IG_URL = "https://www.instagram.com" 3 | BASE_URL = f"{IG_URL}/graphql/" 4 | 5 | # Queries 6 | GET_MEDIA = { 7 | "hash": "477b65a610463740ccdb83135b2014db", 8 | "variables": { 9 | "shortcode": "", 10 | "child_comment_count": 3, 11 | "fetch_comment_count": 40, 12 | "parent_comment_count": 24, 13 | "has_threaded_comments": False, 14 | }, 15 | "keys": ["", ["data", "shortcode_media"]], 16 | } 17 | LOAD_COMMENTS = { 18 | "hash": "f0986789a5c5d17c2400faebf16efd0d", 19 | "variables": {"shortcode": "", "first": 47, "after": ""}, 20 | "keys": ["edge_media_to_comment", ["data", "shortcode_media"]], 21 | } 22 | LOAD_LIKED_BY = { 23 | "hash": "e0f59e4a1c8d78d0161873bc2ee7ec44", 24 | "variables": {"shortcode": "", "include_reel": True, "first": 24, "after": ""}, 25 | "keys": ["edge_liked_by", ["data", "shortcode_media"]], 26 | } 27 | LOAD_TIMELINE = { 28 | "hash": "f2405b236d85e8296cf30347c9f08c2a", 29 | "variables": {"id": "", "first": 12, "after": ""}, 30 | "keys": ["edge_owner_to_timeline_media", ["data", "user"]], 31 | } 32 | LOAD_HASHTAG = { 33 | "hash": "f92f56d47dc7a55b606908374b43a314", 34 | "variables": {"tag_name": "", "show_ranked": False, "first": 12, "after": ""}, 35 | "keys": ["edge_hashtag_to_media", ["data", "hashtag"]], 36 | } 37 | LOAD_LOCATION = { 38 | "hash": "1b84447a4d8b6d6d0426fefb34514485", 39 | "variables": {"id": "", "first": 12, "after": ""}, 40 | "keys": ["edge_location_to_media", ["data", "location"]], 41 | } 42 | 43 | 44 | # Direct Accesses 45 | GET_USER = {"keys": ["", ["entry_data", "ProfilePage", 0, "graphql", "user"]]} 46 | GET_HASHTAG = {"keys": ["", ["entry_data", "TagPage", 0, "graphql", "hashtag"]]} 47 | GET_LOCATION = {"keys": ["", ["entry_data", "LocationsPage", 0, "graphql", "location"]]} 48 | 49 | # Misc 50 | USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" 51 | FORBIDDEN_USERNAMES = ["graphql", "explore"] 52 | -------------------------------------------------------------------------------- /igql/exceptions.py: -------------------------------------------------------------------------------- 1 | class RateLimitExceed(Exception): 2 | pass 3 | 4 | 5 | class NotFound(Exception): 6 | pass 7 | 8 | 9 | class MaxRetries(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /igql/hashtag.py: -------------------------------------------------------------------------------- 1 | import igql 2 | 3 | from . import constants 4 | from .utils import get_value_deep_key, paginator 5 | 6 | 7 | class Hashtag: 8 | def __init__(self, data, api): 9 | self.api = api 10 | self.data = data 11 | 12 | def recent_media(self, variables={}): 13 | data = get_value_deep_key(self.data, constants.LOAD_HASHTAG["keys"][1]) 14 | return paginator( 15 | self.api, 16 | data, 17 | constants.LOAD_HASHTAG["keys"], 18 | { 19 | "query_hash": constants.LOAD_HASHTAG["hash"], 20 | "variables": { 21 | **constants.LOAD_HASHTAG["variables"], 22 | "tag_name": data["name"], 23 | **variables, 24 | }, 25 | }, 26 | ) 27 | 28 | @property 29 | def top_posts(self): 30 | # Since there is no pagination leave it hardcoded. 31 | return get_value_deep_key(self.data, constants.LOAD_HASHTAG["keys"][1])[ 32 | "edge_hashtag_to_top_posts" 33 | ]["edges"] 34 | -------------------------------------------------------------------------------- /igql/igql.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from anyapi import AnyAPI 4 | from anyapi.proxy_handlers import RateLimitProxy 5 | from requests.exceptions import ChunkedEncodingError 6 | 7 | from . import constants 8 | from .exceptions import MaxRetries, NotFound, RateLimitExceed 9 | from .hashtag import Hashtag 10 | from .location import Location 11 | from .media import Media 12 | from .user import User 13 | from .utils import get_shared_data, get_value_deep_key, set_instagram_gis 14 | 15 | 16 | class InstagramGraphQL: 17 | loads = json.loads 18 | dumps = json.dumps 19 | 20 | def __init__(self, rhx_gis="", sessionid="", proxies=[], max_retries=3): 21 | self.last_response = {} 22 | self.max_retries = max_retries 23 | 24 | self.api = AnyAPI( 25 | constants.BASE_URL, 26 | default_headers={ 27 | "user-agent": constants.USER_AGENT, 28 | "cookie": f"sessionid={sessionid}", 29 | }, 30 | scoped_call=self.__retry, 31 | **( 32 | { 33 | "proxy_configuration": { 34 | "default": None, 35 | "proxies": proxies, 36 | "paths": {"/query": 100}, 37 | }, 38 | "proxy_handler": RateLimitProxy, 39 | } 40 | if proxies 41 | else {} 42 | ), 43 | ) 44 | 45 | self.api._filter_response = [ 46 | InstagramGraphQL.__rate_limit_exceed, 47 | InstagramGraphQL.__user_not_found, 48 | InstagramGraphQL.__hashtag_not_found, 49 | InstagramGraphQL.__location_not_found, 50 | InstagramGraphQL.__set_as_json, 51 | InstagramGraphQL.__media_not_found, 52 | self.__set_last_response, 53 | ] 54 | 55 | self.api._filter_request.append( 56 | lambda kwargs: set_instagram_gis(kwargs, rhx_gis) 57 | ) 58 | 59 | def __retry(self, request, retries=0): 60 | try: 61 | return request() 62 | except ChunkedEncodingError as e: 63 | if retries != self.max_retries: 64 | return self.__retry(request, retries=retries + 1) 65 | else: 66 | raise e 67 | 68 | @staticmethod 69 | def __set_as_json(kwargs, response): 70 | if kwargs["path"] == "/query": 71 | return InstagramGraphQL.loads(response.text) 72 | return response.text 73 | 74 | def __set_last_response(self, _, response): 75 | self.last_response = response 76 | return response 77 | 78 | @staticmethod 79 | def __rate_limit_exceed(_, response): 80 | if response.status_code == 429: 81 | raise RateLimitExceed("Rate limit exceed!") 82 | 83 | return response 84 | 85 | @staticmethod 86 | def __media_not_found(kwargs, response): 87 | if kwargs["params"].get("query_hash") == constants.GET_MEDIA[ 88 | "hash" 89 | ] and not response["data"].get("shortcode_media"): 90 | raise NotFound("Media not found!") 91 | 92 | return response 93 | 94 | @staticmethod 95 | def __user_not_found(kwargs, response): 96 | if ( 97 | response.status_code == 404 98 | and kwargs["path"].split("/")[1] not in constants.FORBIDDEN_USERNAMES 99 | ): 100 | raise NotFound("User not found!") 101 | 102 | return response 103 | 104 | @staticmethod 105 | def __hashtag_not_found(kwargs, response): 106 | if response.status_code == 404 and kwargs["path"].startswith("/explore/tags/"): 107 | raise NotFound("Hashtag not found!") 108 | 109 | return response 110 | 111 | @staticmethod 112 | def __location_not_found(kwargs, response): 113 | if response.status_code == 404 and kwargs["path"].startswith( 114 | "/explore/locations/" 115 | ): 116 | raise NotFound("Location not found!") 117 | 118 | return response 119 | 120 | def get_media(self, shortcode, variables={}): 121 | response = self.api.query.GET( 122 | params={ 123 | "query_hash": constants.GET_MEDIA["hash"], 124 | "variables": InstagramGraphQL.dumps( 125 | { 126 | **constants.GET_MEDIA["variables"], 127 | "shortcode": shortcode, 128 | **variables, 129 | } 130 | ), 131 | } 132 | ) 133 | 134 | return Media(response, self.api) 135 | 136 | def get_user(self, username): 137 | response = get_value_deep_key( 138 | get_shared_data(self.api, path=username), constants.GET_USER["keys"][1] 139 | ) 140 | 141 | return User({"data": {"user": response}}, self.api) 142 | 143 | def get_hashtag(self, name): 144 | response = get_value_deep_key( 145 | get_shared_data(self.api, path=f"explore/tags/{name}/"), 146 | constants.GET_HASHTAG["keys"][1], 147 | ) 148 | 149 | return Hashtag({"data": {"hashtag": response}}, self.api) 150 | 151 | def get_location(self, location_id): 152 | response = get_value_deep_key( 153 | get_shared_data(self.api, path=f"explore/locations/{location_id}/"), 154 | constants.GET_LOCATION["keys"][1], 155 | ) 156 | 157 | return Location({"data": {"location": response}}, self.api) 158 | -------------------------------------------------------------------------------- /igql/location.py: -------------------------------------------------------------------------------- 1 | import igql 2 | 3 | from . import constants 4 | from .utils import get_value_deep_key, paginator 5 | 6 | 7 | class Location: 8 | def __init__(self, data, api): 9 | self.api = api 10 | self.data = data 11 | 12 | def recent_media(self, variables={}): 13 | data = get_value_deep_key(self.data, constants.LOAD_LOCATION["keys"][1]) 14 | return paginator( 15 | self.api, 16 | data, 17 | constants.LOAD_LOCATION["keys"], 18 | { 19 | "query_hash": constants.LOAD_LOCATION["hash"], 20 | "variables": { 21 | **constants.LOAD_LOCATION["variables"], 22 | "id": data["id"], 23 | **variables, 24 | }, 25 | }, 26 | ) 27 | 28 | @property 29 | def top_posts(self): 30 | # Since there is no pagination leave it hardcoded. 31 | return get_value_deep_key(self.data, constants.LOAD_LOCATION["keys"][1])[ 32 | "edge_location_to_top_posts" 33 | ]["edges"] 34 | -------------------------------------------------------------------------------- /igql/media.py: -------------------------------------------------------------------------------- 1 | import igql 2 | 3 | from . import constants 4 | from .utils import get_value_deep_key, paginator 5 | 6 | 7 | class Media: 8 | def __init__(self, data, api): 9 | self.api = api 10 | self.data = data 11 | 12 | def comments(self, variables={}): 13 | data = get_value_deep_key(self.data, constants.LOAD_COMMENTS["keys"][1]) 14 | return paginator( 15 | self.api, 16 | data, 17 | constants.LOAD_COMMENTS["keys"], 18 | { 19 | "query_hash": constants.LOAD_COMMENTS["hash"], 20 | "variables": { 21 | **constants.LOAD_COMMENTS["variables"], 22 | "shortcode": data["shortcode"], 23 | **variables, 24 | }, 25 | }, 26 | ) 27 | 28 | def __first_liked_by(self, variables): 29 | return self.api.query.GET( 30 | params={ 31 | "query_hash": constants.LOAD_LIKED_BY["hash"], 32 | "variables": igql.InstagramGraphQL.dumps( 33 | { 34 | **constants.LOAD_LIKED_BY["variables"], 35 | "shortcode": get_value_deep_key( 36 | self.data, constants.LOAD_LIKED_BY["keys"][1] 37 | )["shortcode"], 38 | **variables, 39 | } 40 | ), 41 | } 42 | ) 43 | 44 | def liked_by(self, variables={}): 45 | get_value_deep_key(self.data, constants.LOAD_LIKED_BY["keys"][1])[ 46 | constants.LOAD_LIKED_BY["keys"][0] 47 | ] = get_value_deep_key( 48 | self.__first_liked_by(variables), constants.LOAD_LIKED_BY["keys"][1] 49 | )[ 50 | constants.LOAD_LIKED_BY["keys"][0] 51 | ] 52 | 53 | data = get_value_deep_key(self.data, constants.LOAD_LIKED_BY["keys"][1]) 54 | return paginator( 55 | self.api, 56 | data, 57 | constants.LOAD_LIKED_BY["keys"], 58 | { 59 | "query_hash": constants.LOAD_LIKED_BY["hash"], 60 | "variables": { 61 | **constants.LOAD_LIKED_BY["variables"], 62 | "shortcode": data["shortcode"], 63 | **variables, 64 | }, 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /igql/user.py: -------------------------------------------------------------------------------- 1 | import igql 2 | 3 | from . import constants 4 | from .utils import get_value_deep_key, paginator 5 | 6 | 7 | class User: 8 | def __init__(self, data, api): 9 | self.api = api 10 | self.data = data 11 | 12 | def timeline(self, variables={}): 13 | data = get_value_deep_key(self.data, constants.LOAD_TIMELINE["keys"][1]) 14 | return paginator( 15 | self.api, 16 | data, 17 | constants.LOAD_TIMELINE["keys"], 18 | { 19 | "query_hash": constants.LOAD_TIMELINE["hash"], 20 | "variables": { 21 | **constants.LOAD_TIMELINE["variables"], 22 | "id": data["id"], 23 | **variables, 24 | }, 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /igql/utils.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | import igql 4 | 5 | from .constants import IG_URL 6 | 7 | 8 | def set_instagram_gis(kwargs, rhx_gis): 9 | if "variables" in kwargs["params"]: 10 | kwargs["headers"]["x-instagram-gis"] = md5( 11 | (f'{rhx_gis}:{kwargs["params"]["variables"]}').encode() 12 | ).hexdigest() 13 | return kwargs 14 | 15 | 16 | def get_shared_data(api, path="instagram"): 17 | response = api.GET(url=f"{IG_URL}/{path}") 18 | response = response.split("window._sharedData = ")[1] 19 | response = response.split(";")[0] 20 | response = igql.InstagramGraphQL.loads(response) 21 | 22 | return response 23 | 24 | 25 | def paginator(api, data, keys, params): 26 | yield data[keys[0]]["edges"] 27 | 28 | has_next_page = data[keys[0]]["page_info"]["has_next_page"] 29 | end_cursor = data[keys[0]]["page_info"]["end_cursor"] 30 | 31 | while has_next_page: 32 | if isinstance(params["variables"], str): 33 | params["variables"] = igql.InstagramGraphQL.loads(params["variables"]) 34 | params["variables"]["after"] = end_cursor 35 | params["variables"] = igql.InstagramGraphQL.dumps(params["variables"]) 36 | 37 | data = get_value_deep_key(api.query.GET(params=params), keys[1]) 38 | 39 | has_next_page = data[keys[0]]["page_info"]["has_next_page"] 40 | end_cursor = data[keys[0]]["page_info"]["end_cursor"] 41 | 42 | yield data[keys[0]]["edges"] 43 | 44 | 45 | def get_value_deep_key(data, keys): 46 | for key in keys: 47 | data = data[key] 48 | return data 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | 4 | # The directory containing this file 5 | HERE = pathlib.Path(__file__).parent 6 | 7 | # The text of the README file 8 | README = (HERE / "README.md").read_text() 9 | 10 | # This call to setup() does all the work 11 | setup( 12 | name="igql", 13 | version="1.1.2", 14 | description="InstagramGraphQL Unofficial API", 15 | long_description=README, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/FKLC/IGQL", 18 | author="Fatih Kılıç", 19 | author_email="***REMOVED***", 20 | license="MIT", 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | ], 26 | packages=["igql"], 27 | install_requires=["anyapi==1.1.401", "requests"], 28 | ) 29 | --------------------------------------------------------------------------------