Skip to content

Instantly share code, notes, and snippets.

@Getaji
Last active June 27, 2024 21:55
Show Gist options
  • Save Getaji/96412eee22fe960ba2dc to your computer and use it in GitHub Desktop.
Save Getaji/96412eee22fe960ba2dc to your computer and use it in GitHub Desktop.
なろう小説APIのPythonラッパー(開発中)
response = (narou4py.requester()
.word("百合")
.response_params(NovelInfoParams.TITLE,
NovelInfoParams.STORY,
NovelInfoParams.NOVEL_UPDATED_AT)
.request())
time_now = datetime.datetime.now()
for info in response:
print("「{title}」\n{desc}\n更新日時は {updated_at} です。前回の更新から{datetime_diff}日経過しています。".format(
title=info.title,
desc=info.story,
updated_at=info.novelupdated_at,
datetime_diff=(time_now - info.novelupdated_at).days))
"""
小説投稿サイト「小説家になろう」の小説情報を取得するモジュールです。
"""
import gzip
import json
import requests
from datetime import datetime
from enum import Enum
from json import decoder as json_decoder
from util.collections import merge_dict, frozen_dict
def override(func):
"""
継承した関数であることを示すマーカーデコレーター。
:param func: 関数
:return: 関数
"""
return func
default_params = frozen_dict(dict(
gzip=5,
out="json"
))
"""デフォルトで使用されるパラメータ。\n\
gzip圧縮を最大設定にしてjson出力にする。"""
def dict_value(dic: dict, key):
return dic.get(key, default=None)
class NovelInfo:
"""
小説の情報を表すデータモデルクラス。
JSONを受け取ってラッピングする。
"""
@override
def __init__(self, info: dict):
self.json = info
self.title = info["title"]
self.n_code = info["ncode"]
self.user_id = info["userid"]
self.writer = info["writer"]
self.story = info["story"]
self.genre = info["genre"]
self.orig = info["gensaku"]
self.keyword = info["keyword"]
self.general_first_up = info["general_firstup"]
# todo NovelInfoのフィールド列挙の続き
@override
def __getattr__(self, item):
if hasattr(self, item):
return super.__getattribute__(item)
try:
return self.json[item]
except KeyError:
raise AttributeError
@override
def __repr__(self):
return self.json.__repr__()
@override
def __str__(self):
return self.json.__str__()
class CustomJsonDecoder(json.JSONDecoder):
"""narou4py用のJSONデコーダー。
制御文字を許可し、日時要素をdatetimeインスタンスにする。
"""
@override
def __init__(self):
super().__init__(strict=False)
@staticmethod
def parse_datetime(dt_str: str, dt_format: str="%Y-%m-%d %H:%M:%S") -> datetime:
"""
文字列を指定した書式でフォーマットする。
デフォルトでは 2016-1-1 12:00:00のような形式になる。
:param dt_str: 文字列
:param dt_format: フォーマット書式
:return: datetime
"""
return datetime.strptime(dt_str, dt_format)
@staticmethod
def parse_elm_datetime(elm: dict, *names: [str]):
"""
辞書の指定したアイテムの値を日時型にする。
:param elm: 辞書
:param names: アイテムのキーのリスト
"""
for name in names:
if name in elm:
elm[name] = CustomJsonDecoder.parse_datetime(elm[name])
@override
def decode(self, s, _w=json_decoder.WHITESPACE.match):
result = []
for elm in super().decode(s, _w):
self.parse_elm_datetime(elm,
"general_firstup", "general_lastup",
"novelupdated_at", "updated_at")
result.append(elm)
return result
@override
def raw_decode(self, s, idx=0):
result = super().raw_decode(s, idx)
return result
def request(*params: [dict], include_all_count: bool=False,
return_json: bool=False, return_raw: bool=False) -> [NovelInfo] or [dict]:
"""
小説情報をリクエストします。
:param params: パラメーター辞書(複数指定可能)
:param include_all_count: all_countを含むか
:param return_json: JSONで返すか
:param return_raw: JSON変換前の文字列を返すか
:return: 結果
"""
params = merge_dict(default_params, *params)
if "r18" in params and params["r18"] is True:
endpoint = "http://api.syosetu.com/novel18api/api/"
del params["r18"]
else:
endpoint = "http://api.syosetu.com/novelapi/api/"
response = requests.get(endpoint, params=params)
res_content = gzip.decompress(response.content).decode("utf-8")
if return_raw:
return res_content
response_json = json.loads(res_content, cls=CustomJsonDecoder)
if return_json:
return response_json
return [NovelInfo(info_json) for info_json in (response_json if include_all_count else response_json[1:])]
class NovelType(Enum):
"""短編・連載中などの小説タイプの列挙。
"""
SHORT_STORY = "t" # 短編
NOW_SERIES = "r" # 連載中
COMPLETED_SERIES = "er" # 完結済連載小説
NOW_AND_COMPLETED_SERIES = "re" # すべての連載小説(連載中および完結済)
SHORT_STORY_AND_COMPLETED = "ter" # 短編と完結済連載小説
class NovelInfoParam:
"""小説情報のパラメータのデータモデルクラス。
"""
def __init__(self, res_name, req_name):
"""小説情報パラメータの生成。
:param res_name: レスポンス時のパラメータ名。
:param req_name: リクエスト時のパラメータ名。
"""
self.param_str = res_name
self.short_str = req_name
class NovelInfoParams(Enum):
"""小説情報のリクエスト・レスポンスに用いられるパラメータの列挙。
各要素の詳細は公式ドキュメントを参照されたし。
"""
TITLE = NovelInfoParam("title", "t") # 小説名
N_CODE = NovelInfoParam("ncode", "n") # Nコード
USER_ID = NovelInfoParam("userid", "u") # 作者のユーザID(数値)
WRITER = NovelInfoParam("writer", "w") # 作者名
STORY = NovelInfoParam("story", "s") # 小説のあらすじ
BIG_GENRE = NovelInfoParam("biggenre", "bg") # 大ジャンル
GENRE = NovelInfoParam("genre", "g") # ジャンル
KEYWORD = NovelInfoParam("keyword", "k") # キーワード
GENERAL_FIRST_UP = NovelInfoParam("general_firstup", "gf") # 初回掲載日
GENERAL_LAST_UP = NovelInfoParam("general_lastup", "gl") # 最終掲載日
NOVEL_TYPE = NovelInfoParam("noveltype", "nt") # 連載:1 短編:2
END = NovelInfoParam("end", "e") # 短編・完結済:0 連載中:1
GENERAL_ALL_NO = NovelInfoParam("general_all_no", "ga") # 全掲載話数
LENGTH = NovelInfoParam("length", "l") # 文字数(スペース改行除く)
TIME = NovelInfoParam("time", "ti") # 読了時間(分)
IS_STOP = NovelInfoParam("isstop", "i") # 長期連載中:1 それ以外:0
IS_R15 = NovelInfoParam("isr15", "ir") # R15:1 それ以外:0
IS_BL = NovelInfoParam("isbl", "ibl") # BL:1 それ以外:0
IS_GL = NovelInfoParam("isgl", "igl") # GL:1 それ以外:0
IS_ZANKOKU = NovelInfoParam("iszankoku", "izk") # 残酷:1 それ以外:0
IS_TENSEI = NovelInfoParam("istensei", "its") # 異世界転生:1 それ以外:0
IS_TENNI = NovelInfoParam("istenni", "its") # 異世界転移:1 それ以外:0
PC_OR_MOBILE = NovelInfoParam("pc_or_k", "p") # 1:携帯のみ 2:PCのみ 3:両方
GLOBAL_POINT = NovelInfoParam("global_point", "gp") # 総合得点
FAV_NOVEL_CNT = NovelInfoParam("fav_novel_cnt", "f") # ブックマーク数
REVIEW_CNT = NovelInfoParam("review_cnt", "r") # レビュー数
ALL_POINT = NovelInfoParam("all_point", "a") # 評価点
ALL_EVAL_CNT = NovelInfoParam("all_hyoka_cnt", "ah") # 評価者数
ARTWORK_CNT = NovelInfoParam("sasie_cnt", "sa") # 挿絵の数
CONVERSATION_RATE = NovelInfoParam("kaiwaritu", "ka") # 会話率
NOVEL_UPDATED_AT = NovelInfoParam("novelupdated_at", "nu") # 小説更新日時
UPDATED_AT = NovelInfoParam("updated_at", "ua") # 最終更新日時
class NovelsResponseOrder(Enum):
"""小説情報のレスポンスの順番の列挙。
"""
MOST_FAVORITE = "favnovelcnt" # ブックマーク数の多い順
MOST_REVIEW_CNT = "review_cnt" # レビュー数の多い順
HIGH_EVAL = "hyoka" # 総合評価の高い順
WORST_EVAL = "hyokaasc" # 総合評価の低い順
IMPRESSION_CNT = "impressioncnt" # 感想の多い順
EVAL_CNT_MANY = "hyokacnt" # 評価者数の多い順
EVAL_CNT_FEW = "hyokacntasc" # 評価者数の少ない順
WEEKLY_UNIQUE_USER = "weekly" # 週間ユニークユーザの多い順
DESC_LENGTH_MANY = "lengthdesc" # 小説本文の文字数が多い順
DESC_LENGTH_FEW = "lengthasc" # 小説本文の文字数が少ない順
N_CODE_NEW = "ncodedesc" # Nコードが新しい順
OLD = "old" # 古い順
Order = NovelsResponseOrder
BIG_GENRES = {
1: "恋愛",
2: "ファンタジー",
3: "文芸",
4: "SF",
99: "その他",
98: "ノンジャンル"
}
GENRES = {
101: "異世界〔恋愛〕",
102: "現実世界〔恋愛〕",
201: "ハイファンタジー〔ファンタジー〕",
202: "ローファンタジー〔ファンタジー〕",
301: "純文学〔文芸〕",
302: "ヒューマンドラマ〔文芸〕",
303: "歴史〔文芸〕",
304: "推理〔文芸〕",
305: "ホラー〔文芸〕",
306: "アクション〔文芸〕",
307: "コメディー〔文芸〕",
401: "VRゲーム〔SF〕",
402: "宇宙〔SF〕",
403: "空想科学〔SF〕",
404: "パニック〔SF〕",
9901: "童話〔その他〕",
9902: "詩〔その他〕",
9903: "エッセイ〔その他〕",
9904: "リプレイ〔その他〕",
9999: "その他〔その他〕",
9801: "ノンジャンル〔ノンジャンル〕"
}
class Requester:
"""
小説情報をリクエストするためのクラスです。
メソッド呼び出し形式でパラメータを構築することができます。
"""
def __init__(self, is_keep_param_order: bool=False):
self.queries = []
self.is_keep_param_order = is_keep_param_order
self.static_query = self._create_dict()
self.cache = None
# debug
self.__not_request = False
def clear(self):
"""パラメータをクリアします。
:return: self
"""
self.queries.clear()
self.static_query.clear()
return self
def _create_dict(self, dict_: dict=None):
"""辞書を生成する。内部用。
is_keep_param_orderがTrueなら順序を保持するOrderedDictのインスタンスを返す。
:param dict_: 元にする辞書
:return: 辞書
"""
if dict_ is None:
dict_ = {}
if self.is_keep_param_order:
import collections
return collections.OrderedDict(dict_)
return dict(dict_)
def set_keep_param_order(self, is_keep_param_order: bool=True):
"""パラメータの順序を保持するかを指定します。
:param is_keep_param_order: 順序を保持するか
:return: self
"""
self.is_keep_param_order = is_keep_param_order
self.static_query = self._create_dict(self.static_query)
return self
def r18(self):
"""R18小説をリクエストするようにします。
:return: self
"""
self.static_query["r18"] = True
return self
def param(self, key: str, value: object):
"""キーと値のペアをパラメータとして追加します。
:param key: キー
:param value: 値
:return:self
"""
self.static_query[key] = value
return self
def params(self, **params: dict):
"""辞書型オブジェクトの各要素をパラメータとして追加します。
:param params: 辞書型オブジェクト(複数指定可能)
:return: self
"""
self.queries.append(params)
return self
def word(self, *word: [str], targets: [str]=None):
"""単語検索パラメータを追加します。
:param word: 単語を指定します。空白で区切るとAND抽出になります。部分一致です。
:param targets: 抽出対象を文字列リストで指定します。titleはタイトル、exはあらすじ、keywordはキーワード、wnameは作者名です。未指定ですべてを対象にします。
:return: Requesterインスタンス
"""
self.param("word", " ".join(word))
if not targets:
self.queries.append({t: 1 for t in targets})
return self
def not_word(self, *not_word: [str], targets: [str]=None):
"""単語除外検索パラメータを追加します。
:param not_word: 除外する単語を指定します。空白で区切って複数指定できます。部分一致です。
:param targets: 抽出対象を文字列リストで指定します。titleはタイトル、exはあらすじ、keywordはキーワード、wnameは作者名です。未指定ですべてを対象にします。
:return: Requesterインスタンス
"""
self.param("notword", " ".join(not_word))
if not targets:
self.queries.append({t: 1 for t in targets})
return self
def big_genre(self, *genres: [int]):
"""大ジャンル検索パラメータを追加します。複数指定可能です。
:param genres: 大ジャンル
:return: Requesterインスタンス
"""
return self.param("biggenre", "-".join(genres))
def not_big_genre(self, *genres: [int]):
"""大ジャンル除外検索パラメータを追加します。
:param genres: ジャンル
:return: Requesterインスタンス
"""
return self.param("notbiggenre", "-".join(genres))
def genre(self, *genres: [int]):
"""ジャンル検索パラメータを追加します。複数指定可能です。
:param genres: ジャンル
:return: Requesterインスタンス
"""
return self.param("genre", "-".join(genres))
def not_genre(self, *genres: [int]):
"""ジャンル除外検索パラメータを追加します。
:param genres: ジャンル
:return: Requesterインスタンス
"""
return self.param("notgenre", "-".join(genres))
def user_id(self, *user_ids: [int]):
"""ユーザーID指定パラメータを追加します。複数指定可能です。
:param user_ids: ユーザーID
:return: Requesterインスタンス
"""
return self.param("userid", "-".join(user_ids))
def r15(self, is_not: bool=False):
"""登録必須キーワードに「R15」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: Requesterインスタンス
"""
pre = "not" if is_not else "is"
return self.param(pre + "r15", 1)
def boys_love(self, is_not: bool=False):
"""登録必須キーワードに「ボーイズラブ」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: self
"""
pre = "not" if is_not else "is"
return self.param(pre + "bl", 1)
def girls_love(self, is_not: bool=False):
"""登録必須キーワードに「ガールズラブ」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: self
"""
pre = "not" if is_not else "is"
return self.param(pre + "gl", 1)
def cruel(self, is_not: bool=False):
"""登録必須キーワードに「残酷な描写あり」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: self
"""
pre = "not" if is_not else "is"
return self.param(pre + "zankoku", 1)
def aw_reincarnation(self, is_not: bool=False):
"""登録必須キーワードに「異世界転生」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: self
"""
pre = "not" if is_not else "is"
return self.param(pre + "tensei", 1)
def aw_transition(self, is_not: bool=False):
"""登録必須キーワードに「異世界転移」が含まれている作品を抽出するパラメータを追加します。
:param is_not: Trueの場合除外指定になる
:return: self
"""
pre = "not" if is_not else "is"
return self.param(pre + "tenni", 1)
@staticmethod
def __create_range_param(min_val, max_val, val):
"""”最小値-最大値”のような書式のパラメータを作成する。valがNoneでない場合はvalを返す。
:param min_val: 最小値
:param max_val: 最大値
:param val: 値
:return: self
"""
if val is not None:
return val
param = None
if min_val is not None:
param = str(min_val) + "-"
if max_val is not None:
if param is None:
param = "-"
param += str(max_val)
return param
def text_length(self, min_count: int=None, max_count: int=None, count=None):
"""小説の文字数を指定するパラメータを追加します。
:param min_count: 最小文字数
:param max_count: 最大文字数
:param count: 文字数(指定されると最小値と最大値は無視される)
:return: self
"""
return self.param("length",
self.__create_range_param(min_count, max_count, count))
def conversation_rate(self, min_rate: int=None, max_rate: int=None, rate=None):
"""小説の会話率(%)を指定するパラメータを追加します。
:param min_rate: 最小会話率
:param max_rate: 最大会話率
:param rate: 文字数(指定されると最小値と最大値は無視される)
:return: self
"""
return self.param("kaiwaritu",
self.__create_range_param(min_rate, max_rate, rate))
def artwork_count(self, min_count: int=None, max_count: int=None, count=None):
"""小説の挿絵数を指定するパラメータを追加します。
:param min_count: 最小挿絵数
:param max_count: 最大挿絵数
:param count: 挿絵数(指定されると最小値と最大値は無視される)
:return: self
"""
return self.param("sasie",
self.__create_range_param(min_count, max_count, count))
def read_time(self, min_time: int=None, max_time: int=None, time=None):
"""小説の読了時間(分)を指定するパラメータを追加します。
:param min_time: 最小読了時間
:param max_time: 最大読了時間
:param time: 読了時間(指定されると最小値と最大値は無視される)
:return: self
"""
return self.param("time",
self.__create_range_param(min_time, max_time, time))
def n_code(self, *n_code: [str]):
"""Nコードを指定するパラメータを追加します。
:param n_code: Nコード(複数指定可能)
:return: self
"""
n_code = "-".join(n_code)
return self.param("ncode", n_code)
def type(self, novel_type: str or NovelType):
"""短編、連載中などの小説のタイプを指定するパラメータを追加します。
:param novel_type: 小説のタイプ
:return: self
"""
if type(novel_type) is NovelType:
novel_type = novel_type.value
return self.param("type", novel_type)
def writing_style(self, styles: [int]):
"""
文体を指定するパラメータを追加します。複数指定可能です。
1: 字下げなし/連続改行が多い 2: 字下げなし/平均的な改行数
4: 適切な字下げ/連続改行が多い 6: 適切な字下げ/平均的な改行数
:param styles: 文体
:return: self
"""
return self.param("buntai", "-".join(styles))
def stopping(self):
"""長期連載停止中の作品を指定するパラメータを追加します。
:return: self
"""
return self.param("stop", 2)
def not_stopping(self):
"""長期連載停止中の作品を除外するパラメータを追加します。
:return: self
"""
return self.param("stop", 1)
def pickup(self):
"""ピックアップ条件を満たす作品を指定するパラメータを追加します。
ピックアップ条件は「最終掲載日から60日以内」かつ「短編または完結済または10万文字以上の連載中」です。
:return: self
"""
return self.param("ispickup", 1)
def not_pickup(self):
"""ピックアップ条件を満たさない作品を指定するパラメータを追加します。
ピックアップ条件は「最終掲載日から60日以内」かつ「短編または完結済または10万文字以上の連載中」です。
:return: self
"""
return self.param("ispickup", 0)
def get_params(self) -> dict:
"""現在のパラメータを辞書型オブジェクトで返します。
返したパラメータに対する変更がRequesterパラメータに反映されるかは保証されていません。
:return: パラメータ
"""
return merge_dict(self.static_query, *self.queries)
def order(self, order: str or NovelsResponseOrder):
"""出力順序を指定するパラメータを追加します。指定しない場合は新着順になります。
:param order: 順序
:return: self
"""
if type(order) is NovelsResponseOrder:
order = order.value
return self.param("order", order)
def response_params(self, *params: [NovelInfoParam]):
"""レスポンス項目を指定します。
:param params: 項目(複数指定可能)
:return: self
"""
return self.param("of", "-".join(
[param.value.short_str for param in params]
))
def debug__not_request(self):
"""requestメソッドを呼び出した時にAPIを呼び出さずに空のリストを返すようにします。
:return:
"""
self.__not_request = True
return self
def request(self, cache: bool=False, consumer=None, return_self: bool=False,
return_json: bool=False, return_raw: bool=False) -> [NovelInfo] or [dict]:
"""構築したパラメータで小説情報をリクエストします。
:param cache: Trueの場合、小説情報をキャッシュします。
キャッシュしたデータはconsume_cacheメソッドかcache変数で参照できます。
:param consumer: None以外が渡された場合、小説情報を引数にして呼び出します。
:param return_self: Trueの場合selfを返します。Falseなら小説情報を返します。
:param return_json: JSONを返します。
:param return_raw: JSON変換前の文字列を返します。
:return: 小説情報のリスト
"""
if self.__not_request:
result = []
else:
result = request(self.static_query, *self.queries,
include_all_count=False, return_json=return_json,
return_raw=return_raw)
if cache:
self.cache = result
if consumer is not None:
consumer(result)
return self if return_self else result
def consume_cache(self, consumer):
consumer(self.cache)
return self
requester = Requester
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment