mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-17 06:02:18 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a039b9c8c | ||
|
|
b1014bfbfd | ||
|
|
d246268630 | ||
|
|
23a0e32e8e | ||
|
|
597e5977b2 | ||
|
|
00f6f47fa9 | ||
|
|
61711f3b6d | ||
|
|
be477e8a88 | ||
|
|
02509d3c40 | ||
|
|
6e3c13b5d3 | ||
|
|
84c2a823c4 | ||
|
|
f2660f7c02 | ||
|
|
73f85b3316 | ||
|
|
86f8b2f018 |
+4
-3
@@ -1,11 +1,12 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.5-dev"
|
||||
- "3.6"
|
||||
- "3.6-dev"
|
||||
- "3.7-dev"
|
||||
- "nightly"
|
||||
install:
|
||||
- pip install pylint requests
|
||||
script:
|
||||
- python3 -m pylint -r n -d bad-whitespace,bad-continuation,missing-docstring,multiple-imports,too-many-arguments,locally-disabled instaloader
|
||||
- python3 -m pylint -r n -d bad-whitespace,bad-continuation,missing-docstring,multiple-imports,too-many-arguments,locally-disabled,line-too-long,invalid-sequence-index instaloader
|
||||
|
||||
+10
-14
@@ -5,34 +5,30 @@ Instaloader
|
||||
Installation
|
||||
------------
|
||||
|
||||
instaloader is written in Python, thus ensure having
|
||||
`Python <https://www.python.org/>`__ (at least version 3.3) installed.
|
||||
Instaloader is written in Python, thus ensure having
|
||||
`Python <https://www.python.org/>`__ (at least version 3.5) installed.
|
||||
|
||||
If you intend to use this tool under Windows, it is recommended
|
||||
to install
|
||||
`win-unicode-console <https://pypi.python.org/pypi/win_unicode_console>`__.
|
||||
|
||||
If you have `pip <https://pypi.python.org/pypi/pip>`__ installed, you
|
||||
may install instaloader using
|
||||
may install Instaloader using
|
||||
|
||||
::
|
||||
|
||||
pip3 install instaloader
|
||||
|
||||
Alternatively, you may `download instaloader from
|
||||
github <https://github.com/Thammus/instaloader/releases>`__, unzip it
|
||||
and invoke bundled ``setup.py`` (requiring
|
||||
`setuptools <https://pypi.python.org/pypi/setuptools>`__) to install it:
|
||||
Alternatively, to get the most current version of Instaloader from our
|
||||
`Git repository <https://github.com/Thammus/instaloader>`__:
|
||||
|
||||
::
|
||||
|
||||
python3 setup.py install [--user]
|
||||
pip3 install git+https://github.com/Thammus/instaloader
|
||||
|
||||
(pass ``--user`` to install it for your user only instead of globally)
|
||||
|
||||
instaloader requires
|
||||
Instaloader requires
|
||||
`requests <https://pypi.python.org/pypi/requests>`__, which
|
||||
will be installed automatically by setup.py, if not already installed.
|
||||
will be installed automatically, if it is not already installed.
|
||||
|
||||
How to automatically download pictures from Instagram
|
||||
-----------------------------------------------------
|
||||
@@ -52,7 +48,7 @@ To later update your local copy of that profile, you may run
|
||||
|
||||
instaloader --fast-update profile [profile ...]
|
||||
|
||||
When ``--fast-update`` is given, instaloder terminates when arriving at
|
||||
When ``--fast-update`` is given, Instaloader terminates when arriving at
|
||||
the first already-downloaded picture.
|
||||
|
||||
Instaloader can also be used to **download private profiles**. To do so,
|
||||
@@ -86,7 +82,7 @@ To get a list of other helpful flags, run ``instaloader --help``.
|
||||
Usage as library
|
||||
----------------
|
||||
|
||||
You may also use parts of instaloader as library to do other interesting
|
||||
You may also use parts of Instaloader as library to do other interesting
|
||||
things.
|
||||
|
||||
For example, to get a list of all followers of a profile as well as
|
||||
|
||||
+63
-41
@@ -6,6 +6,9 @@ of profiles (even if private), from your feed or from all followees of a given p
|
||||
import re, json, datetime, shutil, os, time, random, sys, pickle, getpass, tempfile
|
||||
from argparse import ArgumentParser
|
||||
from io import BytesIO
|
||||
from numbers import Real
|
||||
from typing import List, Optional, Any, Dict, Callable
|
||||
|
||||
import requests, requests.utils
|
||||
|
||||
# To get version from setup.py for instaloader --version
|
||||
@@ -58,10 +61,12 @@ def _log(*msg, sep='', end='\n', flush=False, quiet=False):
|
||||
print(*msg, sep=sep, end=end, flush=flush)
|
||||
|
||||
|
||||
def get_json(name, session, max_id=0, sleep=True):
|
||||
def get_json(name: str, session: requests.Session, max_id: int = 0, sleep: bool = True) -> Optional[Dict[str, Any]]:
|
||||
"""Return JSON of a profile"""
|
||||
resp = session.get('http://www.instagram.com/'+name,
|
||||
params={'max_id': max_id})
|
||||
if max_id == 0:
|
||||
resp = session.get('https://www.instagram.com/'+name)
|
||||
else:
|
||||
resp = session.get('https://www.instagram.com/'+name, params={'max_id': max_id})
|
||||
if sleep:
|
||||
time.sleep(4 * random.random() + 1)
|
||||
match = re.search('window\\._sharedData = .*<', resp.text)
|
||||
@@ -71,7 +76,7 @@ def get_json(name, session, max_id=0, sleep=True):
|
||||
return json.loads(match.group(0)[21:-2])
|
||||
|
||||
|
||||
def get_username_by_id(session, profile_id):
|
||||
def get_username_by_id(session: requests.Session, profile_id: int) -> str:
|
||||
"""To get the current username of a profile, given its unique ID, this function can be used.
|
||||
session is required to be a logged-in (i.e. non-anonymous) session."""
|
||||
tempsession = copy_session(session)
|
||||
@@ -92,20 +97,20 @@ def get_username_by_id(session, profile_id):
|
||||
str(profile_id) + ").")
|
||||
|
||||
|
||||
def get_id_by_username(profile):
|
||||
def get_id_by_username(profile: str) -> int:
|
||||
"""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 this function."""
|
||||
data = get_json(profile, get_anonymous_session())
|
||||
if len(data["entry_data"]) == 0 or "ProfilePage" not in data("entry_data"):
|
||||
if len(data["entry_data"]) == 0 or "ProfilePage" not in data["entry_data"]:
|
||||
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
|
||||
return int(data['entry_data']['ProfilePage'][0]['user']['id'])
|
||||
|
||||
|
||||
def _epoch_to_string(epoch):
|
||||
def _epoch_to_string(epoch: Real) -> str:
|
||||
return datetime.datetime.fromtimestamp(epoch).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
|
||||
|
||||
def get_followees(profile, session):
|
||||
def get_followees(profile: str, session: requests.Session) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve list of followees of given profile
|
||||
|
||||
@@ -162,7 +167,7 @@ def get_followees(profile, session):
|
||||
raise LoginRequiredException("Login required to gather followees.")
|
||||
|
||||
|
||||
def download_pic(name, url, date_epoch, outputlabel=None, quiet=False):
|
||||
def download_pic(name: str, url: str, date_epoch: Real, outputlabel: Optional[str] = None, quiet: bool = False) -> bool:
|
||||
"""Downloads and saves picture with given url under given directory with given timestamp.
|
||||
Returns true, if file was actually downloaded, i.e. updated."""
|
||||
if outputlabel is None:
|
||||
@@ -186,7 +191,7 @@ def download_pic(name, url, date_epoch, outputlabel=None, quiet=False):
|
||||
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
|
||||
|
||||
|
||||
def save_caption(name, date_epoch, caption, shorter_output=False, quiet=False):
|
||||
def save_caption(name: str, date_epoch: Real, caption: str, shorter_output: bool = False, quiet: bool = False) -> None:
|
||||
"""Updates picture caption"""
|
||||
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '.txt'
|
||||
pcaption = caption.replace('\n', ' ').strip()
|
||||
@@ -229,7 +234,7 @@ def save_caption(name, date_epoch, caption, shorter_output=False, quiet=False):
|
||||
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
|
||||
|
||||
|
||||
def save_location(name, location_json, date_epoch):
|
||||
def save_location(name: str, location_json: Dict[str, str], date_epoch: Real) -> None:
|
||||
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '_location.txt'
|
||||
location_string = location_json["name"]+"\n" + \
|
||||
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n" \
|
||||
@@ -240,7 +245,7 @@ def save_location(name, location_json, date_epoch):
|
||||
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
|
||||
|
||||
|
||||
def download_profilepic(name, url, quiet=False):
|
||||
def download_profilepic(name: str, url: str, quiet: bool = False) -> None:
|
||||
"""Downloads and saves profile pic with given url."""
|
||||
date_object = datetime.datetime.strptime(requests.head(url).headers["Last-Modified"], \
|
||||
'%a, %d %b %Y %H:%M:%S GMT')
|
||||
@@ -267,14 +272,14 @@ def download_profilepic(name, url, quiet=False):
|
||||
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
|
||||
|
||||
|
||||
def get_default_session_filename(username):
|
||||
def get_default_session_filename(username: str) -> str:
|
||||
"""Returns default session filename for given username."""
|
||||
dirname = tempfile.gettempdir() + "/" + ".instaloader-" + getpass.getuser()
|
||||
filename = dirname + "/" + "session-" + username
|
||||
return filename
|
||||
|
||||
|
||||
def save_session(session, username, filename=None, quiet=False):
|
||||
def save_session(session: requests.Session, username: str, filename: Optional[str] = None, quiet: bool = False) -> None:
|
||||
"""Saves requests.Session object."""
|
||||
if filename is None:
|
||||
filename = get_default_session_filename(username)
|
||||
@@ -288,7 +293,7 @@ def save_session(session, username, filename=None, quiet=False):
|
||||
_log("Saved session to %s." % filename, quiet=quiet)
|
||||
|
||||
|
||||
def load_session(username, filename=None, quiet=False):
|
||||
def load_session(username: str, filename: Optional[str] = None, quiet: bool = False) -> requests.Session:
|
||||
"""Returns loaded requests.Session object, or None if not found."""
|
||||
if filename is None:
|
||||
filename = get_default_session_filename(username)
|
||||
@@ -304,7 +309,7 @@ def load_session(username, filename=None, quiet=False):
|
||||
pass
|
||||
|
||||
|
||||
def copy_session(session):
|
||||
def copy_session(session: requests.Session) -> requests.Session:
|
||||
"""Duplicates a requests.Session."""
|
||||
new = requests.Session()
|
||||
new.cookies = \
|
||||
@@ -313,7 +318,7 @@ def copy_session(session):
|
||||
return new
|
||||
|
||||
|
||||
def test_login(session):
|
||||
def test_login(session: requests.Session) -> Optional[str]:
|
||||
"""Returns the Instagram username to which given requests.Session object belongs, or None."""
|
||||
if session is None:
|
||||
return
|
||||
@@ -324,7 +329,7 @@ def test_login(session):
|
||||
return data['config']['viewer']['username']
|
||||
|
||||
|
||||
def default_http_header(empty_session_only=False):
|
||||
def default_http_header(empty_session_only: bool = False) -> Dict[str, str]:
|
||||
"""Returns default HTTP header we use for requests."""
|
||||
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \
|
||||
'(KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36'
|
||||
@@ -347,7 +352,7 @@ def default_http_header(empty_session_only=False):
|
||||
return header
|
||||
|
||||
|
||||
def get_anonymous_session():
|
||||
def get_anonymous_session() -> requests.Session:
|
||||
"""Returns our default anonymous requests.Session object."""
|
||||
session = requests.Session()
|
||||
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
|
||||
@@ -357,7 +362,7 @@ def get_anonymous_session():
|
||||
return session
|
||||
|
||||
|
||||
def get_session(user, passwd):
|
||||
def get_session(user: str, passwd: str) -> requests.Session:
|
||||
"""Log in to instagram with given username and password and return session object"""
|
||||
session = requests.Session()
|
||||
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
|
||||
@@ -380,7 +385,7 @@ def get_session(user, passwd):
|
||||
raise ConnectionException('Login error! Connection error!')
|
||||
|
||||
|
||||
def get_feed_json(session, end_cursor=None, sleep=True):
|
||||
def get_feed_json(session: requests.Session, end_cursor: str = None, sleep: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Get JSON of the user's feed.
|
||||
|
||||
@@ -425,7 +430,7 @@ def get_feed_json(session, end_cursor=None, sleep=True):
|
||||
return json.loads(resp.text)
|
||||
|
||||
|
||||
def get_location(node, session, sleep=True):
|
||||
def get_location(node: Dict[str, str], session: requests.Session, sleep: bool = True) -> Dict[str, str]:
|
||||
pic_json = get_json("p/" + node["code"], session, sleep=sleep)
|
||||
if pic_json["entry_data"]["PostPage"][0]["media"]["location"] is not None:
|
||||
location_json = get_json("explore/locations/" +
|
||||
@@ -434,9 +439,9 @@ def get_location(node, session, sleep=True):
|
||||
return location_json["entry_data"]["LocationsPage"][0]["location"]
|
||||
|
||||
|
||||
def download_node(node, session, name,
|
||||
download_videos=True, geotags=False,
|
||||
sleep=True, shorter_output=False, quiet=False):
|
||||
def download_node(node: Dict[str, Any], session: requests.Session, name: str,
|
||||
download_videos: bool = True, geotags: bool = False,
|
||||
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> bool:
|
||||
"""
|
||||
Download everything associated with one instagram node, i.e. picture, caption and video.
|
||||
|
||||
@@ -444,6 +449,7 @@ def download_node(node, session, name,
|
||||
:param session: Session
|
||||
:param name: Name of profile to which this node belongs
|
||||
:param download_videos: True, if videos should be downloaded
|
||||
:param geotags: Download geotags
|
||||
:param sleep: Sleep between requests to instagram server
|
||||
:param shorter_output: Shorten log output by not printing captions
|
||||
:param quiet: Suppress output
|
||||
@@ -469,9 +475,10 @@ def download_node(node, session, name,
|
||||
return downloaded
|
||||
|
||||
|
||||
def download_feed_pics(session, max_count=None, fast_update=False, filter_func=None,
|
||||
download_videos=True, geotags=False,
|
||||
shorter_output=False, sleep=True, quiet=False):
|
||||
def download_feed_pics(session: requests.Session, max_count: int = None, fast_update: bool = False,
|
||||
filter_func: Optional[Callable[[Dict[str, Dict[str, Any]]], bool]] = None,
|
||||
download_videos: bool = True, geotags: bool = False,
|
||||
shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
|
||||
"""
|
||||
Download pictures from the user's feed.
|
||||
|
||||
@@ -484,6 +491,7 @@ def download_feed_pics(session, max_count=None, fast_update=False, filter_func=N
|
||||
:param fast_update: If true, abort when first already-downloaded picture is encountered
|
||||
:param filter_func: function(node), which returns True if given picture should not be downloaded
|
||||
:param download_videos: True, if videos should be downloaded
|
||||
:param geotags: Download geotags
|
||||
:param shorter_output: Shorten log output by not printing captions
|
||||
:param sleep: Sleep between requests to instagram server
|
||||
:param quiet: Suppress output
|
||||
@@ -509,7 +517,7 @@ def download_feed_pics(session, max_count=None, fast_update=False, filter_func=N
|
||||
sleep=sleep)
|
||||
|
||||
|
||||
def check_id(profile, session, json_data, quiet=False):
|
||||
def check_id(profile: str, session: requests.Session, json_data: Dict[str, Any], quiet: bool = False) -> str:
|
||||
"""
|
||||
Consult locally stored ID of profile with given name, check whether ID matches and whether name
|
||||
has changed and return current name of the profile, and store ID of profile.
|
||||
@@ -546,8 +554,9 @@ def check_id(profile, session, json_data, quiet=False):
|
||||
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
|
||||
|
||||
|
||||
def download(name, session, profile_pic_only=False, download_videos=True, geotags=False,
|
||||
fast_update=False, shorter_output=False, sleep=True, quiet=False):
|
||||
def download(name: str, session: requests.Session, profile_pic_only: bool = False, download_videos: bool = True,
|
||||
geotags: bool = False,
|
||||
fast_update: bool = False, shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
|
||||
"""Download one profile"""
|
||||
# pylint:disable=too-many-branches,too-many-locals
|
||||
# Get profile main page json
|
||||
@@ -599,7 +608,8 @@ def download(name, session, profile_pic_only=False, download_videos=True, geotag
|
||||
return
|
||||
data = get_json(name, session, max_id=get_last_id(data), sleep=sleep)
|
||||
|
||||
def get_logged_in_session(username, password=None, quiet=False):
|
||||
|
||||
def get_logged_in_session(username: str, password: Optional[str] = None, quiet: bool = False) -> requests.Session:
|
||||
"""Logs in and returns session, asking user for password if needed"""
|
||||
if password is not None:
|
||||
return get_session(username, password)
|
||||
@@ -614,9 +624,12 @@ def get_logged_in_session(username, password=None, quiet=False):
|
||||
print(err, file=sys.stderr)
|
||||
password = None
|
||||
|
||||
def download_profiles(profilelist, username=None, password=None, sessionfile=None,
|
||||
profile_pic_only=False, download_videos=True, geotags=False, fast_update=False,
|
||||
sleep=True, shorter_output=False, quiet=False):
|
||||
|
||||
def download_profiles(profilelist: List[str], username: Optional[str] = None, password: Optional[str] = None,
|
||||
sessionfile: Optional[str] = None,
|
||||
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
|
||||
fast_update: bool = False,
|
||||
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> None:
|
||||
"""Download set of profiles and handle sessions"""
|
||||
# pylint:disable=too-many-branches,too-many-locals
|
||||
# Login, if desired
|
||||
@@ -633,7 +646,7 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
|
||||
try:
|
||||
# Generate set of targets
|
||||
for pentry in profilelist:
|
||||
if pentry[0] == '@':
|
||||
if pentry[0] == '@' and username is not None:
|
||||
_log("Retrieving followees of %s..." % pentry[1:], quiet=quiet)
|
||||
followees = get_followees(pentry[1:], session)
|
||||
targets.update([followee['username'] for followee in followees])
|
||||
@@ -657,8 +670,16 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
|
||||
# Iterate through targets list and download them
|
||||
for target in targets:
|
||||
try:
|
||||
download(target, session, profile_pic_only, download_videos,
|
||||
geotags, fast_update, shorter_output, sleep, quiet)
|
||||
try:
|
||||
download(target, session, profile_pic_only, download_videos,
|
||||
geotags, fast_update, shorter_output, sleep, quiet)
|
||||
except ProfileNotExistsException as err:
|
||||
if username is not None:
|
||||
_log("\"Profile not exists\" - Trying again anonymously, helps in case you are just blocked")
|
||||
download(target, get_anonymous_session(), profile_pic_only, download_videos,
|
||||
geotags, fast_update, shorter_output, sleep, quiet)
|
||||
else:
|
||||
raise err
|
||||
except NonfatalException as err:
|
||||
failedtargets.append(target)
|
||||
print(err, file=sys.stderr)
|
||||
@@ -672,11 +693,12 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
|
||||
save_session(session, username, sessionfile, quiet=quiet)
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
parser = ArgumentParser(description=__doc__,
|
||||
epilog="Report issues at https://github.com/Thammus/instaloader/issues.")
|
||||
parser.add_argument('profile', nargs='*',
|
||||
help='Name of profile to download; @<profile> to download all followees of '
|
||||
help='Name of profile to download; If --login is given: @<profile> to download all followees of '
|
||||
'<profile>; or the special targets :feed-all or :feed-liked to '
|
||||
'download pictures from your feed if --login is given (using '
|
||||
'download pictures from your feed (using '
|
||||
'--fast-update is recommended).')
|
||||
parser.add_argument('--version', action='version',
|
||||
version=__version__)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import sys
|
||||
from setuptools import setup
|
||||
|
||||
if sys.version_info < (3, 3):
|
||||
sys.exit('Instaloader requires Python >= 3.3.')
|
||||
if sys.version_info < (3, 5):
|
||||
sys.exit('Instaloader requires Python >= 3.5.')
|
||||
|
||||
setup(
|
||||
name='instaloader',
|
||||
version='1.2',
|
||||
version='1.2.2',
|
||||
py_modules=['instaloader'],
|
||||
url='https://github.com/Thammus/instaloader',
|
||||
license='MIT',
|
||||
@@ -19,7 +19,7 @@ setup(
|
||||
'profile.',
|
||||
long_description=open('README.rst').read(),
|
||||
install_requires=['requests>=2.4'],
|
||||
python_requires='>=3.3',
|
||||
python_requires='>=3.5',
|
||||
entry_points={'console_scripts': ['instaloader=instaloader:main']},
|
||||
zip_safe=True,
|
||||
keywords='instagram downloader',
|
||||
@@ -29,8 +29,6 @@ setup(
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
|
||||
Reference in New Issue
Block a user