有没有办法获取我的 Stack Exchange 统计信息?

Posted

技术标签:

【中文标题】有没有办法获取我的 Stack Exchange 统计信息?【英文标题】:Is there a way to get my Stack Exchange stats? 【发布时间】:2019-12-16 17:05:45 【问题描述】:

我正在重新创建 Stack Exchange 提供的闪光图像,并且重新创建的响应速度更快,因为我可以将鼠标悬停在站点图标上并显示给定 Stack Exchange 域的统计信息。我目前必须手动更新我的数据,我计划每月执行两次左右,除非有办法通过 Web 服务或类似服务直接从 Stack Exchange 加载该数据。

需要注意的几点:

我将把它托管在一个 ASP.NET Web 应用程序中,这样 C# API 就可以了。 Web 服务也会很完美,因为我可以从 javascript 调用它们。 我需要提供任何服务的文档链接。

以下是我当前的手动重新创建,以防您好奇或不知道 SE 天赋是什么,尽管它确实需要清理并提高效率。

var siteNames = [ 'Stack Exchange',
					   'Puzzling',
					   'Stack Overflow',
					   'Software Engineering',
					   'Mathematics',
					   'Physical Fitness' ]
var reps = [ '6.2k', '4.3k', '954', '410', '224', '220' ];
var golds = [ '1', '0', '0', '1', '0', '0' ];
var silvers = [ '14', '7', '4', '2', '1', '0' ];
var bronzes = [ '98', '50', '20', '10', '8', '10' ];
function getSiteStats(siteID) 
	document.getElementById("site-name").innerText = siteNames[siteID];
	document.getElementById("rep").innerText = reps[siteID];
	document.getElementById("gold").innerText = golds[siteID];
	document.getElementById("silver").innerText = silvers[siteID];
	document.getElementById("bronze").innerText = bronzes[siteID];

function resetSiteStats() 
	getSiteStats(0);
