Last active
June 27, 2024 21:55
-
-
Save Getaji/96412eee22fe960ba2dc to your computer and use it in GitHub Desktop.
なろう小説APIのPythonラッパー(開発中)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
小説投稿サイト「小説家になろう」の小説情報を取得するモジュールです。 | |
""" | |
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