Archive for the 'GZIP' Category

2017/10/10 なろう小説APIで各カテゴリごとの集計を実行

「小説家になろう」サイトのAPI「なろう小説API」を呼び出して、カテゴリごとの該当件数を集計するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。

本AppleScriptは、「小説家になろう」掲載作品を、大カテゴリと小カテゴリでコードを指定して、ループで存在件数の集計を行います。カテゴリごとに分布が偏っているようなので、該当件数が0件のカテゴリは結果出力しないようにしています。筆者の環境では集計に22〜25秒ぐらいかかっています(インターネット接続回線速度に依存)。

http headerにgzip転送リクエスト要求を書きつつ、実際のデータ自体もgzipで圧縮されているので、二重に圧縮している状態です。実測したところ、http headerでgzip指定を行なったほうがトータルで1秒程度速かったので「そんなもんかな」と思いつつ、そのままにしています。

Web APIからのデータ受信時のNSDataからのZip展開にオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用しています。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

ジャンルは数値で指定するようになっていますが、その数値が何を示しているかという情報はAPI側からの出力にはないので、Webサイト上から文字情報をコピペで取得し、AppleScript内に記載して(ハードコーディング)カテゴリコードリストと照合して出力しています。

実際に集計してみると、ノンカテゴリが53%ということで、カテゴリ分けの機能が有効に活用されていないことが見てとれます。

そのことについては運営側も重々承知しているようで、APIの検索オプションに「キーワードに異世界転生があるものを含む」といったものがあるなど、ジャンルよりもキーワード重視するようにしているようです。

そういいつつも、使われているキーワードについては若干の表記ゆらぎがあるようで、単純にこのオプションを指定しても「異世界転生もの」をすべて抽出できていないように見えます。キーワード自体にどの程度「表記揺れ」が存在しているのかを調べてみるとよいのかもしれません。

APIの仕様上、2,000件しか詳細データを取得できないように見えるので、そのあたりがちょっと気になります(どうも全数調査を行いにくい仕様)。

分析するまでもなく、異世界転生ものが多く、ノンジャンル作品でも異世界転生ものばっかりという印象です。掘り出しもので「ソ連の宇宙技術は最強過ぎたのだが、それを西側諸国が完全に理解したのはつい最近だった」という作品に行き当たり、これが強烈に面白いです。

AppleScript名:なろう小説APIで各カテゴリごとの集計を実行
– Created 2017-10-10 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4891
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSNumberFormatterRoundUp : a reference to current application’s NSNumberFormatterRoundUp
property NSNumberFormatterRoundDown : a reference to current application’s NSNumberFormatterRoundDown

set invList to {}

set bgList to {1, 2, 3, 4, 99, 98}
set bigGnereLabel to {“恋愛”, “ファンタジー”, “文芸”, “SF”, “その他”, “ノンジャンル”}

set gList to {101, 102, 201, 202, 301, 302, 303, 304, 305, 306, 307, 401, 402, 403, 404, 9901, 9902, 9903, 9904, 9999, 9801}
set smlGenreLabel to {“異世界〔恋愛〕”, “現実世界〔恋愛〕”, “ハイファンタジー〔ファンタジー〕”, “ローファンタジー〔ファンタジー〕”, “純文学〔文芸〕”, “ヒューマンドラマ〔文芸〕”, “歴史〔文芸〕”, “推理〔文芸〕”, “ホラー〔文芸〕”, “アクション〔文芸〕”, “コメディー〔文芸〕”, “VRゲーム〔SF〕”, “宇宙〔SF〕”, “空想科学〔SF〕”, “パニック〔SF〕”, “童話〔その他〕”, “詩〔その他〕”, “エッセイ〔その他〕”, “リプレイ〔その他〕”, “その他〔その他〕”, “ノンジャンル〔ノンジャンル〕”}

–全体の件数取得
set aRec to {gzip:“5″, out:“json”, lim:“1″}
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
set wholeCount to (allCount of first item of aRESTres)

–カテゴリごとの集計
repeat with i in bgList
  repeat with ii in gList
    set aRec to {gzip:“5″, biggenre:i as string, genre:ii as string, out:“json”, lim:“1″}
    
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
    
set aTotal to allCount of first item of aRESTres
    
    
if aTotal is not equal to 0 then
      set big to contents of i
      
set small to contents of ii
      
