Skip to content

Commit 794903c

Browse files
committed
feat: update contest rating script
1 parent ed54f1c commit 794903c

File tree

3 files changed

+121
-209
lines changed

3 files changed

+121
-209
lines changed

solution/CONTEST_README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
## 段位与荣誉勋章
77

8-
竞赛排名根据 竞赛积分(周赛和双周赛)进行计算,注册新用户的基础分值为 $1500$ 分,在竞赛积分 $\ge1600$ 的用户中,根据比例 $5\%$ / $20\%$ / $75\%$ 设定三档段位,段位每周比赛结束后计算一次。
8+
竞赛排名根据 竞赛积分(周赛和双周赛)进行计算,注册新用户的基础分值为 1500 分,在竞赛积分 ≥1600 的用户中,根据比例 5%, 20%, 75% 设定三档段位,段位每周比赛结束后计算一次。
99

1010
如果竞赛积分处于段位的临界值,在每周比赛结束重新计算后会出现段位升级或降级的情况。段位升级或降级后会自动替换对应的荣誉勋章。
1111

1212
| 段位 | 比例 | 段位名 | 国服分数线 | 勋章展示 |
1313
| ----- | ------ | -------- | --------- | --------------------------------------------------------------------------- |
14-
| $LV3$ | $5\%$ | $Guardian$ | $\ge2217.22$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p> |
15-
| $LV2$ | $20\%$ | $Knight$ | $\ge1862.81$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p> |
16-
| $LV1$ | $75\%$ | - | - | - |
14+
| LV3 | 5% | Guardian | &ge;2217.22 | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p> |
15+
| LV2 | 20% | Knight | &ge;1862.81 | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p> |
16+
| LV1 | 75% | - | - | - |
1717

1818
力扣竞赛 **全国排名前 10** 的用户,全站用户名展示为品牌橙色。
1919

solution/CONTEST_README_EN.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ If you are in the top 5% of the contest rating, you’ll get the “Guardian”
1111

1212
If you are in the top 25% of the contest rating, you’ll get the “Knight” badge.
1313

14-
| Level | Proportion | Badge | Contest Rating | |
14+
| Level | Proportion | Badge | Rating | |
1515
| ----- | ---------- | ---------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- |
16-
| $LV3$ | $5\%$ | $Guardian$ | $\ge2172.04$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p> |
17-
| $LV2$ | $20\%$ | $Knight$ | $\ge1846.44$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p> |
18-
| $LV1$ | $75\%$ | - | - | - |
16+
| LV3 | 5\% | Guardian | &ge;2172.04 | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p> |
17+
| LV2 | 20\% | Knight | &ge;1846.44 | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p> |
18+
| LV1 | 75\% | - | - | - |
1919

2020
For top 10 users (excluding LCCN users), your LeetCode ID will be colored orange on the ranking board. You'll also have the honor with you when you post/comment under discuss.
2121

solution/rating.py

+113-201
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,145 @@
1-
import sys
1+
from string import Template
22

33
import requests
44
import urllib3
55

66
urllib3.disable_warnings()
77

8-
url = 'https://leetcode.cn/graphql'
9-
global_url = 'https://leetcode.com/graphql'
108

11-
12-
class LocalRanking:
13-
"""国内竞赛排名"""
14-
15-
def __init__(self):
16-
self.url = 'https://leetcode.cn/graphql'
17-
self.rank_url = 'https://leetcode.cn/graphql/noj-go/'
9+
class Ranking:
10+
def __init__(self, region='CN', retry=3):
11+
self.retry = retry
12+
self.region = region
13+
if region == 'CN':
14+
self.url = 'https://leetcode.cn/graphql'
15+
self.page_query = Template(
16+
"{\n localRankingV2(page:$page) {\nmyRank {\nattendedContestCount\n"
17+
"currentRatingRanking\ndataRegion\nisDeleted\nuser {\nrealName\n"
18+
"userAvatar\nuserSlug\n__typename\n}\n__typename\n}\npage\ntotalUsers\n"
19+
"userPerPage\nrankingNodes {\nattendedContestCount\ncurrentRatingRanking\n"
20+
"dataRegion\nisDeleted\nuser {\nrealName\nuserAvatar\nuserSlug\n__typename\n}\n__"
21+
"typename\n}\n__typename\n }\n}\n"
22+
)
23+
else:
24+
self.url = 'https://leetcode.com/graphql'
25+
self.page_query = Template(
26+
"{\nglobalRanking(page:$page){\ntotalUsers\nuserPerPage\nmyRank{\nranking\n"
27+
"currentGlobalRanking\ncurrentRating\ndataRegion\nuser{\nnameColor\nactiveBadge{\n"
28+
"displayName\nicon\n__typename\n}\n__typename\n}\n__typename\n}\nrankingNodes{\n"
29+
"ranking\ncurrentRating\ncurrentGlobalRanking\ndataRegion\nuser{\nusername\nnameColor\n"
30+
"activeBadge{\ndisplayName\nicon\n__typename\n}\nprofile{\nuserAvatar\ncountryCode\n"
31+
"countryName\nrealName\n__typename\n}\n__typename\n}\n__typename\n}\n__typename\n}\n}\n"
32+
)
1833

