Implement #771
This commit is contained in:
		
							parent
							
								
									99db09b702
								
							
						
					
					
						commit
						5c37b9cef3
					
				
					 5 changed files with 172 additions and 0 deletions
				
			
		|  | @ -2,6 +2,10 @@ ChangeLog (Release Notes) | ||||||
| ========================= | ========================= | ||||||
| 主に notable な changes を書いていきます | 主に notable な changes を書いていきます | ||||||
| 
 | 
 | ||||||
|  | unreleased | ||||||
|  | ---------- | ||||||
|  | * New: ユーザーページによく使うドメインを表示 (#771) | ||||||
|  | 
 | ||||||
| 2566 (2017/09/07) | 2566 (2017/09/07) | ||||||
| ----------------- | ----------------- | ||||||
| * New: 投稿することの多いキーワードをユーザーページに表示する (#768) | * New: 投稿することの多いキーワードをユーザーページに表示する (#768) | ||||||
|  |  | ||||||
|  | @ -498,6 +498,7 @@ mobile: | ||||||
|       images: "Images" |       images: "Images" | ||||||
|       activity: "Activity" |       activity: "Activity" | ||||||
|       keywords: "Keywords" |       keywords: "Keywords" | ||||||
|  |       domains: "Domains" | ||||||
|       followers-you-know: "Followers you know" |       followers-you-know: "Followers you know" | ||||||
|       last-used-at: "Latest used at" |       last-used-at: "Latest used at" | ||||||
| 
 | 
 | ||||||
|  | @ -512,6 +513,9 @@ mobile: | ||||||
|     mk-user-overview-keywords: |     mk-user-overview-keywords: | ||||||
|       no-keywords: "No keywords" |       no-keywords: "No keywords" | ||||||
| 
 | 
 | ||||||
|  |     mk-user-overview-domains: | ||||||
|  |       no-domains: "No domains" | ||||||
|  | 
 | ||||||
|     mk-user-overview-followers-you-know: |     mk-user-overview-followers-you-know: | ||||||
|       loading: "Loading" |       loading: "Loading" | ||||||
|       no-users: "No users" |       no-users: "No users" | ||||||
|  |  | ||||||
|  | @ -498,6 +498,7 @@ mobile: | ||||||
|       images: "画像" |       images: "画像" | ||||||
|       activity: "アクティビティ" |       activity: "アクティビティ" | ||||||
|       keywords: "キーワード" |       keywords: "キーワード" | ||||||
|  |       domains: "頻出ドメイン" | ||||||
|       followers-you-know: "知り合いのフォロワー" |       followers-you-know: "知り合いのフォロワー" | ||||||
|       last-used-at: "最終ログイン" |       last-used-at: "最終ログイン" | ||||||
| 
 | 
 | ||||||
|  | @ -512,6 +513,9 @@ mobile: | ||||||
|     mk-user-overview-keywords: |     mk-user-overview-keywords: | ||||||
|       no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)" |       no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)" | ||||||
| 
 | 
 | ||||||
|  |     mk-user-overview-domains: | ||||||
|  |       no-domains: "よく表れるドメインは検出されませんでした" | ||||||
|  | 
 | ||||||
|     mk-user-overview-followers-you-know: |     mk-user-overview-followers-you-know: | ||||||
|       loading: "読み込み中" |       loading: "読み込み中" | ||||||
|       no-users: "知り合いのユーザーはいません" |       no-users: "知り合いのユーザーはいません" | ||||||
|  |  | ||||||
							
								
								
									
										120
									
								
								src/tools/analysis/extract-user-domains.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/tools/analysis/extract-user-domains.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,120 @@ | ||||||
|  | import * as URL from 'url'; | ||||||
|  | 
 | ||||||
|  | import Post from '../../api/models/post'; | ||||||
|  | import User from '../../api/models/user'; | ||||||
|  | import parse from '../../api/common/text'; | ||||||
|  | 
 | ||||||
|  | process.on('unhandledRejection', console.dir); | ||||||
|  | 
 | ||||||
|  | function tokenize(text: string) { | ||||||
|  | 	if (text == null) return []; | ||||||
|  | 
 | ||||||
|  | 	// パース
 | ||||||
|  | 	const ast = parse(text); | ||||||
|  | 
 | ||||||
|  | 	const domains = ast | ||||||
|  | 		// URLを抽出
 | ||||||
|  | 		.filter(t => t.type == 'url' || t.type == 'link') | ||||||
|  | 		.map(t => URL.parse(t.url).hostname); | ||||||
|  | 
 | ||||||
|  | 	return domains; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Fetch all users
 | ||||||
|  | User.find({}, { | ||||||
|  | 	fields: { | ||||||
|  | 		_id: true | ||||||
|  | 	} | ||||||
|  | }).then(users => { | ||||||
|  | 	let i = -1; | ||||||
|  | 
 | ||||||
|  | 	const x = cb => { | ||||||
|  | 		if (++i == users.length) return cb(); | ||||||
|  | 		extractDomainsOne(users[i]._id).then(() => x(cb), err => { | ||||||
|  | 			console.error(err); | ||||||
|  | 			setTimeout(() => { | ||||||
|  | 				i--; | ||||||
|  | 				x(cb); | ||||||
|  | 			}, 1000); | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	x(() => { | ||||||
|  | 		console.log('complete'); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function extractDomainsOne(id) { | ||||||
|  | 	return new Promise(async (resolve, reject) => { | ||||||
|  | 		process.stdout.write(`extracting domains of ${id} ...`); | ||||||
|  | 
 | ||||||
|  | 		// Fetch recent posts
 | ||||||
|  | 		const recentPosts = await Post.find({ | ||||||
|  | 			user_id: id, | ||||||
|  | 			text: { | ||||||
|  | 				$exists: true | ||||||
|  | 			} | ||||||
|  | 		}, { | ||||||
|  | 			sort: { | ||||||
|  | 				_id: -1 | ||||||
|  | 			}, | ||||||
|  | 			limit: 10000, | ||||||
|  | 			fields: { | ||||||
|  | 				_id: false, | ||||||
|  | 				text: true | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		// 投稿が少なかったら中断
 | ||||||
|  | 		if (recentPosts.length < 100) { | ||||||
|  | 			process.stdout.write(' >>> -\n'); | ||||||
|  | 			return resolve(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const domains = {}; | ||||||
|  | 
 | ||||||
|  | 		// Extract domains from recent posts
 | ||||||
|  | 		recentPosts.forEach(post => { | ||||||
|  | 			const domainsOfPost = tokenize(post.text); | ||||||
|  | 
 | ||||||
|  | 			domainsOfPost.forEach(domain => { | ||||||
|  | 				if (domains[domain]) { | ||||||
|  | 					domains[domain]++; | ||||||
|  | 				} else { | ||||||
|  | 					domains[domain] = 1; | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		// Calc peak
 | ||||||
|  | 		let peak = 0; | ||||||
|  | 		Object.keys(domains).forEach(domain => { | ||||||
|  | 			if (domains[domain] > peak) peak = domains[domain]; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		// Sort domains by frequency
 | ||||||
|  | 		const domainsSorted = Object.keys(domains).sort((a, b) => domains[b] - domains[a]); | ||||||
|  | 
 | ||||||
|  | 		// Lookup top 10 domains
 | ||||||
|  | 		const topDomains = domainsSorted.slice(0, 10); | ||||||
|  | 
 | ||||||
|  | 		process.stdout.write(' >>> ' + topDomains.join(', ') + '\n'); | ||||||
|  | 
 | ||||||
|  | 		// Make domains object (includes weights)
 | ||||||
|  | 		const domainsObj = topDomains.map(domain => ({ | ||||||
|  | 			domain: domain, | ||||||
|  | 			weight: domains[domain] / peak | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 		// Save
 | ||||||
|  | 		User.update({ _id: id }, { | ||||||
|  | 			$set: { | ||||||
|  | 				domains: domainsObj | ||||||
|  | 			} | ||||||
|  | 		}).then(() => { | ||||||
|  | 			resolve(); | ||||||
|  | 		}, err => { | ||||||
|  | 			reject(err); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | @ -240,6 +240,12 @@ | ||||||
| 			<mk-user-overview-keywords user={ user }/> | 			<mk-user-overview-keywords user={ user }/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</section> | 	</section> | ||||||
|  | 	<section class="domains"> | ||||||
|  | 		<h2><i class="fa fa-globe"></i>%i18n:mobile.tags.mk-user-overview.domains%</h2> | ||||||
|  | 		<div> | ||||||
|  | 			<mk-user-overview-domains user={ user }/> | ||||||
|  | 		</div> | ||||||
|  | 	</section> | ||||||
| 	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }> | 	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }> | ||||||
| 		<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2> | 		<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2> | ||||||
| 		<div> | 		<div> | ||||||
|  | @ -579,6 +585,40 @@ | ||||||
| 	</script> | 	</script> | ||||||
| </mk-user-overview-keywords> | </mk-user-overview-keywords> | ||||||
| 
 | 
 | ||||||
|  | <mk-user-overview-domains> | ||||||
|  | 	<div if={ user.domains != null && user.domains.length > 1 }> | ||||||
|  | 		<virtual each={ domain in user.domains }> | ||||||
|  | 			<a style="opacity: { 0.5 + (domain.weight / 2) }">{ domain.domain }</a> | ||||||
|  | 		</virtual> | ||||||
|  | 	</div> | ||||||
|  | 	<p class="empty" if={ user.domains == null || user.domains.length == 0 }>%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p> | ||||||
|  | 	<style> | ||||||
|  | 		:scope | ||||||
|  | 			display block | ||||||
|  | 
 | ||||||
|  | 			> div | ||||||
|  | 				padding 4px | ||||||
|  | 
 | ||||||
|  | 				> a | ||||||
|  | 					display inline-block | ||||||
|  | 					margin 4px | ||||||
|  | 					color #555 | ||||||
|  | 
 | ||||||
|  | 			> .empty | ||||||
|  | 				margin 0 | ||||||
|  | 				padding 16px | ||||||
|  | 				text-align center | ||||||
|  | 				color #aaa | ||||||
|  | 
 | ||||||
|  | 				> i | ||||||
|  | 					margin-right 4px | ||||||
|  | 
 | ||||||
|  | 	</style> | ||||||
|  | 	<script> | ||||||
|  | 		this.user = this.opts.user; | ||||||
|  | 	</script> | ||||||
|  | </mk-user-overview-domains> | ||||||
|  | 
 | ||||||
| <mk-user-overview-followers-you-know> | <mk-user-overview-followers-you-know> | ||||||
| 	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> | 	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> | ||||||
| 	<div if={ !initializing && users.length > 0 }> | 	<div if={ !initializing && users.length > 0 }> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue