mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 21:52:17 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47db7f7ffa | ||
|
|
2567bec975 | ||
|
|
2df1419d68 | ||
|
|
8211c9daf1 | ||
|
|
969d4e3aa4 | ||
|
|
4cf1ae847d | ||
|
|
a276a0dc59 | ||
|
|
cc34b8340f | ||
|
|
45fe509ac4 | ||
|
|
ae492ed68b | ||
|
|
0e433294ad |
Vendored
+2
@@ -50,6 +50,8 @@
|
||||
<li><a href="https://github.com/instaloader/instaloader">Instaloader on GitHub</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li style="padding-left: 20px;"><a class="github-button" href="https://github.com/instaloader/instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Instaloader on GitHub">Star</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="https://www.paypal.me/aandergr"><b>Donate (via Paypal)</b></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -97,6 +97,9 @@ metadata of a Profile. :class:`Profile` instances can be created with:
|
||||
A reference of the many methods provided by the :mod:`instaloader` module is
|
||||
provided in the remainder of this document.
|
||||
|
||||
For a list of real code examples using the Instaloader module for advanced
|
||||
tasks, see :ref:`codesnippets`.
|
||||
|
||||
``Instaloader`` (Main Class)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Instaloader supports the following targets:
|
||||
**download the user's stories**.
|
||||
|
||||
- ``"#hashtag"``
|
||||
Posts with a certain **hashtag** (the quotes are usually neccessary),
|
||||
Posts with a certain **hashtag** (the quotes are usually necessary),
|
||||
|
||||
- ``:stories``
|
||||
The currently-visible **stories** of your followees (requires
|
||||
@@ -158,7 +158,7 @@ Filter Posts
|
||||
.. py:currentmodule:: instaloader
|
||||
|
||||
The options :option:`--post-filter` and :option:`--storyitem-filter`
|
||||
allows to specify criterias that posts or story items have to
|
||||
allows to specify criteria that posts or story items have to
|
||||
meet to be downloaded. If not given, all posts are downloaded.
|
||||
|
||||
The filter string must be a
|
||||
@@ -273,3 +273,6 @@ consider taking a look at the :ref:`python-module-instaloader`.
|
||||
Instaloader exposes its internally used methods and structures, making it a
|
||||
powerful and intuitive Python API for Instagram, allowing to further customize
|
||||
obtaining media and metadata.
|
||||
|
||||
Also see :ref:`codesnippets`, where we collect example scripts that use
|
||||
Instaloader to achieve more complex tasks.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
.. _codesnippets:
|
||||
|
||||
Advanced Instaloader Examples
|
||||
=============================
|
||||
|
||||
.. currentmodule:: instaloader
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
.. contents::
|
||||
:backlinks: none
|
||||
|
||||
Here we present code examples that use the :ref:`python-module-instaloader` for
|
||||
more advanced Instagram downloading or metadata mining than what is possible
|
||||
with the Instaloader command line interface.
|
||||
|
||||
.. For each code snippet:
|
||||
- title
|
||||
- brief description of what it does / motivation / how it works
|
||||
- code, or link to code
|
||||
- link to discussion issue
|
||||
- link used methods
|
||||
|
||||
Download Posts in a Specific Period
|
||||
-----------------------------------
|
||||
|
||||
To only download Instagram pictures (and metadata) that are within a specific
|
||||
period, you can play around with :func:`~itertools.dropwhile` and
|
||||
:func:`~itertools.takewhile` from :mod:`itertools` like in this snippet.
|
||||
|
||||
.. literalinclude:: codesnippets/121_since_until.py
|
||||
|
||||
See also :class:`Post`, :meth:`Instaloader.download_post`.
|
||||
|
||||
Discussed in :issue:`121`.
|
||||
|
||||
Likes of a Profile / Ghost Followers
|
||||
------------------------------------
|
||||
|
||||
To obtain a list of your inactive followers, i.e. followers that did not like
|
||||
any of your pictures, into a file you can use this approach.
|
||||
|
||||
.. literalinclude:: codesnippets/120_ghost_followers.py
|
||||
|
||||
See also :meth:`Profile.get_posts`, :meth:`Post.get_likes`,
|
||||
:meth:`Profile.get_followers`, :meth:`Instaloader.load_session_from_file`,
|
||||
:meth:`Profile.from_username`.
|
||||
|
||||
Discussed in :issue:`120`.
|
||||
|
||||
Track Deleted Posts
|
||||
-------------------
|
||||
|
||||
This script uses Instaloader to obtain a list of currently-online Instagram and
|
||||
compares it with the set of posts that you already have downloaded. It outputs
|
||||
a list of posts which are online but not offline (i.e. not yet downloaded) and a
|
||||
list of posts which are offline but not online (i.e. deleted in the profile).
|
||||
|
||||
.. literalinclude:: codesnippets/56_track_deleted.py
|
||||
|
||||
See also :func:`load_structure_from_file`, :meth:`Profile.from_username`,
|
||||
:meth:`Profile.get_posts`, :class:`Post`.
|
||||
|
||||
Discussed in :issue:`56`.
|
||||
|
||||
Only one Post per User
|
||||
----------------------
|
||||
|
||||
To download only the one most recent post from each user, this snippet creates a
|
||||
:class:`set` that contains the users of which a post has already been
|
||||
downloaded. While iterating the posts, it checks whether the post's owner
|
||||
already is in the set. If not, the post is downloaded from Instagram and the
|
||||
user is added to that set.
|
||||
|
||||
.. literalinclude:: codesnippets/113_only_one_per_user.py
|
||||
|
||||
See also :class:`Post`, :meth:`Instaloader.download_post`,
|
||||
:attr:`Post.owner_profile`, :class:`Profile`.
|
||||
|
||||
Discussed in :issue:`113`.
|
||||
|
||||
Upgrade Images by Local Copies
|
||||
------------------------------
|
||||
|
||||
The following script finds local versions of images fetched by Instaloader, in
|
||||
order to upgrade the downloaded images by locally-found versions with better
|
||||
quality. It uses image hashing to identify similar images.
|
||||
|
||||
`updgrade-instaloader-images.py <https://gist.github.com/pavelkryukov/15f93d19a99428a284a8bcec27e0187b>`__ (external link to GitHub Gist)
|
||||
|
||||
Discussed in :issue:`46`.
|
||||
|
||||
Add Captions to Images
|
||||
----------------------
|
||||
|
||||
Instaloader does not modify the downloaded JPEG file. However, one could combine
|
||||
it with an imaging library such as Pillow or PIL to render the caption on
|
||||
Instagram pictures. The following shows an approach.
|
||||
|
||||
.. literalinclude:: codesnippets/110_pil_captions.py
|
||||
|
||||
See also :attr:`Post.caption`, :attr:`Post.url`, :meth:`Post.from_shortcode`,
|
||||
:func:`load_structure_from_file`.
|
||||
|
||||
Discussed in :issue:`110`.
|
||||
|
||||
Metadata JSON Files
|
||||
-------------------
|
||||
|
||||
The JSON files Instaloader saves along with each Post contain all the metadata
|
||||
that has been retrieved from Instagram while downloading the picture and
|
||||
associated required information.
|
||||
|
||||
With `jq <https://stedolan.github.io/jq/>`__, a command-line JSON processor, the
|
||||
metadata can be easily post-processed. For example, Instaloader's JSON files can
|
||||
be pretty-formatted with:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
xzcat 2018-05-13_11-18-45_UTC.json.xz | jq .node
|
||||
|
||||
However, Instaloader tries to do as few metadata requests as possible, so,
|
||||
depending on how Instaloader has been invoked, it may occur that these files do
|
||||
not contain the complete available metadata structure. Nevertheless, the file
|
||||
can be loaded into Instaloader with :func:`load_structure_from_file` and the
|
||||
required metadata then be accessed via the :class:`Post` or :class:`Profile`
|
||||
attributes, which trigger an Instagram request if that particular information is
|
||||
not present in the JSON file.
|
||||
@@ -0,0 +1,20 @@
|
||||
from io import BytesIO
|
||||
|
||||
from requests import get
|
||||
from PIL import Image, ImageDraw
|
||||
from instaloader import *
|
||||
|
||||
L = Instaloader()
|
||||
|
||||
# Load Post instance
|
||||
post = load_structure_from_file(L.context, '2017-10-01_18-53-03_UTC.json.xz')
|
||||
# or post = Post.from_shortcode(L.context, SHORTCODE)
|
||||
|
||||
# Render caption
|
||||
image = Image.open(BytesIO(get(post.url).content))
|
||||
draw = ImageDraw.Draw(image)
|
||||
color = 'rgb(0, 0, 0)' # black color
|
||||
draw.text((300,100), post.caption.encode('latin1', errors='ignore'), fill=color)
|
||||
|
||||
# Save image
|
||||
image.save('test.jpg')
|
||||
@@ -0,0 +1,14 @@
|
||||
import instaloader
|
||||
|
||||
L = instaloader.Instaloader()
|
||||
|
||||
posts = L.get_hashtag_posts('milfgarden')
|
||||
|
||||
users = set()
|
||||
|
||||
for post in posts:
|
||||
if not post.owner_profile in users:
|
||||
L.download_post(post, '#milfgarden')
|
||||
users.add(post.owner_profile)
|
||||
else:
|
||||
print("{} from {} skipped.".format(post, post.owner_profile))
|
||||
@@ -0,0 +1,27 @@
|
||||
import instaloader
|
||||
|
||||
L = instaloader.Instaloader()
|
||||
|
||||
USER = 'your_account'
|
||||
PROFILE = USER
|
||||
|
||||
# Your preferred way of logging in:
|
||||
L.load_session_from_file(USER)
|
||||
|
||||
profile = instaloader.Profile.from_username(L.context, PROFILE)
|
||||
|
||||
likes = set()
|
||||
print('Fetching likes of all posts of profile {}.'.format(profile.username))
|
||||
for post in profile.get_posts():
|
||||
print(post)
|
||||
likes = likes | set(post.get_likes())
|
||||
|
||||
print('Fetching followers of profile {}.'.format(profile.username))
|
||||
followers = set(profile.get_followers())
|
||||
|
||||
ghosts = followers - likes
|
||||
|
||||
print('Storing ghosts into file.')
|
||||
with open('/YOUR PATH/inactive-users.txt', 'w') as f:
|
||||
for ghost in ghosts:
|
||||
print(ghost.username, file=f)
|
||||
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
from itertools import dropwhile, takewhile
|
||||
|
||||
import instaloader
|
||||
|
||||
L = instaloader.Instaloader()
|
||||
|
||||
posts = L.get_hashtag_posts('milfgarden')
|
||||
# or
|
||||
# posts = instaloader.Profile.from_username(L.context, PROFILE).get_posts()
|
||||
|
||||
SINCE = datetime(2015, 5, 1)
|
||||
UNTIL = datetime(2015, 3, 1)
|
||||
|
||||
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
|
||||
print(post.date)
|
||||
L.download_post(post, '#milfgarden')
|
||||
@@ -0,0 +1,34 @@
|
||||
from glob import glob
|
||||
from sys import argv
|
||||
from os import chdir
|
||||
|
||||
from instaloader import Instaloader, Post, Profile, load_structure_from_file
|
||||
|
||||
# Instaloader instantiation - you may pass additional arguments to the constructor here
|
||||
L = Instaloader()
|
||||
|
||||
# If desired, load session previously saved with `instaloader -l USERNAME`:
|
||||
#L.load_session_from_file(USERNAME)
|
||||
|
||||
try:
|
||||
TARGET = argv[1]
|
||||
except IndexError:
|
||||
raise SystemExit("Pass profile name as argument!")
|
||||
|
||||
# Obtain set of posts that are on hard disk
|
||||
chdir(TARGET)
|
||||
offline_posts = set(filter(lambda s: isinstance(s, Post),
|
||||
(load_structure_from_file(L.context, file)
|
||||
for file in (glob('*.json.xz') + glob('*.json')))))
|
||||
|
||||
# Obtain set of posts that are currently online
|
||||
post_iterator = Profile.from_username(L.context, TARGET).get_posts()
|
||||
online_posts = set(post_iterator)
|
||||
|
||||
if online_posts - offline_posts:
|
||||
print("Not yet downloaded posts:")
|
||||
print(" ".join(str(p) for p in (online_posts - offline_posts)))
|
||||
|
||||
if offline_posts - online_posts:
|
||||
print("Deleted posts:")
|
||||
print(" ".join(str(p) for p in (offline_posts - online_posts)))
|
||||
+4
-1
@@ -39,7 +39,8 @@ import docs.sphinx_autodoc_typehints as sphinx_autodoc_typehints
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.githubpages',
|
||||
'sphinx.ext.intersphinx'
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.extlinks'
|
||||
]
|
||||
|
||||
autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members']
|
||||
@@ -49,6 +50,8 @@ autodoc_member_order = 'bysource'
|
||||
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
|
||||
'requests': ('http://docs.python-requests.org/en/master', None)}
|
||||
|
||||
extlinks = {'issue': ('https://github.com/instaloader/instaloader/issues/%s', 'issue #')}
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
|
||||
+25
-5
@@ -1,3 +1,5 @@
|
||||
.. _contributing:
|
||||
|
||||
Contributing to Instaloader
|
||||
===========================
|
||||
|
||||
@@ -23,10 +25,10 @@ following in mind:
|
||||
|
||||
- Briefly **check whether the bug has already been reported**. If you find an
|
||||
issue reporting the same bug you encountered, comment there rather than
|
||||
opening a new issue. However, if unsure, it is advised to create a new issue.
|
||||
opening a new issue. However, if unsure, please create a new issue.
|
||||
|
||||
- State **how the bug can be reproduced**, i.e. how Instaloader was invoked
|
||||
when the problem occured (of course, you may anonymize profile names etc.).
|
||||
when the problem occurred (of course, you may anonymize profile names etc.).
|
||||
|
||||
- Include all **error messages and tracebacks** in the report.
|
||||
|
||||
@@ -47,13 +49,13 @@ few things to consider:
|
||||
you forked from the most recent branch.
|
||||
|
||||
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
|
||||
``instaloader.py``. The file ``.travis.yml`` in the project's root directory
|
||||
the source. The file ``.travis.yml`` in the project's root directory
|
||||
shows how it is invoked. Note that sometimes it might be better to disable a
|
||||
warning rather than adapting the code to Pylint's desires.
|
||||
|
||||
- The documentation source is located in the ``docs`` folder. The file
|
||||
``cli-options.rst`` is merely an RST-formatted copy of ``ìnstaloader --help``
|
||||
output, of which the source is near the bottom of ``instaloader.py``.
|
||||
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
|
||||
output, of which the source is in ``instaloader/__main__.py``.
|
||||
|
||||
- Feel free to contact us, even if you "only" have Proof-of-Concepts or
|
||||
not-fully integrated changes. They already might be an advance for the
|
||||
@@ -81,3 +83,21 @@ Please consider the following:
|
||||
- **Be patient**. Naturally, bugs and pull requests have a higher priority than
|
||||
feature suggestions. Keep in mind that this is a free software project, and
|
||||
unfortunately we only have limited time to work on it.
|
||||
|
||||
Donations
|
||||
---------
|
||||
|
||||
.. donations-start
|
||||
|
||||
It is a pleasure for us to share our Instaloader to the world, and we are proud
|
||||
to have attracted such an active and motivating community, with so many users
|
||||
who share their suggestions and ideas with us. Buying a community-sponsored beer
|
||||
or coffee from time to time is very likely to further raise our passion for the
|
||||
development of Instaloader.
|
||||
|
||||
We have a PayPal.Me link for donations: `PayPal.me/aandergr
|
||||
<https://www.paypal.me/aandergr>`__
|
||||
|
||||
.. donations-end
|
||||
|
||||
.. (Discussion in :issue:`130`)
|
||||
|
||||
@@ -59,6 +59,7 @@ Instaloader Documentation
|
||||
basic-usage
|
||||
cli-options
|
||||
as-module
|
||||
codesnippets
|
||||
contributing
|
||||
|
||||
Useful Links
|
||||
@@ -69,10 +70,23 @@ Useful Links
|
||||
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
|
||||
- `Version History <https://github.com/instaloader/instaloader/releases>`__
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: disclaimer-start
|
||||
:end-before: disclaimer-end
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
As an open source project, Instaloader heavily depends on the contributions from
|
||||
its community. See :ref:`contributing` for how you may help Instaloader to
|
||||
become an even greater tool.
|
||||
|
||||
.. include:: contributing.rst
|
||||
:start-after: donations-start
|
||||
:end-before: donations-end
|
||||
|
||||
..
|
||||
* :ref:`genindex`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.0.3'
|
||||
__version__ = '4.0.5'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -270,14 +270,15 @@ class Instaloader:
|
||||
with self.context.get_anonymous_session() as anonymous_session:
|
||||
date_object = datetime.strptime(anonymous_session.head(profile_pic_url).headers["Last-Modified"],
|
||||
'%a, %d %b %Y %H:%M:%S GMT')
|
||||
profile_pic_extension = 'jpg'
|
||||
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
||||
format_string_contains_key(self.dirname_pattern, 'target'))):
|
||||
filename = '{0}/{1}_UTC_profile_pic.{2}'.format(self.dirname_pattern.format(profile=profile.username.lower(),
|
||||
target=profile.username.lower()),
|
||||
_epoch_to_string(date_object), profile_pic_url[-3:])
|
||||
_epoch_to_string(date_object), profile_pic_extension)
|
||||
else:
|
||||
filename = '{0}/{1}_{2}_UTC_profile_pic.{3}'.format(self.dirname_pattern.format(), profile.username.lower(),
|
||||
_epoch_to_string(date_object), profile_pic_url[-3:])
|
||||
_epoch_to_string(date_object), profile_pic_extension)
|
||||
if os.path.isfile(filename):
|
||||
self.context.log(filename + ' already exists')
|
||||
return None
|
||||
|
||||
@@ -93,10 +93,9 @@ class Post:
|
||||
return b64encode(mediaid.to_bytes(9, 'big'), b'-_').decode().replace('A', ' ').lstrip().replace(' ', 'A')
|
||||
|
||||
def _asdict(self):
|
||||
node = self._node
|
||||
if self._full_metadata_dict:
|
||||
node = self._full_metadata_dict
|
||||
else:
|
||||
node = self._node
|
||||
node.update(self._full_metadata_dict)
|
||||
if self._owner_profile:
|
||||
node['owner'] = self.owner_profile._asdict()
|
||||
if self._location:
|
||||
@@ -206,10 +205,7 @@ class Post:
|
||||
@property
|
||||
def typename(self) -> str:
|
||||
"""Type of post, GraphImage, GraphVideo or GraphSidecar"""
|
||||
if '__typename' in self._node:
|
||||
return self._node['__typename']
|
||||
# if __typename is not in node, it is an old image or video
|
||||
return 'GraphImage'
|
||||
return self._field('__typename')
|
||||
|
||||
def get_sidecar_nodes(self) -> Iterator[PostSidecarNode]:
|
||||
"""Sidecar nodes of a Post with typename==GraphSidecar."""
|
||||
|
||||
@@ -16,6 +16,9 @@ NORMAL_MAX_COUNT = 2
|
||||
PAGING_MAX_COUNT = 15
|
||||
PRIVATE_PROFILE = "aandergr"
|
||||
|
||||
# Preserve query timestamps (rate control) between tests to not get rate limited
|
||||
instaloadercontext_query_timestamps = list()
|
||||
|
||||
|
||||
class TestInstaloaderAnonymously(unittest.TestCase):
|
||||
|
||||
@@ -27,23 +30,31 @@ class TestInstaloaderAnonymously(unittest.TestCase):
|
||||
download_comments=True,
|
||||
save_metadata=True)
|
||||
self.L.context.raise_all_errors = True
|
||||
self.L.context.query_timestamps = instaloadercontext_query_timestamps
|
||||
|
||||
def tearDown(self):
|
||||
global instaloadercontext_query_timestamps
|
||||
instaloadercontext_query_timestamps = self.L.context.query_timestamps
|
||||
self.L.close()
|
||||
os.chdir('/')
|
||||
print("Removing {}".format(self.dir))
|
||||
shutil.rmtree(self.dir)
|
||||
|
||||
def post_paging_test(self, iterator):
|
||||
previous_post = None
|
||||
for post in islice(iterator, PAGING_MAX_COUNT):
|
||||
print(post)
|
||||
if previous_post:
|
||||
self.assertTrue(post.date_utc < previous_post.date_utc)
|
||||
previous_post = post
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_public_profile_download(self):
|
||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
|
||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
|
||||
|
||||
def test_public_profile_paging(self):
|
||||
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
|
||||
|
||||
def test_profile_pic_download(self):
|
||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic_only=True)
|
||||
@@ -87,10 +98,7 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
||||
print(item)
|
||||
|
||||
def test_private_profile_paging(self):
|
||||
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
||||
|
||||
def test_feed_download(self):
|
||||
self.L.download_feed_posts(NORMAL_MAX_COUNT)
|
||||
|
||||
Reference in New Issue
Block a user