html, body 
	margin: 0;
	height: 100%;
	width: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-direction: column;
	background-color: #6aa4ed;
	background-image: linear-gradient(45deg, #6aa4ed, #141d33);
	background-image: -webkit-linear-gradient(45deg, #6aa4ed, #141d33);

h1, h5 
	color: #fff;
	font-family: Arial, Helvetica, sans-serif;
	font-weight: 100;
	text-align: center;
	margin: 0;

h1 
	font-size: 10vh;

h5 
	margin-bottom: 10px;

.flair 
	padding: 15px;
	background-color: #fff;
	border-radius: 5px;
	box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
	display: flex;

.flair img 
	width: 40px;
	height: 40px;
	margin: 5px;
	cursor: pointer;

.flair .profile 
	width: 175px;
	height: 175px;
	margin: 0;
	margin-right: 15px;
	box-shadow: 2px 2px 4px rgba(12,13,14,0.5);
	cursor: default;

.flair a 
	color: #37f;
	text-decoration: none;
	margin: 5px;

.flair a:hover 
	color: #15a;

.flair ul 
	list-style-type: none;
	margin: 0;
	padding: 0;

.flair ul > li 
	display: inline-block;
	margin: 5px;

.flair p 
	margin: 0;
	margin-left: 5px;

.badge div 
	display: inline-block;
	height: 7px;
	width: 7px;
	border-radius: 50%;
	transform: translateY(-3px) translateX(3px);

.gold 
	background-color: #fc0;

.silver 
	background-color: #ccc;

.bronze 
	background-color: #da6;
<h1>Stack Exchange Flair</h1>
<h5>Not Mobile Friendly (Yet)</h5>
<h5>Hover Over Site Icons</h5>
<div class="flair">
	<img class="profile" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2940219/blue.jpg" />
	<div class="account">
		<a href="#">PerpetualJ</a>
		<p id="site-name">Stack Exchange</p>
		<ul>
			<li><strong id="rep">6.2k</strong></li>
			<li>
				<div class="badge">
					<div class="gold"></div>
					<span id="gold">1</span>
				</div>
			</li>
			<li>
				<div class="badge">
					<div class="silver"></div>
					<span id="silver">14</span>
				</div>
			</li>
			<li>
				<div class="badge">
					<div class="bronze"></div>
					<span id="bronze">98</span>
				</div>
			</li>
		</ul>
		<ul>
						<li onmouseover="getSiteStats(1);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/puzzling/img/icon-48.png"/></li>
			<li onmouseover="getSiteStats(2);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/***/img/apple-touch-icon.png"/></li>
			<li onmouseover="getSiteStats(3);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/softwareengineering/img/icon-48.png"/></li>
			<li onmouseover="getSiteStats(4);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/math/img/apple-touch-icon.png"/></li>
			<li onmouseover="getSiteStats(5);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/fitness/img/icon-48.png?v=f5a02f85db94"/></li>
		</ul>
		<p>How fast do you have to slap a chicken to cook it?</p>
	</div>
</div>

我有什么方法可以调用 API、Web 服务或类似的东西,让我可以提取给定 Stack Exchange 网站的当前统计信息?

另外,我不希望进行任何类型的网页抓取或类似操作。我希望它来自合法的 Stack Exchange 服务。

注意:如果这属于元数据,请告诉我,以便可以迁移。

On-Topic:根据help center,此问题被视为主题:

我们认为最好的 Stack Overflow 问题都包含一些源代码,但如果您的问题通常涵盖……

程序员常用的软件工具;并且是 软件开发独有的实用且可解答的问题

...那么您来对地方了!

鉴于以上引用,API 是程序员常用的工具,通过询问 Stack Exchange 是否有,这个问题是一个实用且可回答的问题。不过,我确实相信这可能更适合 Meta,但我无法迁移它。

【问题讨论】:

【参考方案1】:

我最近发现 Stack Exchange 确实为这些类型的东西提供了一个 API。我强烈建议在使用 API 之前阅读他们的 documentation。为了完成我在这里询问的任务,我需要使用以下 API 调用:

/users/ids /users/ids/associated

我同时利用这两个调用来重新创建 Stack Exchange 风格,以防万一您不知道这种风格是什么:

首先,我编写了一组简单的方法来处理我对 API 的请求:

function getWebServiceResponse(requestUrl, callback) 
    let request = new XMLHttpRequest();
    request.open('GET', requestUrl, true);
    request.onload = function() 
        if (request.status < 200 || request.status >= 400)
            callback("An unexpected error occurred.");
        else
            callback(JSON.parse(this.response));
    ;
    request.send();

function getSEWebServiceResponse(request, callback) 
    let apiRoot = 'https://api.stackexchange.com/2.2/';
    let key = 'key=s29XM)Eqn2x3YxhjLgFwBQ((';
    if (request.indexOf('?') >= 0)
        key = '&' + key;
    else
        key = '?' + key;

    getWebServiceResponse(apiRoot + request + key, function(response)  callback(response); );

这里需要key帮助防止throttling 的后续请求过多:

每个应用程序都受到基于 IP 的并发请求限制。如果单个 IP 每秒发出超过 30 个请求,则会丢弃新请求。

从这里开始,实施非常简单,是一个很棒的学习过程!

/users/ids

获取在ids 中标识的用户。

通常,当您从其他来源(例如 /questions)获取用户 ID 时,将调用此方法来获取用户配置文件。

ids 最多可以包含 100 个以分号分隔的 ID。

function getAssociatedAccountDetails(userID, siteName, fullSiteName, callback) 
    let url = 'users/' + userID +'?order=desc&sort=reputation&site=' + siteName;
    getSEWebServiceResponse(url, function(response) 
        if (!response.items)
            return;

        let account = response.items[0];
        userCard.reputation += account.reputation;
        userCard.badges.gold += account.badge_counts.gold;
        userCard.badges.silver += account.badge_counts.silver;
        userCard.badges.bronze += account.badge_counts.bronze;

        if (userCard.siteUrls.length < 7) 
            var siteProfileCombo = account.link + '|<IMG>|' + fullSiteName;
            siteProfileCombo = siteProfileCombo.replace('<IMG>', getSiteIcon(siteName));
            userCard.siteUrls.push(siteProfileCombo);
        
        if (userCard.username.length < 1)
            userCard.username = account.display_name;
        if (userCard.profileImageUrl.length < 1)
            userCard.profileImageUrl = account.profile_image;

        callback();
    );

/users/ids/关联

返回用户的所有关联帐户,给定他们在ids 中的account_ids

ids 最多可以包含 100 个以分号分隔的 ID。

function getAssociatedAccounts(accountID, callback) 
    let url = 'users/' + accountID + '/associated';
    getSEWebServiceResponse(url, function(response) 
        if (!response.items)
            return;

        var accounts = sortAccountsByReputation(response.items);
        var accountsProcessed = 0;
        for (let i = 0; i < accounts.length; i++) 
            let siteName = accounts[i].site_url.replace('https://', '');
            siteName = siteName.replace('.stackexchange', '');
            siteName = siteName.replace('.com', '');
            getAssociatedAccountDetails(accounts[i].user_id, siteName, accounts[i].site_name, function() 
                if (++accountsProcessed >= accounts.length)
                    callback();
            );
        
    );


全面实施

/* Definitions */
var CardType =  Wheel: "wheel", Card: "card", Box: "box" 
var userCard = 
	username: '',
	profileImageUrl: '',
	reputation: 0,
	badges: 
		gold: 0,
		silver: 0,
		bronze: 0
	,
	siteUrls: []


/* Initial Calls */
var accountID = '13342919';
generateCard('user-flair-wheel', accountID, CardType.Wheel);

/* Required Events */
function showSitename(tooltipID, siteName) 
	var tooltip = document.getElementById(tooltipID);
	tooltip.innerHTML = siteName.replace('Stack Exchange', '');
	tooltip.classList.add('active');

function hideSitename(tooltipID) 
	document.getElementById(tooltipID).classList.remove('active');


/* UI Generation Functions */
function generateCard(containerid, accountid, cardType) 
	getAssociatedAccounts(accountID, function() 
		var className = cardType.toString().toLowerCase();
		var container = document.getElementById(containerid);
		container.classList.add("flair");
		container.classList.add(className);
		
		// Build the card.
		addProfile(container);
		addScores(container, className);
		addSites(container, className);
		container.innerHTML += '<div id="' + containerid +
									  '-tooltip" class="se-tooltip"></div>';
	);

function addProfile(container) 
	container.innerHTML += '<img class="user-image" src="' +
								   userCard.profileImageUrl + '"/>';
	container.innerHTML += '<h1 class="username display-4">' +
									userCard.username + '</h1>';

function addScores(container, cardType) 
	var badges = '<ul class="badges">';
	badges += '<li><i class="fas fa-trophy"></i> <span id="reputation-' +
				 cardType + '">' + userCard.reputation + '</span></li>';
	badges += '<li><span id="gold-badges-' + cardType + '">' +
				 userCard.badges.gold + '</span></li>';
	badges += '<li><span id="silver-badges-' + cardType + '">' +
				 userCard.badges.silver + '</span></li>';
	badges += '<li><span id="bronze-badges-' + cardType + '">' +
				 userCard.badges.bronze + '</span></li>';
	badges += '</ul>';
	container.innerHTML += badges;

function addSites(container, cardType) 
	var sites = '<ul id="sites-' + cardType + '" class="sites">';
	for (var i = 0; i < userCard.siteUrls.length; i++) 
		var site = '<li>';
		var siteLinkSplit = userCard.siteUrls[i].split('|');
		site += '<a href="' + siteLinkSplit[0] + '">';
		
		var tooltipID = container.id +'-tooltip';
		var linkElement = '<a href="' + siteLinkSplit[0] + '"';
		linkElement += ' onmouseover="showSitename(\'' + tooltipID + '\',\'' + siteLinkSplit[2] + '\')"';
		linkElement += ' onmouseout="hideSitename(\'' + tooltipID + '\');"';
		site += linkElement + '>';
		site += '<img src="' + (siteLinkSplit[1] == '<IMG>' ? '#' : siteLinkSplit[1]) + '"/></a></li>';
		sites += site;
	
	
	sites += '</ul>';
	container.innerHTML += sites;


/* Stack Exchange API Based Functions */
function getAssociatedAccounts(accountID, callback) 
	let url = 'users/' + accountID + '/associated';
	getSEWebServiceResponse(url, function(response) 
		if (!response.items)
			return;
		
		var accounts = sortAccountsByReputation(response.items);
		var accountsProcessed = 0;
		for (let i = 0; i < accounts.length; i++) 
			let siteName = accounts[i].site_url.replace('https://', '');
			siteName = siteName.replace('.stackexchange', '');
			siteName = siteName.replace('.com', '');
			getAssociatedAccountDetails(accounts[i].user_id, siteName, accounts[i].site_name, function() 
				if (++accountsProcessed >= accounts.length)
					callback();
			);
		
	);

function getAssociatedAccountDetails(userID, siteName, fullSiteName, callback) 
	let url = 'users/' + userID +'?order=desc&sort=reputation&site=' + siteName;
	getSEWebServiceResponse(url, function(response) 
		if (!response.items)
			return;
		
		let account = response.items[0];
		userCard.reputation += account.reputation;
		userCard.badges.gold += account.badge_counts.gold;
		userCard.badges.silver += account.badge_counts.silver;
		userCard.badges.bronze += account.badge_counts.bronze;
		
		if (userCard.siteUrls.length < 7) 
			var siteProfileCombo = account.link + '|<IMG>|' + fullSiteName;
			siteProfileCombo = siteProfileCombo.replace('<IMG>', getSiteIcon(siteName));
			userCard.siteUrls.push(siteProfileCombo);
		
		if (userCard.username.length < 1)
			userCard.username = account.display_name;
		if (userCard.profileImageUrl.length < 1)
			userCard.profileImageUrl = account.profile_image;
		
		callback();
	);


/* Helper Functions */
function getSEWebServiceResponse(request, callback) 
	let apiRoot = 'https://api.stackexchange.com/2.2/';
	let key = 'key=s29XM)Eqn2x3YxhjLgFwBQ((';
	if (request.indexOf('?') >= 0)
		key = '&' + key;
	else
		key = '?' + key;
	
	getWebServiceResponse(apiRoot + request + key, function(response)  callback(response); );

function getWebServiceResponse(requestUrl, callback) 
	let request = new XMLHttpRequest();
	request.open('GET', requestUrl, true);
	request.onload = function() 
		if (request.status < 200 || request.status >= 400)
			callback("An unexpected error occurred.");
		else
			callback(JSON.parse(this.response));
	;
	request.send();

function sortAccountsByReputation(accounts) 
	return accounts.sort(function(a, b)  return b.reputation - a.reputation; );

function getSiteIcon(siteName) 
	if (siteName == "meta")
		return 'https://meta.stackexchange.com/content/Sites/stackexchangemeta/img/icon-48.png';
	
	return 'https://cdn.sstatic.net/Sites/' + siteName + '/img/apple-touch-icon.png';
/* Flair Styles */
.flair 
	position: relative;
	margin: 15px;

.flair > .se-tooltip 
	position: absolute;
	left: 50%;
	transform: translate(-50%);
	width: 250px;
	bottom: 50px;
	opacity: 0;
	background-color: #fff;
	color: #555;
	text-shadow: none;
	border-radius: 25px;
	padding: 5px 10px;
	box-shadow: 2px 2px 3px #0005;

.flair > .se-tooltip.active 
	bottom: 10px;
	opacity: 1;


/* Flair Wheel Styles */
.flair.wheel 
	width: 200px;
	height: 250px;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-direction: column;
	text-shadow: 1px 1px 2px #0005;

.flair.wheel .user-image 
	width: 100px;
	height: 100px;
	border-radius: 50%;
	box-shadow: 2px 2px 3px #0005;

.flair.wheel .username 
	font-size: 30px;
	margin: 0;

.flair.wheel .badges > li > span  position: relative; 
.flair.wheel .badges > li:first-of-type > i  color: #5c9; 
.flair.wheel .badges > li:not(:first-of-type) > span::before 
	content: '';
	position: absolute;
	top: 50%;
	left: -15px;
	transform: translateY(-40%);
	width: 10px;
	height: 10px;
	border-radius: 50%;

.flair.wheel .badges > li:nth-child(2) > span::before  background-color: #fb3; 
.flair.wheel .badges > li:nth-child(3) > span::before  background-color: #aaa; 
.flair.wheel .badges > li:nth-child(4) > span::before  background-color: #c95; 

.flair.wheel .sites 
	position: absolute;
	top: 10px;
	left: 0;
	width: 100%;
	height: 55%;

.flair.wheel .sites > li  position: absolute; 
.flair.wheel .sites > li > a > img 
	width: 35px;
	height: 35px;
	background-color: #fffa;
	border-radius: 50%;
	padding: 2px;
	box-shadow: 2px 2px 3px #0005;
	cursor: pointer;
	transition: 0.3s cubic-bezier(0.5, -2.5, 1.0, 1.2) all;
	z-index: 1;

.flair.wheel .sites > li > a:hover > img 
	width: 40px;
	height: 40px;
	background-color: #fff;

.flair.wheel .sites > li:nth-child(1) 
	top: -15px;
	left: 50%;
	transform: translate(-50%);

.flair.wheel .sites > li:nth-child(2) 
	top: 0px;
	left: 15%;
	transform: translate(-20%);

.flair.wheel .sites > li:nth-child(3) 
	top: 0px;
	left: 70%;
	transform: translate(-20%);

.flair.wheel .sites > li:nth-child(4) 
	top: 45%;
	left: 80%;
	transform: translate(-20%, -50%);

.flair.wheel .sites > li:nth-child(5) 
	top: 45%;
	left: -5px;
	transform: translateY(-50%);

.flair.wheel .sites > li:nth-child(6) 
	top: 79%;
	left: 3px;
	transform: translateY(-50%);

.flair.wheel .sites > li:nth-child(7) 
	top: 79%;
	right: 3px;
	transform: translateY(-50%);


/* To Organize in a Row instead of Column */
.user-flair-container 
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: center;
	flex-wrap: wrap;


/* Global Styles */
ul 
	padding: 0;
	listy-style-type: none;

ul > li 
	display: inline-block;
	padding: 0 10px;


/* Template Overrides */
html, body 
  margin: 0;
  height: 100%;
	background-color: #333 !important;
	background-image: linear-gradient(45deg, #333, #555) !important;
	background-image: -webkit-linear-gradient(45deg, #333, #555) !important;

.primary-content 
  height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;

.primary-content > .lead  font-size: 25px; 
<link href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2940219/PerpetualJ.css" rel="stylesheet"/>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" rel="stylesheet"/>
<div id="primary-content" class="primary-content">
	<div class="user-flair-container">
		<div id="user-flair-wheel"></div>
	</div>
</div>

祝大家在未来的努力中好运!

【讨论】:

以上是关于有没有办法获取我的 Stack Exchange 统计信息?的主要内容,如果未能解决你的问题,请参考以下文章

json Stack Exchange热门标签

ini Stack Exchange HAProxy

ini Stack Exchange HAProxy

exchange控制台没有重置用户密码选项解决办法

如何使用 curl 登录 Stack Exchange?

如何对 Stack Exchange Data Explorer (SEDE) 结果进行分页?