├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── aiolearn ├── Course.py ├── File.py ├── Message.py ├── Semester.py ├── User.py ├── Work.py ├── __init__.py └── config.py ├── doc ├── .DS_Store ├── Makefile └── source │ ├── aiolearn.rst │ ├── conf.py │ ├── index.rst │ └── modules.rst ├── examples ├── __init__.py ├── context.py ├── get_all_courses.py ├── login.py └── walk_a_course.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | serect.json 59 | .idea/ 60 | secret.json 61 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.5-dev" # 3.5 development branch 5 | - "nightly" # currently points to 3.6-dev 6 | # command to install dependencies 7 | install: "pip install -r requirements.txt" 8 | # command to run tests 9 | script: nosetests 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 柯豪 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)]() 2 | [![Build Status](https://travis-ci.org/kehao95/aiolearn.svg?branch=master)](https://travis-ci.org/kehao95/aiolearn) 3 | # aiolearn 4 | Thu Learn Spider for asyncio (PEP-3156) 5 | ## 关于这个项目 6 | 之前我完成了thu_learn项目作为清华大学的网络爬虫,但是效率一直是一个问题。 7 | 在那个项目中我使用的是requests作为网络库,请求效率虽然不慢,但是由于是阻塞的,会浪费大量的时间在网络等待上。 8 | 在这个项目中,我使用了python3.5的asyncio语法换用了异步的aiohttp库作为网络请求,使得爬虫的大部分行为变为异步的。 9 | 同时更改了类的结构,将用户及其session作为了类的一部分,使之可以同时处理多用户连接。 10 | 由于异步地重构使得库的使用方法大大改变,必须使用异步的方法进行调用,因此有了这个全新的项目。 11 | 12 | ![性能对比图](http://ww4.sinaimg.cn/large/bc2a20f8jw1eyfgutzss8j20kn0ctmxp.jpg) 13 | 14 | 15 | ## 简单理解 asynico: 16 | 通过 `async def` 来声明的函数为协程函数。协程函数的执行非同步,而是在异步执行,当遇见IO block的时候,如在本项目的网络请求,可以近乎并行执行。 17 | 协程需要使用asynico的event.loop来包裹执行。 18 | 19 | ## 示例使用 20 | ```python 21 | import asyncio 22 | import aiolearn 23 | import getpass 24 | 25 | async def main(): 26 |    user = aiolearn.User(username=input('input username'), password=getpass.getpass("input password:")) 27 | semester = aiolearn.Semester(user, current=False) 28 | courses = await semester.courses 29 | for course in courses: 30 | print(course.name) 31 | works = await course.works 32 | messages = await course.messages 33 | files = await course.files 34 | print('\n>>works') 35 | for work in works: 36 | print(work.title) 37 | print('\n>>messages') 38 | for message in messages: 39 | print(message.title) 40 | print('\n>>files') 41 | for file in files: 42 | print(file.name) 43 | 44 | if __name__ == "__main__": 45 | loop = asyncio.get_event_loop() 46 | loop.run_until_complete(main()) 47 | 48 | ``` 49 | 50 | semester.courses即协程对象,需要使用await代表获得其值。 51 | 52 | -------------------------------------------------------------------------------- /aiolearn/Course.py: -------------------------------------------------------------------------------- 1 | import re 2 | import asyncio 3 | from datetime import datetime 4 | from .Message import Message 5 | from .File import File 6 | from .Work import Work 7 | from .config import ( 8 | _COURSE_WORK, _COURSE_MSG, _COURSE_FILES, 9 | _COURSE_MSG_NEW, _COURSE_WORK_NEW, _COURSE_FILE_NEW, 10 | _URL_PREF, _ID_COURSE_URL, _PAGE_FILE, _PAGE_MSG) 11 | from bs4 import Comment 12 | 13 | 14 | def parseStamp(stamp): 15 | return datetime.fromtimestamp(int(stamp)/1000).strftime('%Y-%m-%d') 16 | 17 | 18 | class Course: 19 | 20 | def __init__(self, user, id, name=None, url=None, is_new=False): 21 | self.id = id 22 | self.name = name 23 | self.user = user 24 | self.is_new = is_new 25 | if url is None: 26 | self.url = _ID_COURSE_URL % id 27 | else: 28 | self.url = url 29 | 30 | @property 31 | async def works(self): 32 | async def get_work(item): 33 | tds = item.find_all('td') 34 | url = _URL_PREF + item.find('a')['href'] 35 | ids = re.findall(r'id=(\d+)', url) 36 | id = ids[0] 37 | course_id = ids[1] 38 | title = item.find('a').contents[0] 39 | start_time = tds[1].contents[0] 40 | end_time = tds[2].contents[0] 41 | submitted = ("已经提交" in tds[3].contents[0]) 42 | graded = not tds[5].contents[3].attrs.get('disabled') 43 | completion = 2 if graded else (1 if submitted else 0) 44 | return Work( 45 | user = user, 46 | id = id, 47 | course_id = course_id, 48 | title = title, 49 | url = url, # deferred fetch 50 | start_time= start_time, 51 | end_time = end_time, 52 | completion= completion 53 | ) 54 | async def get_work_new(item): 55 | info = item['courseHomeworkInfo'] 56 | record = item['courseHomeworkRecord'] 57 | 58 | return Work( 59 | user = user, 60 | id = info['homewkId'], 61 | course_id = info['courseId'], 62 | title = info['title'], 63 | detail_new= info['detail'], # 64 | start_time= parseStamp(info['beginDate']), 65 | end_time = parseStamp(info['endDate']), 66 | completion= int(record['status']) 67 | ) 68 | 69 | user = self.user 70 | if not self.is_new: 71 | works_url = _COURSE_WORK % self.id 72 | works_soup = await self.user.make_soup(works_url) 73 | tasks = [get_work(i) for i 74 | in works_soup.find_all('tr', class_=['tr1', 'tr2'])] 75 | else: 76 | works_url = _COURSE_WORK_NEW % self.id 77 | works_json = await self.user.cook_json(works_url) 78 | tasks = [get_work_new(i) for i in works_json['resultList']] 79 | works = await asyncio.gather(*tasks) 80 | return works 81 | 82 | @property 83 | async def messages(self): 84 | async def get_message(item): 85 | tds = item.find_all('td') 86 | title = tds[1].contents[1].text 87 | url = _PAGE_MSG % tds[1].contents[1]['href'] 88 | ids = re.findall(r'id=(\d+)', url) 89 | id = ids[0] 90 | course_id = ids[1] 91 | date = tds[3].text 92 | return Message( 93 | user = user, 94 | id = id, 95 | course_id = course_id, 96 | title = title, 97 | url = url, # deffered fetch 98 | date = date 99 | ) 100 | async def get_message_new(item): 101 | notice = item['courseNotice'] 102 | return Message( 103 | user = user, 104 | id = notice['id'], 105 | course_id = notice['courseId'], 106 | title = notice['title'], 107 | detail_new= notice['detail'], # 108 | date = notice['regDate'] 109 | ) 110 | 111 | user = self.user 112 | if not self.is_new: 113 | msg_url = _COURSE_MSG % self.id 114 | msg_soup = await self.user.make_soup(msg_url) 115 | tasks = [get_message(i) for i in msg_soup.find_all('tr', class_=['tr1', 'tr2'])] 116 | else: 117 | msg_url = _COURSE_MSG_NEW % self.id 118 | msg_json = await self.user.cook_json(msg_url) 119 | tasks = [get_message_new(i) for i in msg_json['paginationList']['recordList']] 120 | messages = await asyncio.gather(*tasks) 121 | return messages 122 | 123 | @property 124 | async def files(self): 125 | async def get_file(item): 126 | name, id = re.search(r'getfilelink=([^&]+)&id=(\d+)', str(item.find(text=lambda text: isinstance(text, Comment)))).groups() 127 | a = item.find('a') 128 | url = _PAGE_FILE % (self.id, name) 129 | title = re.sub(r'[\n\r\t ]', '', a.contents[0]) 130 | name = re.sub(r'_[^_]+\.', '.', name) 131 | return File( 132 | user = user, 133 | id = id, 134 | name = name, 135 | url = url, # 136 | title=title, 137 | size = 0, # TODO 138 | date = 0 # TODO 139 | ) 140 | async def get_file_new(item): 141 | res = item['resourcesMappingByFileId'] 142 | return File( 143 | user = user, 144 | id = res['fileId'], 145 | name = res['fileName'], 146 | title= item['title'], 147 | size = res['fileSize'], 148 | date = res['regDate'] 149 | ) 150 | 151 | 152 | user = self.user 153 | if (not self.is_new): 154 | file_url = _COURSE_FILES % self.id 155 | files_soup = await self.user.make_soup(file_url) 156 | tasks = [get_file(item) for item in files_soup.find_all('tr', class_=['tr1', 'tr2'])] 157 | else: 158 | file_url = _COURSE_FILE_NEW % self.id 159 | files_json = await self.user.cook_json(file_url) 160 | def first_value(dict): 161 | return next(iter(dict.values())) 162 | tasks = [get_file_new(item) for item in 163 | first_value( 164 | first_value( 165 | files_json['resultList'] 166 | )['childMapData'] 167 | )['courseCoursewareList'] 168 | ] 169 | files = await asyncio.gather(*tasks) 170 | return files 171 | 172 | @property 173 | def dict(self): 174 | d = self.__dict__.copy() 175 | user = self.user.__dict__.copy() 176 | del user['session'] 177 | d['user'] = user 178 | return d 179 | -------------------------------------------------------------------------------- /aiolearn/File.py: -------------------------------------------------------------------------------- 1 | class File: 2 | def __init__(self, user, id, name, title, date, size, url=None): 3 | self.user = user 4 | self.id = id 5 | self.name = name 6 | self.url = url 7 | self.title = title.strip() 8 | self.date = date 9 | self.size = size 10 | -------------------------------------------------------------------------------- /aiolearn/Message.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Message: 5 | def __init__(self, user, id, course_id, title, date, detail_new=None, url=None): 6 | self.title = title.strip() 7 | self.url = url 8 | self.date = date 9 | self.user = user 10 | self.id = id 11 | self.course_id = course_id 12 | self.detail_new = detail_new 13 | 14 | @property 15 | async def detail(self): 16 | if(not self.detail_new is None): 17 | return self.detail_new 18 | soup = await self.user.make_soup(self.url) 19 | detail = soup.find_all('td', class_='tr_l2')[1] 20 | detail = detail.text.replace('\xa0', ' ') 21 | detail = re.sub('(\\xa0)+', ' ', detail) 22 | detail = re.sub('\n+', '\n', detail) 23 | return detail 24 | 25 | @property 26 | async def dict(self): 27 | d = self.__dict__.copy() 28 | d["detail"] = await self.detail 29 | del d['user'] 30 | return d 31 | -------------------------------------------------------------------------------- /aiolearn/Semester.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | from .Course import Course 4 | from .config import _URL_CURRENT_SEMESTER, _URL_PAST_SEMESTER, _URL_BASE 5 | 6 | 7 | class Semester: 8 | def __init__(self, user, current=True): 9 | if current is True: # TODO: three possible values when summer semester is comming 10 | self.url = _URL_CURRENT_SEMESTER 11 | else: 12 | self.url = _URL_PAST_SEMESTER 13 | self.user = user 14 | 15 | @property 16 | async def courses(self): 17 | async def get_course_one(item): 18 | i = item.find('a') 19 | url = i['href'] 20 | 21 | name = i.contents[0].strip() 22 | 23 | # remove trailing `(2016-2017秋季学期)` 24 | name = re.sub(r'\(\d+-\d+\w+\)$', '', name) 25 | # remove trailing `(1)` 26 | name = re.sub(r'\(\d+\)$', '', name) 27 | 28 | if url.startswith('/Mult'): 29 | # Old WebLearning 30 | url = _URL_BASE + url 31 | id = url[-6:] # TODO: magic number 32 | return Course( 33 | user = user, 34 | id = id, 35 | name = name, 36 | is_new= False 37 | ) 38 | else: 39 | # New WebLearning 40 | # substring starting from past the last `/` 41 | id = re.search(r'/([^/]+)$', url).group(1) 42 | return Course( 43 | user = user, 44 | id = id, 45 | name = name, 46 | is_new = True 47 | ) 48 | 49 | 50 | user = self.user 51 | soup = await self.user.make_soup(self.url) 52 | tasks = [get_course_one(i) for i 53 | in soup.find_all('tr', class_=['info_tr', 'info_tr2'])] 54 | courses = [c for c in await asyncio.gather(*tasks) if c is not None] 55 | return courses 56 | -------------------------------------------------------------------------------- /aiolearn/User.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import getpass 3 | import logging 4 | import asyncio 5 | import json 6 | from bs4 import BeautifulSoup 7 | from .config import _URL_LOGIN, _URL_CURRENT_SEMESTER, _URL_BASE_NEW 8 | logging.basicConfig(level=logging.DEBUG) 9 | _logger = logging.getLogger(__name__) 10 | loop = asyncio.get_event_loop() 11 | 12 | 13 | class User: 14 | def __init__(self, username, password): 15 | if username is None or password is None: 16 | username = input("TsinghuaId:") 17 | password = getpass.getpass("Password:") 18 | self.username = username 19 | self.password = password 20 | self.session = None 21 | self.session_new = None 22 | self.cache = None 23 | 24 | def __del__(self): 25 | if self.session is not None: 26 | self.session.close() 27 | self.session_new.close() 28 | 29 | async def wrapped_get(self, url): 30 | if self.session is None: await self.login() 31 | _logger.debug("%s GET %s" % (self.username, url)) 32 | 33 | if url == _URL_CURRENT_SEMESTER: 34 | cache = self.cache 35 | if not (cache is None): 36 | self.cache = None 37 | _logger.debug("%s cache hit" % self.username) 38 | return cache 39 | _logger.debug("%s cache unavailable" % self.username) 40 | 41 | 42 | 43 | if not url.startswith(_URL_BASE_NEW): 44 | r = await self.session.get(url) 45 | else: 46 | r = await self.session_new.get(url) 47 | text = await r.text() 48 | 49 | if url == _URL_CURRENT_SEMESTER: 50 | self.cache = text 51 | _logger.debug("%s writing cache" % self.username) 52 | 53 | return text 54 | 55 | async def session_post(self, url, body): 56 | if self.session is None: await self.login() 57 | pass 58 | 59 | async def make_soup(self, url): 60 | html_text = await self.wrapped_get(url) 61 | return BeautifulSoup(html_text, "html.parser") 62 | 63 | async def cook_json(self, url): 64 | json_text = await self.wrapped_get(url) # TODO: response.json() 65 | return json.loads(json_text) 66 | 67 | async def login(self): 68 | _logger.debug("%s: login()" % self.username) 69 | data = dict( 70 | userid=self.username, 71 | userpass=self.password, 72 | ) 73 | self.session = aiohttp.ClientSession(loop=loop) 74 | self.session_new = aiohttp.ClientSession(loop=loop) 75 | r = await self.session.post(_URL_LOGIN, data=data) 76 | content = await r.text() 77 | if len(content) > 120: 78 | raise RuntimeError(r) 79 | 80 | # New WebLearning 81 | soup = await self.make_soup(_URL_CURRENT_SEMESTER) # cache in play 82 | url_ticket = soup.iframe['src'] 83 | await self.wrapped_get(url_ticket) 84 | -------------------------------------------------------------------------------- /aiolearn/Work.py: -------------------------------------------------------------------------------- 1 | class Work: 2 | def __init__(self, user, id, course_id, title, start_time, end_time, completion, url=None, detail_new=None): 3 | self.id = id 4 | self.course_id = course_id 5 | self.title = title.strip() 6 | self.start_time = start_time 7 | self.end_time = end_time 8 | self.completion = completion # 0 for 尚未提交, 1 for 已经提交, 2 for 已经批改 9 | self.user = user 10 | self.url = url 11 | self.detail_new = detail_new 12 | 13 | @property 14 | async def grading(self): 15 | return "" # TODO 16 | 17 | @property 18 | async def detail(self): 19 | if(not self.detail_new is None): 20 | return self.detail_new 21 | soup = await self.user.make_soup(self.url) 22 | try: 23 | detail = soup.find_all('td', class_='tr_2')[1].textarea.contents[0] 24 | except IndexError: 25 | detail = "" 26 | return detail 27 | 28 | @property 29 | async def dict(self): 30 | d = self.__dict__.copy() 31 | d["detail"] = await self.detail 32 | del d['user'] 33 | return d 34 | -------------------------------------------------------------------------------- /aiolearn/__init__.py: -------------------------------------------------------------------------------- 1 | from .Semester import Semester 2 | from .Course import Course 3 | from .Message import Message 4 | from .File import File 5 | from .Work import Work 6 | from .User import User 7 | __all__ = ["Semester", "Course", "Work", "Message", "File", "User"] 8 | -------------------------------------------------------------------------------- /aiolearn/config.py: -------------------------------------------------------------------------------- 1 | _URL_BASE = 'https://learn.tsinghua.edu.cn/MultiLanguage/' 2 | _URL_LOGIN = _URL_BASE + 'lesson/teacher/loginteacher.jsp' 3 | _URL_PREF = 'http://learn.tsinghua.edu.cn/MultiLanguage/lesson/student/' 4 | # Semesters 5 | _URL_CURRENT_SEMESTER = _URL_PREF + 'MyCourse.jsp?typepage=1' 6 | _URL_PAST_SEMESTER = _URL_PREF + 'MyCourse.jsp?typepage=2' 7 | _URL_PERSONAL_INFO = _URL_BASE + 'vspace/vspace_userinfo1.jsp' 8 | # Courses 9 | _ID_COURSE_URL = _URL_PREF + 'course_locate.jsp?course_id=%s' 10 | # Differet Sections of Course 11 | _COURSE_MSG = _URL_BASE + 'public/bbs/getnoteid_student.jsp?course_id=%s' 12 | _COURSE_INFO = _URL_PREF + 'course_info.jsp?course_id=%s' 13 | _COURSE_FILES = _URL_PREF + 'download.jsp?course_id=%s' 14 | _COURSE_LIST = _URL_PREF + 'ware_list.jsp?course_id=%s' 15 | _COURSE_WORK = _URL_PREF + 'hom_wk_brw.jsp?course_id=%s' 16 | # Object Detail Page 17 | _PAGE_MSG = _URL_BASE + 'public/bbs/%s' 18 | _PAGE_FILE = 'http://learn.tsinghua.edu.cn/kejian/data/%s/download/%s' 19 | 20 | # for new WebLearning 21 | _GET_TICKET = _URL_PREF + 'MyCourse.jsp?language=cn' 22 | _URL_BASE_NEW = 'http://learn.cic.tsinghua.edu.cn/' 23 | _URL_PREF_NEW = 'http://learn.cic.tsinghua.edu.cn/b/myCourse/' 24 | _COURSE_MSG_NEW = _URL_PREF_NEW + 'notice/listForStudent/%s' 25 | _COURSE_WORK_NEW = _URL_PREF_NEW + 'homework/list4Student/%s/0' 26 | _COURSE_FILE_NEW = _URL_PREF_NEW + 'tree/getCoursewareTreeData/%s/0' 27 | -------------------------------------------------------------------------------- /doc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kehao95/aiolearn/0e5575c54cc051cfbf46e2ee7860382dbdc1a61c/doc/.DS_Store -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiolearn.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiolearn.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/aiolearn" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiolearn" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /doc/source/aiolearn.rst: -------------------------------------------------------------------------------- 1 | aiolearn package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiolearn.Course module 8 | ---------------------- 9 | 10 | .. automodule:: aiolearn.Course 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | aiolearn.File module 16 | -------------------- 17 | 18 | .. automodule:: aiolearn.File 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | aiolearn.Message module 24 | ----------------------- 25 | 26 | .. automodule:: aiolearn.Message 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | aiolearn.Semester module 32 | ------------------------ 33 | 34 | .. automodule:: aiolearn.Semester 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | aiolearn.User module 40 | -------------------- 41 | 42 | .. automodule:: aiolearn.User 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | aiolearn.Work module 48 | -------------------- 49 | 50 | .. automodule:: aiolearn.Work 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | aiolearn.config module 56 | ---------------------- 57 | 58 | .. automodule:: aiolearn.config 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: aiolearn 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aiolearn documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Sep 6 09:52:18 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../..')) 23 | print("path: ", os.path.abspath('../..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'aiolearn' 55 | copyright = '2016, Hao Ke' 56 | author = 'Hao Ke' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '1.0' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '1.0' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = 'en' 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'alabaster' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | #html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'aiolearndoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'aiolearn.tex', 'aiolearn Documentation', 231 | 'Hao Ke', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'aiolearn', 'aiolearn Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'aiolearn', 'aiolearn Documentation', 275 | author, 'aiolearn', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. aiolearn documentation master file, created by 2 | sphinx-quickstart on Tue Sep 6 09:52:18 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to aiolearn's documentation! 7 | ==================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /doc/source/modules.rst: -------------------------------------------------------------------------------- 1 | aiolearn 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | aiolearn 8 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kehao95/aiolearn/0e5575c54cc051cfbf46e2ee7860382dbdc1a61c/examples/__init__.py -------------------------------------------------------------------------------- /examples/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 4 | import aiolearn 5 | -------------------------------------------------------------------------------- /examples/get_all_courses.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | from context import aiolearn 4 | import getpass 5 | 6 | async def main(): 7 | user = aiolearn.User(username='keh13', password=getpass.getpass("input password:")) 8 | semester = aiolearn.Semester(user, current=False) 9 | courses = await semester.courses 10 | for course in courses: 11 | print(course.name) 12 | 13 | if __name__ == "__main__": 14 | loop = asyncio.get_event_loop() 15 | loop.run_until_complete(main()) 16 | -------------------------------------------------------------------------------- /examples/login.py: -------------------------------------------------------------------------------- 1 | from context import aiolearn 2 | import getpass 3 | user = aiolearn.User(username='keh13', 4 | password=getpass.getpass("input password:")) 5 | semester = aiolearn.Semester(user) 6 | print(user) 7 | -------------------------------------------------------------------------------- /examples/walk_a_course.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | from context import aiolearn 4 | import getpass 5 | 6 | async def main(): 7 | user = aiolearn.User(username='keh13', 8 | password=getpass.getpass("input password:")) 9 | semester = aiolearn.Semester(user, current=False) 10 | courses = await semester.courses 11 | course = courses[0] 12 | print(course.name) 13 | works = await course.works 14 | messages = await course.messages 15 | files = await course.files 16 | print('\n>>works') 17 | for work in works: 18 | print(work.title) 19 | print('\n>>messages') 20 | for message in messages: 21 | print(message.title) 22 | print('\n>>files') 23 | for file in files: 24 | print(file.name) 25 | 26 | 27 | if __name__ == "__main__": 28 | loop = asyncio.get_event_loop() 29 | loop.run_until_complete(main()) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==0.22.5 2 | beautifulsoup4==4.5.1 3 | 4 | --------------------------------------------------------------------------------