Skip to content

Commit ed54f1c

Browse files
committed
feat: add global contest rating
添加全球竞赛相关分数
1 parent 149a18a commit ed54f1c

File tree

3 files changed

+250
-123
lines changed

3 files changed

+250
-123
lines changed

solution/CONTEST_README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
| 段位 | 比例 | 段位名 | 国服分数线 | 勋章展示 |
1313
| ----- | ------ | -------- | --------- | --------------------------------------------------------------------------- |
14-
| $LV3$ | $5\%$ | $Guardian$ | $\ge2216$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p> |
15-
| $LV2$ | $20\%$ | $Knight$ | $\ge1863$ | <p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p> |
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> |
1616
| $LV1$ | $75\%$ | - | - | - |
1717

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

solution/CONTEST_README_EN.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ The contest badge is calculated based on the contest rating.
99
For LeetCoders with rating >=1600,
1010
If you are in the top 5% of the contest rating, you’ll get the “Guardian” badge.
1111

12-
<p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Guardian.gif" style="width: 80px;" /></p>
13-
1412
If you are in the top 25% of the contest rating, you’ll get the “Knight” badge.
1513

16-
<p><img alt="" src="https://fastly.jsdelivr.net/gh/doocs/leetcode@main/images/Knight.gif" style="width: 80px;" /></p>
14+
| Level | Proportion | Badge | Contest Rating | |
15+
| ----- | ---------- | ---------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- |
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\%$ | - | - | - |
1719

1820
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.
1921

solution/rating.py

+243-118
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,252 @@
1-
import re
21
import sys
3-
from functools import cache
42

53
import requests
4+
import urllib3
5+
6+
urllib3.disable_warnings()
67

78
url = 'https://leetcode.cn/graphql'
9+
global_url = 'https://leetcode.com/graphql'
10+
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/'
18+
19+
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:
32+
resp = requests.post(url=self.url, json={'query': query}, verify=False)
33+
if resp.status_code == 200:
34+
nodes = resp.json()['data']['localRankingV2']['rankingNodes']
35+
return [
36+
(int(nd['currentRatingRanking']), nd['user']['userSlug'])
37+
for nd in nodes
38+
]
39+
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()
8132

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']
150+
return [
151+
(int(nd['currentGlobalRanking']), nd['user']['username'])
152+
for nd in nodes
153+
]
154+
else:
155+
retry += 1
156+
return None
9157

