├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── helper
├── __init__.py
├── config.py
├── constants.py
├── extractor.py
├── login.py
├── main.py
├── node.py
├── problems.py
├── templates.py
└── utils.py
├── imgs
├── example_cn.png
├── example_en.png
├── leetcode-logo.png
├── problem.png
└── run.png
├── run.py
└── tests
├── __init__.py
└── test_config.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | db/
2 | .vscode
3 | config.json
4 | **/__pycache__
5 | **/.ipynb_checkpoints/
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 KivenChen
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 | # LeetCode_Helper
7 |
8 | ## 概述
9 |
10 | Python 实现的 LeetCode 仓库美化程序。爬取 LeetCode-cn AC 的题目描述和提交的代码,并整理至相应的文件夹,生成相应的 README 文件。
11 |
12 | 本项目参考了:
13 |
14 | - [leetcode-spider](https://github.com/zhantong/leetcode-spider)
15 | - [LeetCodeCrawler](https://github.com/ZhaoxiZhang/LeetCodeCrawler)
16 |
17 | ## 特点
18 |
19 | - 支持爬取题目列表(中英文),保存为指定目录下的 README 和 README_EN 文件
20 | - 支持爬取题目描述(中英文),保存为对应 title 文件夹下的 README 和 README_EN 文件
21 | - 支持爬取用户提交的代码,保存为对应 title 文件夹下的 AC 源码(可以是任意语言)
22 | - 支持修改导出数据的模板
23 | - 异步下载题目描述,高速并发导出文件
24 | - 支持增量更新,当 LeetCode-cn 有新内容(题目/提交的代码)时,可以选择增量形式更新
25 |
26 | ## 使用
27 |
28 | 使用 `git clone` 或直接下载本仓库代码至本地
29 |
30 | 本项目需要用到第三方库 `requests` 和 `aiohttp`,可通过 `pip` 命令安装。
31 |
32 | 运行 `python run.py`
33 |
34 | 
35 |
36 | ## 效果
37 |
38 | 具体效果以及爬取的具体数据可参看我的 repo: [LeetCode](https://github.com/KivenCkl/LeetCode)
39 |
40 | 
41 |
42 | 
43 |
44 | 
45 |
46 | 你可以根据你自己的需求爱好修改 `templates.py` 其中的模板
47 |
48 | 可以修改其根目录下的 `config.json` 文件:
49 |
50 | ``` json
51 | {
52 | "username": "leetcode-cn@leetcode",
53 | "password": "leetcode",
54 | "outputDir": "../LeetCode",
55 | "timeInterval": 0.1
56 | }
57 | ```
58 |
59 | - `username` 和 `password` 对应你的 LeetCode-cn 账号和密码
60 | - `outputDir` 对应你希望存放源码文件的目录
61 | - `timeInterval` 为访问 LeetCode-cn 的时间间隔,默认为 0.1s
62 |
--------------------------------------------------------------------------------
/helper/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/helper/__init__.py
--------------------------------------------------------------------------------
/helper/config.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-22
4 | @LastEditTime: 2019-05-05
5 | '''
6 | import json
7 | import os
8 |
9 |
10 | class Config:
11 | '''
12 | 获取配置信息并储存至 `config.json`
13 | '''
14 | def __init__(self):
15 | self.data = self.__getConfig()
16 |
17 | def __getConfig(self):
18 | path = os.path.join(os.path.abspath(os.path.join(__file__, "../..")),
19 | 'config.json')
20 | if not os.path.exists(path):
21 | username = input('请输入您的用户名: ')
22 | password = input('请输入您的密码: ')
23 | outputDir = input('请选择您要输出的目录: ')
24 | data = dict(username=username,
25 | password=password,
26 | outputDir=outputDir,
27 | timeInterval=0.1)
28 | with open(path, 'w') as f:
29 | json.dump(data, f)
30 | return data
31 | else:
32 | with open(path, 'r') as f:
33 | return json.load(f)
34 |
35 | def __getData(self, item):
36 | return self.data.get(item) if self.data else None
37 |
38 | @property
39 | def username(self):
40 | return self.__getData('username')
41 |
42 | @property
43 | def password(self):
44 | return self.__getData('password')
45 |
46 | @property
47 | def outputDir(self):
48 | return self.__getData('outputDir')
49 |
50 | @property
51 | def timeInterval(self):
52 | return self.__getData('timeInterval') or 0.1
53 |
54 |
55 | config = Config()
56 |
--------------------------------------------------------------------------------
/helper/constants.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-22
4 | @LastEditTime: 2019-05-23
5 | '''
6 |
7 | # LeetCode 相关链接
8 | LEETCODE = "https://leetcode-cn.com"
9 | GRAPHQL = LEETCODE + "/graphql"
10 | LOGIN = LEETCODE + "/accounts/login/"
11 | PROBLEMS = LEETCODE + "/api/problems/all/"
12 | SUBMISSIONS_FORMAT = LEETCODE + "/api/submissions/?offset={}&limit=20"
13 | CODE_FORMAT = LEETCODE + "/submissions/latest/?qid={}&lang={}"
14 | TAG_FORMAT = LEETCODE + "/tag/{}"
15 | PROBLEM_FORMAT = LEETCODE + "/problems/{}/"
16 |
17 | HEADERS = {
18 | 'Origin':
19 | LEETCODE,
20 | 'User-Agent':
21 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134'
22 | }
23 |
24 | DIFFICULTY_CN = ['', '简单', '中等', '困难']
25 | DIFFICULTY_EN = ['', 'easy', 'medium', 'hard']
26 |
27 | LANGS = {
28 | 'bash': {
29 | 'lang': 'Shell',
30 | 'ext': 'sh',
31 | 'style': '#'
32 | },
33 | 'c': {
34 | 'lang': 'C',
35 | 'ext': 'c',
36 | 'style': '//'
37 | },
38 | 'cpp': {
39 | 'lang': 'C++',
40 | 'ext': 'cpp',
41 | 'style': '//'
42 | },
43 | 'csharp': {
44 | 'lang': 'C#',
45 | 'ext': 'cs',
46 | 'style': '//'
47 | },
48 | 'golang': {
49 | 'lang': 'Go',
50 | 'ext': 'go',
51 | 'style': '//'
52 | },
53 | 'java': {
54 | 'lang': 'Java',
55 | 'ext': 'java',
56 | 'style': '//'
57 | },
58 | 'javascript': {
59 | 'lang': 'JavaScript',
60 | 'ext': 'js',
61 | 'style': '//'
62 | },
63 | 'kotlin': {
64 | 'lang': 'Kotlin',
65 | 'ext': 'kt',
66 | 'style': '//'
67 | },
68 | 'mysql': {
69 | 'lang': 'SQL',
70 | 'ext': 'sql',
71 | 'style': '--'
72 | },
73 | 'python': {
74 | 'lang': 'Python',
75 | 'ext': 'py',
76 | 'style': '#'
77 | },
78 | 'python3': {
79 | 'lang': 'Python3',
80 | 'ext': 'py',
81 | 'style': '#'
82 | },
83 | 'ruby': {
84 | 'lang': 'Ruby',
85 | 'ext': 'rb',
86 | 'style': '#'
87 | },
88 | 'rust': {
89 | 'lang': 'Rust',
90 | 'ext': 'rs',
91 | 'style': '//'
92 | },
93 | 'scala': {
94 | 'lang': 'Scala',
95 | 'ext': 'scala',
96 | 'style': '//'
97 | },
98 | 'swift': {
99 | 'lang': 'Swift',
100 | 'ext': 'swift',
101 | 'style': '//'
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/helper/extractor.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-26
4 | @LastEditTime: 2019-05-05
5 | '''
6 | import os
7 | import time
8 | import concurrent.futures
9 | from .templates import *
10 | from .constants import *
11 |
12 |
13 | class Extractor:
14 | '''
15 | 高速并发导出数据类
16 | '''
17 | def __init__(self, output_dir, author):
18 | self.output_dir = output_dir
19 | self.author = author
20 |
21 | def extractInfo(self, info, datas):
22 | '''导出 LeetCode README 文件'''
23 | # 当前时间
24 | cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
25 | readme_cn_path = os.path.join(self.output_dir, 'README.md')
26 | with open(readme_cn_path, 'w', encoding='utf-8') as f:
27 | f.write(
28 | TEMPLATE_README_CN.format(
29 | user_name=info.user_name,
30 | num_solved=info.num_solved,
31 | num_total=info.num_total,
32 | ac_easy=info.ac_easy,
33 | ac_medium=info.ac_medium,
34 | ac_hard=info.ac_hard,
35 | time=cur_time))
36 | solutions = []
37 | for i, data in enumerate(datas):
38 | solutions.append('[{0}](Problemset/{1}/{1}.{2})'.format(
39 | data["language"], data["title_slug"],
40 | LANGS[data["lang"]]["ext"]))
41 | title = '[{}](Problemset/{}/README.md)'.format(
42 | data["title_cn"], data["title_slug"])
43 | # 判断同一问题是否有多个解
44 | if i == len(datas) - 1 or datas[i]['title_en'] != datas[
45 | i + 1]['title_en']:
46 | f.write(
47 | TEMPLATE_README_APPEND.format(
48 | frontend_id=data['frontend_id'],
49 | title=title,
50 | paid_only=data['paid_only'],
51 | is_favor=data['is_favor'],
52 | solutions='
'.join(solutions),
53 | ac_rate=data['ac_rate'],
54 | difficulty=DIFFICULTY_CN[data['difficulty']],
55 | tags=data['tags_cn'].replace('- ', '').replace(
56 | '\n', '
')))
57 | solutions = []
58 | print(f'{os.path.abspath(readme_cn_path)} done!')
59 | readme_en_path = os.path.join(self.output_dir, 'README_EN.md')
60 | with open(readme_en_path, 'w', encoding='utf-8') as f:
61 | f.write(
62 | TEMPLATE_README_EN.format(
63 | user_name=info.user_name,
64 | num_solved=info.num_solved,
65 | num_total=info.num_total,
66 | ac_easy=info.ac_easy,
67 | ac_medium=info.ac_medium,
68 | ac_hard=info.ac_hard,
69 | time=cur_time))
70 | solutions = []
71 | for i, data in enumerate(datas):
72 | solutions.append('[{0}](Problemset/{1}/{1}.{2})'.format(
73 | data["language"], data["title_slug"],
74 | LANGS[data["lang"]]["ext"]))
75 | title = '[{}](Problemset/{}/README_EN.md)'.format(
76 | data["title_en"], data["title_slug"])
77 | if i == len(datas) - 1 or datas[i]['title_en'] != datas[
78 | i + 1]['title_en']:
79 | f.write(
80 | TEMPLATE_README_APPEND.format(
81 | frontend_id=data['frontend_id'],
82 | title=title,
83 | paid_only=data['paid_only'],
84 | is_favor=data['is_favor'],
85 | solutions='
'.join(solutions),
86 | ac_rate=data['ac_rate'],
87 | difficulty=DIFFICULTY_EN[data['difficulty']],
88 | tags=data['tags_en'].replace('- ', '').replace(
89 | '\n', '
')))
90 | solutions = []
91 | print(f'{os.path.abspath(readme_en_path)} done!')
92 |
93 | def __extractDesc(self, data):
94 | if data['d_stored'] == 1:
95 | return
96 | folder_path = os.path.join(self.output_dir, 'Problemset',
97 | data['title_slug'])
98 | if not os.path.exists(folder_path):
99 | # 创建问题文件夹
100 | os.makedirs(folder_path, exist_ok=True)
101 | readme_cn_path = os.path.join(folder_path, 'README.md')
102 | title_cn = '[{}. {}]({})'.format(
103 | data['frontend_id'], data['title_cn'],
104 | PROBLEM_FORMAT.format(data['title_slug']))
105 | with open(readme_cn_path, 'w', encoding='utf-8') as f:
106 | f.write(
107 | TEMPLATE_DESC_CN.format(
108 | title_cn=title_cn,
109 | content_cn=data['content_cn'],
110 | tags_cn=data['tags_cn'],
111 | similar_questions_cn=data['similar_questions_cn']))
112 | print(f'{os.path.abspath(readme_cn_path)} done!')
113 |
114 | readme_en_path = os.path.join(folder_path, 'README_EN.md')
115 | title_en = '[{}. {}]({})'.format(
116 | data['frontend_id'], data['title_en'],
117 | PROBLEM_FORMAT.format(data['title_slug']))
118 | with open(readme_en_path, 'w', encoding='utf-8') as f:
119 | f.write(
120 | TEMPLATE_DESC_EN.format(
121 | title_en=title_en,
122 | content_en=data['content_en'],
123 | tags_en=data['tags_en'],
124 | similar_questions_en=data['similar_questions_en']))
125 | print(f'{os.path.abspath(readme_en_path)} done!')
126 |
127 | def extractDesc(self, datas):
128 | '''导出问题描述'''
129 | with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
130 | # futures = {executor.submit(self.__extractDesc, data): data for data in datas}
131 | # for future in concurrent.futures.as_completed(futures):
132 | # data = futures[future]
133 | for _ in executor.map(self.__extractDesc, datas):
134 | pass
135 |
136 | def __extractCode(self, data):
137 | if data['s_stored'] == 1:
138 | return
139 | folder_path = os.path.join(self.output_dir, 'Problemset',
140 | data['title_slug'])
141 | if not os.path.exists(folder_path):
142 | os.makedirs(folder_path)
143 | code_path = os.path.join(
144 | folder_path, f'{data["title_slug"]}.{LANGS[data["lang"]]["ext"]}')
145 | with open(code_path, 'w', encoding='utf-8') as f:
146 | f.write(
147 | TEMPLATE_CODE.format(
148 | style=LANGS[data['lang']]['style'],
149 | title_cn=data['title_cn'],
150 | title_en=data['title_en'],
151 | author=self.author,
152 | timestamp=data['timestamp'],
153 | runtime=data['runtime'],
154 | memory=data['memory'],
155 | code=data['code']))
156 | print(f'{os.path.abspath(code_path)} done!')
157 |
158 | def extractCode(self, datas):
159 | '''导出代码'''
160 | with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
161 | for _ in executor.map(self.__extractCode, datas):
162 | pass
163 |
--------------------------------------------------------------------------------
/helper/login.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-22
4 | @LastEditTime: 2019-05-05
5 | '''
6 | import requests
7 | from .constants import LEETCODE, LOGIN, HEADERS
8 |
9 |
10 | class Login:
11 | '''
12 | 登录 LeetCode-cn, 获取 cookies 值
13 | '''
14 | def __init__(self, username, password):
15 | self.username = username
16 | self.password = password
17 | self.__cookies = ''
18 | self.status = False
19 |
20 | def doLogin(self):
21 | resp = requests.get(LEETCODE, headers=HEADERS)
22 | # token = resp.cookies['csrftoken']
23 | token = ""
24 | headers = HEADERS.copy()
25 | headers.update({
26 | 'referer': LOGIN,
27 | 'x-csrftoken': token,
28 | 'x-requested-with': 'XMLHttpRequest'
29 | })
30 | payload = {
31 | 'login': self.username,
32 | 'password': self.password,
33 | 'csrfmiddlewaretoken': token
34 | }
35 | cookies = {'csrftoken': token}
36 | resp = requests.post(
37 | LOGIN, data=payload, headers=headers, cookies=cookies)
38 | if resp.status_code == 200:
39 | self.status = True
40 | self.__cookies = resp.cookies
41 | # user = resp.json()['form']['fields']['login']['value']
42 | if self.status:
43 | print(f'{self.username} 登录成功!')
44 | else:
45 | print('登录失败!')
46 | print('请检查用户名和密码!')
47 |
48 | @property
49 | def cookies(self):
50 | if not self.status:
51 | self.doLogin()
52 | return self.__cookies
53 |
--------------------------------------------------------------------------------
/helper/main.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-23
4 | '''
5 | from .problems import Problems
6 | import asyncio
7 |
8 |
9 | class Main:
10 | '''主程序'''
11 | def __init__(self):
12 | self.problems = Problems()
13 |
14 | def __info(self):
15 | print(self.problems.info)
16 |
17 | def update(self):
18 | '''更新数据'''
19 | self.__info()
20 | loop = asyncio.get_event_loop()
21 | loop.run_until_complete(self.problems.update())
22 |
23 | def rebuild(self):
24 | '''重建数据'''
25 | self.__info()
26 | self.problems.clearDB()
27 | self.update()
28 |
--------------------------------------------------------------------------------
/helper/node.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-24
4 | @LastEditTime: 2019-05-05
5 | '''
6 | import json
7 | import re
8 | import time
9 | from .constants import LANGS, TAG_FORMAT
10 |
11 |
12 | class ProblemInfoNode:
13 | '''解析问题基本信息'''
14 | def __init__(self, json_data):
15 | self.difficulty = json_data['difficulty']['level']
16 | self.is_favor = self.__formFavor(json_data['is_favor'])
17 | self.paid_only = self.__formPaid(json_data['paid_only'])
18 | self.status = json_data['status']
19 | self.id = json_data['stat']['question_id']
20 | # self.frontend_id = self.__formId(
21 | # json_data['stat']['frontend_question_id'])
22 | self.frontend_id = json_data['stat']['frontend_question_id']
23 | self.title_en = json_data['stat']['question__title']
24 | self.title_slug = json_data['stat']['question__title_slug']
25 | self.total_acs = json_data['stat']['total_acs']
26 | self.total_submitted = json_data['stat']['total_submitted']
27 |
28 | @property
29 | def ac_rate(self):
30 | return '{:.1%}'.format(self.total_acs / (self.total_submitted + 1))
31 |
32 | def __formFavor(self, is_favor):
33 | return '❤️' if is_favor else ''
34 |
35 | def __formPaid(self, paid_only):
36 | return '🔒' if paid_only else ''
37 |
38 | def __formId(self, id):
39 | return '{:0>4d}'.format(id)
40 |
41 |
42 | class ProblemDescNode:
43 | '''解析问题描述信息'''
44 | def __init__(self, json_data):
45 | self.id = json_data['data']['question']['questionId']
46 | self.content_en = self.__formContentEN(
47 | json_data['data']['question']['content'])
48 | self.title_cn = json_data['data']['question']['translatedTitle']
49 | self.content_cn = self.__formContentCN(
50 | json_data['data']['question']['translatedContent'])
51 | self.similar_questions_cn, self.similar_questions_en = self.__formSimilarQuestions(
52 | json_data['data']['question']['similarQuestions'])
53 | self.tags_cn, self.tags_en = self.__formTags(
54 | json_data['data']['question']['topicTags'])
55 | # self.hints = '\n'.join(json_data['data']['question']['hints'])
56 |
57 | def __formSimilarQuestions(self, similar_questions):
58 | question_list = re.findall(r'{.*?}', similar_questions)
59 | similar_questions_cn, similar_questions_en = [], []
60 | if question_list:
61 | for q in question_list:
62 | data = json.loads(q)
63 | similar_questions_cn.append('- [{}](../{}/README.md)'.format(
64 | data['translatedTitle'], data['titleSlug']))
65 | similar_questions_en.append(
66 | '- [{}](../{}/README_EN.md)'.format(
67 | data['title'], data['titleSlug']))
68 | return '\n'.join(similar_questions_cn), '\n'.join(similar_questions_en)
69 |
70 | def __formTags(self, tags):
71 | tags_cn, tags_en = [], []
72 | for tag in tags:
73 | tags_cn.append(
74 | f'- [{tag["translatedName"]}]({TAG_FORMAT.format(tag["slug"])})'
75 | )
76 | tags_en.append(
77 | f'- [{tag["name"]}]({TAG_FORMAT.format(tag["slug"])})')
78 | return '\n'.join(tags_cn), '\n'.join(tags_en)
79 |
80 | def __formContentCN(self, content):
81 | if not content:
82 | return
83 | return content.replace('↵↵', '').replace('↵', '\n')
84 |
85 | def __formContentEN(self, content):
86 | if not content:
87 | return
88 | return content.replace('↵', '').replace('\r\n', '\n')
89 |
90 |
91 | class SubmissionNode:
92 | '''解析提交的代码信息'''
93 | def __init__(self, dic):
94 | self.submission_id = dic['id']
95 | self.lang = dic['lang']
96 | self.memory = dic['memory']
97 | self.runtime = dic['runtime']
98 | self.timestamp = self.__formTime(int(dic['timestamp']))
99 | self.title_slug = dic['title_slug']
100 |
101 | @property
102 | def language(self):
103 | return LANGS[self.lang]['lang']
104 |
105 | def __formTime(self, timeStamp):
106 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timeStamp))
107 |
108 | def __formCode(self, code):
109 | return code.replace('↵', '\n')
110 |
111 |
112 | class InfoNode:
113 | '''解析用户基本信息'''
114 | def __init__(self, json_data):
115 | self.user_name = json_data.get('user_name')
116 | self.ac_easy = json_data.get('ac_easy')
117 | self.ac_medium = json_data.get('ac_medium')
118 | self.ac_hard = json_data.get('ac_hard')
119 | self.num_solved = json_data.get('num_solved')
120 | self.num_total = json_data.get('num_total')
121 |
122 | def __repr__(self):
123 | return (
124 | f'user_name: {self.user_name}\nac_easy: {self.ac_easy}\nac_medium: {self.ac_medium}\nac_hard: {self.ac_hard}\nnum_solved: {self.num_solved}\nnum_total: {self.num_total}'
125 | )
126 |
--------------------------------------------------------------------------------
/helper/problems.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-23
4 | @LastEditTime: 2019-05-23
5 | '''
6 | import os
7 | import sqlite3
8 | import requests
9 | import asyncio
10 | import aiohttp
11 | from .config import config
12 | from .login import Login
13 | from .extractor import Extractor
14 | from .utils import handle_tasks
15 | from .node import InfoNode, ProblemInfoNode, ProblemDescNode, SubmissionNode
16 | from .constants import PROBLEMS, HEADERS, GRAPHQL, CODE_FORMAT
17 |
18 |
19 | class Problems:
20 | '''核心逻辑'''
21 | def __init__(self):
22 | self.login = Login(config.username, config.password)
23 | self.__db_dir = os.path.abspath(os.path.join(__file__, "../..", "db"))
24 | if not os.path.exists(self.__db_dir):
25 | os.makedirs(self.__db_dir)
26 | self.db_path = os.path.join(self.__db_dir, "leetcode.db")
27 | self.__cookies = self.login.cookies
28 | self.problems_json = self.__getProblemsJson()
29 |
30 | def __getProblemsJson(self):
31 | resp = requests.get(PROBLEMS, headers=HEADERS, cookies=self.__cookies)
32 | if resp.status_code == 200:
33 | return resp.json()
34 |
35 | @property
36 | def info(self):
37 | '''获取用户基本信息'''
38 | return InfoNode(self.problems_json)
39 |
40 | def __dict_factory(self, cursor, row):
41 | '''修改 SQLite 数据呈现方式'''
42 | d = {}
43 | for idx, col in enumerate(cursor.description):
44 | d[col[0]] = row[idx]
45 | return d
46 |
47 | def updateProblemsInfo(self):
48 | '''更新问题基本信息'''
49 | problems_list = self.problems_json.get('stat_status_pairs')
50 | conn = sqlite3.connect(self.db_path)
51 | c = conn.cursor()
52 | c.execute('''
53 | CREATE TABLE IF NOT EXISTS problem (
54 | id INTEGER,
55 | frontend_id TEXT,
56 | title_en TEXT,
57 | title_slug TEXT,
58 | difficulty INTEGER,
59 | paid_only INTEGER,
60 | is_favor INTEGER,
61 | status TEXT,
62 | total_acs INTEGER,
63 | total_submitted INTEGER,
64 | ac_rate TEXT,
65 | PRIMARY KEY(id)
66 | )
67 | ''')
68 | c.execute('DELETE FROM problem')
69 | for problem in problems_list:
70 | p = ProblemInfoNode(problem)
71 | c.execute(
72 | '''
73 | INSERT INTO problem (
74 | id, frontend_id, title_en, title_slug, difficulty, paid_only, is_favor, status, total_acs, total_submitted, ac_rate
75 | )
76 | VALUES (
77 | ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
78 | )
79 | ''', (p.id, p.frontend_id, p.title_en, p.title_slug,
80 | p.difficulty, p.paid_only, p.is_favor, p.status,
81 | p.total_acs, p.total_submitted, p.ac_rate))
82 | conn.commit()
83 | conn.close()
84 |
85 | async def __getProblemDesc(self, title_slug):
86 | payload = {
87 | 'query': '''
88 | query questionData($titleSlug: String!) {
89 | question(titleSlug: $titleSlug) {
90 | questionId
91 | content
92 | translatedTitle
93 | translatedContent
94 | similarQuestions
95 | topicTags {
96 | name
97 | slug
98 | translatedName
99 | }
100 | hints
101 | }
102 | }
103 | ''',
104 | 'operationName': 'questionData',
105 | 'variables': {
106 | 'titleSlug': title_slug
107 | }
108 | }
109 | async with aiohttp.ClientSession(cookies=self.__cookies) as session:
110 | async with session.post(GRAPHQL, json=payload,
111 | headers=HEADERS) as resp:
112 | return await resp.json()
113 |
114 | async def storeProblemsDesc(self):
115 | '''存储 AC 问题描述信息'''
116 | conn = sqlite3.connect(self.db_path)
117 | c = conn.cursor()
118 | c.execute("SELECT title_slug FROM problem WHERE status == 'ac'")
119 | res = c.fetchall()
120 | if not res:
121 | return
122 | loop = asyncio.get_event_loop()
123 | problems_list = await handle_tasks(
124 | loop, self.__getProblemDesc, [dict(title_slug=t[0]) for t in res])
125 | c.execute('''
126 | CREATE TABLE IF NOT EXISTS description (
127 | id INTEGER,
128 | content_en TEXT,
129 | title_cn TEXT,
130 | content_cn TEXT,
131 | similar_questions_cn TEXT,
132 | similar_questions_en TEXT,
133 | tags_cn TEXT,
134 | tags_en TEXT,
135 | d_stored INTEGER DEFAULT 0,
136 | PRIMARY KEY(id)
137 | )
138 | ''')
139 | for problem in problems_list:
140 | p = ProblemDescNode(problem)
141 | c.execute(
142 | '''
143 | INSERT OR IGNORE INTO description (
144 | id, content_en, title_cn, content_cn, similar_questions_cn, similar_questions_en, tags_cn, tags_en
145 | )
146 | VALUES (
147 | ?, ?, ?, ?, ?, ?, ?, ?
148 | )
149 | ''', (p.id, p.content_en, p.title_cn, p.content_cn,
150 | p.similar_questions_cn, p.similar_questions_en,
151 | p.tags_cn, p.tags_en))
152 | conn.commit()
153 | conn.close()
154 |
155 | def updateProblemsDesc(self):
156 | '''更新处理后的 description 数据库'''
157 | conn = sqlite3.connect(self.db_path)
158 | c = conn.cursor()
159 | c.execute('UPDATE description SET d_stored=1')
160 | conn.commit()
161 | conn.close()
162 |
163 | async def __getSubmissions(self, title_slug, offset=0, limit=500):
164 | payload = {
165 | 'query': '''
166 | query submissions($offset: Int!, $limit: Int!, $lastKey: String, $questionSlug: String!) {
167 | submissionList(offset: $offset, limit: $limit, lastKey: $lastKey, questionSlug: $questionSlug) {
168 | lastKey
169 | hasNext
170 | submissions {
171 | id
172 | statusDisplay
173 | lang
174 | runtime
175 | timestamp
176 | url
177 | isPending
178 | memory
179 | __typename
180 | }
181 | __typename
182 | }
183 | }
184 | ''',
185 | 'operationName': 'submissions',
186 | 'variables': {
187 | 'limit': limit,
188 | 'offset': offset,
189 | 'questionSlug': title_slug
190 | }
191 | }
192 | async with aiohttp.ClientSession(cookies=self.__cookies) as session:
193 | async with session.post(GRAPHQL, json=payload,
194 | headers=HEADERS) as resp:
195 | return await resp.json(), title_slug
196 |
197 | async def __getCode(self, qid, lang):
198 | url = CODE_FORMAT.format(qid, lang)
199 | async with aiohttp.ClientSession(cookies=self.__cookies) as session:
200 | async with session.get(url, headers=HEADERS) as resp:
201 | return await resp.json(), qid, lang
202 |
203 | async def storeSubmissions(self):
204 | '''存储提交的代码信息'''
205 | conn = sqlite3.connect(self.db_path)
206 | c = conn.cursor()
207 | c.execute("SELECT title_slug FROM problem WHERE status == 'ac'")
208 | res = c.fetchall()
209 | if not res:
210 | return
211 | loop = asyncio.get_event_loop()
212 | submissions_list = await handle_tasks(
213 | loop, self.__getSubmissions, [dict(title_slug=t[0]) for t in res])
214 | data = []
215 | for submissions, title_slug in submissions_list:
216 | dic = set()
217 | for submission in submissions['data']['submissionList'][
218 | 'submissions']:
219 | status = submission['statusDisplay']
220 | key = submission['lang']
221 | if status == 'Accepted' and key not in dic:
222 | data.append(submission)
223 | data[-1]['title_slug'] = title_slug
224 | dic.add(key)
225 | c.execute('''
226 | CREATE TABLE IF NOT EXISTS submission (
227 | submission_id INTEGER,
228 | lang TEXT,
229 | language TEXT,
230 | memory TEXT,
231 | runtime TEXT,
232 | timestamp TEXT,
233 | title_slug TEXT,
234 | s_stored INTEGER DEFAULT 0,
235 | PRIMARY KEY(submission_id)
236 | )
237 | ''')
238 | for submission in data:
239 | s = SubmissionNode(submission)
240 | c.execute(
241 | '''
242 | INSERT OR IGNORE INTO submission (
243 | submission_id, lang, language, memory, runtime, timestamp, title_slug
244 | )
245 | VALUES (
246 | ?, ?, ?, ?, ?, ?, ?
247 | )
248 | ''', (s.submission_id, s.lang, s.language, s.memory, s.runtime,
249 | s.timestamp, s.title_slug))
250 | conn.commit()
251 | conn.close()
252 |
253 | async def storeCodes(self):
254 | '''存储提交的代码'''
255 | conn = sqlite3.connect(self.db_path)
256 | c = conn.cursor()
257 | c.execute(
258 | "SELECT p.id, lang FROM submission s LEFT JOIN problem p ON s.title_slug=p.title_slug"
259 | )
260 | res = c.fetchall()
261 | if not res:
262 | return
263 | loop = asyncio.get_event_loop()
264 | codes_list = await handle_tasks(
265 | loop, self.__getCode, [dict(qid=t[0], lang=t[1]) for t in res])
266 | try:
267 | c.execute("ALTER TABLE submission ADD COLUMN code TEXT")
268 | except sqlite3.OperationalError:
269 | pass
270 | for code, qid, lang in codes_list:
271 | c.execute(
272 | """
273 | UPDATE submission SET code = ?
274 | WHERE title_slug=(SELECT title_slug
275 | FROM problem
276 | WHERE id=?)
277 | AND lang=?""", (code.get("code", ""), qid, lang))
278 | conn.commit()
279 | conn.close()
280 |
281 | def updateSubmissions(self):
282 | '''更新处理后的 submission 数据库'''
283 | conn = sqlite3.connect(self.db_path)
284 | c = conn.cursor()
285 | c.execute('UPDATE submission SET s_stored=1')
286 | conn.commit()
287 | conn.close()
288 |
289 | async def update(self):
290 | '''增量式更新数据'''
291 | output_dir = config.outputDir
292 | if not os.path.exists(output_dir):
293 | os.makedirs(output_dir)
294 | extractor = Extractor(output_dir, config.username)
295 | self.updateProblemsInfo()
296 | await self.storeProblemsDesc()
297 | await self.storeSubmissions()
298 | await self.storeCodes()
299 | conn = sqlite3.connect(self.db_path)
300 | conn.row_factory = self.__dict_factory
301 | c = conn.cursor()
302 | # 获取新的数据
303 | c.execute('''
304 | SELECT *
305 | FROM description d
306 | JOIN problem p
307 | ON p.id=d.id
308 | JOIN submission s
309 | ON p.title_slug=s.title_slug
310 | WHERE (d.d_stored=0 OR s.s_stored=0)
311 | ORDER BY p.id DESC
312 | ''')
313 | datas = c.fetchall()
314 | if datas:
315 | extractor.extractDesc(datas)
316 | self.updateProblemsDesc()
317 | extractor.extractCode(datas)
318 | self.updateSubmissions()
319 | await self.storeCodes()
320 | c.execute('''
321 | SELECT *
322 | FROM description d
323 | JOIN problem p
324 | ON p.id=d.id
325 | JOIN submission s
326 | ON p.title_slug=s.title_slug
327 | ORDER BY p.id DESC
328 | ''')
329 | datas = c.fetchall()
330 | extractor.extractInfo(self.info, datas)
331 | print('数据已更新!')
332 | conn.close()
333 |
334 | def clearDB(self):
335 | '''重建数据'''
336 | conn = sqlite3.connect(self.db_path)
337 | c = conn.cursor()
338 | c.execute('''
339 | SELECT name
340 | FROM sqlite_master
341 | WHERE type="table" AND name="problem"
342 | ''')
343 | if c.fetchone():
344 | # 删除 problem 表
345 | c.execute('DROP TABLE problem')
346 | c.execute('''
347 | SELECT name
348 | FROM sqlite_master
349 | WHERE type="table" AND name="description"
350 | ''')
351 | if c.fetchone():
352 | # 删除 description 表
353 | c.execute('DROP TABLE description')
354 | c.execute('''
355 | SELECT name
356 | FROM sqlite_master
357 | WHERE type="table" AND name="submission"
358 | ''')
359 | if c.fetchone():
360 | # 删除 submission 表
361 | c.execute('DROP TABLE submission')
362 | conn.commit()
363 | conn.close()
364 |
--------------------------------------------------------------------------------
/helper/templates.py:
--------------------------------------------------------------------------------
1 | '''
2 | @Author: KivenChen
3 | @Date: 2019-04-27
4 | @LastEditTime: 2019-05-05
5 | '''
6 |
7 | # 根目录下的 README 英文模板
8 | TEMPLATE_README_EN = '''
9 | | English | [简体中文](README.md) |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 | Last updated: {time}
24 |
25 |
The source code is fetched using the tool LeetCode_Helper.
28 | 29 | | # | Title | Solutions | Acceptance | Difficulty | Tags | 30 | |:--:|:-----|:---------:|:----:|:----:|:----:| 31 | ''' 32 | 33 | # 根目录下的 README 中文模板 34 | TEMPLATE_README_CN = ''' 35 | | [English](README_EN.md) | 简体中文 | 36 | 37 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 | 最近一次更新: {time}
50 |
51 |
The source code is fetched using the tool LeetCode_Helper.
54 | 55 | | # | 题名 | 解答 | 通过率 | 难度 | 标签 | 56 | |:--:|:-----|:---------:|:----:|:----:|:----:| 57 | ''' 58 | 59 | # 根目录下 README 中的题目概要信息 60 | TEMPLATE_README_APPEND = '|{frontend_id}|{title}{paid_only}{is_favor}|{solutions}|{ac_rate}|{difficulty}|{tags}|\n' 61 | 62 | # 题目描述 README 英文模板 63 | TEMPLATE_DESC_EN = ''' 64 | | English | [简体中文](README.md) | 65 | 66 | # {title_en} 67 | 68 | ## Description 69 | 70 | {content_en} 71 | 72 | ## Related Topics 73 | 74 | {tags_en} 75 | 76 | ## Similar Questions 77 | 78 | {similar_questions_en} 79 | ''' 80 | 81 | # 题目描述 README 中文模板 82 | TEMPLATE_DESC_CN = ''' 83 | | [English](README_EN.md) | 简体中文 | 84 | 85 | # {title_cn} 86 | 87 | ## 题目描述 88 | 89 | {content_cn} 90 | 91 | ## 相关话题 92 | 93 | {tags_cn} 94 | 95 | ## 相似题目 96 | 97 | {similar_questions_cn} 98 | ''' 99 | 100 | # 题目代码模板 101 | TEMPLATE_CODE = ''' 102 | {style} @Title: {title_cn} ({title_en}) 103 | {style} @Author: {author} 104 | {style} @Date: {timestamp} 105 | {style} @Runtime: {runtime} 106 | {style} @Memory: {memory} 107 | 108 | {code} 109 | ''' 110 | -------------------------------------------------------------------------------- /helper/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @Author: KivenChen 3 | @Date: 2019-04-23 4 | @LastEditTime: 2019-05-05 5 | ''' 6 | import asyncio 7 | import aiohttp 8 | import os 9 | import time 10 | from functools import partial 11 | from .config import config 12 | 13 | 14 | def mkdir(path): 15 | if not os.path.exists(path): 16 | os.makedirs(path) 17 | print(f'{path} 已创建!') 18 | else: 19 | print(f'{path} 已存在!') 20 | return os.path.abspath(path) 21 | 22 | 23 | # 异步 request 24 | async def request(url='', method='get', cookies='', headers='', **kwargs): 25 | async with aiohttp.ClientSession(cookies=cookies) as session: 26 | async with session.request(method=method, 27 | url=url, 28 | headers=headers, 29 | **kwargs) as resp: 30 | return await resp.read() 31 | 32 | 33 | # 异步调度函数 34 | async def handle_tasks(loop, func, args): 35 | if isinstance(args, list): 36 | tasks = { 37 | asyncio.ensure_future(func(**arg)): partial(func, **arg) 38 | for arg in args 39 | } 40 | # loop.run_until_complete(asyncio.wait(tasks)) 41 | pending = set(tasks.keys()) 42 | res = [] 43 | while pending: 44 | finished, pending = await asyncio.wait( 45 | pending, return_when=asyncio.FIRST_COMPLETED) 46 | time.sleep(config.timeInterval) 47 | for task in finished: 48 | if task.exception(): 49 | coro = tasks[task] 50 | # print(f"{coro} retry...") 51 | new_task = asyncio.ensure_future(coro()) 52 | tasks[new_task] = coro 53 | pending.add(new_task) 54 | else: 55 | res.append(task.result()) 56 | return res 57 | elif isinstance(args, str): 58 | task = asyncio.ensure_future(func(args)) 59 | loop.run_until_complete(task) 60 | return [task.result()] 61 | -------------------------------------------------------------------------------- /imgs/example_cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/imgs/example_cn.png -------------------------------------------------------------------------------- /imgs/example_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/imgs/example_en.png -------------------------------------------------------------------------------- /imgs/leetcode-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/imgs/leetcode-logo.png -------------------------------------------------------------------------------- /imgs/problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/imgs/problem.png -------------------------------------------------------------------------------- /imgs/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/imgs/run.png -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @Author: KivenChen 3 | @Date: 2019-04-23 4 | @LastEditTime: 2019-05-02 5 | ''' 6 | from helper.main import Main 7 | 8 | if __name__ == "__main__": 9 | while True: 10 | print('欢迎使用 LeetCode_Helper, 请选择: ') 11 | print('1. 更新') 12 | print('2. 重建') 13 | print('q. 退出') 14 | key = input() 15 | if key == 'q': 16 | break 17 | elif key == '1': 18 | Main().update() 19 | break 20 | elif key == '2': 21 | Main().rebuild() 22 | break 23 | else: 24 | print('请重新选择!') 25 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KivenCkl/LeetCode_Helper/08c4fe612047ebb34ddaf2dfee463db845042486/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @Author: KivenChen 3 | @Date: 2019-04-22 4 | @LastEditTime: 2019-04-30 5 | ''' 6 | import unittest 7 | import os 8 | import sys 9 | sys.path.append(os.path.abspath(os.path.join(__file__, "../.."))) 10 | from helper.config import Config 11 | 12 | 13 | class TestConfig(unittest.TestCase): 14 | def test_init(self): 15 | pass 16 | 17 | def test_getUsername(self): 18 | config = Config() 19 | self.assertIsNotNone(config.username) 20 | 21 | def test_getPassword(self): 22 | config = Config() 23 | self.assertIsNotNone(config.password) 24 | 25 | def test_getOutputDir(self): 26 | config = Config() 27 | self.assertIsNotNone(config.outputDir) 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | --------------------------------------------------------------------------------