明日から Qiita Advent Calendar 2015 が始まります。
先日 @takoratta さんからアナウンスがあったように、なんと今年は Advent Calendar のランキングが発表されるようです!ランキングは、「購読者数」と「総ストック数」の二つでランク付けされるようです。せっかくのお祭りなので盛り上がって楽しみたいですね😃
そこで気になるのが Swift の二つのカレンダーの対決です。 Swift には現在、 @shimesaba さんの一人カレンダーを除いて、二つのカレンダーがあります。
普通に考えれば "その1" が有利なんですが、 "その1" が募集開始 3 時間ほどで埋まってしまったため、多くの人が "その1" から漏れてしまいました。そのため、 "その2" のメンバーもかなり充実しています。
たとえば、↓は Swift タグのストック数ランキング ですが、 1 位の @susieyy さん、 3 位の僕、 9 位の @g08m11 さんが "その2" に参加しているのに対して、 "その1" 参加者で TOP 10 入りしているのは 2 位の @mono0926 さん一人です。
- @susieyy (その2)
- @mono0926 (その1)
- @koher (その2)
- @nori0620 (不参加)
- @dankogai (不参加)
- @mochizukikotaro (不参加)
- @merrill (不参加)
- @Night___ (不参加)
- @g08m11 (その2)
- @cotrpepe (不参加)
「購読者数」では "その1" が 189 、 "その2" が 125 と大きく差を開けられていますが( 2015-11-28 03:43 時点)、「総ストック数」の勝敗はどちらに転ぶかわかりません。 特に初日の @mono0926 さんと @susieyy さんの、ランキング TOP 2 同士の直接対決は要注目です!
Qiita API + Swift スクリプトで戦力比較
それだけではおもしろくないので、 Qiita API を使って、それぞれのカレンダーの全参加者の情報を取得して戦力を分析してみました。
下記の意味でのサンプルとしてもご活用下さい。
- Swift によるスクリプト
- Swift による関数型プログラミング
- Swift の各種ライブラリ(下記)の利用
各種ライブラリは次のような用途で利用しています。
- Alamofire によるネットワークアクセス
- Fuzi による HTML のスクレイピング
- Argo による JSON のデコード
-
PromiseK による
Promise
を使った非同期処理 -
Runes の関数型プログラミングで使われる演算子(
>>-
,<^>
,<*>
など) - Curry によるカリー化
- Carthage によるライブラリ管理
基本的な処理の流れは次の通りです。
- カレンダーのページの HTML を Alamofire で取得( PromiseK で非同期処理を Promise 化)
- Fuzi で HTML から参加者のユーザー ID をスクレイピング
- Alamofire で Qiita API を叩いて各参加者の情報を取得し Argo, Runes, Curry でデコード
- 得られた情報を元に戦力を計算して出力
せっかくのなので、 "Swift" と "Swift その2" だけでなく、 Swift の最大のライバル Go についても調べました。
Go のカレンダーは Swift 以上のスピードで埋まり、現段階で "その3" まで埋まっています。 Swift と Go は近年生まれた言語の中で特に人気を博している二つですが、その設計思想は大きく異なります。僕の主観では、 Swift がこれまでの言語の進化を更に推し進めて、構文と型システムを洗練させることでシンプルにまとめた言語なら、 Go はこれまでの言語の進化を見直して、それほど多用しないものはばっさりと切り捨てることでシンプルにまとめた言語です。そんな二つの言語が共に人気なのはおもしろいです。
他にも、この投稿を書いている内に続々とカレンダーが埋まって来たので、次の言語についても計測してみました。
- Java
- Python
- C#
- Haskell
- Clojure
戦力の計算方法
戦力の計算は色々な方法が考えられますが、ここでは単純に各ユーザーの投稿の平均ストック数を求め、 25 人分の合計を使うことにします。
ただし、そのカレンダーのテーマと関係のない投稿は除外します。 Swift のカレンダーであれば Swift タグがついたものだけを対象として計算します。そうする理由は、たとえば、僕の場合 Swift 関連の投稿ではそれなりのストックがありますが、他の言語について書いたときに期待できるストック数は小さくなると考えられるなど、テーマによって期待できるストック数が異なるからです。
また、 Qiita API の関係で、一度のリクエストで 100 件までしかデータを取得できないので、(ページングは面倒ですし実質影響は小さいと思うので)最新の投稿 100 件のみを対象とします。
投稿数が 0 にユーザーについては計算ができないのでスコア上 0.0 として扱います。
なお、 Qiita API v2 ではストック数がとれなかったので Qiita API v1 を利用しています。
結果
カレンダー | スコア |
---|---|
Swift | 1042.62 |
Swift その2 | 911.093 |
Go | 522.824 |
Go その2 | 285.48 |
Go その3 | 252.452 |
Java | 256.362 |
Python | 342.092 |
C# | 103.255 |
Haskell | 161.258 |
Clojure | 305.476 |
( 2015-11-28 未明時点 )
"Swift" にはおよびませんでしたが、予想通り "Swift その2" もほとんど遜色ありません。ますます "Swift" と "Swift その2" の対決が楽しみです! "Swift" を購読してるけど "Swift その2" はまだ購読していないという人は、この機会にぜひ購読下さい!
Swift が Go より高めに出ているのは、 Swift の方が Qiita で書いている人が多いということが大きいと思います。 Qiita Advent Calendar は外部ブログからでも参加できるので、 Qiita を使っている人の割合が少ないと当然スコアも低くなります。 Java や C#, Haskell は投稿数 0 の人が多かったので、外部ブログ利用者が多いのだと思われます。
このスコアは言語の優劣をつけるためのものではなくて、あくまで Advent Calendar ランキングの順位予測のためのものです。外部ブログはランキングにカウントされないようなので、外部ブログ利用者が多いカレンダーでスコアが低くなるのは正しい結果だと言えます。
スコアを見る限りは Swift が優勢なように見えますが、実際の順位は一つの投稿がバズっただけで順位はひっくり返ります。どんな結果になるか楽しみですね!
詳細
スクリプト はすべてのユーザーのスコアを出力しますが、ここでは Swift と Go の上位三人を除いて伏せておきます。興味がある人はリンク先のリポジトリを clone してスクリプトを実行してみてください。次のようにコマンドライン引数を変えれば任意1のカレンダーについて戦力を計算できます。
# 引数は <カレンダー名> <タグ名>
swift -F Carthage/Build/Mac/ -I /usr/include/libxml2 main.swift go2 go
Swift
担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |
---|---|---|---|---|
12/1 | @mono0926 | 4921 | 27 | 182.259 |
12/18 | @tonkotsuboy_com | 439 | 3 | 146.333 |
12/23 | @k_kinukawa | 509 | 4 | 127.25 |
... | ||||
戦力 | 1042.62 |
Swift その2
担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |
---|---|---|---|---|
12/1 | @susieyy | 7380 | 30 | 246.0 |
12/10 | @kazu0620 | 589 | 4 | 147.25 |
12/7 | @koher | 3555 | 28 | 126.964 |
... | ||||
戦力 | 911.093 |
Go
担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |
---|---|---|---|---|
12/5 | @naoina | 198 | 1 | 198.0 |
12/1 | @tenntenn | 2176 | 33 | 65.9394 |
12/25 | @cubicdaiya | 405 | 10 | 40.5 |
... | ||||
戦力 | 522.824 |
Go その2
担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |
---|---|---|---|---|
12/17 | @mattn | 427 | 7 | 61.0 |
12/18 | @umisama | 841 | 17 | 49.4706 |
12/16 | @hogedigo | 155 | 4 | 38.75 |
... | ||||
戦力 | 285.48 |
Go その3
担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |
---|---|---|---|---|
12/9 | @masahikoofjoyto | 197 | 4 | 49.25 |
12/17 | @methane | 503 | 11 | 45.7273 |
12/24 | @shibukawa | 421 | 10 | 42.1 |
... | ||||
戦力 | 252.452 |
ソースコード
実行方法についての詳細は GitHub を御覧下さい。
エラーが発生した場合はエラーの理由は捨てて、 Optional
を使ってエラーが発生したことだけを検出しています。 PromiseK の Promise
はエラー処理を担当しないので、エラーはすべて Optional
に任せています。もし、エラー情報を保持したければ、 Result や Either を使うこともできます。
Promise
の非同期処理をチェーンして全体を一つの式にすることもできますが、可読性が悪くなりすぎるのであえて分割してます。
import Foundation
import Alamofire
import Argo
import Curry
import Fuzi
import PromiseK
import Runes
///// Alamofire を Promise 化するための拡張 /////
extension Request {
public func promisedResponse(queue queue: dispatch_queue_t? = nil)
-> Promise<(NSURLRequest?, NSHTTPURLResponse?, NSData?, NSError?)> {
return Promise { resolve in
self.response(queue: queue) { resolve(pure($0)) }
}
}
public func promisedResponseJSON(options options: NSJSONReadingOptions = .AllowFragments)
-> Promise<Response<AnyObject, NSError>> {
return Promise { resolve in
self.responseJSON(options: options) { resolve(pure($0)) }
}
}
}
///// 非同期処理中にプログラムが終了してしまわないように Promise を同期的に待たせるための拡張 /////
extension Promise {
func wait() {
var finished = false
self.flatMap { (value: T) -> Promise<()> in
finished = true
return Promise<()>()
}
while (!finished){
NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.1))
}
}
}
///// このスクリプトで利用するデータ型 /////
// 投稿
struct Item: Decodable {
let tags: [String]
let stockCount: Int
static func decode(j: JSON) -> Decoded<Item> { // Argo のデコード用
let tags: Decoded<[String]> = (j <|| "tags")
.flatMap { tagJsons in sequence(tagJsons.map { $0 <| "url_name" }) }
return curry(Item.init)
<^> tags
<*> j <| "stock_count"
}
}
// ユーザー
struct User {
let id: String
let items: [Item]
var stockCount: Int {
return items.reduce(0) { $0 + $1.stockCount } // 各投稿のストック数の合計
}
var score: Float {
return items.count == 0 ? 0.0 : Float(stockCount) / Float(items.count)
}
}
///// 処理の本体 /////
// コマンドライン引数を取得
let calendarName = Process.arguments[1]
let tag = Process.arguments[2]
// 1. カレンダーのページの HTML を Alamofire で取得
let html: Promise<String?> = Alamofire.request(Method.GET, "http://qiita.com/advent-calendar/2015/\(calendarName)")
.promisedResponse().map { response in
switch response {
case let (_, _, .Some(data), _):
return NSString(data: data, encoding: NSUTF8StringEncoding).map { $0 as String }
default:
return nil
}
}
// 2. Fuzi で HTML から参加者のユーザー ID をスクレイピング
let userIds: Promise<[String]?> = html.map {
$0.flatMap { html in // nil でない場合
// HTML 文字列を XMLDocument に変換
try? XMLDocument(string: html)
// XMLDocument から CSS セレクタで要素を取得
}?.css(".adventCalendarCalendar_day .adventCalendarCalendar_author a")
// ユーザー ID を含んだ href 属性を取得
.map { $0.attributes["href"]! }
// 1 文字目の "/" を除去してユーザー ID に変換
.map { $0[$0.startIndex.successor()..<$0.endIndex] }
}
// 3. Alamofire で Qiita API を叩いて各参加者の情報を取得し Argo でデコード
let users: Promise<[User]?> = userIds >>- { $0.map { userIds in // nil でない場合
// ユーザーの投稿を一人ずつダウンロード
userIds.reduce(Promise([])) { users, userId in
users >>- { usersOrNil -> Promise<[User]?> in
Alamofire.request(Method.GET, "https://qiita.com/api/v1/users/\(userId)/items", parameters: ["per_page": 100])
// Qiita API でユーザーの投稿一覧を取得
.promisedResponseJSON().map { response in
let userOrNil: [User]? = response.result.value
// JSON をデコードして [Item] を取得
.flatMap { decode($0) }
// [Item] から指定したタグを含まないものを除去
.map { items in items.filter { $0.tags.contains(tag) } }
// [Item] を User に変換し、連結するために [User] に変換
.map { items in [User(id: userId, items: items)] }
// ダウンロード済みの [User] と連結
return curry(+) <^> usersOrNil <*> userOrNil
}
}
}
} }
// 4. 得られた情報を元に戦力を計算して出力
let end: Promise<()> = users.map {
if let users = $0 {
print("| 担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |")
print("|:---|:--:|---:|---:|---:|")
zip(1...25, users).forEach { date, user in
print("| 12/\(date) | @\(user.id) | \(user.stockCount) | \(user.items.count) | \(user.score) |")
}
let score = users.reduce(0.0) { $0 + $1.score }
print("| 戦力 | | | | \(score) |")
} else {
print("エラーが発生しました。")
}
}
// 非同期処理が完了するまで待機
end.wait()
まとめ
Qiita API と Swift を使ってカレンダーごとの予測ストック数を計算してみました。
ストック数はあくまでひとつの目安にすぎないので、それで言語やカレンダーの優劣が決まるようなものではありません。それでも、今年は公式に「総ストック数」のランキングが作られるということですし、せっかくのお祭りに乗っかって楽しみましょう!
-
ただし、 25 人そろっていないカレンダーは未対応です。一応動きますが、出力の「担当日」がずれます。 ↩