1934
def load_page(self, page):
20-
"""分页加载排名列表"""
21-
query = (
22-
"{\n localRankingV2(page:"
23-
+ str(page)
24-
+ ") {\nmyRank {\nattendedContestCount\ncurrentRatingRanking\ndataRegion\nisDeleted\n"
25-
"user {\nrealName\nuserAvatar\nuserSlug\n__typename\n}"
26-
"\n__typename\n}\npage\ntotalUsers\nuserPerPage\n"
27-
"rankingNodes {\nattendedContestCount\ncurrentRatingRanking\ndataRegion\nisDeleted\n"
28-
"user {\nrealName\nuserAvatar\nuserSlug\n__typename\n}\n__typename\n}\n__typename\n }\n}\n"
29-
)
30-
retry = 0
31-
while retry < 3:
35+
query = self.page_query.substitute(page=page)
36+
for _ in range(self.retry):
3237
resp = requests.post(url=self.url, json={'query': query}, verify=False)
33-
if resp.status_code == 200:
34-
nodes = resp.json()['data']['localRankingV2']['rankingNodes']
38+
if resp.status_code != 200:
39+
continue
40+
nodes = resp.json()['data'][
41+
'localRankingV2' if self.region == 'CN' else 'globalRanking'
42+
]['rankingNodes']
43+
if self.region == 'CN':
3544
return [
36-
(int(nd['currentRatingRanking']), nd['user']['userSlug'])
37-
for nd in nodes
45+
(int(v['currentRatingRanking']), v['user']['userSlug'])
46+
for v in nodes
3847
]
3948
else:
40-
retry += 1
41-
return None
42-
43-
def get_user_rank(self, uid):
44-
"""根据用户名获取其个人主页显示的真实分数,因为四舍五入会导致一部分 1599.xxx 的用户也显示为 1600 分"""
45-
query = (
46-
"\nquery userContestRankingInfo($userSlug:String!){\nuserContestRanking(userSlug:$userSlug){\n"
47-
"attendedContestsCount\nrating\nglobalRanking\nlocalRanking\nglobalTotalParticipants\n"
48-
"localTotalParticipants\ntopPercentage\n}\nuserContestRankingHistory(userSlug:$userSlug){\n"
49-
"attended\ntotalProblems\ntrendingDirection\nfinishTimeInSeconds\nrating\nscore\nranking\ncontest{\n"
50-
"title\ntitleCn\nstartTime\n}\n}\n}\n"
51-
)
52-
variables = {'userSlug': uid}
53-
retry = 0
54-
while retry < 3:
55-
resp = requests.post(
56-
url=self.rank_url,
57-
json={'query': query, 'variables': variables},
58-
verify=False,
59-
)
60-
if resp.status_code == 200:
61-
ranking = resp.json()['data']['userContestRanking']
62-
if ranking and 'localRanking' in ranking and 'rating' in ranking:
63-
return int(ranking['localRanking']), float(ranking['rating'])
64-
return None, None
65-
else:
66-
retry += 1
67-
return None, None
68-
69-
def get_1600_count(self):
70-
"""使用二分的方式获取1600分以上的人数,并使用 get_user_rank 方法校准"""
71-
l, r = 1, 1000
72-
while l < r:
73-
mid = (l + r + 1) >> 1
74-
page = self.load_page(mid)
75-
print(f'第 {mid} 页:', page)
76-
if not page:
77-
return 0
78-
x, score = self.get_user_rank(page[0][1])
79-
if score < 1600:
80-
r = mid - 1
81-
else:
82-
l = mid
83-
page = self.load_page(l)
84-
print('校准中...')
85-
l, r = 0, len(page)
86-
while l < r:
87-
mid = (l + r + 1) >> 1
88-
_, score = self.get_user_rank(page[mid][1])
89-
if score < 1600:
90-
r = mid - 1
91-
else:
92-
l = mid
93-
94-
return self.get_user_rank(page[l][1])[0]
95-
96-
def get_user(self, rank):
97-
"""获取指定排名的用户"""
98-
if rank <= 0:
99-
raise Exception('无效的排名')
100-
p = (rank - 1) // 25 + 1
101-
off = (rank - 1) % 25
102-
page = self.load_page(p)
103-
_, score = self.get_user_rank(page[off][1])
104-
return score, page[off][1]
105-
106-
def fetch_ranking_data(self):
107-
"""获取排名数据"""
108-
total = self.get_1600_count()
109-
if not total:
110-
print('网络故障')
111-
sys.exit()
112-
print(f'1600 分以上共计 {total} 人')
113-
114-
guardian = int(total * 0.05)
115-
knight = int(total * 0.25)
116-
g_first, g_last = self.get_user(1), self.get_user(guardian)
117-
print(
118-
f'Guardian(top 5%): 共 {guardian} 名,守门员 {g_last[0]} 分(uid: {g_last[1]}),最高 {g_first[0]} 分(uid: {g_first[1]})'
119-
)
120-
k_first, k_last = self.get_user(guardian + 1), self.get_user(knight)
121-
print(
122-
f'Knight(top 25%): 共 {knight} 名,守门员 {k_last[0]} 分(uid: {k_last[1]}),最高 {k_first[0]} 分(uid: {k_first[1]})'
123-
)
124-
125-
126-
class GlobalRanking:
127-
"""全球竞赛排名"""
128-
129-
def __init__(self):
130-
self.url = 'https://leetcode.com/graphql'
131-
self.lk = LocalRanking()
132-
133-
def load_page(self, page):
134-
"""分页加载排名列表"""
135-
query = (
136-
"{\nglobalRanking(page:"
137-
+ str(page)
138-
+ "){\ntotalUsers\nuserPerPage\nmyRank{\nranking\ncurrentGlobalRanking\ncurrentRating\ndataRegion\n"
139-
"user{\nnameColor\nactiveBadge{\ndisplayName\nicon\n__typename\n}\n__typename\n}\n__typename\n}\n"
140-
"rankingNodes{\nranking\ncurrentRating\ncurrentGlobalRanking\ndataRegion\nuser{\n"
141-
"username\nnameColor\n"
142-
"activeBadge{\ndisplayName\nicon\n__typename\n}\nprofile{\nuserAvatar\ncountryCode\ncountryName\n"
143-
"realName\n__typename\n}\n__typename\n}\n__typename\n}\n__typename\n}\n}\n"
144-
)
145-
retry = 0
146-
while retry < 3:
147-
resp = requests.post(url=self.url, json={'query': query}, verify=False)
148-
if resp.status_code == 200:
149-
nodes = resp.json()['data']['globalRanking']['rankingNodes']
15049
return [
151-
(int(nd['currentGlobalRanking']), nd['user']['username'])
152-
for nd in nodes
50+
(int(v['currentGlobalRanking']), v['user']['username'])
51+
for v in nodes
15352
]
154-
else:
155-
retry += 1
156-
return None
53+
return []
54+
55+
def _user_ranking(self, region, uid):
56+
if region == 'CN':
57+
key = 'userSlug'
58+
url = 'https://leetcode.cn/graphql/noj-go/'
59+
query = (
60+
"\nquery userContestRankingInfo($userSlug:String!){\nuserContestRanking"
61+
"(userSlug:$userSlug){\nattendedContestsCount\nrating\nglobalRanking\nlocalRanking\n"
62+
"globalTotalParticipants\nlocalTotalParticipants\ntopPercentage\n}\n"
63+
"userContestRankingHistory(userSlug:$userSlug){\nattended\ntotalProblems\n"
64+
"trendingDirection\nfinishTimeInSeconds\nrating\nscore\nranking\ncontest{\n"
65+
"title\ntitleCn\nstartTime\n}\n}\n}\n"
66+
)
67+
else:
68+
key = 'username'
69+
url = 'https://leetcode.com/graphql'
70+
query = (
71+
"\nquery userContestRankingInfo($username:String!){\nuserContestRanking"
72+
"(username:$username){\nattendedContestsCount\nrating\nglobalRanking\n"
73+
"totalParticipants\ntopPercentage\nbadge{\nname\n}\n}\nuserContestRankingHistory"
74+
"(username:$username){\nattended\ntrendDirection\nproblemsSolved\n"
75+
"totalProblems\nfinishTimeInSeconds\nrating\nranking\ncontest{\n"
76+
"title\nstartTime\n}\n}\n}\n "
77+
)
15778