10-
# 分页加载排名列表
11-
@cache
12-
def load_page(page):
13-
query = (
14-
"{\n localRankingV2(page:"
15-
+ str(page)
16-
+ ") {\nmyRank {\nattendedContestCount\ncurrentRatingRanking\ndataRegion\nisDeleted\n"
17-
"user {\nrealName\nuserAvatar\nuserSlug\n__typename\n}\n__typename\n}\npage\ntotalUsers\nuserPerPage\n"
18-
"rankingNodes {\nattendedContestCount\ncurrentRatingRanking\ndataRegion\nisDeleted\n"
19-
"user {\nrealName\nuserAvatar\nuserSlug\n__typename\n}\n__typename\n}\n__typename\n }\n}\n"
20-
)
21-
retry = 0
22-
while retry < 3:
23-
resp = requests.post(url=url, json={'query': query})
24-
if resp.status_code == 200:
25-
nodes = resp.json()['data']['localRankingV2']['rankingNodes']
26-
return [
27-
(int(nd['currentRatingRanking']), nd['user']['userSlug'])
28-
for nd in nodes
29-
]
30-
else:
31-
retry += 1
32-
return None
33-
34-
35-
# 根据用户名获取其个人主页显示的真实分数,因为四舍五入会导致一部分 1599.xxx 的用户也显示为 1600 分
36-
@cache
37-
def get_user_rank(uid):
38-
operation_name = "userContest"
39-
query = (
40-
"query userContest($userSlug: String!){\n userContestRanking(userSlug: $userSlug){"
41-
"\ncurrentRatingRanking\nratingHistory\n}\n}\n "
42-
)
43-
variables = {'userSlug': uid}
44-
retry = 0
45-
while retry < 3:
46-
resp = requests.post(
47-
url=url,
48-
json={
49-
'operationName': operation_name,
50-
'query': query,
51-
'variables': variables,
52-
},
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 "
53166
)
54-
if resp.status_code == 200:
55-
ranking = resp.json()['data']['userContestRanking']
56-
score = None
57-
if ranking and 'ratingHistory' in ranking:
58-
s = ranking['ratingHistory']
59-
mth = re.search(r'(\d+(?:\.\d+)?)(?:, null)*]$', s)
60-
if mth:
61-
score = mth.group(1)
62-
return (
63-
(ranking['currentRatingRanking'], float(score))
64-
if score
65-
else (None, None)
167+
variables = {'username': uid}
168+
retry = 0
169+
while retry < 3:
170+
resp = requests.post(
171+
url=self.url,
172+
json={'query': query, 'variables': variables},
173+
verify=False,
66174
)
67-
else:
68-
retry += 1
69-
return None, None
70-
71-
72-
# 使用二分的方式获取1600分以上的人数,并使用 get_user_rank 方法校准
73-
def get_1600_count() -> int:
74-
l, r = 1, 1000
75-
while l < r:
76-
mid = (l + r + 1) >> 1
77-
page = load_page(mid)
78-
print(f'第 {mid} 页:', page)
79-
if not page:
80-
return 0
81-
_, score = get_user_rank(page[0][1])
82-
if score < 1600:
83-
r = mid - 1
84-
else:
85-
l = mid
86-
page = load_page(l)
87-
print('校准中...')
88-
l, r = 0, len(page)
89-
while l < r:
90-
mid = (l + r + 1) >> 1
91-
_, score = get_user_rank(page[mid][1])
92-
if score < 1600:
93-
r = mid - 1
94-
else:
95-
l = mid
96-
97-
return get_user_rank(page[l][1])[0]
98-
99-
100-
# 获取指定排名的用户
101-
@cache
102-
def get_user(rank):
103-
if rank <= 0:
104-
raise Exception('无效的排名')
105-
p = (rank - 1) // 25 + 1
106-
off = (rank - 1) % 25
107-
page = load_page(p)
108-
_, score = get_user_rank(page[off][1])
109-
return score, page[off][1]
110-
111-
112-
total = get_1600_count()
113-
if not total:
114-
print('网络故障')
115-
sys.exit()
116-
print(f'1600 分以上共计 {total} 人')
117-
118-
guardian = int(total * 0.05)
119-
knight = int(total * 0.25)
120-
g_first, g_last = get_user(1), get_user(guardian)
121-
print(
122-
f'Guardian(top 5%): 共 {guardian} 名,守门员 {g_last[0]} 分(uid: {g_last[1]}),最高 {g_first[0]} 分(uid: {g_first[1]})'
123-
)
124-
k_first, k_last = get_user(guardian + 1), get_user(knight)
125-
print(
126-
f'Knight(top 25%): 共 {knight} 名,守门员 {k_last[0]} 分(uid: {k_last[1]}),最高 {k_first[0]} 分(uid: {k_first[1]})'
127-
)
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'])
179+
return int(ranking['globalRanking']), score
180+
return None, None
181+
else:
182+
retry += 1
183+
return None, None
184+
185+
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
190+
page = self.load_page(mid)
191+
print(f'第 {mid} 页:', page)
192+
if not page:
193+
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
199+
else:
200+
l = mid
201+
page = self.load_page(l)
202+
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
211+
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]
215+
216+
def get_user(self, rank):
217+
"""获取指定排名的用户"""
218+
if rank <= 0:
219+
raise Exception('无效的排名')
220+
p = (rank - 1) // 25 + 1
221+
off = (rank - 1) % 25
222+
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]
227+
228+
def fetch_ranking_data(self):
229+
"""获取排名数据"""
230+
total = self.get_1600_count()
231+
if not total:
232+
print('网络故障')
233+
sys.exit()
234+
print(f'1600 分以上共计 {total} 人')
235+
236+
guardian = int(total * 0.05)
237+
knight = int(total * 0.25)
238+
g_first, g_last = self.get_user(1), self.get_user(guardian)
239+
print(
240+
f'Guardian(top 5%): 共 {guardian} 名,守门员 {g_last[0]} 分(uid: {g_last[1]}),最高 {g_first[0]} 分(uid: {g_first[1]})'
241+
)
242+
k_first, k_last = self.get_user(guardian + 1), self.get_user(knight)
243+
print(
244+
f'Knight(top 25%): 共 {knight} 名,守门员 {k_last[0]} 分(uid: {k_last[1]}),最高 {k_first[0]} 分(uid: {k_first[1]})'
245+
)
246+
247+
248+
local = LocalRanking()
249+
local.fetch_ranking_data()
250+
251+
gk = GlobalRanking()
252+
gk.fetch_ranking_data()

0 commit comments

Comments
 (0)