1
0
mirror of https://github.com/instaloader/instaloader.git synced 2024-10-05 14:57:08 +02:00

Update as-module.rst and docstrings

This commit is contained in:
Alexander Graf 2018-04-20 16:36:37 +02:00
parent b9ac40899d
commit 2c50972e08
6 changed files with 148 additions and 66 deletions

View File

@ -10,6 +10,9 @@ Python Module :mod:`instaloader`
.. highlight:: python
.. contents::
:backlinks: none
Instaloader exposes its internally used methods as a Python module, making it a
**powerful and easy-to-use Python API for Instagram**, allowing to further
customize obtaining media and metadata.
@ -42,34 +45,8 @@ Besides :func:`Instaloader.get_hashtag_posts`, there is
Also, :class:`Post` instances can be created with :func:`Post.from_shortcode`
and :func:`Post.from_mediaid`.
Further, information about profiles can be easily obtained. For example, you may
print a list of all followees and followers of a profile with::
# Print followees
print(PROFILE + " follows these profiles:")
for f in L.get_followees(PROFILE):
print("\t%s\t%s" % (f['username'], f['full_name']))
# Print followers
print("Followers of " + PROFILE + ":")
for f in L.get_followers(PROFILE):
print("\t%s\t%s" % (f['username'], f['full_name']))
Then, you may download all pictures of all followees with::
for f in L.get_followees(PROFILE):
L.download_profile(f['username'])
Each Instagram profile has its own unique ID which stays unmodified even if a
user changes his/her username. To get said ID, given the profile's name, you may
call::
L.get_id_by_username(PROFILE_NAME)
A reference of the many methods provided by the :mod:`instaloader` module is
provided in the remainder of this document. Feel free to direct any issue or
contribution to
`Instaloader on Github <https://github.com/instaloader/instaloader>`__.
provided in the remainder of this document.
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -77,24 +54,34 @@ contribution to
.. autoclass:: Instaloader
:no-show-inheritance:
``Profile`` Class
^^^^^^^^^^^^^^^^^
Instagram Structures
^^^^^^^^^^^^^^^^^^^^
.. autoclass:: Profile
:no-show-inheritance:
.. autofunction:: load_structure_from_file
``Post`` Class
^^^^^^^^^^^^^^
.. autofunction:: save_structure_to_file
``Post``
""""""""
.. autofunction:: mediaid_to_shortcode
.. autofunction:: shortcode_to_mediaid
.. autoclass:: Post
:no-show-inheritance:
Miscellaneous Functions
^^^^^^^^^^^^^^^^^^^^^^^
``StoryItem``
"""""""""""""
.. autofunction:: shortcode_to_mediaid
.. autoclass:: StoryItem
:no-show-inheritance:
.. autofunction:: mediaid_to_shortcode
``Profile``
"""""""""""
.. autoclass:: Profile
:no-show-inheritance:
Exceptions
^^^^^^^^^^
@ -102,6 +89,8 @@ Exceptions
.. autoexception:: InstaloaderException
:no-show-inheritance:
.. autoexception:: QueryReturnedBadRequestException
.. autoexception:: QueryReturnedNotFoundException
.. autoexception:: QueryReturnedForbiddenException
@ -123,3 +112,9 @@ Exceptions
.. autoexception:: ConnectionException
.. autoexception:: TooManyRequestsException
``InstaloaderContext`` (Low-level functions)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: InstaloaderContext
:no-show-inheritance:

View File

@ -369,13 +369,5 @@ current_release_date = subprocess.check_output(["git", "log", "-1", "--tags", "-
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
def skip(app, what, name, obj, skip, options):
# Ensure constructors are documented
if name == "__init__":
return False
return skip
def setup(app):
app.connect('autodoc-skip-member', skip)
app.add_stylesheet("style.css")

View File

@ -14,5 +14,6 @@ else:
from .exceptions import *
from .instaloader import Instaloader
from .instaloadercontext import InstaloaderContext
from .structures import (Post, Profile, Story, StoryItem, load_structure_from_file, mediaid_to_shortcode,
save_structure_to_file, shortcode_to_mediaid)

View File

@ -71,6 +71,36 @@ class _PostPathFormatter(_ArbitraryItemFormatter):
class Instaloader:
"""Instaloader Class.
::
L = Instaloader()
# Optionally, login or load session
L.login(USER, PASSWORD) # (login)
L.interactive_login(USER) # (ask password on terminal)
L.load_session_from_file(USER) # (load session created w/
# `instaloader -l USERNAME`)
:mod:`instaloader` provides the :class:`Post` structure, which represents a
picture, video or sidecar (set of multiple pictures/videos) posted in a user's
profile. :class:`Instaloader` provides methods to iterate over Posts from a
certain source::
for post in L.get_hashtag_posts('cat'):
# post is an instance of Post
L.download_post(post, target='#cat')
Besides :func:`Instaloader.get_hashtag_posts`, there is
:func:`Instaloader.get_feed_posts`, :func:`Profile.get_posts` and
:func:`Profile.get_saved_posts`.
Also, :class:`Post` instances can be created with :func:`Post.from_shortcode`
and :func:`Post.from_mediaid`.
Also, this class provides methods :meth:`Instaloader.download_profile`,
:meth:`Instaloader.download_hashtag` and many more to download targets.
"""
def __init__(self,
sleep: bool = True, quiet: bool = False,
@ -119,6 +149,7 @@ class Instaloader:
new_loader.close()
def close(self):
"""Close associated session objects and repeat error log."""
self.context.close()
def __enter__(self):
@ -176,7 +207,7 @@ class Instaloader:
self.context.log('comments', end=' ', flush=True)
def save_caption(self, filename: str, mtime: datetime, caption: str) -> None:
"""Updates picture caption"""
"""Updates picture caption / Post metadata info"""
filename += '.txt'
caption += '\n'
pcaption = caption.replace('\n', ' ').strip()
@ -214,6 +245,7 @@ class Instaloader:
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
def save_location(self, filename: str, location_json: Dict[str, str], mtime: datetime) -> None:
"""Save post location name and Google Maps link."""
filename += '_location.txt'
location_string = (location_json["name"] + "\n" +
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location_json["lat"],
@ -250,7 +282,10 @@ class Instaloader:
@_requires_login
def save_session_to_file(self, filename: Optional[str] = None) -> None:
"""Saves internally stored :class:`requests.Session` object."""
"""Saves internally stored :class:`requests.Session` object.
:param filename: Filename, or None to use default filename.
"""
if filename is None:
filename = get_default_session_filename(self.context.username)
dirname = os.path.dirname(filename)
@ -437,7 +472,10 @@ class Instaloader:
@_requires_login
def get_feed_posts(self) -> Iterator[Post]:
"""Get Posts of the user's feed."""
"""Get Posts of the user's feed.
:return: Iterator over Posts of the user's feed.
"""
data = self.context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]
@ -514,7 +552,10 @@ class Instaloader:
@_requires_login
def get_explore_posts(self) -> Iterator[Post]:
"""Get Posts which are worthy of exploring suggested by Instagram."""
"""Get Posts which are worthy of exploring suggested by Instagram.
:return: Iterator over Posts of the user's suggested posts.
"""
data = self.context.get_json('explore/', {})
yield from (Post(self.context, node)
for node in self.context.graphql_node_list("df0dcc250c2b18d9fd27c5581ef33c7c",

View File

@ -139,9 +139,11 @@ class InstaloaderContext:
return session
def save_session_to_file(self, sessionfile):
"""Not meant to be used directly, use :meth:`Instaloader.save_session_to_file`."""
pickle.dump(requests.utils.dict_from_cookiejar(self._session.cookies), sessionfile)
def load_session_from_file(self, username, sessionfile):
"""Not meant to be used directly, use :meth:`Instaloader.load_session_to_file`."""
session = requests.Session()
session.cookies = requests.utils.cookiejar_from_dict(pickle.load(sessionfile))
session.headers.update(self._default_http_header())
@ -150,10 +152,12 @@ class InstaloaderContext:
self.username = username
def test_login(self) -> Optional[str]:
"""Not meant to be used directly, use :meth:`Instaloader.test_login`."""
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
def login(self, user, passwd):
"""Not meant to be used directly, use :meth:`Instaloader.login`."""
session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'csrftoken': '',

View File

@ -28,23 +28,26 @@ class Post:
Structure containing information about an Instagram post.
Created by methods :meth:`Profile.get_posts`, :meth:`Instaloader.get_hashtag_posts`,
:meth:`Instaloader.get_feed_posts` and :meth:`Profile.get_saved_posts`.
:meth:`Instaloader.get_feed_posts` and :meth:`Profile.get_saved_posts`, which return iterators of Posts::
L = Instaloader()
for post in L.get_hashtag_posts(HASHTAG):
L.download_post(post, target='#'+HASHTAG)
Might also be created with::
post = Post.from_shortcode(L.context, SHORTCODE)
This class unifies access to the properties associated with a post. It implements == and is
hashable.
The properties defined here are accessible by the filter expressions specified with the :option:`--only-if`
parameter and exported into JSON files with :option:`--metadata-json`.
:param context: :attr:`Instaloader.context` used for additional queries if neccessary..
:param node: Node structure, as returned by Instagram.
:param owner_profile: The Profile of the owner, if already known at creation.
"""
def __init__(self, context: InstaloaderContext, node: Dict[str, Any],
owner_profile: Optional['Profile'] = None):
"""Create a Post instance from a node structure as returned by Instagram.
:param context: :class:`InstaloaderContext` instance used for additional queries if neccessary.
:param node: Node structure, as returned by Instagram.
:param owner_profile: The Profile of the owner, if already known at creation.
"""
assert 'shortcode' in node or 'code' in node
self._context = context
@ -296,7 +299,7 @@ class Post:
self._rhx_gis)
def get_location(self) -> Optional[Dict[str, str]]:
"""If the Post has a location, returns a dictionary with fields 'lat' and 'lng'."""
"""If the Post has a location, returns a dictionary with fields 'lat' and 'lng' and 'name'."""
loc_dict = self._field("location")
if loc_dict is not None:
location_json = self._context.get_json("explore/locations/{0}/".format(loc_dict["id"]),
@ -311,7 +314,25 @@ class Profile:
Provides methods for accessing profile properties, as well as :meth:`Profile.get_posts` and for own profile
:meth:`Profile.get_saved_posts`.
This class implements == and is hashable.
Get instances with :meth:`Post.owner_profile`, :meth:`StoryItem.owner_profile`, :meth:`Profile.get_followees`,
:meth:`Profile.get_followers` or::
L = Instaloader()
profile = Profile.from_username(L.context, USERNAME)
Provides :meth:`Profile.get_posts` and for own profile :meth:`Profile.get_saved_posts` to iterate over associated
:class:`Post` objects::
for post in profile.get_posts():
L.download_post(post, target=profile.username)
:meth:`Profile.get_followees` and :meth:`Profile.get_followers`::
print("{} follows these profiles:".format(profile.username))
for followee in profile.get_followees():
print(followee.username)
Also, this class implements == and is hashable.
"""
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
assert 'username' in node
@ -321,6 +342,12 @@ class Profile:
@classmethod
def from_username(cls, context: InstaloaderContext, username: str):
"""Create a Profile instance from a given username, raise exception if it does not exist.
:param context: :attr:`Instaloader.context`
:param username: Username
:raises: :class:`ProfileNotExistsException`
"""
# pylint:disable=protected-access
profile = cls(context, {'username': username.lower()})
profile._obtain_metadata() # to raise ProfileNotExistException now in case username is invalid
@ -328,6 +355,13 @@ class Profile:
@classmethod
def from_id(cls, context: InstaloaderContext, profile_id: int):
"""If logged in, create a Profile instance from a given userid. If possible, use :meth:`Profile.from_username`
or constructor directly rather than this method, since does many requests.
:param context: :attr:`Instaloader.context`
:param profile_id: userid
:raises: :class:`ProfileNotExistsException`, :class:`LoginRequiredException`, :class:`ProfileHasNoPicsException`
"""
if not context.is_logged_in:
raise LoginRequiredException("--login=USERNAME required to obtain profile metadata from its ID number.")
data = context.graphql_query("472f257a40c653c64c666ce877d59d2b",
@ -377,10 +411,12 @@ class Profile:
@property
def userid(self) -> int:
"""User ID"""
return int(self._metadata('id'))
@property
def username(self) -> str:
"""Profile Name"""
return self._metadata('username').lower()
def __repr__(self):
@ -478,8 +514,7 @@ class Profile:
def get_followers(self) -> Iterator['Profile']:
"""
Retrieve list of followers of given profile.
To use this, one needs to be logged in and private profiles has to be followed,
otherwise this returns an empty list.
To use this, one needs to be logged in and private profiles has to be followed.
"""
if not self._context.is_logged_in:
raise LoginRequiredException("--login required to get a profile's followers.")
@ -494,8 +529,7 @@ class Profile:
def get_followees(self) -> Iterator['Profile']:
"""
Retrieve list of followees (followings) of given profile.
To use this, one needs to be logged in and private profiles has to be followed,
otherwise this returns an empty list.
To use this, one needs to be logged in and private profiles has to be followed.
"""
if not self._context.is_logged_in:
raise LoginRequiredException("--login required to get a profile's followees.")
@ -712,7 +746,16 @@ class Story:
JsonExportable = Union[Post, Profile, StoryItem]
def save_structure_to_file(structure: JsonExportable, filename: str):
def save_structure_to_file(structure: JsonExportable, filename: str) -> None:
"""Saves a :class:`Post`, :class:`Profile` or :class:`StoryItem` to a '.json' or '.json.xz' file such that it can
later be loaded by :func:`load_structure_from_file`.
If the specified filename ends in '.xz', the file will be LZMA compressed. Otherwise, a pretty-printed JSON file
will be created.
:param structure: :class:`Post`, :class:`Profile` or :class:`StoryItem`
:param filename: Filename, ends in '.json' or '.json.xz'
"""
json_structure = {'node': structure.get_node(),
'instaloader': {'version': __version__, 'node_type': structure.__class__.__name__}}
compress = filename.endswith('.xz')
@ -725,6 +768,12 @@ def save_structure_to_file(structure: JsonExportable, filename: str):
def load_structure_from_file(context: InstaloaderContext, filename: str) -> JsonExportable:
"""Loads a :class:`Post`, :class:`Profile` or :class:`StoryItem` from a '.json' or '.json.xz' file that
has been saved by :func:`save_structure_from_file`.
:param context: :attr:`Instaloader.context` linked to the new object, used for additional queries if neccessary.
:param filename: Filename, ends in '.json' or '.json.xz'
"""
compressed = filename.endswith('.xz')
if compressed:
fp = lzma.open(filename, 'rt')