158-
def get_user_rank(self, uid):
159-
"""根据用户名获取其个人主页显示的真实分数,因为四舍五入会导致一部分 1599.xxx 的用户也显示为 1600 分"""
160-
query = (
161-
"\nquery userContestRankingInfo($username:String!){\nuserContestRanking(username:$username){\n"
162-
"attendedContestsCount\nrating\nglobalRanking\ntotalParticipants\ntopPercentage\nbadge{\n"
163-
"name\n}\n}\nuserContestRankingHistory(username:$username){\nattended\ntrendDirection\n"
164-
"problemsSolved\ntotalProblems\nfinishTimeInSeconds\nrating\nranking\ncontest{\n"
165-
"title\nstartTime\n}\n}\n}\n "
166-
)
167-
variables = {'username': uid}
168-
retry = 0
169-
while retry < 3:
79+
variables = {key: uid}
80+
for _ in range(self.retry):
17081
resp = requests.post(
171-
url=self.url,
172-
json={'query': query, 'variables': variables},
173-
verify=False,
82+
url=url, json={'query': query, 'variables': variables}, verify=False
17483
)
175-
if resp.status_code == 200:
176-
ranking = resp.json()['data']['userContestRanking']
177-
if ranking and 'globalRanking' in ranking and 'rating' in ranking:
178-
score = float(ranking['rating'])
84+
if resp.status_code != 200:
85+
continue
86+
res = resp.json()
87+
if 'errors' in res:
88+
break
89+
ranking = res['data']['userContestRanking']
90+
if ranking and 'rating' in ranking:
91+
score = float(ranking['rating'])
92+
if 'localRanking' in ranking:
93+
return int(ranking['localRanking']), score
94+
if 'globalRanking' in ranking:
17995
return int(ranking['globalRanking']), score
180-
return None, None
181-
else:
182-
retry += 1
183-
return None, None
96+
return None, None
97+
98+
def get_user_ranking(self, uid):
99+
for region in ['CN', 'US']:
100+
# 美国站会有国服用户,这里最多需要查询两次
101+
ranking, score = self._user_ranking(region, uid)
102+
if score is not None:
103+
return ranking, score
104+
return None
184105

