├── MANIFEST.in ├── tiktok ├── __init__.py ├── __version__.py ├── structures │ ├── base.py │ ├── __init__.py │ ├── hot.py │ ├── address.py │ ├── user.py │ ├── video.py │ ├── music.py │ └── topic.py ├── utils │ ├── __init__.py │ ├── parse.py │ ├── common.py │ ├── signature.py │ ├── fetch.py │ ├── type.py │ ├── tranform.py │ └── scripts │ │ └── byted-acrawler.js ├── downloaders │ ├── __init__.py │ ├── music.py │ ├── video.py │ └── base.py ├── hot │ ├── __init__.py │ ├── search.py │ ├── video.py │ ├── energy.py │ ├── music.py │ └── trend.py ├── handlers │ ├── __init__.py │ ├── base.py │ ├── video.py │ ├── music.py │ ├── db.py │ └── file.py └── config.py ├── examples ├── initial_database.py ├── retrying_test.py ├── save_trends_to_mongo_and_video_music_file.py ├── save_trends_to_mongo.py ├── save_trends_to_file.py ├── save_trends_to_mongo_and_file.py ├── save_hot_videos_and_musics_to_file.py ├── parse_params.py └── request_test.py ├── requirements.txt ├── LICENSE ├── README.MD ├── .gitignore └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include *.tmpl 4 | 5 | include LICENSE -------------------------------------------------------------------------------- /tiktok/__init__.py: -------------------------------------------------------------------------------- 1 | import tiktok.hot 2 | import tiktok.handlers 3 | import tiktok.downloaders -------------------------------------------------------------------------------- /tiktok/__version__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 3, 6) 2 | 3 | # define version 4 | __version__ = '.'.join(map(str, VERSION)) 5 | -------------------------------------------------------------------------------- /tiktok/structures/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /tiktok/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import urllib3 2 | 3 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 4 | 5 | from tiktok.utils.fetch import fetch 6 | -------------------------------------------------------------------------------- /tiktok/downloaders/__init__.py: -------------------------------------------------------------------------------- 1 | from tiktok.downloaders.base import Downloader 2 | from tiktok.downloaders.video import VideoDownloader 3 | from tiktok.downloaders.music import MusicDownloader 4 | -------------------------------------------------------------------------------- /tiktok/hot/__init__.py: -------------------------------------------------------------------------------- 1 | from tiktok.hot.search import search 2 | from tiktok.hot.video import video 3 | from tiktok.hot.energy import energy 4 | from tiktok.hot.music import music 5 | from tiktok.hot.trend import trend 6 | -------------------------------------------------------------------------------- /tiktok/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from tiktok.handlers.base import Handler 2 | from tiktok.handlers.file import FileHandler 3 | from tiktok.handlers.db import DBHandler 4 | from tiktok.handlers.music import MusicFileHandler 5 | from tiktok.handlers.video import VideoFileHandler 6 | -------------------------------------------------------------------------------- /examples/initial_database.py: -------------------------------------------------------------------------------- 1 | from tiktok.handlers import DBHandler 2 | from tiktok.structures.base import Base 3 | 4 | 5 | if __name__ == '__main__': 6 | db_handler = DBHandler() 7 | Base.metadata.drop_all(bind=db_handler.get_engine()) 8 | Base.metadata.create_all(bind=db_handler.get_engine()) 9 | -------------------------------------------------------------------------------- /tiktok/handlers/base.py: -------------------------------------------------------------------------------- 1 | class Handler(object): 2 | 3 | def process(self, obj, **kwargs): 4 | """ 5 | parent method, you must implement this method in child object 6 | :param obj: 7 | :param kwargs: 8 | :return: 9 | """ 10 | raise NotImplementedError 11 | -------------------------------------------------------------------------------- /tiktok/utils/parse.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse, parse_qs 2 | 3 | 4 | def parse_query(url): 5 | """ 6 | get query dict of url 7 | :param url: 8 | :return: 9 | """ 10 | result = urlparse(url) 11 | query = result.query 12 | query_dict = parse_qs(query) 13 | query_dict = {k: v[0] for k, v in query_dict.items()} 14 | return query_dict 15 | -------------------------------------------------------------------------------- /examples/retrying_test.py: -------------------------------------------------------------------------------- 1 | from retrying import retry 2 | 3 | 4 | def need_retry(exception): 5 | return isinstance(exception, RuntimeError) 6 | 7 | 8 | @retry(stop_max_attempt_number=4, wait_random_min=1000, wait_random_max=3000, retry_on_exception=need_retry) 9 | def run(): 10 | print('Retrying...') 11 | raise RuntimeError 12 | 13 | 14 | if __name__ == '__main__': 15 | run() 16 | -------------------------------------------------------------------------------- /tiktok/structures/__init__.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures.hot import * 2 | from tiktok.structures.base import Base 3 | from tiktok.structures.music import TikTokMusic as Music 4 | from tiktok.structures.user import TikTokUser as User 5 | from tiktok.structures.video import TikTokVideo as Video 6 | from tiktok.structures.address import TikTokAddress as Address 7 | from tiktok.structures.topic import TikTokTopic as Topic 8 | -------------------------------------------------------------------------------- /tiktok/structures/hot.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | HotSearch = collections.namedtuple('HotSearch', ['datetime', 'data']) 4 | HotVideo = collections.namedtuple('HotVideo', ['datetime', 'data']) 5 | HotEnergy = collections.namedtuple('HotVideo', ['datetime', 'data']) 6 | HotMusic = collections.namedtuple('HotMusic', ['datetime', 'data']) 7 | HotTrend = collections.namedtuple('HotTrend', ['datetime', 'data', 'count', 'offset']) -------------------------------------------------------------------------------- /tiktok/handlers/video.py: -------------------------------------------------------------------------------- 1 | from tiktok.handlers import FileHandler 2 | from tiktok.structures import Video 3 | 4 | 5 | class VideoFileHandler(FileHandler): 6 | 7 | async def process(self, obj, **kwargs): 8 | """ 9 | process video obj 10 | :param obj: 11 | :param kwargs: 12 | :return: 13 | """ 14 | if isinstance(obj, Video): 15 | return await self._process(obj, **kwargs) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.5.4 2 | async==0.6.2 3 | async-timeout==3.0.1 4 | attrs==19.1.0 5 | certifi==2019.3.9 6 | chardet==3.0.4 7 | dateparser==0.7.1 8 | idna==2.8 9 | motor==2.0.0 10 | multidict==4.5.2 11 | Naked==0.1.31 12 | pymongo==3.8.0 13 | python-dateutil==2.8.0 14 | pytz==2019.1 15 | PyYAML==5.1 16 | regex==2019.4.14 17 | requests==2.21.0 18 | retrying==1.3.3 19 | six==1.12.0 20 | SQLAlchemy==1.3.3 21 | tqdm==4.31.1 22 | tzlocal==1.5.1 23 | urllib3==1.24.2 24 | yarl==1.3.0 25 | -------------------------------------------------------------------------------- /tiktok/handlers/music.py: -------------------------------------------------------------------------------- 1 | from tiktok.handlers import FileHandler 2 | from tiktok.structures import Music, Video 3 | 4 | 5 | class MusicFileHandler(FileHandler): 6 | 7 | async def process(self, obj, **kwargs): 8 | """ 9 | process music 10 | :param obj: 11 | :param kwargs: 12 | :return: 13 | """ 14 | if isinstance(obj, Video): 15 | obj = obj.music 16 | if isinstance(obj, Music): 17 | return await self._process(obj, **kwargs) 18 | -------------------------------------------------------------------------------- /tiktok/utils/common.py: -------------------------------------------------------------------------------- 1 | import dateparser 2 | 3 | 4 | def parse_datetime(string): 5 | """ 6 | parse string to datetime safely 7 | :param string: str to parse 8 | :return: datetime 9 | """ 10 | if not string: 11 | return None 12 | return dateparser.parse(str(string)) 13 | 14 | 15 | def first(array): 16 | """ 17 | get first element of list or None 18 | :param array: 19 | :return: 20 | """ 21 | if isinstance(array, list) and len(array) > 1: 22 | return array[0] 23 | return None 24 | -------------------------------------------------------------------------------- /tiktok/downloaders/music.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Music 2 | from tiktok.handlers import Handler 3 | from tiktok.downloaders import Downloader 4 | 5 | 6 | class MusicDownloader(Downloader): 7 | 8 | async def process_item(self, obj, **kwargs): 9 | """ 10 | process item 11 | :param obj: single obj 12 | :return: 13 | """ 14 | if isinstance(obj, Music): 15 | print('Processing', obj, '...') 16 | for handler in self.handlers: 17 | if isinstance(handler, Handler): 18 | await handler.process(obj, **kwargs) 19 | -------------------------------------------------------------------------------- /tiktok/downloaders/video.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Video 2 | from tiktok.handlers import Handler 3 | from tiktok.downloaders import Downloader 4 | 5 | 6 | class VideoDownloader(Downloader): 7 | 8 | async def process_item(self, obj, **kwargs): 9 | """ 10 | process item 11 | :param obj: single obj 12 | :return: 13 | """ 14 | if isinstance(obj, Video): 15 | print('Processing', obj, '...') 16 | for handler in self.handlers: 17 | if isinstance(handler, Handler): 18 | await handler.process(obj, **kwargs) 19 | -------------------------------------------------------------------------------- /tiktok/hot/search.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import HotSearch 2 | from tiktok.utils import fetch 3 | 4 | 5 | def search(hot_search_url, **kwargs): 6 | """ 7 | get hot search result 8 | :return: HotSearch object 9 | """ 10 | result = fetch(hot_search_url, **kwargs) 11 | # process json data 12 | datetime = result.get('data', {}).get('active_time') 13 | word_list = result.get('data', {}).get('word_list', []) 14 | data = [{'item': item.get('word'), 'hot_value': item.get('hot_value')} for item in word_list] 15 | # construct HotSearch object and return 16 | return HotSearch(datetime=datetime, data=data) 17 | -------------------------------------------------------------------------------- /tiktok/utils/signature.py: -------------------------------------------------------------------------------- 1 | from Naked.toolshed.shell import muterun_js 2 | import os 3 | 4 | 5 | def generate_signature(value): 6 | root_dir = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | response = muterun_js(root_dir + '/scripts/byted-acrawler.js', value) 9 | if response.exitcode == 0: 10 | # the command was successful, handle the standard output 11 | result = response.stdout.rstrip() 12 | else: 13 | # the command failed or the executable was not present, handle the standard error 14 | standard_err = response.stderr 15 | exit_code = response.exitcode 16 | print('Exit Status ' + exit_code + ': ' + standard_err) 17 | result = None 18 | return result 19 | -------------------------------------------------------------------------------- /tiktok/hot/video.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import HotVideo 2 | from tiktok.utils import fetch 3 | from tiktok.utils.tranform import data_to_video, parse_datetime 4 | 5 | 6 | def video(hot_video_url, **kwargs): 7 | """ 8 | get hot video result 9 | :return: HotVideo object 10 | """ 11 | result = fetch(hot_video_url, **kwargs) 12 | # process json data 13 | datetime = parse_datetime(result.get('active_time')) 14 | video_list = result.get('aweme_list', []) 15 | videos = [] 16 | for item in video_list: 17 | video = data_to_video(item.get('aweme_info')) 18 | video.hot_count = item.get('hot_value') 19 | videos.append(video) 20 | # construct HotVideo object and return 21 | return HotVideo(datetime=datetime, data=videos) 22 | -------------------------------------------------------------------------------- /tiktok/hot/energy.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import HotEnergy 2 | from tiktok.utils import fetch 3 | from tiktok.utils.common import parse_datetime 4 | from tiktok.utils.tranform import data_to_video 5 | 6 | 7 | def energy(hot_energy_url, **kwargs): 8 | """ 9 | get hot energy result 10 | :return: HotEnergy object 11 | """ 12 | result = fetch(hot_energy_url, **kwargs) 13 | # process json data 14 | datetime = parse_datetime(result.get('active_time')) 15 | video_list = result.get('aweme_list', []) 16 | videos = [] 17 | for item in video_list: 18 | video = data_to_video(item.get('aweme_info')) 19 | video.hot_count = item.get('hot_value') 20 | videos.append(video) 21 | # construct HotEnergy object and return 22 | return HotEnergy(datetime=datetime, data=videos) 23 | -------------------------------------------------------------------------------- /tiktok/hot/music.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import HotMusic 2 | from tiktok.utils.fetch import fetch 3 | from tiktok.utils.common import parse_datetime 4 | from tiktok.utils.tranform import data_to_music 5 | 6 | 7 | def music(hot_music_url, **kwargs): 8 | """ 9 | get hot music result 10 | :return: HotMusic object 11 | """ 12 | result = fetch(hot_music_url, **kwargs) 13 | # process json data 14 | datetime = parse_datetime(result.get('active_time')) 15 | # video_list = result.get('music_list', []) 16 | musics = [] 17 | music_list = result.get('music_list', []) 18 | for item in music_list: 19 | music = data_to_music(item.get('music_info', {})) 20 | music.hot_count = item.get('hot_value') 21 | musics.append(music) 22 | # construct HotMusic object and return 23 | return HotMusic(datetime=datetime, data=musics) 24 | -------------------------------------------------------------------------------- /examples/save_trends_to_mongo_and_video_music_file.py: -------------------------------------------------------------------------------- 1 | import tiktok 2 | from tiktok.structures import Topic, Music 3 | 4 | # define file handler and specify folder 5 | video_file_handler = tiktok.handlers.VideoFileHandler(folder='./videos') 6 | music_file_handler = tiktok.handlers.MusicFileHandler(folder='./musics') 7 | # define db handler 8 | db_handler = tiktok.handlers.DBHandler(conn_uri='localhost') 9 | # define downloader 10 | downloader = tiktok.downloaders.VideoDownloader([db_handler, video_file_handler, music_file_handler]) 11 | 12 | for result in tiktok.hot.trend(): 13 | for item in result.data: 14 | # download videos of topic/music for 30 max per 15 | if isinstance(item, Topic): 16 | print('Item', item) 17 | downloader.download(item.videos(max=30)) 18 | if isinstance(item, Music): 19 | print('Item', item) 20 | downloader.download(item.videos(max=30)) 21 | -------------------------------------------------------------------------------- /examples/save_trends_to_mongo.py: -------------------------------------------------------------------------------- 1 | import tiktok 2 | from tiktok.config import hot_trend_url, common_headers, common_parameter 3 | from tiktok.structures import Topic, Music 4 | 5 | # define handler 6 | db_handler = tiktok.handlers.DBHandler() 7 | file_handler = tiktok.handlers.FileHandler(folder='./videos') 8 | 9 | # define downloader and specify handler 10 | downloader = tiktok.downloaders.VideoDownloader([db_handler]) 11 | 12 | for result in tiktok.hot.trend(hot_trend_url, count=1, headers=common_headers): 13 | for item in result.data: 14 | # download videos of topic/music for 100 max per 15 | if isinstance(item, Topic): 16 | print('Get topic', item) 17 | downloader.download(item.videos(max=1), headers=common_headers, params=common_parameter) 18 | if isinstance(item, Music): 19 | print('Item', item) 20 | downloader.download(item.videos(max=1), headers=common_headers, params=common_parameter) 21 | -------------------------------------------------------------------------------- /examples/save_trends_to_file.py: -------------------------------------------------------------------------------- 1 | import tiktok 2 | from tiktok.config import hot_trend_url, common_headers, common_parameter 3 | from tiktok.structures import Topic, Music 4 | 5 | # define handler 6 | db_handler = tiktok.handlers.DBHandler() 7 | file_handler = tiktok.handlers.FileHandler(folder='./videos') 8 | 9 | # define downloader and specify handler 10 | downloader = tiktok.downloaders.VideoDownloader([file_handler]) 11 | 12 | for result in tiktok.hot.trend(hot_trend_url, count=1, headers=common_headers): 13 | for item in result.data: 14 | # download videos of topic/music for 100 max per 15 | if isinstance(item, Topic): 16 | print('Get topic', item) 17 | downloader.download(item.videos(max=1), headers=common_headers, params=common_parameter) 18 | if isinstance(item, Music): 19 | print('Item', item) 20 | downloader.download(item.videos(max=1), headers=common_headers, params=common_parameter) 21 | -------------------------------------------------------------------------------- /examples/save_trends_to_mongo_and_file.py: -------------------------------------------------------------------------------- 1 | import tiktok 2 | from tiktok.structures import Topic, Music 3 | from tiktok.config import hot_trend_url, common_headers, common_parameter 4 | 5 | # define file handler and specify folder 6 | file_handler = tiktok.handlers.FileHandler(folder='./videos') 7 | # define db handler 8 | db_handler = tiktok.handlers.DBHandler() 9 | # define downloader 10 | downloader = tiktok.downloaders.VideoDownloader([file_handler, db_handler]) 11 | 12 | for result in tiktok.hot.trend(hot_trend_url, count=1, headers=common_headers): 13 | for item in result.data: 14 | # download videos of topic/music for 200 max per 15 | if isinstance(item, Topic): 16 | print('Item', item) 17 | downloader.download(item.videos(max=30), headers=common_parameter, params=common_parameter) 18 | if isinstance(item, Music): 19 | print('Item', item) 20 | downloader.download(item.videos(max=30), headers=common_parameter, params=common_parameter) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019, Germey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tiktok/hot/trend.py: -------------------------------------------------------------------------------- 1 | from tiktok.utils import fetch 2 | from tiktok.config import common_parameter 3 | from tiktok.utils.tranform import data_to_music, data_to_topic 4 | from tiktok.structures.hot import HotTrend 5 | from tiktok.utils.common import parse_datetime 6 | 7 | 8 | def trend(hot_trend_url, count, **kwargs): 9 | """ 10 | get trend result 11 | :return: 12 | """ 13 | offset = 0 14 | while True: 15 | common_parameter['cursor'] = str(offset) 16 | result = fetch(hot_trend_url, params=common_parameter, **kwargs) 17 | category_list = result.get('category_list') 18 | datetime = parse_datetime(result.get('extra', {}).get('now')) 19 | final = [] 20 | for item in category_list: 21 | # process per category 22 | if item.get('desc') == '热门话题': 23 | final.append(data_to_topic(item.get('challenge_info', {}))) 24 | if item.get('desc') == '热门音乐': 25 | final.append(data_to_music(item.get('music_info', {}))) 26 | yield HotTrend(datetime=datetime, data=final, offset=offset, count=count) 27 | offset += 10 28 | -------------------------------------------------------------------------------- /tiktok/handlers/db.py: -------------------------------------------------------------------------------- 1 | from tiktok.handlers import Handler 2 | from sqlalchemy import create_engine, MetaData 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | 6 | class DBHandler(Handler): 7 | 8 | def __init__(self, conn_uri=None): 9 | """ 10 | init save folder 11 | :param folder: 12 | """ 13 | super().__init__() 14 | if not conn_uri: 15 | # conn_uri = "postgres://admin:password@localhost:5432/tiktok" 16 | conn_uri = "sqlite:///../app.sqlite" 17 | self.db = create_engine(conn_uri) 18 | 19 | def get_engine(self): 20 | return self.db 21 | 22 | async def process(self, obj, **kwargs): 23 | """ 24 | download to file 25 | :param url: resource url 26 | :param name: save name 27 | :param kwargs: 28 | :return: 29 | """ 30 | db_session = sessionmaker(bind=self.db) 31 | session = db_session() 32 | 33 | # save to db 34 | print('Saving', obj, 'to db...') 35 | session.merge(obj) 36 | session.commit() 37 | print('Saved', obj, 'to db successfully') 38 | -------------------------------------------------------------------------------- /tiktok/config.py: -------------------------------------------------------------------------------- 1 | # urls 2 | hot_search_url = 'https://aweme.snssdk.com/aweme/v1/hot/search/list/' 3 | hot_video_url = 'https://aweme.snssdk.com/aweme/v1/hotsearch/aweme/billboard/' 4 | hot_energy_url = 'https://aweme.snssdk.com/aweme/v1/hotsearch/positive_energy/billboard/' 5 | hot_music_url = 'https://aweme.snssdk.com/aweme/v1/hotsearch/music/billboard/' 6 | hot_trend_url = 'https://aweme.snssdk.com/aweme/v1/category/list/' 7 | topic2video_url = 'https://t.tiktok.com/aweme/v1/challenge/aweme/' 8 | music2video_url = 'https://t.tiktok.com/aweme/v1/music/aweme/' 9 | 10 | # http 11 | fetch_timeout = 5 12 | common_headers = { 13 | # 'User-Agent': 'Aweme 2.9.1 rv:29101 (iPhone; iOS 12.0; zh_CN) Cronet', 14 | 'User-Agent': 'Aweme 3.1.0 rv:31006 (iPhone; iOS 12.0; zh_CN) Cronet' 15 | } 16 | common_parameter = { 17 | 'version_code': '5.7.0', 18 | 'app_name': 'aweme', 19 | 'app_version': '5.7.0', 20 | 'channel': 'App Store', 21 | 'aid': '1128', 22 | 'os_version': '12.1.4', 23 | 'device_platform': 'iphone', 24 | 'device_type': 'iPhone10,1' 25 | } 26 | 27 | # retrying 28 | retry_max_number = 10 29 | retry_min_random_wait = 1000 # ms 30 | retry_max_random_wait = 5000 # ms 31 | -------------------------------------------------------------------------------- /examples/save_hot_videos_and_musics_to_file.py: -------------------------------------------------------------------------------- 1 | import tiktok 2 | 3 | # HotVideo 4 | from tiktok.config import hot_video_url, common_headers, common_parameter, hot_music_url 5 | 6 | # HotMusic 7 | result = tiktok.hot.music(hot_music_url, headers=common_headers, params=common_parameter) 8 | # music objects 9 | musics = result.data 10 | # print every music 11 | for music in musics: 12 | print(music) 13 | 14 | # define handler and specify folder 15 | handler = tiktok.handlers.FileHandler(folder='./musics') 16 | # define downloader 17 | downloader = tiktok.downloaders.MusicDownloader([handler]) 18 | # download musics 19 | downloader.download(musics, headers=common_headers, params=common_parameter) 20 | 21 | result = tiktok.hot.video(hot_video_url, headers=common_headers, params=common_parameter) 22 | # video objects 23 | videos = result.data 24 | # print every video 25 | for video in videos: 26 | print(video) 27 | 28 | # define handler and specify folder 29 | handler = tiktok.handlers.FileHandler(folder='./videos') 30 | # define downloader 31 | downloader = tiktok.downloaders.VideoDownloader([handler]) 32 | # download videos 33 | downloader.download(videos, headers=common_headers, params=common_parameter) 34 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Tiktok 2 | 3 | Tiktok Crawler App. 4 | 5 | ## Installation 6 | 7 | ``` 8 | pip3 install . 9 | ``` 10 | 11 | ## Usage 12 | 13 | Here is the sample code: 14 | python examples/save_trends_to_file.py 15 | 16 | then you can get: 17 | 18 | ``` 19 | Item > 20 | Processing > ... 21 | Processing > ... 22 | ... 23 | Processing > ... 24 | Processing > ... 25 | 0%| | 0/10 [00:00> ... 28 | Saving > to mongodb... 29 | Processing > ... 30 | Saving > to mongodb... 31 | Processing > ... 32 | .... 33 | Downloading > ... 34 | Saved > to mongodb successfully 35 | Downloading > ... 36 | Saved > to mongodb successfully 37 | ``` 38 | 39 | ## Other examples 40 | 41 | See [examples](./examples) -------------------------------------------------------------------------------- /tiktok/structures/address.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Base 2 | from sqlalchemy import Column, String, Float 3 | 4 | 5 | class TikTokAddress(Base): 6 | 7 | __tablename__ = 'tiktok_address' 8 | 9 | id = Column(String(20), primary_key=True) 10 | province = Column(String(50)) 11 | city = Column(String(50)) 12 | district = Column(String(50)) 13 | full = Column(String(500)) 14 | address = Column(String(250)) 15 | place = Column(String(250)) 16 | postal_code = Column(String(50)) 17 | longitude = Column(Float()) 18 | latitude = Column(Float()) 19 | 20 | def __init__(self, **kwargs): 21 | """ 22 | init address object 23 | :param kwargs: 24 | """ 25 | self.id = kwargs.get('id') 26 | self.province = kwargs.get('province') 27 | self.city = kwargs.get('city') 28 | self.district = kwargs.get('district') 29 | self.full = kwargs.get('full') 30 | self.address = kwargs.get('address') 31 | self.place = kwargs.get('place') 32 | self.postal_code = kwargs.get('postal_code') 33 | self.longitude = kwargs.get('longitude') 34 | self.latitude = kwargs.get('latitude') 35 | 36 | def __repr__(self): 37 | """ 38 | address to str 39 | :return: 40 | """ 41 | return '>' % (self.id, self.place) 42 | -------------------------------------------------------------------------------- /tiktok/structures/user.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Base 2 | from sqlalchemy import Column, String, DateTime 3 | 4 | 5 | class TikTokUser(Base): 6 | __tablename__ = 'tiktok_user' 7 | 8 | id = Column(String(20), primary_key=True) 9 | gender = Column(String(200)) 10 | name = Column(String(200)) 11 | create_time = Column(DateTime(), nullable=True) 12 | birthday = Column(String(50), nullable=True) 13 | sign = Column(String(500)) 14 | alias = Column(String(50)) 15 | avatar = Column(String(200)) 16 | verify = Column(String(200)) 17 | verify_info = Column(String(2000)) 18 | 19 | def __init__(self, **kwargs): 20 | """ 21 | init user object 22 | :param kwargs: 23 | """ 24 | self.id = kwargs.get('id') 25 | self.gender = kwargs.get('gender') 26 | self.name = kwargs.get('name') 27 | self.create_time = kwargs.get('create_time') 28 | self.birthday = kwargs.get('birthday') 29 | self.sign = kwargs.get('sign') 30 | self.alias = kwargs.get('alias') 31 | self.avatar = kwargs.get('avatar') 32 | self.verify = kwargs.get('verify') 33 | self.verify_info = kwargs.get('verify_info') 34 | 35 | def __repr__(self): 36 | """ 37 | user to str 38 | :return: 39 | """ 40 | return '>' % (self.alias, self.name) 41 | -------------------------------------------------------------------------------- /tiktok/utils/fetch.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from retrying import retry 4 | 5 | from tiktok.config import retry_max_number, retry_min_random_wait, retry_max_random_wait, fetch_timeout 6 | 7 | 8 | def need_retry(exception): 9 | """ 10 | need to retry 11 | :param exception: 12 | :return: 13 | """ 14 | result = isinstance(exception, (requests.ConnectionError, requests.ReadTimeout)) 15 | if result: 16 | print('Exception', type(exception), 'occurred, retrying...') 17 | return result 18 | 19 | 20 | def fetch(url, **kwargs): 21 | """ 22 | warp _fetch method 23 | :param url: fetch url 24 | :param kwargs: other requests params 25 | :return: result of _fetch 26 | """ 27 | 28 | @retry(stop_max_attempt_number=retry_max_number, wait_random_min=retry_min_random_wait, 29 | wait_random_max=retry_max_random_wait, retry_on_exception=need_retry) 30 | def _fetch(url, **kwargs): 31 | """ 32 | fetch api response 33 | :param url: fetch url 34 | :param kwargs: other requests params 35 | :return: json of response 36 | """ 37 | response = requests.get(url, **kwargs) 38 | if response.status_code != 200: 39 | raise requests.ConnectionError('Expected status code 200, but got {}'.format(response.status_code)) 40 | return response.json() if response.content else {} 41 | 42 | try: 43 | result = _fetch(url, **kwargs) 44 | return result 45 | # give up retrying 46 | except (requests.ConnectionError, requests.ReadTimeout): 47 | return {} 48 | -------------------------------------------------------------------------------- /tiktok/handlers/file.py: -------------------------------------------------------------------------------- 1 | from os.path import join, exists 2 | from os import makedirs 3 | 4 | from tiktok.config import common_headers 5 | from tiktok.handlers import Handler 6 | from tiktok.utils.type import mime_to_ext 7 | import aiohttp 8 | import ssl 9 | 10 | 11 | class FileHandler(Handler): 12 | 13 | def __init__(self, folder): 14 | """ 15 | init save folder 16 | :param folder: 17 | """ 18 | super().__init__() 19 | self.folder = folder 20 | if not exists(self.folder): 21 | makedirs(self.folder) 22 | 23 | async def _process(self, obj, **kwargs): 24 | """ 25 | download to file 26 | :param url: resource url 27 | :param name: save name 28 | :param kwargs: 29 | :return: 30 | """ 31 | print('Downloading', obj, '...') 32 | kwargs.update({'timeout': 60}) 33 | async with aiohttp.ClientSession() as session: 34 | async with session.get(obj.play_url, **kwargs) as response: 35 | if response.status == 200: 36 | extension = mime_to_ext(response.headers.get('Content-Type')) 37 | full_path = join(self.folder, '%s.%s' % (obj.id, extension)) 38 | with open(full_path, 'wb') as f: 39 | f.write(await response.content.read()) 40 | print('Downloaded file to', full_path) 41 | else: 42 | print('Cannot download %s, response status %s' % (obj.id, response.status)) 43 | 44 | async def process(self, obj, **kwargs): 45 | """ 46 | process obj 47 | :param obj: 48 | :param kwargs: 49 | :return: 50 | """ 51 | return await self._process(obj, **kwargs) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | .idea/ 8 | videos/ 9 | musics/ 10 | scores/ 11 | # C extensions 12 | *.so 13 | checkpoints/ 14 | notebooks/.ipynb_checkpoints 15 | debug.log 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | tiktok-*/ 20 | examples/checkpoints/ 21 | examples/events/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | *.pyc 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | downloads/ 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ -------------------------------------------------------------------------------- /tiktok/structures/video.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import relationship, backref 2 | 3 | from tiktok.structures import Base 4 | from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey 5 | 6 | 7 | class TikTokVideo(Base): 8 | __tablename__ = 'tiktok_video' 9 | 10 | id = Column(String(20), primary_key=True) 11 | desc = Column(String(2000)) 12 | like_count = Column(Integer()) 13 | comment_count = Column(Integer()) 14 | share_count = Column(Integer()) 15 | hot_count = Column(Integer()) 16 | play_url = Column(String(100)) 17 | is_ads = Column(Boolean()) 18 | duration = Column(Integer()) 19 | create_time = Column(DateTime(), nullable=True) 20 | share_url = Column(String(100)) 21 | ratio = Column(Integer()) 22 | cover_url = Column(String(100)) 23 | address = Column(String(100)) 24 | 25 | author_id = Column(String, ForeignKey('tiktok_user.id')) 26 | author = relationship('TikTokUser', backref=backref("tiktok_user", uselist=False)) 27 | music_id = Column(String, ForeignKey('tiktok_music.id')) 28 | music = relationship('TikTokMusic', backref=backref("tiktok_music", uselist=False)) 29 | 30 | def __init__(self, **kwargs): 31 | """ 32 | init video object 33 | :param kwargs: 34 | """ 35 | self.id = kwargs.get('id') 36 | self.desc = kwargs.get('desc') 37 | self.author = kwargs.get('author') 38 | self.music = kwargs.get('music') 39 | self.like_count = kwargs.get('like_count') 40 | self.comment_count = kwargs.get('comment_count') 41 | self.share_count = kwargs.get('share_count') 42 | self.hot_count = kwargs.get('hot_count') 43 | self.play_url = kwargs.get('play_url') 44 | self.is_ads = kwargs.get('is_ads') or False 45 | self.duration = kwargs.get('duration') 46 | self.create_time = kwargs.get('create_time') 47 | self.share_url = kwargs.get('share_url') 48 | self.ratio = kwargs.get('ratio') 49 | self.cover_url = kwargs.get('cover_url') 50 | self.address = kwargs.get('address') 51 | 52 | def __repr__(self): 53 | """ 54 | video to str 55 | :return: str 56 | """ 57 | return '>' % (self.id, self.desc[:10].strip() if self.desc else None) 58 | -------------------------------------------------------------------------------- /tiktok/structures/music.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Base 2 | from tiktok.utils.fetch import fetch 3 | from tiktok.config import music2video_url, common_headers 4 | from sqlalchemy import Column, String, Integer 5 | 6 | 7 | class TikTokMusic(Base): 8 | __tablename__ = 'tiktok_music' 9 | 10 | id = Column(String(20), primary_key=True) 11 | name = Column(String(50)) 12 | cover_url = Column(String(250)) 13 | play_url = Column(String(250)) 14 | owner_id = Column(String(50)) 15 | owner_name = Column(String(50)) 16 | hot_count = Column(Integer()) 17 | 18 | def __init__(self, **kwargs): 19 | """ 20 | music init args 21 | :param kwargs: 22 | """ 23 | self.id = kwargs.get('id') 24 | self.name = kwargs.get('name') 25 | self.cover_url = kwargs.get('cover_url') 26 | self.play_url = kwargs.get('play_url') 27 | self.owner_id = kwargs.get('owner_id') 28 | self.owner_name = kwargs.get('owner_name') 29 | self.hot_count = kwargs.get('hot_count') 30 | 31 | def __repr__(self): 32 | """ 33 | music to str 34 | :return: 35 | """ 36 | return '>' % (self.id, self.name) 37 | 38 | def videos(self, max=None): 39 | """ 40 | get videos of topic 41 | :return: 42 | """ 43 | if max and not isinstance(max, int): 44 | raise RuntimeError('`max` param must be int') 45 | from tiktok.utils.tranform import data_to_video 46 | query = { 47 | 'device_id': '33333333', 48 | 'music_id': self.id, 49 | 'count': '18', 50 | } 51 | offset, count = 0, 0 52 | while True: 53 | # define cursor 54 | query['cursor'] = str(offset) 55 | result = fetch(music2video_url, params=query, headers=common_headers, verify=False) 56 | aweme_list = result.get('aweme_list', []) 57 | for item in aweme_list: 58 | video = data_to_video(item) 59 | count += 1 60 | yield video 61 | if max and count >= max: 62 | return 63 | # next page 64 | if result.get('has_more'): 65 | offset += 18 66 | else: 67 | break 68 | -------------------------------------------------------------------------------- /tiktok/utils/type.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | mimes_map = { 4 | 'txt': 'text/plain', 5 | 'htm': 'text/html', 6 | 'html': 'text/html', 7 | 'php': 'text/html', 8 | 'css': 'text/css', 9 | 'js': 'application/javascript', 10 | 'json': 'application/json', 11 | 'xml': 'application/xml', 12 | 'swf': 'application/x-shockwave-flash', 13 | 'flv': 'video/x-flv', 14 | 15 | # images 16 | 'png': 'image/png', 17 | 'jpe': 'image/jpeg', 18 | 'jpeg': 'image/jpeg', 19 | 'jpg': 'image/jpeg', 20 | 'gif': 'image/gif', 21 | 'bmp': 'image/bmp', 22 | 'ico': 'image/vnd.microsoft.icon', 23 | 'tiff': 'image/tiff', 24 | 'tif': 'image/tiff', 25 | 'svg': 'image/svg+xml', 26 | 'svgz': 'image/svg+xml', 27 | 28 | # archives 29 | 'zip': 'application/zip', 30 | 'rar': 'application/x-rar-compressed', 31 | 'exe': 'application/x-msdownload', 32 | 'msi': 'application/x-msdownload', 33 | 'cab': 'application/vnd.ms-cab-compressed', 34 | 35 | # audio/video 36 | 'mp3': 'audio/mpeg', 37 | 'ogg': 'audio/ogg', 38 | 'qt': 'video/quicktime', 39 | 'mp4': 'video/mp4', 40 | 'mov': 'video/quicktime', 41 | 'wav': 'audio/x-wav', 42 | 'avi': 'application/octet-stream', 43 | 44 | # adobe 45 | 'pdf': 'application/pdf', 46 | 'psd': 'image/vnd.adobe.photoshop', 47 | 'ai': 'application/postscript', 48 | 'eps': 'application/postscript', 49 | 'ps': 'application/postscript', 50 | 51 | # ms office 52 | 'doc': 'application/msword', 53 | 'rtf': 'application/rtf', 54 | 'xls': 'application/vnd.ms-excel', 55 | 'ppt': 'application/vnd.ms-powerpoint', 56 | 57 | # open office 58 | 'odt': 'application/vnd.oasis.opendocument.text', 59 | 'ods': 'application/vnd.oasis.opendocument.spreadsheet', 60 | } 61 | 62 | exts_map = {v: k for k, v in mimes_map.items()} 63 | 64 | 65 | def ext_to_mime(ext): 66 | """ 67 | extension to mime 68 | :param ext: str 69 | :rtype: str 70 | """ 71 | if ext in mimes_map.keys(): 72 | return mimes_map[ext] 73 | else: 74 | return 'application/octet-stream' 75 | 76 | 77 | def mime_to_ext(mime): 78 | """ 79 | mime to extension 80 | :param mime: 81 | :return: 82 | """ 83 | if mime in exts_map.keys(): 84 | return exts_map[mime] 85 | else: 86 | return '' 87 | -------------------------------------------------------------------------------- /tiktok/structures/topic.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import Base 2 | from tiktok.utils.fetch import fetch 3 | from tiktok.config import topic2video_url, common_headers, common_parameter 4 | from tiktok.utils.signature import generate_signature 5 | from sqlalchemy import Column, String, Integer 6 | 7 | 8 | class TikTokTopic(Base): 9 | __tablename__ = 'tiktok_topic' 10 | 11 | id = Column(String(20), primary_key=True) 12 | view_count = Column(Integer()) 13 | user_count = Column(Integer()) 14 | name = Column(String(50)) 15 | desc = Column(String(50)) 16 | 17 | def __init__(self, **kwargs): 18 | """ 19 | init topic object 20 | :param kwargs: 21 | """ 22 | self.id = kwargs.get('id') 23 | self.view_count = kwargs.get('view_count') 24 | self.user_count = kwargs.get('user_count') 25 | self.name = kwargs.get('name') 26 | self.desc = kwargs.get('desc') 27 | 28 | def __repr__(self): 29 | """ 30 | music to str 31 | :return: 32 | """ 33 | return '>' % (self.id, self.name) 34 | 35 | def videos(self, max=None): 36 | """ 37 | get videos of topic 38 | :return: 39 | """ 40 | from tiktok.utils.tranform import data_to_video 41 | if max and not isinstance(max, int): 42 | raise RuntimeError('`max` param must be int') 43 | query = { 44 | 'count': '9', 45 | 'cursor': '0', 46 | 'aid': '1128', 47 | 'screen_limit': '3', 48 | 'download_click_limit': '0', 49 | 'ch_id': self.id, 50 | '_signature': generate_signature(str(self.id)) 51 | } 52 | cursor, video_count = None, 0 53 | while True: 54 | # define cursor 55 | if cursor: 56 | query['cursor'] = str(cursor) 57 | query['_signature'] = generate_signature(str(self.id) + '9' + str(cursor)) 58 | result = fetch(topic2video_url, params=query, headers=common_headers) 59 | aweme_list = result.get('aweme_list', []) 60 | if not aweme_list: 61 | break 62 | for aweme in aweme_list: 63 | video = data_to_video(aweme) 64 | video_count += 1 65 | yield video 66 | if result.get('has_more'): 67 | cursor = result.get('cursor') 68 | else: 69 | break 70 | -------------------------------------------------------------------------------- /tiktok/downloaders/base.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | import asyncio 3 | import math 4 | import types 5 | 6 | 7 | class Downloader(object): 8 | 9 | def __init__(self, handlers=[], batch=10): 10 | """ 11 | init attributes 12 | :param handlers: 13 | """ 14 | self.handlers = handlers 15 | self.batch = batch 16 | 17 | def add_handler(self, handler): 18 | """ 19 | add one handler 20 | :param handler: handler object 21 | :return: 22 | """ 23 | self.handlers.append(handler) 24 | 25 | def set_handlers(self, handlers): 26 | """ 27 | set handlers 28 | :param handlers: 29 | :return: 30 | """ 31 | self.handlers = handlers 32 | 33 | def get_handlers(self): 34 | """ 35 | get handlers of downloader 36 | :return: 37 | """ 38 | return self.handlers 39 | 40 | def update_progress(self, _): 41 | """ 42 | update progress bar 43 | :return: 44 | """ 45 | self.bar.update(1) 46 | 47 | async def process_item(self, obj, **kwargs): 48 | """ 49 | process item 50 | :param obj: single obj 51 | :return: 52 | """ 53 | raise NotImplementedError 54 | 55 | def process_items(self, objs, **kwargs): 56 | """ 57 | process items 58 | :param objs: objs 59 | :return: 60 | """ 61 | # define progress bar 62 | with tqdm(total=len(objs)) as self.bar: 63 | # init event loop 64 | loop = asyncio.get_event_loop() 65 | # get num of batches 66 | total_step = int(math.ceil(len(objs) / self.batch)) 67 | # for every batch 68 | for step in range(total_step): 69 | start, end = step * self.batch, (step + 1) * self.batch 70 | print('Processing %d-%d of files' % (start + 1, end)) 71 | # get batch of objs 72 | objs_batch = objs[start: end] 73 | # define tasks and run loop 74 | tasks = [asyncio.ensure_future(self.process_item(obj, **kwargs)) for obj in objs_batch] 75 | for task in tasks: 76 | task.add_done_callback(self.update_progress) 77 | loop.run_until_complete(asyncio.wait(tasks)) 78 | 79 | def download(self, inputs, **kwargs): 80 | """ 81 | download video or video lists 82 | :param data: 83 | :return: 84 | """ 85 | if isinstance(inputs, types.GeneratorType): 86 | for input in inputs: 87 | print('Processing', input, '...') 88 | asyncio.run(self.process_item(input, **kwargs)) 89 | else: 90 | inputs = inputs if isinstance(inputs, list) else [inputs] 91 | self.process_items(inputs, **kwargs) 92 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from os.path import join, isfile 5 | from os import walk 6 | import io 7 | import os 8 | import sys 9 | from shutil import rmtree 10 | from setuptools import find_packages, setup, Command 11 | 12 | 13 | def read_file(filename): 14 | with open(filename) as fp: 15 | return fp.read().strip() 16 | 17 | 18 | def read_requirements(filename): 19 | return [line.strip() for line in read_file(filename).splitlines() 20 | if not line.startswith('#')] 21 | 22 | 23 | NAME = 'tiktok' 24 | FOLDER = 'tiktok' 25 | DESCRIPTION = 'Api of tiktok for humans' 26 | URL = 'https://github.com/hackertogether/tiktok-crawler/crawler' 27 | EMAIL = 'allen.m.zhang@gmail.com.com' 28 | AUTHOR = 'Germey, hackertogether' 29 | REQUIRES_PYTHON = '>=3.4.0' 30 | VERSION = None 31 | 32 | REQUIRED = read_requirements('requirements.txt') 33 | 34 | here = os.path.abspath(os.path.dirname(__file__)) 35 | 36 | try: 37 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 38 | long_description = '\n' + f.read() 39 | except FileNotFoundError: 40 | long_description = DESCRIPTION 41 | 42 | about = {} 43 | if not VERSION: 44 | with open(os.path.join(here, FOLDER, '__version__.py')) as f: 45 | exec(f.read(), about) 46 | else: 47 | about['__version__'] = VERSION 48 | 49 | 50 | def package_files(directories): 51 | paths = [] 52 | for item in directories: 53 | if isfile(item): 54 | paths.append(join('..', item)) 55 | continue 56 | for (path, directories, filenames) in walk(item): 57 | for filename in filenames: 58 | paths.append(join('..', path, filename)) 59 | return paths 60 | 61 | 62 | class UploadCommand(Command): 63 | """Support setup.py upload.""" 64 | 65 | description = 'Build and publish the package.' 66 | user_options = [] 67 | 68 | @staticmethod 69 | def status(s): 70 | """Prints things in bold.""" 71 | print('\033[1m{0}\033[0m'.format(s)) 72 | 73 | def initialize_options(self): 74 | pass 75 | 76 | def finalize_options(self): 77 | pass 78 | 79 | def run(self): 80 | try: 81 | self.status('Removing previous builds…') 82 | rmtree(os.path.join(here, 'dist')) 83 | except OSError: 84 | pass 85 | 86 | self.status('Building Source and Wheel (universal) distribution…') 87 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 88 | 89 | self.status('Uploading the package to PyPI via Twine…') 90 | os.system('twine upload dist/*') 91 | 92 | self.status('Pushing git tags…') 93 | os.system('git tag v{0}'.format(about['__version__'])) 94 | os.system('git push --tags') 95 | 96 | sys.exit() 97 | 98 | 99 | # Where the magic happens: 100 | setup( 101 | name=NAME, 102 | version=about['__version__'], 103 | description=DESCRIPTION, 104 | long_description=long_description, 105 | long_description_content_type='text/markdown', 106 | author=AUTHOR, 107 | author_email=EMAIL, 108 | python_requires=REQUIRES_PYTHON, 109 | url=URL, 110 | packages=find_packages(exclude=('tests',)), 111 | install_requires=REQUIRED, 112 | include_package_data=True, 113 | license='MIT', 114 | classifiers=[ 115 | # Trove classifiers 116 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 117 | 'License :: OSI Approved :: MIT License', 118 | 'Programming Language :: Python', 119 | 'Programming Language :: Python :: 3', 120 | 'Programming Language :: Python :: 3.6', 121 | 'Programming Language :: Python :: Implementation :: CPython', 122 | 'Programming Language :: Python :: Implementation :: PyPy' 123 | ], 124 | package_data={ 125 | '': package_files([ 126 | 'requirements.txt', 127 | ]) 128 | }, 129 | # $ setup.py publish support. 130 | cmdclass={ 131 | 'upload': UploadCommand, 132 | }, 133 | ) -------------------------------------------------------------------------------- /tiktok/utils/tranform.py: -------------------------------------------------------------------------------- 1 | from tiktok.structures import * 2 | from tiktok.utils.common import first, parse_datetime 3 | 4 | 5 | def data_to_video(data): 6 | """ 7 | transfer json data to video object 8 | :param data: 9 | :return: 10 | """ 11 | statistics = data.get('statistics', {}) 12 | like_count = statistics.get('digg_count') 13 | comment_count = statistics.get('comment_count') 14 | share_count = statistics.get('share_count') 15 | id = statistics.get('aweme_id') 16 | desc = data.get('desc') 17 | is_ads = data.get('is_ads') 18 | duration = data.get('duration') 19 | create_time = parse_datetime(data.get('create_time')) 20 | share_url = data.get('share_url') 21 | ratio = data.get('video', {}).get('ratio') 22 | cover_url = first(data.get('video', {}).get('origin_cover', {}).get('url_list')) 23 | play_url = data.get('video', {}).get('play_addr', {}).get('url_list')[-1] 24 | author = data_to_user(data.get('author', {})) 25 | music = data_to_music(data.get('music', {})) 26 | address = data_to_address(data.get('poi_info', {})) 27 | return Video( 28 | id=id, 29 | desc=desc, 30 | like_count=like_count, 31 | share_count=share_count, 32 | comment_count=comment_count, 33 | is_ads=is_ads, 34 | duration=duration, 35 | create_time=create_time, 36 | share_url=share_url, 37 | ratio=ratio, 38 | cover_url=cover_url, 39 | play_url=play_url, 40 | author=author, 41 | music=music, 42 | address=address 43 | ) if id else None 44 | 45 | 46 | def data_to_music(data): 47 | """ 48 | transfer data to music object 49 | :param data: 50 | :return: 51 | """ 52 | id = data.get('mid') 53 | name = data.get('title') 54 | play_url = data.get('play_url', {}).get('url_list', [])[-1] 55 | owner_id = data.get('owner_id') 56 | owner_name = data.get('owner_nickname') 57 | cover_url = first(data.get('cover_large', {}).get('url_list', [])) 58 | return Music( 59 | id=id, 60 | name=name, 61 | play_url=play_url, 62 | owner_id=owner_id, 63 | owner_name=owner_name, 64 | cover_url=cover_url 65 | ) if id else None 66 | 67 | 68 | def data_to_user(data): 69 | """ 70 | transfer data to user object 71 | :param data: 72 | :return: 73 | """ 74 | alias = data.get('unique_id') or data.get('short_id') 75 | _id = data.get('uid') 76 | name = data.get('nickname') 77 | sign = data.get('signature') 78 | avatar = first(data.get('avatar_larger', {}).get('url_list', [])) 79 | gender = data.get('gender') 80 | birthday = data.get('birthday') 81 | create_time = parse_datetime(data.get('create_time')) 82 | verify = bool(data.get('custom_verify').strip()) 83 | verify_info = data.get('custom_verify').strip() 84 | return User( 85 | id=_id, 86 | alias=alias, 87 | name=name, 88 | sign=sign, 89 | avatar=avatar, 90 | gender=gender, 91 | verify=verify, 92 | verify_info=verify_info, 93 | create_time=create_time, 94 | birthday=birthday 95 | ) if _id else None 96 | 97 | 98 | def data_to_address(data): 99 | """ 100 | transfer data to address object 101 | :param data: 102 | :return: 103 | """ 104 | _id = data.get('poi_id') 105 | address_info = data.get('address_info', {}) 106 | province = address_info.get('province') 107 | city = address_info.get('city') 108 | district = address_info.get('district') 109 | full = address_info.get('simple_addr') 110 | address = address_info.get('address') 111 | postal_code = data.get('type_code') 112 | longitude = data.get('longitude') 113 | latitude = data.get('latitude') 114 | place = data.get('poi_name') 115 | return Address( 116 | id=_id, 117 | province=province, 118 | city=city, 119 | district=district, 120 | address=address, 121 | full=full, 122 | postal_code=postal_code, 123 | longitude=longitude, 124 | latitude=latitude, 125 | place=place 126 | ) if _id else None 127 | 128 | 129 | def data_to_topic(data): 130 | """ 131 | transfer data to topic object 132 | :param data: 133 | :return: 134 | """ 135 | _id = data.get('cid') 136 | view_count = data.get('view_count') 137 | user_count = data.get('user_count') 138 | name = data.get('cha_name') 139 | desc = data.get('desc') 140 | return Topic( 141 | id=_id, 142 | view_count=view_count, 143 | user_count=user_count, 144 | name=name, 145 | desc=desc 146 | ) if _id else None 147 | -------------------------------------------------------------------------------- /examples/parse_params.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import urlparse, parse_qs 3 | 4 | # url = 'https://api.amemv.com/aweme/v1/hotsearch/aweme/billboard/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&mas=01e3e5cef0de029bfcdd34f97a802bb868733f3265261a5cdb072e&as=a13500cd74892b29d04593&ts=1540360596' 5 | 6 | # url = 'https://api.amemv.com/aweme/v1/hot/search/list/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&detail_list=0&mas=01b4002e3d345720d85d1983fe6b328535431c67b1f365be1f2f38&as=a1e5000df9532bbfa03287&ts=1540362041' 7 | 8 | # url = 'https://api.amemv.com/aweme/v1/search/item/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&keyword=%E4%B8%89%E5%BA%AD%E4%BA%94%E7%9C%BC%E6%A0%87%E5%87%86%E8%84%B8&count=12&offset=60&source=video_search&hot_search=1&mas=01dd88c948b345ad9d54935e1c9cb91e2e52195833e37a0d4a04ee&as=a1856aed60b72b4d628753&ts=1540533616' 9 | 10 | # url = 'https://api.amemv.com/aweme/v1/category/list/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&count=10&cursor=30&mas=017cf50c73008bad543b552ca0369777473911b0e7c8fbc462a721&as=a195ab9d916dfba5629896&ts=1540535761' 11 | 12 | # url = 'https://api.amemv.com/aweme/v1/music/aweme/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&cursor=0&music_id=6606196836371794691&pull_type=2&count=18&type=6&mas=0143c9f3d621b358454717211b3d5b91be242eaad18243a010d6a8&as=a1756b5dab96bb15627918&ts=1540535659' 13 | 14 | # url = 'https://api.amemv.com/aweme/v1/challenge/aweme/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&query_type=0&cursor=72&ch_id=1579168686354445&count=18&pull_type=2&type=5&mas=01fa6c53471c45dc2f83db17c2c208d492d353529e1eae812f2728&as=a1656b8d763eabc5a28997&ts=1540535782' 15 | 16 | # url = 'https://api.amemv.com/aweme/v1/challenge/aweme/?ac=WIFI&iid=46961748949&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=29101&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=2.9.1&version_code=2.9.1&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&query_type=0&cursor=0&ch_id=1614257803840525&count=18&pull_type=2&type=5&mas=019e89de1c6917f6ff17e5a3fb15e9f4a042dc4b112dd70bc304ee&as=a115357dd5d06b22130853&ts=1540575749' 17 | 18 | url = 'https://api.amemv.com/aweme/v1/challenge/aweme/?ac=WIFI&iid=49530073634&device_id=58097798464&os_api=18&app_name=aweme&channel=App%20Store&idfa=B6B7BF1B-AADD-42E6-83AB-D29C93620305&device_platform=iphone&build_number=31006&vid=5A56818A-EC4B-4C9B-843F-881E99603F5A&openudid=825a48a41a70c4182b21cc442993c6bf6f1ed6e6&device_type=iPhone9,2&app_version=3.1.0&js_sdk_version=1.3.0.1&version_code=3.1.0&os_version=12.0&screen_width=1242&aid=1128&pass-region=1&query_type=0&cursor=0&ch_id=1615917132956695&count=18&pull_type=2&type=5&mas=019a446388982299eccaa62c24afeab1fa2c9ae7543ea3cb48af31&as=a16575fd5f15fbfaad0086&ts=1541233247' 19 | 20 | from tiktok.utils.parse import parse_query 21 | 22 | print(json.dumps(parse_query(url), ensure_ascii=False, indent=2)) 23 | 24 | # result = urlparse(url) 25 | # print(result) 26 | # 27 | # query = result.query 28 | # print(query) 29 | # 30 | # query_dict = parse_qs(query) 31 | # print(query_dict) 32 | # 33 | # query_dict = {k: v[0] for k, v in query_dict.items()} 34 | # print(query_dict) 35 | # 36 | # print(json.dumps(query_dict, indent=2, ensure_ascii=False)) 37 | -------------------------------------------------------------------------------- /examples/request_test.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # query = { 4 | # "ac": "WIFI", 5 | # "iid": "46961748949", 6 | # "device_id": "58097798464", 7 | # "os_api": "18", 8 | # "app_name": "aweme", 9 | # "channel": "App Store", 10 | # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 11 | # "device_platform": "iphone", 12 | # "build_number": "29101", 13 | # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 14 | # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 15 | # "device_type": "iPhone9,2", 16 | # "app_version": "2.9.1", 17 | # "version_code": "2.9.1", 18 | # "os_version": "12.0", 19 | # "screen_width": "1242", 20 | # "aid": "1128", 21 | # "pass-region": "1", 22 | # "keyword": "三庭五眼标准脸", 23 | # "count": "12", 24 | # "offset": "60", 25 | # "source": "video_search", 26 | # "hot_search": "1", 27 | # "mas": "01dd88c948b345ad9d54935e1c9cb91e2e52195833e37a0d4a04ee", 28 | # "as": "a1856aed60b72b4d628753", 29 | # "ts": "1540533616" 30 | # } 31 | 32 | # query = { 33 | # # "ac": "WIFI", 34 | # # "iid": "46961748949", 35 | # # "device_id": "58097798464", 36 | # # "os_api": "18", 37 | # # "app_name": "aweme", 38 | # # "channel": "App Store", 39 | # # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 40 | # # "device_platform": "iphone", 41 | # # "build_number": "29101", 42 | # # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 43 | # # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 44 | # # "device_type": "iPhone9,2", 45 | # # "app_version": "2.9.1", 46 | # "version_code": "2.9.1", 47 | # # "os_version": "12.0", 48 | # # "screen_width": "1242", 49 | # # "aid": "1128", 50 | # # "pass-region": "1", 51 | # "count": "10", 52 | # "cursor": "30", 53 | # # "mas": "017cf50c73008bad543b552ca0369777473911b0e7c8fbc462a721", 54 | # # "as": "a195ab9d916dfba5629896", 55 | # # "ts": "1540535761" 56 | # } 57 | 58 | # query = { 59 | # # "ac": "WIFI", 60 | # # "iid": "46961748949", 61 | # "device_id": "33333333", 62 | # # "os_api": "18", 63 | # # "app_name": "aweme", 64 | # # "channel": "App Store", 65 | # # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 66 | # # "device_platform": "iphone", 67 | # # "build_number": "29101", 68 | # # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 69 | # # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 70 | # # "device_type": "iPhone9,2", 71 | # # "app_version": "2.9.1", 72 | # # "version_code": "2.9.1", 73 | # # "os_version": "12.0", 74 | # # "screen_width": "1242", 75 | # # "aid": "1128", 76 | # # "pass-region": "1", 77 | # "cursor": "0", 78 | # "music_id": "6606196836371794691", 79 | # # "pull_type": "2", 80 | # "count": "11", 81 | # # "type": "6", 82 | # # "mas": "0143c9f3d621b358454717211b3d5b91be242eaad18243a010d6a8", 83 | # # "as": "a1756b5dab96bb15627918", 84 | # # "ts": "1540535659" 85 | # } 86 | 87 | # query = { 88 | # "ac": "WIFI", 89 | # "iid": "46961748949", 90 | # "device_id": "58097798464", 91 | # "os_api": "18", 92 | # "app_name": "aweme", 93 | # "channel": "App Store", 94 | # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 95 | # "device_platform": "iphone", 96 | # "build_number": "29101", 97 | # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 98 | # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 99 | # "device_type": "iPhone9,2", 100 | # "app_version": "2.9.1", 101 | # "version_code": "2.9.1", 102 | # "os_version": "12.0", 103 | # "screen_width": "1242", 104 | # "aid": "1128", 105 | # "pass-region": "1", 106 | # "query_type": "0", 107 | # "cursor": "72", 108 | # "ch_id": "1579168686354445", 109 | # "count": "18", 110 | # "pull_type": "2", 111 | # "type": "5", 112 | # "mas": "01fa6c53471c45dc2f83db17c2c208d492d353529e1eae812f2728", 113 | # "as": "a1656b8d763eabc5a28997", 114 | # "ts": "1540535782" 115 | # } 116 | 117 | # query = { 118 | # "ac": "WIFI", 119 | # "iid": "46961748949", 120 | # "device_id": "222222", 121 | # "os_api": "18", 122 | # "app_name": "aweme", 123 | # "channel": "App Store", 124 | # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 125 | # "device_platform": "iphone", 126 | # "build_number": "29101", 127 | # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 128 | # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 129 | # "device_type": "iPhone9,2", 130 | # "app_version": "2.9.1", 131 | # "version_code": "2.9.1", 132 | # "os_version": "12.0", 133 | # "screen_width": "1242", 134 | # "aid": "1129", 135 | # "pass-region": "1", 136 | # "query_type": "0", 137 | # "cursor": "0", 138 | # "ch_id": "1614257803840525", 139 | # "count": "18", 140 | # "pull_type": "2", 141 | # "type": "5", 142 | # "mas": "019e89de1c6917f6ff17e5a3fb15e9f4a042dc4b112dd70bc304ee", 143 | # "as": "a115357dd5d06b22130853", 144 | # "ts": "1540575749" 145 | # } 146 | 147 | query = { 148 | # "ac": "WIFI", 149 | # "iid": "49530073634", 150 | "device_id": "58097798460", 151 | # "os_api": "18", 152 | # "app_name": "aweme", 153 | # "channel": "App Store", 154 | # "idfa": "B6B7BF1B-AADD-42E6-83AB-D29C93620305", 155 | # "device_platform": "iphone", 156 | # "build_number": "31006", 157 | # "vid": "5A56818A-EC4B-4C9B-843F-881E99603F5A", 158 | # "openudid": "825a48a41a70c4182b21cc442993c6bf6f1ed6e6", 159 | # "device_type": "iPhone9,2", 160 | # "app_version": "3.1.0", 161 | # "js_sdk_version": "1.3.0.1", 162 | # "version_code": "3.1.0", 163 | # "os_version": "12.0", 164 | # "screen_width": "1242", 165 | "aid": "1129", 166 | # "pass-region": "1", 167 | "query_type": "0", 168 | "cursor": "0", 169 | "ch_id": "1615917132956695", 170 | "count": "18", 171 | # "pull_type": "2", 172 | # "type": "5", 173 | # "mas": "019a446388982299eccaa62c24afeab1fa2c9ae7543ea3cb48af31", 174 | # "as": "a16575fd5f15fbfaad0086", 175 | # "ts": "1541233247" 176 | } 177 | 178 | headers = { 179 | # 'Host': 'api.amemv.com', 180 | # 'x-Tt-Token': '0077066df5297676b148df690836bac9ead09e8e3d4fe366c5ab3ab74a827035cd3506b08117028a67cd66bded5e96645a45', 181 | # 'User-Agent': 'Aweme 2.9.1 rv:29101 (iPhone; iOS 12.0; zh_CN) Cronet', 182 | 'User-Agent': 'Aweme 3.1.0 rv:31006 (iPhone; iOS 12.0; zh_CN) Cronet' 183 | # 'Cookie': 'odin_tt=673293b03d79c15c86fc2bafd05bea71b96c3f1a97a17a96dca9afff9187ae4dc0cbd2c9b6f0ac8037fd90e276c65844; sid_guard=77066df5297676b148df690836bac9ea%7C1539280576%7C5184000%7CMon%2C+10-Dec-2018+17%3A56%3A16+GMT; uid_tt=b1fe77b7423959544e77e71126f0f0ef; sid_tt=77066df5297676b148df690836bac9ea; sessionid=77066df5297676b148df690836bac9ea; install_id=46961748949; ttreq=1$3154adbaa6f2ab4377c851ceb5e0878b8b73a80a' 184 | } 185 | 186 | # base_url = 'https://api.amemv.com/aweme/v1/category/list/' 187 | 188 | base_url = 'https://api.amemv.com/aweme/v1/challenge/aweme/' 189 | 190 | response = requests.get(base_url, params=query, verify=False, headers=headers) 191 | print(response.url) 192 | print(response.status_code) 193 | print(response.json()) 194 | -------------------------------------------------------------------------------- /tiktok/utils/scripts/byted-acrawler.js: -------------------------------------------------------------------------------- 1 | function generateSignature(userId) { 2 | this.navigator = { 3 | userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1" 4 | } 5 | var e = {} 6 | 7 | var r = (function () { 8 | function e(e, a, r) { 9 | return (b[e] || (b[e] = t("x,y", "return x " + e + " y")))(r, a) 10 | } 11 | 12 | function a(e, a, r) { 13 | return (k[r] || (k[r] = t("x,y", "return new x[y](" + Array(r + 1).join(",x[++y]").substr(1) + ")")))(e, a) 14 | } 15 | 16 | function r(e, a, r) { 17 | var n, t, s = {}, b = s.d = r ? r.d + 1 : 0; 18 | for (s["$" + b] = s, t = 0; t < b; t++) s[n = "$" + t] = r[n]; 19 | for (t = 0, b = s.length = a.length; t < b; t++) s[t] = a[t]; 20 | return c(e, 0, s) 21 | } 22 | 23 | function c(t, b, k) { 24 | function u(e) { 25 | v[x++] = e 26 | } 27 | 28 | function f() { 29 | return g = t.charCodeAt(b++) - 32, t.substring(b, b += g) 30 | } 31 | 32 | function l() { 33 | try { 34 | y = c(t, b, k) 35 | } catch (e) { 36 | h = e, y = l 37 | } 38 | } 39 | 40 | for (var h, y, d, g, v = [], x = 0; ;) switch (g = t.charCodeAt(b++) - 32) { 41 | case 1: 42 | u(!v[--x]); 43 | break; 44 | case 4: 45 | v[x++] = f(); 46 | break; 47 | case 5: 48 | u(function (e) { 49 | var a = 0, r = e.length; 50 | return function () { 51 | var c = a < r; 52 | return c && u(e[a++]), c 53 | } 54 | }(v[--x])); 55 | break; 56 | case 6: 57 | y = v[--x], u(v[--x](y)); 58 | break; 59 | case 8: 60 | if (g = t.charCodeAt(b++) - 32, l(), b += g, g = t.charCodeAt(b++) - 32, y === c) b += g; else if (y !== l) return y; 61 | break; 62 | case 9: 63 | v[x++] = c; 64 | break; 65 | case 10: 66 | u(s(v[--x])); 67 | break; 68 | case 11: 69 | y = v[--x], u(v[--x] + y); 70 | break; 71 | case 12: 72 | for (y = f(), d = [], g = 0; g < y.length; g++) d[g] = y.charCodeAt(g) ^ g + y.length; 73 | u(String.fromCharCode.apply(null, d)); 74 | break; 75 | case 13: 76 | y = v[--x], h = delete v[--x][y]; 77 | break; 78 | case 14: 79 | v[x++] = t.charCodeAt(b++) - 32; 80 | break; 81 | case 59: 82 | u((g = t.charCodeAt(b++) - 32) ? (y = x, v.slice(x -= g, y)) : []); 83 | break; 84 | case 61: 85 | u(v[--x][t.charCodeAt(b++) - 32]); 86 | break; 87 | case 62: 88 | g = v[--x], k[0] = 65599 * k[0] + k[1].charCodeAt(g) >>> 0; 89 | break; 90 | case 65: 91 | h = v[--x], y = v[--x], v[--x][y] = h; 92 | break; 93 | case 66: 94 | u(e(t[b++], v[--x], v[--x])); 95 | break; 96 | case 67: 97 | y = v[--x], d = v[--x], u((g = v[--x]).x === c ? r(g.y, y, k) : g.apply(d, y)); 98 | break; 99 | case 68: 100 | u(e((g = t[b++]) < "<" ? (b--, f()) : g + g, v[--x], v[--x])); 101 | break; 102 | case 70: 103 | u(!1); 104 | break; 105 | case 71: 106 | v[x++] = n; 107 | break; 108 | case 72: 109 | v[x++] = +f(); 110 | break; 111 | case 73: 112 | u(parseInt(f(), 36)); 113 | break; 114 | case 75: 115 | if (v[--x]) { 116 | b++; 117 | break 118 | } 119 | case 74: 120 | g = t.charCodeAt(b++) - 32 << 16 >> 16, b += g; 121 | break; 122 | case 76: 123 | u(k[t.charCodeAt(b++) - 32]); 124 | break; 125 | case 77: 126 | y = v[--x], u(v[--x][y]); 127 | break; 128 | case 78: 129 | g = t.charCodeAt(b++) - 32, u(a(v, x -= g + 1, g)); 130 | break; 131 | case 79: 132 | g = t.charCodeAt(b++) - 32, u(k["$" + g]); 133 | break; 134 | case 81: 135 | h = v[--x], v[--x][f()] = h; 136 | break; 137 | case 82: 138 | u(v[--x][f()]); 139 | break; 140 | case 83: 141 | h = v[--x], k[t.charCodeAt(b++) - 32] = h; 142 | break; 143 | case 84: 144 | v[x++] = !0; 145 | break; 146 | case 85: 147 | v[x++] = void 0; 148 | break; 149 | case 86: 150 | u(v[x - 1]); 151 | break; 152 | case 88: 153 | h = v[--x], y = v[--x], v[x++] = h, v[x++] = y; 154 | break; 155 | case 89: 156 | u(function () { 157 | function e() { 158 | return r(e.y, arguments, k) 159 | } 160 | 161 | return e.y = f(), e.x = c, e 162 | }()); 163 | break; 164 | case 90: 165 | v[x++] = null; 166 | break; 167 | case 91: 168 | v[x++] = h; 169 | break; 170 | case 93: 171 | h = v[--x]; 172 | break; 173 | case 0: 174 | return v[--x]; 175 | default: 176 | u((g << 16 >> 16) - 16) 177 | } 178 | } 179 | 180 | var n = this, t = n.Function, s = Object.keys || function (e) { 181 | var a = {}, r = 0; 182 | for (var c in e) a[r++] = c; 183 | return a.length = r, a 184 | }, b = {}, k = {}; 185 | return r 186 | })() 187 | ('gr$Daten Иb/s!l y͒yĹg,(lfi~ah`{mv,-n|jqewVxp{rvmmx,&effkx[!cs"l".Pq%widthl"@q&heightl"vr*getContextx$"2d[!cs#l#,*;?|u.|uc{uq$fontl#vr(fillTextx$$龘ฑภ경2<[#c}l#2q*shadowBlurl#1q-shadowOffsetXl#$$limeq+shadowColorl#vr#arcx88802[%c}l#vr&strokex[ c}l"v,)}eOmyoZB]mx[ cs!0s$l$Pb>>s!0s%yA0s"l"l!r&lengthb&l!l Bd>&+l!l &+l!l 6d>&+l!l &+ s,y=o!o!]/q"13o!l q"10o!],l 2d>& s.{s-yMo!o!]0q"13o!]*Ld>>b|s!o!l q"10o!],l!& s/yIo!o!].q"13o!],o!]*Jd>>b|&o!]+l &+ s0l-l!&l-l!i\'1z141z4b/@d