mirror of https://github.com/doocs/leetcode.git
165 lines
6.6 KiB
Python
165 lines
6.6 KiB
Python
from string import Template
|
||
|
||
import requests
|
||
import urllib3
|
||
|
||
urllib3.disable_warnings()
|
||
|
||
|
||
class Ranking:
|
||
def __init__(self, region='CN', retry=3):
|
||
self.retry = retry
|
||
self.region = region
|
||
if region == 'CN':
|
||
self.url = 'https://leetcode.cn/graphql'
|
||
self.page_query = Template(
|
||
"{\n localRankingV2(page:$page) {\nmyRank {\nattendedContestCount\n"
|
||
"currentRatingRanking\ndataRegion\nisDeleted\nuser {\nrealName\n"
|
||
"userAvatar\nuserSlug\n__typename\n}\n__typename\n}\npage\ntotalUsers\n"
|
||
"userPerPage\nrankingNodes {\nattendedContestCount\ncurrentRatingRanking\n"
|
||
"dataRegion\nisDeleted\nuser {\nrealName\nuserAvatar\nuserSlug\n__typename\n}\n__"
|
||
"typename\n}\n__typename\n }\n}\n"
|
||
)
|
||
else:
|
||
self.url = 'https://leetcode.com/graphql'
|
||
self.page_query = Template(
|
||
"{\nglobalRanking(page:$page){\ntotalUsers\nuserPerPage\nmyRank{\nranking\n"
|
||
"currentGlobalRanking\ncurrentRating\ndataRegion\nuser{\nnameColor\nactiveBadge{\n"
|
||
"displayName\nicon\n__typename\n}\n__typename\n}\n__typename\n}\nrankingNodes{\n"
|
||
"ranking\ncurrentRating\ncurrentGlobalRanking\ndataRegion\nuser{\nusername\nnameColor\n"
|
||
"activeBadge{\ndisplayName\nicon\n__typename\n}\nprofile{\nuserAvatar\ncountryCode\n"
|
||
"countryName\nrealName\n__typename\n}\n__typename\n}\n__typename\n}\n__typename\n}\n}\n"
|
||
)
|
||
|
||
def load_page(self, page):
|
||
query = self.page_query.substitute(page=page)
|
||
for _ in range(self.retry):
|
||
resp = requests.post(url=self.url, json={'query': query}, verify=False)
|
||
if resp.status_code != 200:
|
||
continue
|
||
nodes = resp.json()['data'][
|
||
'localRankingV2' if self.region == 'CN' else 'globalRanking'
|
||
]['rankingNodes']
|
||
if self.region == 'CN':
|
||
return [
|
||
(int(v['currentRatingRanking']), v['user']['userSlug'])
|
||
for v in nodes
|
||
]
|
||
else:
|
||
return [
|
||
(int(v['currentGlobalRanking']), v['user']['username'])
|
||
for v in nodes
|
||
]
|
||
return []
|
||
|
||
def _user_ranking(self, region, uid):
|
||
if region == 'CN':
|
||
key = 'userSlug'
|
||
url = 'https://leetcode.cn/graphql/noj-go/'
|
||
query = (
|
||
"\nquery userContestRankingInfo($userSlug:String!){\nuserContestRanking"
|
||
"(userSlug:$userSlug){\nattendedContestsCount\nrating\nglobalRanking\nlocalRanking\n"
|
||
"globalTotalParticipants\nlocalTotalParticipants\ntopPercentage\n}\n"
|
||
"userContestRankingHistory(userSlug:$userSlug){\nattended\ntotalProblems\n"
|
||
"trendingDirection\nfinishTimeInSeconds\nrating\nscore\nranking\ncontest{\n"
|
||
"title\ntitleCn\nstartTime\n}\n}\n}\n"
|
||
)
|
||
else:
|
||
key = 'username'
|
||
url = 'https://leetcode.com/graphql'
|
||
query = (
|
||
"\nquery userContestRankingInfo($username:String!){\nuserContestRanking"
|
||
"(username:$username){\nattendedContestsCount\nrating\nglobalRanking\n"
|
||
"totalParticipants\ntopPercentage\nbadge{\nname\n}\n}\nuserContestRankingHistory"
|
||
"(username:$username){\nattended\ntrendDirection\nproblemsSolved\n"
|
||
"totalProblems\nfinishTimeInSeconds\nrating\nranking\ncontest{\n"
|
||
"title\nstartTime\n}\n}\n}\n "
|
||
)
|
||
|
||
variables = {key: uid}
|
||
for _ in range(self.retry):
|
||
resp = requests.post(
|
||
url=url, json={'query': query, 'variables': variables}, verify=False
|
||
)
|
||
if resp.status_code != 200:
|
||
continue
|
||
res = resp.json()
|
||
if 'errors' in res:
|
||
break
|
||
ranking = res['data']['userContestRanking']
|
||
if ranking and 'rating' in ranking:
|
||
score = float(ranking['rating'])
|
||
if 'localRanking' in ranking:
|
||
return int(ranking['localRanking']), score
|
||
if 'globalRanking' in ranking:
|
||
return int(ranking['globalRanking']), score
|
||
return None, None
|
||
|
||
def get_user_ranking(self, uid):
|
||
for region in ['CN', 'US']:
|
||
# 美国站会有国服用户,这里最多需要查询两次
|
||
ranking, score = self._user_ranking(region, uid)
|
||
if score is not None:
|
||
return ranking, score
|
||
return None
|
||
|
||
def get_1600_count(self):
|
||
left, right = 1, 4000
|
||
while left < right:
|
||
mid = (left + right + 1) >> 1
|
||
page = self.load_page(mid)
|
||
print(f'第 {mid} 页:', page)
|
||
if not page:
|
||
return 0
|
||
ranking, score = self.get_user_ranking(page[0][1])
|
||
if score >= 1600:
|
||
left = mid
|
||
else:
|
||
right = mid - 1
|
||
page = [uid for _, uid in self.load_page(left) if uid]
|
||
print('校准中...')
|
||
left, right = 0, len(page) - 1
|
||
while left < right:
|
||
mid = (left + right + 1) >> 1
|
||
ranking, score = self.get_user_ranking(page[mid])
|
||
if score >= 1600:
|
||
left = mid
|
||
else:
|
||
right = mid - 1
|
||
return self.get_user_ranking(page[left])[0]
|
||
|
||
def get_user(self, rank):
|
||
p = (rank - 1) // 25 + 1
|
||
offset = (rank - 1) % 25
|
||
page = self.load_page(p)
|
||
_, score = self.get_user_ranking(page[offset][1])
|
||
return score, page[offset][1]
|
||
|
||
def fetch_ranking_data(self):
|
||
total = self.get_1600_count()
|
||
if not total:
|
||
return
|
||
print(f'[{self.region}] 1600 分以上共计 {total} 人')
|
||
|
||
guardian = int(total * 0.05)
|
||
knight = int(total * 0.25)
|
||
g_first, g_last = self.get_user(1), self.get_user(guardian)
|
||
print(
|
||
f'Guardian(top 5%): 共 {guardian} 名,守门员 {g_last[0]} 分(uid: {g_last[1]}),最高 {g_first[0]} 分(uid: {g_first[1]})'
|
||
)
|
||
k_first, k_last = self.get_user(guardian + 1), self.get_user(knight)
|
||
print(
|
||
f'Knight(top 25%): 共 {knight} 名,守门员 {k_last[0]} 分(uid: {k_last[1]}),最高 {k_first[0]} 分(uid: {k_first[1]})'
|
||
)
|
||
|
||
|
||
# 国服竞赛排名
|
||
lk = Ranking(region='CN')
|
||
lk.fetch_ranking_data()
|
||
|
||
print('\n------------------------------\n')
|
||
|
||
# 全球竞赛排名
|
||
gk = Ranking(region='US')
|
||
gk.fetch_ranking_data()
|