185106
def get_1600_count(self):
186-
"""使用二分的方式获取1600分以上的人数,并使用 get_user_rank 方法校准"""
187-
l, r = 1, 3000
188-
while l < r:
189-
mid = (l + r + 1) >> 1
107+
left, right = 1, 1000 if self.region == 'CN' else 3000
108+
while left < right:
109+
mid = (left + right + 1) >> 1
190110
page = self.load_page(mid)
191111
print(f'第 {mid} 页:', page)
192112
if not page:
193113
return 0
194-
x, score = self.get_user_rank(page[0][1])
195-
if score is None:
196-
x, score = self.lk.get_user_rank(page[0][1])
197-
if score < 1600:
198-
r = mid - 1
114+
ranking, score = self.get_user_ranking(page[0][1])
115+
if score >= 1600:
116+
left = mid
199117
else:
200-
l = mid
201-
page = self.load_page(l)
118+
right = mid - 1
119+
page = self.load_page(left)
202120
print('校准中...')
203-
l, r = 0, len(page)
204-
while l < r:
205-
mid = (l + r + 1) >> 1
206-
_, score = self.get_user_rank(page[mid][1])
207-
if score is None:
208-
x, score = self.lk.get_user_rank(page[mid][1])
209-
if score < 1600:
210-
r = mid - 1
121+
left, right = 0, len(page)
122+
while left < right:
123+
mid = (left + right + 1) >> 1
124+
ranking, score = self.get_user_ranking(page[mid][1])
125+
if score >= 1600:
126+
left = mid
211127
else:
212-
l = mid
213-
214-
return self.get_user_rank(page[l][1])[0] or self.lk.get_user_rank(page[l][1])[0]
128+
right = mid - 1
129+
return self.get_user_ranking(page[left][1])[0]
215130

216131
def get_user(self, rank):
217-
"""获取指定排名的用户"""
218-
if rank <= 0:
219-
raise Exception('无效的排名')
220132
p = (rank - 1) // 25 + 1
221-
off = (rank - 1) % 25
133+
offset = (rank - 1) % 25
222134
page = self.load_page(p)
223-
_, score = self.get_user_rank(page[off][1])
224-
if score is None:
225-
_, score = self.lk.get_user_rank(page[off][1])
226-
return score, page[off][1]
135+
_, score = self.get_user_ranking(page[offset][1])
136+
return score, page[offset][1]
227137

228138
def fetch_ranking_data(self):
229-
"""获取排名数据"""
230139
total = self.get_1600_count()
231140
if not total:
232-
print('网络故障')
233-
sys.exit()
234-
print(f'1600 分以上共计 {total} 人')
141+
return
142+
print(f'[{self.region}] 1600 分以上共计 {total} 人')
235143

236144
guardian = int(total * 0.05)
237145
knight = int(total * 0.25)
@@ -245,8 +153,12 @@ def fetch_ranking_data(self):
245153
)
246154

247155

248-
local = LocalRanking()
249-
local.fetch_ranking_data()
156+
# 国服竞赛排名
157+
lk = Ranking(region='CN')
158+
lk.fetch_ranking_data()
159+
160+
print('\n------------------------------\n')
250161

251-
gk = GlobalRanking()
162+
# 全球竞赛排名
163+
gk = Ranking(region='US')
252164
gk.fetch_ranking_data()

0 commit comments

Comments
 (0)