├── ocr
└── __init__.py
├── spider
├── __init__.py
├── user_spider.py
├── comment_spider.py
└── article_spider.py
├── requirements.txt
├── setting.py
├── log.py
├── README.md
├── LICENSE
├── utils.py
├── .gitignore
├── zhihu_client.py
├── print_beautify.py
├── data_extractor.py
├── static
├── encrypt_old.js
└── encrypt.js
└── main.py
/ocr/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spider/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.5.4
2 | async-timeout==3.0.1
3 | attrs==19.1.0
4 | brotlipy==0.7.0
5 | cffi==1.12.3
6 | chardet==3.0.4
7 | html2text==2018.1.9
8 | idna==2.8
9 | multidict==4.5.2
10 | Pillow==6.1.0
11 | pycparser==2.19
12 | PyExecJS==1.5.1
13 | six==1.12.0
14 | yarl==1.3.0
15 | pyquery==1.4.0
--------------------------------------------------------------------------------
/setting.py:
--------------------------------------------------------------------------------
1 | """
2 | 路径使用绝对路径
3 | """
4 | DEBUG = False
5 |
6 | LOG_DIR = '' # df: /tmp/zhihu debug模式下存放日志
7 |
8 | COOKIE_FILE = '' # df: /tmp/cookies.pick 缓存的cookie文件
9 |
10 | SAVE_DIR = '' # df: /tmp/zhihu_save 保存回答到本地
11 |
12 | USER = '' # 必填 账号
13 |
14 | PASSWORD = '' # 必填 密码
15 |
--------------------------------------------------------------------------------
/spider/user_spider.py:
--------------------------------------------------------------------------------
1 | from utils import SpiderBaseclass
2 |
3 |
4 | class UserSpider(SpiderBaseclass):
5 | """用户信息爬取"""
6 | async def get_self_info(self) -> dict:
7 | """
8 | 获取我的信息
9 | :return:
10 | """
11 | url = 'https://www.zhihu.com/api/v4/me?include=ad_type;available_message_types,' \
12 | 'default_notifications_count,follow_notifications_count,vote_thank_notifications_count,' \
13 | 'messages_count;draft_count;following_question_count;account_status,is_bind_phone,' \
14 | 'is_force_renamed,email,renamed_fullname;ad_type'
15 |
16 | async with self.client.get(url) as resp:
17 | result = await resp.json()
18 | self.logger.debug(result)
19 | return result
--------------------------------------------------------------------------------
/log.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | from logging.handlers import RotatingFileHandler
4 | from setting import DEBUG
5 | from setting import LOG_DIR
6 |
7 |
8 | def get_logger():
9 | """
10 | 获取日志对象
11 | :return:
12 | """
13 | log_dir = LOG_DIR if LOG_DIR else '/tmp/zhihu/'
14 | if not os.path.exists(log_dir):
15 | os.makedirs(log_dir)
16 | log = logging.getLogger(__name__)
17 | log.setLevel(logging.DEBUG) if DEBUG else log.setLevel(logging.ERROR)
18 | log_file = os.path.join(log_dir, 'log.log')
19 | handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024 * 30, backupCount=10)
20 | handler1 = logging.StreamHandler()
21 | default_format = logging.Formatter(
22 | '[%(levelname)1.1s %(asctime)s.%(msecs)03d %(module)s:%(lineno)d]%(' 'message)s ')
23 | handler.setFormatter(fmt=default_format)
24 | handler1.setFormatter(fmt=default_format)
25 | log.addHandler(handler)
26 | log.addHandler(handler1)
27 | log.debug('----------初始化日志-----------')
28 | return log
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zhihu-terminal
2 | 命令行版知乎
3 | 灵感来自[duduainankai/zhihu-terminal](https://github.com/duduainankai/zhihu-terminal)
4 |
5 | ## 目前功能
6 |
7 |
8 |
9 | ## GIF演示
10 |
11 |
12 |
13 | ## 项目介绍
14 | 本项目为知乎的终端版实现,基于asyncio实现
15 |
16 | 运行此项目即可以使用命令行的方式来操作知乎,功能涵盖:浏览,点赞,感谢等功能,未来将实现知乎网页端的绝大部分功能
17 |
18 | ## 运行环境
19 | Python 3.7
20 |
21 | 项目在Mac OSX 10.14.5 进行开发,目前未进行Windows系统的适配
22 |
23 | ## 准备
24 |
25 | 建议拉取时仅拉取最近一次提交,历史提交中存在一个比较大的动图拉取时间会较长
26 | ```
27 | git clone --depth 1 git@github.com:wf1314/zhihu-terminal.git
28 | ```
29 | 安装Python 3.7的环境后执行
30 | ```
31 | pip install -r requirements.txt
32 | ```
33 |
34 | ## 本地运行
35 |
36 | 找到项目中的[setting.py](/setting.py)修改账号,密码等必填项
37 |
38 | 执行:
39 |
40 | ```
41 | python main.py
42 | ```
43 |
44 | ## 开发中的功能:
45 |
46 | 获取关注内容(TODO)
47 |
48 | 收藏回答(TODO)
49 |
50 | 回复评论(TODO)
51 |
52 | 验证码自动识别(TODO)
53 |
54 | 查看用户主页信息(TODO)
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 huvvao
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 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Any
3 | """
4 | 前景色 背景色 颜色
5 | 30 40 黑色
6 | 31 41 红色
7 | 32 42 绿色
8 | 33 43 黃色
9 | 34 44 蓝色(有问题)
10 | 35 45 紫红色
11 | 36 46 青蓝色
12 | 37 47 白色
13 |
14 | 显示方式 意义
15 | 0 终端默认设置
16 | 1 高亮显示
17 | 4 使用下划线
18 | 5 闪烁
19 | 7 反白显示
20 | 8 不可见
21 | """
22 | colour_map = {
23 | 'black': '30',
24 | 'red': '31',
25 | 'green': '32',
26 | 'yellow': '33',
27 | 'blue': '34',
28 | 'purple': '35',
29 | 'ultramarine': '36',
30 | 'white': '37',
31 | }
32 |
33 | cmd_func_map = {
34 | 'up': 'endorse_answer',
35 | 'down': 'endorse_answer',
36 | 'neutral': 'endorse_answer',
37 | 'thank': 'thank_answer',
38 | 'unthank': 'thank_answer',
39 | 'read-cmt': 'get_comments',
40 | }
41 |
42 |
43 | def get_com_func(cmd):
44 | return cmd_func_map[cmd]
45 |
46 |
47 | def print_colour(s: Any, colour: str='green', way: int=0, **kwargs):
48 | """打印颜色"""
49 | print(f'\033[{way};{colour_map[colour]};m{s}', **kwargs)
50 |
51 |
52 | abs_dir = lambda: os.path.dirname(os.path.abspath(__file__))
53 |
54 |
55 | class SpiderBaseclass(object):
56 |
57 | def __init__(self, client):
58 | self.client = client
59 | self.logger = self.client.logger
60 |
61 |
62 | if __name__ == '__main__':
63 | ...
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | /static/cookies.pick
107 | .idea/
108 | cookies.pick
109 |
--------------------------------------------------------------------------------
/spider/comment_spider.py:
--------------------------------------------------------------------------------
1 | from utils import SpiderBaseclass
2 |
3 |
4 | class CommentSpider(SpiderBaseclass):
5 | """评论爬取"""
6 |
7 | async def get_comments(self, uid: str, typ: str='answer') -> dict:
8 | """
9 | 获取评论
10 | :param uid:
11 | :param typ:
12 | :return:
13 | """
14 | # uid = '720626601'
15 | url = f'https://www.zhihu.com/api/v4/{typ}s/{uid}/root_comments'
16 | params = {
17 | 'order': 'normal',
18 | 'limit': '20',
19 | 'offset': '0',
20 | 'status': 'open',
21 | }
22 |
23 | r = await self.client.get(url, params=params)
24 | self.logger.debug(await r.text())
25 | result = await r.json()
26 | self.logger.debug(result)
27 | return result
28 |
29 | async def get_comments_by_url(self, url) -> dict:
30 | """
31 | 获取评论
32 | :param uid:
33 | :param typ:
34 | :return:
35 | """
36 | r = await self.client.get(url)
37 | self.logger.debug(await r.text())
38 | result = await r.json()
39 | self.logger.debug(result)
40 | return result
41 |
42 | async def endorse_comment(self, uid: str, delete: bool = False) -> dict:
43 | """
44 | 赞同评论
45 | :param uid:
46 | :param delete:
47 | :return:
48 | """
49 | url = f'https://www.zhihu.com/api/v4/comments/{uid}/actions/like'
50 | if delete:
51 | r = await self.client.delete(url)
52 | else:
53 | r = await self.client.post(url)
54 | result = await r.json()
55 | self.logger.debug(result)
56 | return result
57 |
58 |
59 | if __name__ == '__main__':
60 | import asyncio
61 | from setting import USER, PASSWORD
62 | from zhihu_client import ZhihuClient
63 |
64 | async def test():
65 | client = ZhihuClient(user=USER, password=PASSWORD)
66 | await client.login(load_cookies=True)
67 | spider = CommentSpider(client)
68 | await spider.get_comments('123')
69 | await client.close()
70 |
71 | asyncio.run(test())
--------------------------------------------------------------------------------
/spider/article_spider.py:
--------------------------------------------------------------------------------
1 | """
2 | 知乎api
3 | """
4 | import re
5 | import asyncio
6 | from zhihu_client import ZhihuClient
7 | from utils import SpiderBaseclass
8 |
9 |
10 | class ArticleSpider(SpiderBaseclass):
11 | """文章相关"""
12 |
13 | async def get_recommend_article(self) -> dict:
14 | """
15 | 获取推荐文章
16 | :return:
17 | """
18 | url = 'https://www.zhihu.com'
19 | for _ in range(2):
20 | async with self.client.get(url) as r:
21 | resp = await r.text()
22 | session_token = re.findall(r'session_token=(.*?)\&', resp)
23 | if session_token:
24 | session_token = session_token[0]
25 | break
26 | else:
27 | raise AssertionError('获取session_token失败')
28 | url = 'https://www.zhihu.com/api/v3/feed/topstory/recommend?'
29 | data = {
30 | 'session_token': session_token,
31 | 'desktop': 'true',
32 | 'page_number': '1',
33 | 'limit': '6',
34 | 'action': 'down',
35 | 'after_id': '5',
36 | }
37 | async with self.client.get(url, params=data) as r:
38 | result = await r.json()
39 | self.logger.debug(result)
40 | return result
41 |
42 | async def endorse_answer(self, uid: str, typ: str = 'up') -> dict:
43 | """
44 | 赞同回答
45 | :param uid:
46 | :param typ: up赞同, down踩, neutral中立
47 | :return:
48 | """
49 | # 724073802
50 | url = f'https://www.zhihu.com/api/v4/answers/{uid}/voters'
51 | data = {
52 | 'type': typ
53 | }
54 | r = await self.client.post(url, json=data)
55 | result = await r.json()
56 | self.logger.debug(result)
57 | return result
58 |
59 | async def thank_answer(self, uid: str, delete: bool = False) -> dict:
60 | """
61 | 感谢回答
62 | :param uid:
63 | :param delete:
64 | :return:
65 | """
66 | url = f'https://www.zhihu.com/api/v4/answers/{uid}/thankers'
67 | if delete:
68 | r = await self.client.delete(url)
69 | else:
70 | r = await self.client.post(url)
71 | result = await r.json()
72 | self.logger.debug(result)
73 | return result
74 |
75 | async def get_question_article_first(self, question_id: str, uid: str):
76 | """
77 |
78 | :param uid:
79 | :param question_id:
80 | :return:
81 | """
82 | url = f'https://www.zhihu.com/question/{question_id}/answer/{uid}'
83 | r = await self.client.get(url)
84 | resp = await r.text()
85 | self.logger.debug(resp)
86 | return resp
87 |
88 | async def get_article_by_question(self, question_id, offset: int = 0, limit: int = 3):
89 | """
90 |
91 | :param question_id:
92 | :param offset:
93 | :param limit:
94 | :return:
95 | """
96 | url = f'https://www.zhihu.com/api/v4/questions/{question_id}/answers'
97 | params = {
98 | 'include': 'data[*].is_normal,admin_closed_comment,reward_info,'
99 | 'is_collapsed,annotation_action,annotation_detail,collapse_reason,'
100 | 'is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,'
101 | 'content,editable_content,voteup_count,reshipment_settings,comment_permission,'
102 | 'created_time,updated_time,review_info,relevant_info,question,excerpt,'
103 | 'relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,is_labeled,'
104 | 'is_recognized,paid_info,paid_info_content;data[*].mark_infos[*].url;data[*].'
105 | 'author.follower_count,badge[*].topics',
106 | 'offset': offset,
107 | 'limit': limit,
108 | 'sort_by': 'default',
109 | 'platform': 'desktop',
110 | }
111 | r = await self.client.get(url, params=params)
112 | result = await r.json()
113 | self.logger.debug(result)
114 | return result
115 |
116 | async def get_article_by_question_url(self, url):
117 | """
118 |
119 | :param question_id:
120 | :param offset:
121 | :param limit:
122 | :return:
123 | """
124 | r = await self.client.get(url)
125 | result = await r.json()
126 | self.logger.debug(result)
127 | return result
128 |
129 |
130 | if __name__ == '__main__':
131 | from setting import USER, PASSWORD
132 |
133 |
134 | async def test():
135 | client = ZhihuClient(user=USER, password=PASSWORD)
136 | await client.login(load_cookies=True)
137 | spider = ArticleSpider(client)
138 | await spider.get_recommend_article()
139 | await client.close()
140 |
141 |
142 | asyncio.run(test())
143 |
--------------------------------------------------------------------------------
/zhihu_client.py:
--------------------------------------------------------------------------------
1 | """
2 | 保存有知乎登录cookie的ClientSession
3 | """
4 | import aiohttp
5 | import asyncio
6 | import base64
7 | import execjs
8 | import hmac
9 | import hashlib
10 | import json
11 | import re
12 | import os
13 | import sys
14 | import time
15 | # import threading
16 | from typing import Union
17 | from PIL import Image
18 | from urllib.parse import urlencode
19 | from utils import print_colour
20 | from log import get_logger
21 | from setting import COOKIE_FILE
22 |
23 |
24 | class ZhihuClient(aiohttp.ClientSession):
25 | """扩展ClientSession"""
26 |
27 | def __init__(self, user='', password='', *args, **kwargs):
28 | super().__init__(*args, **kwargs)
29 | self.user = user
30 | self.password = password
31 | headers = {
32 | 'Host': 'www.zhihu.com',
33 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
34 | '(KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586',
35 | 'Connection': 'Keep-Alive',
36 | 'Referer': 'https://www.zhihu.com/',
37 | 'accept-encoding': 'gzip, deflate',
38 | }
39 | self._default_headers = headers
40 | self.logger = get_logger()
41 | self.cookie_file = COOKIE_FILE or '/tmp/cookies.pick'
42 |
43 | def get(self, url, **kwargs):
44 | """Perform HTTP GET request."""
45 | return super().get(url, ssl=False, **kwargs)
46 |
47 | def post(self, url, data=None, **kwargs):
48 | """Perform HTTP POST request."""
49 | return super().post(url, ssl=False, data=data, **kwargs)
50 |
51 | def put(self, url, data=None, **kwargs):
52 | """Perform HTTP PUT request."""
53 | return super().put(url, ssl=False, data=data, **kwargs)
54 |
55 | async def login(self, load_cookies: bool=False) -> None:
56 | """
57 | 登录
58 | :param load_cookies: 是否加载cookie
59 | :return:
60 | """
61 | if load_cookies:
62 | self.cookie_jar.load(self.cookie_file)
63 | self.logger.debug(f'加载cookies从:{self.cookie_file}')
64 | is_succ = await self.check_login()
65 | if is_succ:
66 | print_colour('登录成功!', colour='green')
67 | return
68 | else:
69 | print_colour('通过缓存登录失败尝试重新登录', 'red')
70 | self.cookie_jar.clear()
71 | os.remove(self.cookie_file)
72 |
73 | login_data = {
74 | 'client_id': 'c3cef7c66a1843f8b3a9e6a1e3160e20',
75 | 'grant_type': 'password',
76 | 'source': 'com.zhihu.web',
77 | 'username': self.user,
78 | 'password': self.password,
79 | 'lang': 'en', # en 4位验证码, cn 中文验证码
80 | 'ref_source': 'other_https://www.zhihu.com/signin?next=%2F',
81 | 'utm_source': ''
82 | }
83 | xsrf = await self._get_xsrf()
84 | captcha = await self._get_captcha()
85 | timestamp = int(time.time() * 1000)
86 | login_data.update({
87 | 'captcha': captcha,
88 | 'timestamp': timestamp,
89 | 'signature': self._get_signature(timestamp, login_data)
90 | })
91 | headers = {
92 | 'accept-encoding': 'gzip, deflate, br',
93 | 'Host': 'www.zhihu.com',
94 | 'Referer': 'https://www.zhihu.com/',
95 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
96 | '(KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586',
97 | 'content-type': 'application/x-www-form-urlencoded',
98 | 'x-zse-83': '3_2.0',
99 | 'x-xsrftoken': xsrf
100 | }
101 | data = self._encrypt(login_data)
102 | url = 'https://www.zhihu.com/api/v3/oauth/sign_in'
103 | async with self.post(url, data=data, headers=headers) as r:
104 | resp = await r.text()
105 | if 'error' in resp:
106 | print_colour(json.loads(resp)['error'], 'red')
107 | self.logger.debug(f"登录失败:{json.loads(resp)['error']}")
108 | sys.exit()
109 | self.logger.debug(resp)
110 | is_succ = await self.check_login()
111 | if is_succ:
112 | print_colour('登录成功!', colour='green')
113 | else:
114 | print_colour('登录失败!', colour='red')
115 | sys.exit()
116 |
117 | async def _get_captcha(self) -> str:
118 | """
119 | 请求验证码的 API 接口,无论是否需要验证码都需要请求一次
120 | 如果需要验证码会返回图片的 base64 编码
121 | :return: 验证码的 POST 参数
122 | """
123 |
124 | url = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
125 | async with self.get(url) as r:
126 | resp = await r.text()
127 | show_captcha = re.search(r'true', resp)
128 | if show_captcha:
129 | async with self.put(url) as r:
130 | resp = await r.text()
131 | json_data = json.loads(resp)
132 | img_base64 = json_data['img_base64'].replace(r'\n', '')
133 | with open(f'./captcha.jpg', 'wb') as f:
134 | f.write(base64.b64decode(img_base64))
135 | img = Image.open(f'./captcha.jpg')
136 | # if lang == 'cn':
137 | # import matplotlib.pyplot as plt
138 | # plt.imshow(img)
139 | # print('点击所有倒立的汉字,在命令行中按回车提交')
140 | # points = plt.ginput(7)
141 | # capt = json.dumps({'img_size': [200, 44],
142 | # 'input_points': [[i[0] / 2, i[1] / 2] for i in points]})
143 | # else:
144 | # img_thread = threading.Thread(target=img.show, daemon=True)
145 | # img_thread.start()
146 | # TODO 验证码自动识别实现
147 | loop = asyncio.get_running_loop()
148 | loop.run_in_executor(None, img.show)
149 | capt = input('请输入图片里的验证码:')
150 | # 这里必须先把参数 POST 验证码接口
151 | await self.post(url, data={'input_text': capt})
152 | return capt
153 | return ''
154 |
155 | async def check_login(self) -> bool:
156 | """
157 | 检查登录状态,访问登录页面出现跳转则是已登录,
158 | 如登录成功保存当前 Cookies
159 | :return: bool
160 | """
161 | url = 'https://www.zhihu.com/'
162 | async with self.get(url, allow_redirects=False) as r:
163 | if r.status == 200:
164 | self.cookie_jar.save(self.cookie_file)
165 | self.logger.debug(f'保存cookies到->{self.cookie_file}')
166 | return True
167 | else:
168 | self.logger.debug(await r.text())
169 | self.logger.debug(r.headers)
170 | self.logger.debug(r.status)
171 | return False
172 |
173 | async def _get_xsrf(self) -> str:
174 | """
175 | 从登录页面获取 xsrf
176 | :return: str
177 | """
178 | async with self.get('https://www.zhihu.com/', allow_redirects=False) as r:
179 | self.logger.debug('尝试获取xsrf token')
180 | if r.cookies.get('_xsrf'):
181 | self.logger.debug(f'获取成功{r.cookies.get("_xsrf").value}')
182 | return r.cookies.get('_xsrf').value
183 | raise AssertionError('获取 xsrf 失败')
184 |
185 | def _get_signature(self, timestamp: Union[int, str], login_data: dict) -> str:
186 | """
187 | 通过 Hmac 算法计算返回签名
188 | 实际是几个固定字符串加时间戳
189 | :param timestamp: 时间戳
190 | :return: 签名
191 | """
192 | ha = hmac.new(b'd1b964811afb40118a12068ff74a12f4', digestmod=hashlib.sha1)
193 | grant_type = login_data['grant_type']
194 | client_id = login_data['client_id']
195 | source = login_data['source']
196 | ha.update(bytes((grant_type + client_id + source + str(timestamp)), 'utf-8'))
197 | return ha.hexdigest()
198 |
199 | # @staticmethod
200 | # def _encrypt(form_data: dict) -> str:
201 | # with open(f'./static/encrypt_old.js') as f:
202 | # js = execjs.compile(f.read())
203 | # return js.call('Q', urlencode(form_data))
204 |
205 | @staticmethod
206 | def _encrypt(form_data: dict):
207 | with open('./static/encrypt.js') as f:
208 | js = execjs.compile(f.read())
209 | return js.call('b', urlencode(form_data))
210 |
211 |
212 | if __name__ == '__main__':
213 | from setting import USER, PASSWORD
214 |
215 | async def test():
216 | client = ZhihuClient(user=USER, password=PASSWORD)
217 | await client.login(load_cookies=False)
218 | await client.close()
219 |
220 | asyncio.run(test())
221 |
--------------------------------------------------------------------------------
/print_beautify.py:
--------------------------------------------------------------------------------
1 | import os
2 | import html2text
3 | from utils import print_colour
4 | from setting import SAVE_DIR
5 |
6 |
7 | def print_logo():
8 | os.system("clear")
9 | logo = '''
10 | ;$$;
11 | #############
12 | #############;#####o
13 | ## o#########################
14 | ##### $###############################
15 | ## ###$ ######! ##########################
16 | ## ### $### ################### ######
17 | ### ### ##o#######################
18 | ###### ;### #### #####################
19 | ## ### ###### ######&###############
20 | ## ### ###### ## ############ #######
21 | o## ######## ## ##################
22 | ##o ### #### #######o#######
23 | ## ###### ###############
24 | ## #### #############!
25 | ### #########
26 | #####& ## o####
27 | ###### ## ####*
28 | ## !## #####
29 | ## ##* ####; ##
30 | ##### #####o #####
31 | #### ### ### $###o
32 | ### ## ####! $###
33 | ## #####
34 | ## ##
35 | ;## ### ;
36 | ##$ ##
37 | ####### ##
38 | ##### # ##
39 | ### ### ###
40 | ### ### ##
41 | ## ;## ##
42 | ## ### ##
43 | ### ### ##
44 | #### ##
45 | ### ##
46 | ##; ##
47 | ##$ ##&
48 | ## ##
49 | ##; ##
50 | ## ##;
51 | ### ### ##$
52 | ### ### ##
53 | ###################### #####&&&&&&&&&&&##
54 | ### $#####$ ############&$o$###############################
55 | # $#######&o
56 | '''
57 | print_colour(logo, 'ultramarine')
58 |
59 |
60 | def print_recommend_article(output: list):
61 | """
62 | 打印推荐文章简述
63 | :param output:
64 | :return:
65 | """
66 | for d in output:
67 | print_colour('=' * 60, 'white')
68 | print_colour(f'article_id:{d["id"]}', 'purple')
69 | print_colour(f'question_id:{d["question"]["id"]}', 'purple')
70 | print_colour(d['question']['title'], 'purple', end='')
71 | print_colour(f"({d['author']['name']})", 'purple')
72 | print_colour(d['excerpt'])
73 | print_colour(f"*赞同数{d.get('voteup_count')} 感谢数{d.get('thanks_count', 0)} "
74 | f"评论数{d.get('comment_count')} 浏览数{d.get('visited_count')}*", 'purple')
75 |
76 |
77 | def print_article_content(output: dict):
78 | """
79 | 打印文章内容
80 | :param output:
81 | :return:
82 | """
83 | content = output['content']
84 | title = output['question']['title']
85 | question_id = output['question']['id']
86 | article_id = output["id"]
87 | typ = output['type']
88 | if typ == 'zvideo':
89 | url = f'https://www.zhihu.com/zvideo/{article_id}'
90 | elif article_id and not question_id:
91 | url = f'https://zhuanlan.zhihu.com/p/{article_id}'
92 | else:
93 | url = f'https://www.zhihu.com/question/{question_id}/answer/{article_id}'
94 | content = html2text.html2text(content)
95 | print_colour(content)
96 | print_colour('-----------------------------------------------------', 'purple')
97 | print_colour(f'|article_id:{article_id}', 'purple')
98 | print_colour(f'|question_id:{question_id}', 'purple')
99 | print_colour(f'|title:{title}', 'purple')
100 | print_colour(f'|原文链接:{url}', 'purple')
101 | print_colour('-----------------------------------------------------', 'purple')
102 |
103 |
104 | def print_question(question: dict):
105 | """
106 | 打印问题及第默认排序下的第一个回答
107 | :param output:
108 | :return:
109 | """
110 | title = question['title']
111 | # question_id = question['id']
112 | question_content = question['detail']
113 | question_content = html2text.html2text(question_content)
114 | print_colour('*' * 50, 'purple')
115 | print_colour(f'标题:{title}')
116 | print_colour('问题详情:')
117 | print_colour(question_content)
118 | print_colour('*' * 50, 'purple')
119 |
120 |
121 | def print_comments(output: list):
122 | """
123 | 打印评论
124 | :param output:
125 | :return:
126 | """
127 | for d in output:
128 | author = d.get('author').get('name')
129 | reply_to_author = d.get('reply_to_author').get('name')
130 | content = d.get('content')
131 | vote_count = d.get('vote_count')
132 | comment_id = d.get('id')
133 | child_comments = d.get('child_comments')
134 | print_colour(f'comment_id:{comment_id}', 'purple')
135 | if d.get('featured'):
136 | print_colour('热评🔥', end='')
137 | if reply_to_author:
138 | print_colour(f'{author}->{reply_to_author}', end='')
139 | else:
140 | print_colour(f'{author}', end='')
141 | print_colour(f'(赞:{vote_count}):{content}')
142 | if child_comments:
143 | for clild in child_comments:
144 | author = clild.get('author').get('name')
145 | reply_to_author = clild.get('reply_to_author').get('name')
146 | content = clild.get('content')
147 | vote_count = clild.get('vote_count')
148 | comment_id = clild.get('id')
149 | print_colour(f' comment_id:{comment_id}', 'purple')
150 | if d.get('featured'):
151 | print_colour(' 热评🔥', end='')
152 | if reply_to_author:
153 | print_colour(f' {author}->{reply_to_author}', end='')
154 | else:
155 | print_colour(f' {author}', end='')
156 | print_colour(f' (赞:{vote_count}):{content}')
157 | print_colour(' *********************************************************', 'blue')
158 | print_colour('==========================================================', 'blue')
159 |
160 |
161 | def print_vote_thank(output: dict, typ: str):
162 | """
163 | 打印赞同感谢 up', 'down', 'neutral'
164 | :param output:
165 | :return:
166 | """
167 | if output.get('error'):
168 | print_colour(output.get('error'), 'red')
169 | elif typ == 'thank':
170 | print_colour(f'感谢成功!感谢总数{output["thanks_count"]}')
171 | elif typ == 'unthank':
172 | print_colour(f'取消感谢!感谢总数{output["thanks_count"]}')
173 | elif typ == 'up':
174 | print_colour(f'赞同成功!赞同总数{output["voteup_count"]}')
175 | elif typ == 'down':
176 | print_colour(f'反对成功!赞同总数{output["voteup_count"]}')
177 | else:
178 | print_colour(f'保持中立!赞同总数{output["voteup_count"]}')
179 |
180 |
181 | def print_vote_comments(output: dict, typ: str):
182 | """
183 | 打印赞同感谢 up', 'down', 'neutral'
184 | :param output:
185 | :return:
186 | """
187 | if output.get('error'):
188 | print_colour(output.get('error'), 'red')
189 | elif typ == 'up':
190 | print_colour(f'点赞评论成功!被赞总数{output["vote_count"]}')
191 | elif typ == 'neutral':
192 | print_colour(f'保持中立!被赞总数{output["vote_count"]}')
193 |
194 |
195 | def print_save(article: dict):
196 | """
197 | 保存文章到本地
198 | :param article:
199 | :return:
200 | """
201 | uid = article.get('id')
202 | title = article.get('question').get('title')
203 | content = article.get('content')
204 | save_dir = SAVE_DIR or '/tmp/zhihu_save'
205 | file = f'{save_dir}/{title}_{uid}.html'
206 | with open(file, 'w') as f:
207 | head = '
Hello world
`); 4 | window = dom.window; 5 | document = window.document; 6 | 7 | function t(e) { 8 | return (t = 'function' == typeof Symbol && 'symbol' == typeof Symbol.A ? function (e) { 9 | return typeof e; 10 | } 11 | : function (e) { 12 | return e && 'function' == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? 'symbol' : typeof e; 13 | } 14 | )(e); 15 | } 16 | 17 | Object.defineProperty(exports, '__esModule', { 18 | value: !0, 19 | }); 20 | var A = '2.0' 21 | , __g = {}; 22 | 23 | function s() { 24 | } 25 | 26 | function i(e) { 27 | this.t = (2048 & e) >> 11, 28 | this.s = (1536 & e) >> 9, 29 | this.i = 511 & e, 30 | this.h = 511 & e; 31 | } 32 | 33 | function h(e) { 34 | this.s = (3072 & e) >> 10, 35 | this.h = 1023 & e; 36 | } 37 | 38 | function a(e) { 39 | this.a = (3072 & e) >> 10, 40 | this.c = (768 & e) >> 8, 41 | this.n = (192 & e) >> 6, 42 | this.t = 63 & e; 43 | } 44 | 45 | function c(e) { 46 | this.s = e >> 10 & 3, 47 | this.i = 1023 & e; 48 | } 49 | 50 | function n() { 51 | } 52 | 53 | function e(e) { 54 | this.a = (3072 & e) >> 10, 55 | this.c = (768 & e) >> 8, 56 | this.n = (192 & e) >> 6, 57 | this.t = 63 & e; 58 | } 59 | 60 | function o(e) { 61 | this.h = (4095 & e) >> 2, 62 | this.t = 3 & e; 63 | } 64 | 65 | function r(e) { 66 | this.s = e >> 10 & 3, 67 | this.i = e >> 2 & 255, 68 | this.t = 3 & e; 69 | } 70 | 71 | s.prototype.e = function (e) { 72 | e.o = !1; 73 | } 74 | , 75 | i.prototype.e = function (e) { 76 | switch (this.t) { 77 | case 0: 78 | e.r[this.s] = this.i; 79 | break; 80 | case 1: 81 | e.r[this.s] = e.k[this.h]; 82 | } 83 | } 84 | , 85 | h.prototype.e = function (e) { 86 | e.k[this.h] = e.r[this.s]; 87 | } 88 | , 89 | a.prototype.e = function (e) { 90 | switch (this.t) { 91 | case 0: 92 | e.r[this.a] = e.r[this.c] + e.r[this.n]; 93 | break; 94 | case 1: 95 | e.r[this.a] = e.r[this.c] - e.r[this.n]; 96 | break; 97 | case 2: 98 | e.r[this.a] = e.r[this.c] * e.r[this.n]; 99 | break; 100 | case 3: 101 | e.r[this.a] = e.r[this.c] / e.r[this.n]; 102 | break; 103 | case 4: 104 | e.r[this.a] = e.r[this.c] % e.r[this.n]; 105 | break; 106 | case 5: 107 | e.r[this.a] = e.r[this.c] == e.r[this.n]; 108 | break; 109 | case 6: 110 | e.r[this.a] = e.r[this.c] >= e.r[this.n]; 111 | break; 112 | case 7: 113 | e.r[this.a] = e.r[this.c] || e.r[this.n]; 114 | break; 115 | case 8: 116 | e.r[this.a] = e.r[this.c] && e.r[this.n]; 117 | break; 118 | case 9: 119 | e.r[this.a] = e.r[this.c] !== e.r[this.n]; 120 | break; 121 | case 10: 122 | e.r[this.a] = t(e.r[this.c]); 123 | break; 124 | case 11: 125 | e.r[this.a] = e.r[this.c] in e.r[this.n]; 126 | break; 127 | case 12: 128 | e.r[this.a] = e.r[this.c] > e.r[this.n]; 129 | break; 130 | case 13: 131 | e.r[this.a] = -e.r[this.c]; 132 | break; 133 | case 14: 134 | e.r[this.a] = e.r[this.c] < e.r[this.n]; 135 | break; 136 | case 15: 137 | e.r[this.a] = e.r[this.c] & e.r[this.n]; 138 | break; 139 | case 16: 140 | e.r[this.a] = e.r[this.c] ^ e.r[this.n]; 141 | break; 142 | case 17: 143 | e.r[this.a] = e.r[this.c] << e.r[this.n]; 144 | break; 145 | case 18: 146 | e.r[this.a] = e.r[this.c] >>> e.r[this.n]; 147 | break; 148 | case 19: 149 | e.r[this.a] = e.r[this.c] | e.r[this.n]; 150 | break; 151 | case 20: 152 | e.r[this.a] = !e.r[this.c]; 153 | } 154 | } 155 | , 156 | c.prototype.e = function (e) { 157 | e.Q.push(e.C), 158 | e.B.push(e.k), 159 | e.C = e.r[this.s], 160 | e.k = []; 161 | for (var t = 0; t < this.i; t++) 162 | e.k.unshift(e.f.pop()); 163 | e.g.push(e.f), 164 | e.f = []; 165 | } 166 | , 167 | n.prototype.e = function (e) { 168 | e.C = e.Q.pop(), 169 | e.k = e.B.pop(), 170 | e.f = e.g.pop(); 171 | } 172 | , 173 | e.prototype.e = function (e) { 174 | switch (this.t) { 175 | case 0: 176 | e.u = e.r[this.a] >= e.r[this.c]; 177 | break; 178 | case 1: 179 | e.u = e.r[this.a] <= e.r[this.c]; 180 | break; 181 | case 2: 182 | e.u = e.r[this.a] > e.r[this.c]; 183 | break; 184 | case 3: 185 | e.u = e.r[this.a] < e.r[this.c]; 186 | break; 187 | case 4: 188 | e.u = e.r[this.a] == e.r[this.c]; 189 | break; 190 | case 5: 191 | e.u = e.r[this.a] != e.r[this.c]; 192 | break; 193 | case 6: 194 | e.u = e.r[this.a]; 195 | break; 196 | case 7: 197 | e.u = !e.r[this.a]; 198 | } 199 | } 200 | , 201 | o.prototype.e = function (e) { 202 | switch (this.t) { 203 | case 0: 204 | e.C = this.h; 205 | break; 206 | case 1: 207 | e.u && (e.C = this.h); 208 | break; 209 | case 2: 210 | e.u || (e.C = this.h); 211 | break; 212 | case 3: 213 | e.C = this.h, 214 | e.w = null; 215 | } 216 | e.u = !1; 217 | } 218 | , 219 | r.prototype.e = function (e) { 220 | switch (this.t) { 221 | case 0: 222 | for (var t = [], n = 0; n < this.i; n++) 223 | t.unshift(e.f.pop()); 224 | e.r[3] = e.r[this.s](t[0], t[1]); 225 | break; 226 | case 1: 227 | for (var r = e.f.pop(), i = [], o = 0; o < this.i; o++) 228 | i.unshift(e.f.pop()); 229 | e.r[3] = e.r[this.s][r](i[0], i[1]); 230 | break; 231 | case 2: 232 | for (var a = [], s = 0; s < this.i; s++) 233 | a.unshift(e.f.pop()); 234 | e.r[3] = new e.r[this.s](a[0], a[1]); 235 | } 236 | } 237 | ; 238 | var k = function (e) { 239 | for (var t = 66, n = [], r = 0; r < e.length; r++) { 240 | var i = 24 ^ e.charCodeAt(r) ^ t; 241 | n.push(String.fromCharCode(i)), 242 | t = i; 243 | } 244 | return n.join(''); 245 | }; 246 | 247 | function Q(e) { 248 | this.t = (4095 & e) >> 10, 249 | this.s = (1023 & e) >> 8, 250 | this.i = 1023 & e, 251 | this.h = 63 & e; 252 | } 253 | 254 | function C(e) { 255 | this.t = (4095 & e) >> 10, 256 | this.a = (1023 & e) >> 8, 257 | this.c = (255 & e) >> 6; 258 | } 259 | 260 | function B(e) { 261 | this.s = (3072 & e) >> 10, 262 | this.h = 1023 & e; 263 | } 264 | 265 | function f(e) { 266 | this.h = 4095 & e; 267 | } 268 | 269 | function g(e) { 270 | this.s = (3072 & e) >> 10; 271 | } 272 | 273 | function u(e) { 274 | this.h = 4095 & e; 275 | } 276 | 277 | function w(e) { 278 | this.t = (3840 & e) >> 8, 279 | this.s = (192 & e) >> 6, 280 | this.i = 63 & e; 281 | } 282 | 283 | function G() { 284 | this.r = [0, 0, 0, 0], 285 | this.C = 0, 286 | this.Q = [], 287 | this.k = [], 288 | this.B = [], 289 | this.f = [], 290 | this.g = [], 291 | this.u = !1, 292 | this.G = [], 293 | this.b = [], 294 | this.o = !1, 295 | this.w = null, 296 | this.U = null, 297 | this.F = [], 298 | this.R = 0, 299 | this.J = { 300 | 0: s, 301 | 1: i, 302 | 2: h, 303 | 3: a, 304 | 4: c, 305 | 5: n, 306 | 6: e, 307 | 7: o, 308 | 8: r, 309 | 9: Q, 310 | 10: C, 311 | 11: B, 312 | 12: f, 313 | 13: g, 314 | 14: u, 315 | 15: w, 316 | }; 317 | } 318 | 319 | Q.prototype.e = function (e) { 320 | switch (this.t) { 321 | case 0: 322 | e.f.push(e.r[this.s]); 323 | break; 324 | case 1: 325 | e.f.push(this.i); 326 | break; 327 | case 2: 328 | e.f.push(e.k[this.h]); 329 | break; 330 | case 3: 331 | e.f.push(k(e.b[this.h])); 332 | } 333 | } 334 | , 335 | C.prototype.e = function (A) { 336 | switch (this.t) { 337 | case 0: 338 | var t = A.f.pop(); 339 | A.r[this.a] = A.r[this.c][t]; 340 | break; 341 | case 1: 342 | var s = A.f.pop() 343 | , i = A.f.pop(); 344 | A.r[this.c][s] = i; 345 | break; 346 | case 2: 347 | var h = A.f.pop(); 348 | A.r[this.a] = eval(h); 349 | } 350 | } 351 | , 352 | B.prototype.e = function (e) { 353 | e.r[this.s] = k(e.b[this.h]); 354 | } 355 | , 356 | f.prototype.e = function (e) { 357 | e.w = this.h; 358 | } 359 | , 360 | g.prototype.e = function (e) { 361 | throw e.r[this.s]; 362 | } 363 | , 364 | u.prototype.e = function (e) { 365 | var t = this 366 | , n = [0]; 367 | e.k.forEach(function (e) { 368 | n.push(e); 369 | }); 370 | var r = function (r) { 371 | var i = new G; 372 | return i.k = n, 373 | i.k[0] = r, 374 | i.v(e.G, t.h, e.b, e.F), 375 | i.r[3]; 376 | }; 377 | r.toString = function () { 378 | return '() { [native code] }'; 379 | } 380 | , 381 | e.r[3] = r; 382 | } 383 | , 384 | w.prototype.e = function (e) { 385 | switch (this.t) { 386 | case 0: 387 | for (var t = {}, n = 0; n < this.i; n++) { 388 | var r = e.f.pop(); 389 | t[e.f.pop()] = r; 390 | } 391 | e.r[this.s] = t; 392 | break; 393 | case 1: 394 | for (var i = [], o = 0; o < this.i; o++) 395 | i.unshift(e.f.pop()); 396 | e.r[this.s] = i; 397 | } 398 | } 399 | , 400 | G.prototype.D = function (e) { 401 | for (var t = window.atob(e), n = t.charCodeAt(0) << 8 | t.charCodeAt(1), r = [], i = 2; i < n + 2; i += 2) 402 | r.push(t.charCodeAt(i) << 8 | t.charCodeAt(i + 1)); 403 | this.G = r; 404 | for (var o = [], a = n + 2; a < t.length;) { 405 | var s = t.charCodeAt(a) << 8 | t.charCodeAt(a + 1) 406 | , c = t.slice(a + 2, a + 2 + s); 407 | o.push(c), 408 | a += s + 2; 409 | } 410 | this.b = o; 411 | } 412 | , 413 | G.prototype.v = function (e, t, n) { 414 | for (t = t || 0, 415 | n = n || [], 416 | this.C = t, 417 | 'string' == typeof e ? this.D(e) : (this.G = e, 418 | this.b = n), 419 | this.o = !0, 420 | this.R = Date.now(); this.o;) { 421 | var r = this.G[this.C++]; 422 | if ('number' != typeof r) 423 | break; 424 | var i = Date.now(); 425 | if (500 < i - this.R) 426 | return; 427 | this.R = i; 428 | try { 429 | this.e(r); 430 | } catch (e) { 431 | this.U = e, 432 | this.w && (this.C = this.w); 433 | } 434 | } 435 | } 436 | , 437 | G.prototype.e = function (e) { 438 | var t = (61440 & e) >> 12; 439 | new this.J[t](e).e(this); 440 | } 441 | , 442 | (new G).v('AxjgB5MAnACoAJwBpAAAABAAIAKcAqgAMAq0AzRJZAZwUpwCqACQACACGAKcBKAAIAOcBagAIAQYAjAUGgKcBqFAuAc5hTSHZAZwqrAIGgA0QJEAJAAYAzAUGgOcCaFANRQ0R2QGcOKwChoANECRACQAsAuQABgDnAmgAJwMgAGcDYwFEAAzBmAGcSqwDhoANECRACQAGAKcD6AAGgKcEKFANEcYApwRoAAxB2AGcXKwEhoANECRACQAGAKcE6AAGgKcFKFANEdkBnGqsBUaADRAkQAkABgCnBagAGAGcdKwFxoANECRACQAGAKcGKAAYAZx+rAZGgA0QJEAJAAYA5waoABgBnIisBsaADRAkQAkABgCnBygABoCnB2hQDRHZAZyWrAeGgA0QJEAJAAYBJwfoAAwFGAGcoawIBoANECRACQAGAOQALAJkAAYBJwfgAlsBnK+sCEaADRAkQAkABgDkACwGpAAGAScH4AJbAZy9rAiGgA0QJEAJACwI5AAGAScH6AAkACcJKgAnCWgAJwmoACcJ4AFnA2MBRAAMw5gBnNasCgaADRAkQAkABgBEio0R5EAJAGwKSAFGACcKqAAEgM0RCQGGAYSATRFZAZzshgAtCs0QCQAGAYSAjRFZAZz1hgAtCw0QCQAEAAgB7AtIAgYAJwqoAASATRBJAkYCRIANEZkBnYqEAgaBxQBOYAoBxQEOYQ0giQKGAmQABgAnC6ABRgBGgo0UhD/MQ8zECALEAgaBxQBOYAoBxQEOYQ0gpEAJAoYARoKNFIQ/zEPkAAgChgLGgkUATmBkgAaAJwuhAUaCjdQFAg5kTSTJAsQCBoHFAE5gCgHFAQ5hDSCkQAkChgBGgo0UhD/MQ+QACAKGAsaCRQCOYGSABoAnC6EBRoKN1AUEDmRNJMkCxgFGgsUPzmPkgAaCJwvhAU0wCQFGAUaCxQGOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQMOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQSOZISPzZPkQAaCJwvhAU0wCQFGAkSAzRBJAlz/B4FUAAAAwUYIAAIBSITFQkTERwABi0GHxITAAAJLwMSGRsXHxMZAAk0Fw8HFh4NAwUABhU1EBceDwAENBcUEAAGNBkTGRcBAAFKAAkvHg4PKz4aEwIAAUsACDIVHB0QEQ4YAAsuAzs7AAoPKToKDgAHMx8SGQUvMQABSAALORoVGCQgERcCAxoACAU3ABEXAgMaAAsFGDcAERcCAxoUCgABSQAGOA8LGBsPAAYYLwsYGw8AAU4ABD8QHAUAAU8ABSkbCQ4BAAFMAAktCh8eDgMHCw8AAU0ADT4TGjQsGQMaFA0FHhkAFz4TGjQsGQMaFA0FHhk1NBkCHgUbGBEPAAFCABg9GgkjIAEmOgUHDQ8eFSU5DggJAwEcAwUAAUMAAUAAAUEADQEtFw0FBwtdWxQTGSAACBwrAxUPBR4ZAAkqGgUDAwMVEQ0ACC4DJD8eAx8RAAQ5GhUYAAFGAAAABjYRExELBAACWhgAAVoAQAg/PTw0NxcQPCQ5C3JZEBs9fkcnDRcUAXZia0Q4EhQgXHojMBY3MWVCNT0uDhMXcGQ7AUFPHigkQUwQFkhaAkEACjkTEQspNBMZPC0ABjkTEQsrLQ=='); 443 | var b = function (e) { 444 | return __g._encrypt(encodeURIComponent(e)); 445 | }; 446 | exports.ENCRYPT_VERSION = A, 447 | exports.default = b; 448 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | from zhihu_client import ZhihuClient 5 | from data_extractor import DataExtractor 6 | 7 | from print_beautify import print_recommend_article 8 | from print_beautify import print_article_content 9 | from print_beautify import print_comments 10 | from print_beautify import print_question 11 | from print_beautify import print_vote_thank 12 | from print_beautify import print_vote_comments 13 | from print_beautify import print_logo 14 | from print_beautify import print_save 15 | 16 | from utils import print_colour 17 | from utils import get_com_func 18 | 19 | from setting import USER 20 | from setting import PASSWORD 21 | from setting import SAVE_DIR 22 | 23 | 24 | def help_main(): 25 | output = "\n" \ 26 | "**********************************************************\n" \ 27 | "** remd: 查看推荐内容\n" \ 28 | "** aten: 查看动态内容\n" \ 29 | "** q: 退出系统\n" \ 30 | "**********************************************************\n" 31 | return output 32 | 33 | 34 | def help_recommend(): 35 | output = "\n" \ 36 | "**********************************************************\n" \ 37 | "** f: 刷新推荐内容\n" \ 38 | "** r: 再次显示(重新显示回答)\n" \ 39 | "** read:article_id 查看回答具体内容(进入下一级菜单)\n" \ 40 | "** question:question_id 查看问题下的其他回答(进入下一级菜单)\n" \ 41 | "** back: 返回上层\n" \ 42 | "** q: 退出系统\n" \ 43 | "**********************************************************\n" 44 | return output 45 | 46 | 47 | def help_article(): 48 | output = "\n" \ 49 | "**********************************************************\n" \ 50 | "** back 返回上层\n" \ 51 | "** q 退出系统\n" \ 52 | "** save 保存到本地\n" \ 53 | "** enshrine 收藏回答\n" \ 54 | "** question 查看问题下的其他回答\n" \ 55 | "** up 赞同\n" \ 56 | "** down 反对\n" \ 57 | "** neutral 中立,可以取消对回答的赞同或反对\n" \ 58 | "** thank 感谢\n" \ 59 | "** unthank 取消感谢\n"\ 60 | "** comment 评论相关(查看评论, 回复评论等将进入下一级菜单)\n"\ 61 | "**********************************************************\n" 62 | return output 63 | 64 | 65 | def help_comments(): 66 | output = "\n" \ 67 | "**********************************************************\n" \ 68 | "** back 返回上层\n" \ 69 | "** q 退出系统\n" \ 70 | "** n 显示下一页\n" \ 71 | "** p 显示上一页\n" \ 72 | "** com:comment_id 回复评论,点赞等功能(进入下级菜单)\n" \ 73 | "**********************************************************\n" 74 | return output 75 | 76 | 77 | def help_comments2(): 78 | output = "\n" \ 79 | "**********************************************************\n" \ 80 | "** back 返回上层\n" \ 81 | "** q 退出系统\n" \ 82 | "** up 点赞\n" \ 83 | "** neutral 中立,可以取消对点赞\n" \ 84 | "** reply:content 回复评论\n" \ 85 | "**********************************************************\n" 86 | return output 87 | 88 | 89 | def help_question(): 90 | output = "\n" \ 91 | "**********************************************************\n" \ 92 | "** back 返回上层\n" \ 93 | "** q 退出系统\n" \ 94 | "** qsdl 查看问题详情\n" \ 95 | "** read:article_id 查看回答具体内容(进入下一级菜单)\n" \ 96 | "** n 显示下一页\n" \ 97 | "** p 显示上一页\n" \ 98 | "** r 再次显示(重新显示回答)\n" \ 99 | "**********************************************************\n" 100 | return output 101 | 102 | 103 | def exit(cmd: str): 104 | if cmd in('q', 'quit', 'exit'): 105 | sys.exit() 106 | 107 | 108 | def clear(): 109 | os.system("clear") 110 | 111 | 112 | async def deal_comments_by_id(spider, uid): 113 | """ 114 | 对应id评论相关 115 | :param spider: 116 | :return: 117 | """ 118 | while True: 119 | print_colour('', 'yellow') 120 | com2_cmd = input(help_comments2()).lower() 121 | com2_cmd = com2_cmd.split(':') 122 | if not com2_cmd[0]: 123 | print_colour('输入有误!', 'red') 124 | continue 125 | exit(com2_cmd[0]) 126 | if com2_cmd[0] == 'back': 127 | break 128 | elif com2_cmd[0] == 'up': 129 | result = await spider.endorse_comment(uid, False) 130 | print_vote_comments(result, 'up') 131 | elif com2_cmd[0] == 'neutral': 132 | result = await spider.endorse_comment(uid, True) 133 | print_colour(result) 134 | print_vote_comments(result, 'neutral') 135 | elif com2_cmd[0] == 'reply' and len(com2_cmd) == 2: 136 | # todo 回复评论 137 | data = { 138 | 'content': com2_cmd[1], 139 | 'replyToId': uid, 140 | } 141 | print_colour('功能还在开发中...', 'red') 142 | continue 143 | else: 144 | print_colour('输入有误!', 'red') 145 | continue 146 | pass 147 | 148 | 149 | async def deal_comments(spider, result, paging): 150 | """ 151 | 处理评论命令 152 | :param spider: 153 | :return: 154 | """ 155 | # all_coments = [] 156 | while True: 157 | comment_ids = [] 158 | for d in result: 159 | comment_ids.append(d['id']) 160 | for clild in d.get('child_comments'): 161 | comment_ids.append(clild['id']) 162 | comment_ids = list(set(comment_ids)) 163 | print_colour('', 'yellow') 164 | comm_cmd = input(help_comments()).lower() 165 | comm_cmd = comm_cmd.split(':') 166 | if not comm_cmd: 167 | print_colour('输入有误!', 'red') 168 | continue 169 | exit(comm_cmd[0]) 170 | if comm_cmd[0] == 'back': 171 | break 172 | elif comm_cmd[0] == 'n': 173 | if paging.get('is_end'): 174 | print_colour('已是最后一页!', 'red') 175 | continue 176 | # url = paging['next'].replace('https://www.zhihu.com/', 'https://www.zhihu.com/api/v4/') 177 | url = paging['next'] 178 | result, paging = await spider.get_comments_by_url(url) 179 | print_comments(result) 180 | continue 181 | elif comm_cmd[0] == 'p': 182 | if paging.get('is_start'): 183 | print_colour('已是第一页!', 'red') 184 | continue 185 | # url = paging['previous'].replace('https://www.zhihu.com/', 'https://www.zhihu.com/api/v4/') 186 | url = paging['previous'] 187 | result, paging = await spider.get_comments_by_url(url) 188 | print_comments(result) 189 | continue 190 | elif comm_cmd[0] == 'com': 191 | if len(comm_cmd) != 2: 192 | print_colour('输入有误!', 'red') 193 | continue 194 | if comm_cmd[1] not in comment_ids: 195 | print_colour('输入id有误!', 'red') 196 | continue 197 | await deal_comments_by_id(spider, comm_cmd[1]) 198 | continue 199 | else: 200 | print_colour('输入有误!', 'red') 201 | continue 202 | 203 | 204 | async def deal_article(spider, article): 205 | """ 206 | 处理文章内容命令 207 | :param spider: 208 | :param recommend_articles: 209 | :param ids: 210 | :return: 211 | """ 212 | while True: 213 | print_colour('', 'yellow') 214 | arl_cmd = input(help_article()).lower() 215 | if not arl_cmd: 216 | print_colour('输入有误!', 'red') 217 | continue 218 | exit(arl_cmd) 219 | if arl_cmd == 'back': 220 | break 221 | 222 | elif arl_cmd in ('up', 'down', 'neutral', 'thank', 'unthank'): 223 | 224 | uid = article.get('id') 225 | func = get_com_func(arl_cmd) 226 | result = await getattr(spider, func)(uid) 227 | print_vote_thank(result, arl_cmd) 228 | continue 229 | elif arl_cmd == 'comment': 230 | typ = article['type'] 231 | uid = article.get('id') 232 | result, paging = await spider.get_comments(uid, typ) 233 | print_comments(result) 234 | await deal_comments(spider, result, paging) 235 | continue 236 | elif arl_cmd == 'save': 237 | print_save(article) 238 | continue 239 | elif arl_cmd == 'enshrine': 240 | # todo 收藏回答 241 | print_colour('功能还在开发中...', 'red') 242 | continue 243 | elif arl_cmd == 'question': 244 | await deal_question(spider, article.get('question').get('id'), article.get('id')) 245 | continue 246 | else: 247 | print_colour('输入有误!', 'red') 248 | continue 249 | 250 | 251 | async def deal_question(spider, question_id, uid): 252 | """ 253 | 处理问题命令 254 | :param spider: 255 | :param uid: 256 | :param id_map: 257 | :return: 258 | """ 259 | is_print = True 260 | while True: 261 | if is_print: 262 | question_articles, paging = await spider.get_article_by_question(question_id) 263 | ids = [d.get('id') for d in question_articles] 264 | print_recommend_article(question_articles) 265 | is_print = False 266 | print_colour('', 'yellow') 267 | ques_cmd = input(help_question()).lower() 268 | ques_cmd = ques_cmd.split(':') 269 | if not ques_cmd: 270 | print_colour('输入有误!', 'red') 271 | continue 272 | exit(ques_cmd[0]) 273 | if ques_cmd[0] == 'read': 274 | if len(ques_cmd) != 2: 275 | print_colour('输入有误!', 'red') 276 | continue 277 | if ques_cmd[1] not in ids: 278 | print_colour('输入id有误!', 'red') 279 | continue 280 | output = [d for d in question_articles if d['id'] == ques_cmd[1]][0] 281 | print_article_content(output) 282 | await deal_article(spider, output) 283 | continue 284 | elif ques_cmd[0] == 'qsdl': 285 | question_detail = await spider.get_question_details(question_id, uid) 286 | print_question(question_detail) 287 | elif ques_cmd[0] == 'n': 288 | if paging.get('is_end'): 289 | print_colour('已是最后一页!', 'red') 290 | continue 291 | url = paging['next'] 292 | question_articles, paging = await spider.get_article_by_question_url(url) 293 | ids = [d.get('id') for d in question_articles] 294 | print_recommend_article(question_articles) 295 | continue 296 | elif ques_cmd[0] == 'p': 297 | if paging.get('is_start'): 298 | print_colour('已是第一页!', 'red') 299 | continue 300 | url = paging['previous'] 301 | question_articles, paging = await spider.get_article_by_question_url(url) 302 | ids = [d.get('id') for d in question_articles] 303 | print_recommend_article(question_articles) 304 | elif ques_cmd[0] == 'r': 305 | print_recommend_article(question_articles) 306 | continue 307 | elif ques_cmd[0] == 'back': 308 | break 309 | else: 310 | print_colour('输入有误!', 'red') 311 | continue 312 | 313 | 314 | async def deal_remd(spider): 315 | """ 316 | 处理推荐文章命令 317 | :param spider: 318 | :return: 319 | """ 320 | is_print = True 321 | while True: 322 | if is_print: 323 | recommend_articles = await spider.get_recommend_article() 324 | ids = [d.get('id') for d in recommend_articles] 325 | print_recommend_article(recommend_articles) 326 | is_print = False 327 | print_colour('', 'yellow') 328 | remd_cmd = input(help_recommend()).lower() 329 | remd_cmd = remd_cmd.split(':') 330 | if not remd_cmd: 331 | print_colour('输入有误!', 'red') 332 | continue 333 | exit(remd_cmd[0]) 334 | if remd_cmd[0] == 'f': 335 | is_print = True 336 | continue 337 | elif remd_cmd[0] == 'r': 338 | print_recommend_article(recommend_articles) 339 | continue 340 | elif remd_cmd[0] == 'read': 341 | if len(remd_cmd) != 2: 342 | print_colour('输入有误!', 'red') 343 | continue 344 | if remd_cmd[1] not in ids: 345 | print_colour('输入id有误!', 'red') 346 | continue 347 | output = [d for d in recommend_articles if d['id'] == remd_cmd[1]][0] 348 | print_article_content(output) 349 | await deal_article(spider, output) 350 | continue 351 | elif remd_cmd[0] == 'question': 352 | question_ids = [d.get('question').get('id') for d in recommend_articles] 353 | if len(remd_cmd) != 2: 354 | print_colour('输入有误!', 'red') 355 | continue 356 | if remd_cmd[1] not in question_ids: 357 | print_colour('输入id有误!', 'red') 358 | continue 359 | assert len(ids) == len(question_ids) 360 | id_map = dict(zip(question_ids, ids)) 361 | uid = id_map[remd_cmd[1]] 362 | await deal_question(spider, remd_cmd[1], uid) 363 | continue 364 | elif remd_cmd[0] == 'back': 365 | break 366 | else: 367 | print_colour('输入有误!', 'red') 368 | continue 369 | 370 | 371 | async def run(client): 372 | spider = DataExtractor(client) 373 | output = await spider.get_self_info() 374 | print_colour(f'hello {output["name"]} 欢迎使用terminal-zhihu!', 'ultramarine') 375 | flag = True 376 | while flag: 377 | print_colour('', 'yellow') 378 | cmd = input(help_main()).lower() 379 | if not cmd: 380 | print_colour('输入有误!', 'red') 381 | continue 382 | exit(cmd) 383 | if cmd == 'remd': 384 | await deal_remd(spider) 385 | elif cmd == 'aten': 386 | # todo 获取关注动态 387 | print_colour('功能还在开发中...', 'red') 388 | continue 389 | else: 390 | print_colour('输入有误!', 'red') 391 | continue 392 | 393 | 394 | def check_setting(): 395 | save_dir = SAVE_DIR or '/tmp/zhihu_save' 396 | if not os.path.exists(save_dir): 397 | os.makedirs(save_dir) 398 | 399 | 400 | async def login(user, password): 401 | """ 402 | 登录 403 | :param user: 404 | :param password: 405 | :return: 406 | """ 407 | client = ZhihuClient(user, password) 408 | load_cookies = False 409 | if os.path.exists(client.cookie_file): 410 | # 如果cookie缓存存在优先读取缓存 411 | load_cookies = True 412 | if not load_cookies and (not USER or not PASSWORD): 413 | print_colour('请正确配置USER, PASSWORD', 'red') 414 | sys.exit() 415 | await client.login(load_cookies=load_cookies) 416 | return client 417 | 418 | 419 | async def main(): 420 | try: 421 | check_setting() 422 | client = await login(USER, PASSWORD) 423 | print_logo() 424 | await run(client) 425 | # except Exception as e: 426 | # print_colour(e, 'red') 427 | finally: 428 | print_colour('欢迎再次使用') 429 | await asyncio.sleep(0) 430 | await client.close() 431 | 432 | 433 | if __name__ == '__main__': 434 | # asyncio.run(main()) 435 | asyncio.get_event_loop().run_until_complete(main()) 436 | --------------------------------------------------------------------------------