set bigLabel to getLabelFromNum(bgList, bigGnereLabel, big) of me
      
set smlLabel to getLabelFromNum(gList, smlGenreLabel, small) of me
      
set aPerCentatge to roundingDownNumStr(((aTotal / wholeCount) * 100), 1) of me
      
set the end of invList to {biggenre:bigLabel, genre:smlLabel, totalNum:aTotal, percentage:aPerCentatge}
    end if
  end repeat
end repeat

set bList to sortRecListByLabel(invList, “totalNum”, false) of me –降順ソート
–> {{totalNum:274072, biggenre:”ノンジャンル”, percentage:53.1, genre:”ノンジャンル〔ノンジャンル〕”}, {totalNum:47121, biggenre:”ファンタジー”, percentage:9.1, genre:”ハイファンタジー〔ファンタジー〕”}, {totalNum:28883, biggenre:”恋愛”, percentage:5.6, genre:”現実世界〔恋愛〕”}, {totalNum:23217, biggenre:”文芸”, percentage:4.5, genre:”ヒューマンドラマ〔文芸〕”}, {totalNum:21320, biggenre:”ファンタジー”, percentage:4.1, genre:”ローファンタジー〔ファンタジー〕”}, {totalNum:17079, biggenre:”恋愛”, percentage:3.3, genre:”異世界〔恋愛〕”}, {totalNum:16798, biggenre:”その他”, percentage:3.2, genre:”その他〔その他〕”}, {totalNum:13892, biggenre:”その他”, percentage:2.6, genre:”詩〔その他〕”}, {totalNum:13341, biggenre:”文芸”, percentage:2.5, genre:”コメディー〔文芸〕”}, {totalNum:10120, biggenre:”文芸”, percentage:1.9, genre:”ホラー〔文芸〕”}, {totalNum:9502, biggenre:”その他”, percentage:1.8, genre:”エッセイ〔その他〕”}, {totalNum:8486, biggenre:”文芸”, percentage:1.6, genre:”純文学〔文芸〕”}, {totalNum:7211, biggenre:”文芸”, percentage:1.3, genre:”アクション〔文芸〕”}, {totalNum:6199, biggenre:”SF”, percentage:1.2, genre:”空想科学〔SF〕”}, {totalNum:5780, biggenre:”その他”, percentage:1.1, genre:”童話〔その他〕”}, {totalNum:3295, biggenre:”文芸”, percentage:0.6, genre:”推理〔文芸〕”}, {totalNum:3217, biggenre:”文芸”, percentage:0.6, genre:”歴史〔文芸〕”}, {totalNum:2606, biggenre:”SF”, percentage:0.5, genre:”VRゲーム〔SF〕”}, {totalNum:1471, biggenre:”SF”, percentage:0.2, genre:”パニック〔SF〕”}, {totalNum:1454, biggenre:”SF”, percentage:0.2, genre:”宇宙〔SF〕”}, {totalNum:190, biggenre:”その他”, percentage:0.0, genre:”リプレイ〔その他〕”}}

on callNarouAPI(aRec, callFrom, callNum)
  set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
  
  
–set aRec to {gzip:”5″, |st|:callFrom as string, out:”json”, lim:callNum as string}
  
set aURL to retURLwithParams(reqURLStr, aRec) of me
  
set aRes to callRestGETAPIAndParseResults(aURL) of me
  
  
set aRESCode to (responseCode of aRes) as integer
  
if aRESCode is not equal to 200 then return false
  
  
set aRESHeader to responseHeader of aRes
  
set aRESTres to (json of aRes) as list
  
end callNarouAPI

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList as list, aLabelStr as string, ascendF as boolean)
  set aArray to NSArray’s arrayWithArray:aRecList
  
  
set sortDesc to NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
  
set bList to sortedArray as list
  
return bList
end sortRecListByLabel

on getLabelFromNum(aList, labelLIst, aNum)
  set aInd to offsetOf(aList, aNum) of me
  
set anItem to contents of item aInd of labelLIst
  
return anItem
end getLabelFromNum

on offsetOf(aList as list, aTarg)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set aIndex to aArray’s indexOfObjectIdenticalTo:aTarg
  
return (aIndex + 1)
end offsetOf

on roundingDownNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundDown)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingDownNumStr

on roundingUpNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundUp)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingUpNumStr

★Click Here to Open This Script