diff --git a/README.md b/README.md index f31feaf..e6fab39 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ ```diff + 🚀 真白萌小说站 https://masiro.me 已经得到支持 🚀 ++ 🚩 轻小说文库 https://www.wenku8.net/login.php 已经得到支持 🚩 ``` # linovelib2epub -Crawl light novel from [哔哩轻小说(linovelib)](https://w.linovelib.com/) and convert to epub. +Crawl light novel from [哔哩轻小说 (linovelib)](https://w.linovelib.com/) and convert to epub. [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg?style=flat)](https://github.com/pypa/hatch) [![flake8](https://img.shields.io/badge/linter-flake8-brightgreen)](https://github.com/PyCQA/flake8) @@ -36,25 +37,27 @@ Crawl light novel from [哔哩轻小说(linovelib)](https://w.linovelib.com/) an ## Supported Websites (plan) -| 序号 | 网站名称 | 语言 | 爬虫难度 | 支持进度 | 备注 | 技术难点 | -|-----|----------------------------------------------|-----|------|-------------------------------------|------------------------|--------------------------------| -| 1 | [哔哩轻小说(Mobile)](https://w.linovelib.com/) | 简/繁 | 中😰 | | `不用登录` `一章多页` | `JS加密` `章节链接破损` `Cloudflare保护` | -| 2 | ~~[哔哩轻小说(Web)](https://www.linovelib.com/)~~ | 简/繁 | 中😰 | 🚫 | 资源同Mobile,没必要。 | N/A | -| 3 | ~~[轻之国度](https://www.lightnovel.us/)~~ | 简/繁 | 高🤣 | 🚫 | `需要登录` | `轻币门槛` `导航混乱` | -| 4 | [无限轻小说](https://www.8novel.com/) | 繁 | 中😰 | ? | `不用登录` `一章多页` || -| 5 | [轻小说文库](https://www.wenku8.net/) | 简/繁 | 中😰 | WIP | `不用登录` `一章一页` || -| 6 | ~~[轻小说百科](https://lnovel.org/)~~ | 简/繁 | 低😆 | 🚫 | `不用登录` `一章一页` `插图清晰度低` | N/A | -| 7 | [真白萌](https://masiro.me/admin/novels ) | 简/繁 | 中😰 | | `一章一页` | `需要登录` `积分购买` `等级限制` | +| 序号 | 网站名称 | 语言 | 爬虫难度 | 支持进度 | 备注 | 技术难点 | +|-----|----------------------------------------------|-------|------|-------------------------------------|------------------------------|--------------------------------------| +| 1 | [哔哩轻小说(Mobile)](https://w.linovelib.com/) | 简 / 繁 | 中😰 | | ` 不用登录 ` ` 一章多页 ` | `JS 加密 ` ` 章节链接破损 ` `Cloudflare 保护 ` | +| 2 | ~~[哔哩轻小说(Web)](https://www.linovelib.com/)~~ | 简 / 繁 | 中😰 | 🚫 | 资源同 Mobile,没必要。 | N/A | +| 3 | ~~[轻之国度](https://www.lightnovel.us/)~~ | 简 / 繁 | 高🤣 | 🚫 | ` 需要登录 ` | ` 轻币门槛 ` ` 导航混乱 ` | +| 4 | [无限轻小说](https://www.8novel.com/) | 繁 | 中😰 | ? | ` 不用登录 ` ` 一章多页 ` || +| 5 | [轻小说文库](https://www.wenku8.net/) | 简 / 繁 | 中😰 | | ` 不用登录 ` ` 一章一页 ` || +| 6 | ~~[轻小说百科](https://lnovel.org/)~~ | 简 / 繁 | 低😆 | 🚫 | ` 不用登录 ` ` 一章一页 ` ` 插图清晰度低 ` | N/A | +| 7 | [真白萌](https://masiro.me/admin/novels) | 简 / 繁 | 中😰 | | ` 一章一页 ` | ` 需要登录 ` ` 积分购买 ` ` 等级限制 ` | > 爬虫友好度有两个重要指标: -> - 1.访问门槛。是否需要登陆以及积分。 -> - 2.页面结构。一个章节多页渲染的视为中等难度。 +> - +1. 访问门槛。是否需要登陆以及积分。 +> - +2. 页面结构。一个章节多页渲染的视为中等难度。 -优质的轻小说目标源标准:资源丰富,更新迅速,插图清晰,爬虫门槛合理。可以在issue发起补充。 +优质的轻小说目标源标准:资源丰富,更新迅速,插图清晰,爬虫门槛合理。可以在 issue 发起补充。 其他代码参考: -- 真白萌/轻之国度/百合会旧站参考:https://github.com/ilusrdbb/lightnovel-pydownloader +- 真白萌 / 轻之国度 / 百合会旧站参考:https://github.com/ilusrdbb/lightnovel-pydownloader - bilinovel/wenku8 参考:https://github.com/Montaro2017/bili_novel_packer ## Installation @@ -93,7 +96,7 @@ python -m pip install -e . ### install from pypi -> 注意: 由于爬虫程序对时效非常敏感,而pypi发布的版本一般是滞后的,因此不推荐这种安装方式。 +> 注意: 由于爬虫程序对时效非常敏感,而 pypi 发布的版本一般是滞后的,因此不推荐这种安装方式。 1. Install this package from pypi: @@ -173,26 +176,40 @@ MASIRO_LOGIN_PASSWORD = '' - 如果当前挑选的所有章节都是免费积分,或者你之前已经全部购买过,那么程序会直接往下执行。 - 如果当前挑选的所有章节存在需要积分购买的情况,程序会再次提示,要求做出选择,此时可以选择退出或者选择继续。 +### Wenku8 + +> target site: https://www.wenku8.net + +```python +from linovelib2epub import Linovelib2Epub, TargetSite + +if __name__ == '__main__': + linovelib_epub = Linovelib2Epub(book_id=2961, target_site=TargetSite.WENKU8) + linovelib_epub.run() +``` + +Don't need login, no threshold. + ## Options -| Parameters | type | required | default | description | -|-------------------------|---------|----------|-------------------------------|-------------------------------------------------------------------------------| -| book_id | number | YES | None | 书籍ID。 | -| target_site | Enum | NO | `TargetSite.LINOVELIB_MOBILE` | 参阅 TargetSite python 枚举类。当前可用值: TargetSite.LINOVELIB_MOBILE、TargetSite.MASIRO | -| divide_volume | boolean | NO | False | 是否分卷 | -| select_volume_mode | boolean | NO | False | 选择卷模式,它为True时 divide_volume 强制为True。 | -| has_illustration | boolean | NO | True | 是否下载插图 | -| image_download_folder | string | NO | "novel_images" | 图片下载临时文件夹. 不允许以相对路径../开头。 | -| pickle_temp_folder | string | NO | "pickle" | pickle临时数据保存的文件夹。 | -| clean_artifacts | boolean | NO | True | 是否删除临时数据/工件,指的是pickle和下载的图片文件。 | -| http_timeout | number | NO | 10 | 一个HTTP请求的超时等待时间(秒)。代表connect和read timeout。 | -| http_retries | number | NO | 5 | 当一个HTTP请求失败后,重试的最大次数。 | -| http_cookie | string | NO | '' | 自定义HTTP cookie。 | -| custom_style_cover | string | NO | '' | 自定义cover.xhtml的样式 | -| custom_style_nav | string | NO | '' | 自定义nav.xhtml的样式 | -| custom_style_chapter | string | NO | '' | 自定义每章(?.xhtml)的样式 | -| disable_proxy | boolean | NO | True | 是否禁用所在的代理环境,默认禁用 | -| image_download_strategy | string | NO | 'ASYNCIO' | 枚举值:"ASYNCIO"、"MULTIPROCESSING"、"MULTITHREADING"(未实现) | +| Parameters | type | required | default | description | +|-------------------------|---------|----------|-------------------------------|-------------------------------------------------------| +| book_id | number | YES | None | 书籍 ID。 | +| target_site | Enum | NO | `TargetSite.LINOVELIB_MOBILE` | 其他可用值参阅 TargetSite python 枚举类以及使用文档 | +| divide_volume | boolean | NO | False | 是否分卷 | +| select_volume_mode | boolean | NO | False | 选择卷模式,它为 True 时 divide_volume 强制为 True。 | +| has_illustration | boolean | NO | True | 是否下载插图 | +| image_download_folder | string | NO | "novel_images" | 图片下载临时文件夹. 不允许以相对路径../ 开头。 | +| pickle_temp_folder | string | NO | "pickle" | pickle 临时数据保存的文件夹。 | +| clean_artifacts | boolean | NO | True | 是否删除临时数据 / 工件,指的是 pickle 和下载的图片文件。 | +| http_timeout | number | NO | 10 | 一个 HTTP 请求的超时等待时间 (秒)。代表 connect 和 read timeout。 | +| http_retries | number | NO | 5 | 当一个 HTTP 请求失败后,重试的最大次数。 | +| http_cookie | string | NO | '' | 自定义 HTTP cookie。 | +| custom_style_cover | string | NO | '' | 自定义 cover.xhtml 的样式 | +| custom_style_nav | string | NO | '' | 自定义 nav.xhtml 的样式 | +| custom_style_chapter | string | NO | '' | 自定义每章 (?.xhtml) 的样式 | +| disable_proxy | boolean | NO | True | 是否禁用所在的代理环境,默认禁用 | +| image_download_strategy | string | NO | 'ASYNCIO' | 枚举值:"ASYNCIO"、"MULTIPROCESSING"、"MULTITHREADING"(未实现) | ## Todo diff --git a/analyze/wenku8/2379.html b/analyze/wenku8/2379.html new file mode 100644 index 0000000..d73c8fb --- /dev/null +++ b/analyze/wenku8/2379.html @@ -0,0 +1,596 @@ + + + + + 暴食狂战士 唯有我突破了所谓「等级」的概念 - 一色一凛 - 其他文库 - 轻小说文库 + + + + + + + + + + + + + +
+
 轻小说文库欢迎您,wdpm [退出登录]
+ + +
+ +
+ +
+ +
+
+ + +
+
+ + +
+ + +
+
+
+ + + + + + + + + + + + + + +
+ + + + + +
暴食狂战士 唯有我突破了所谓「等级」的概念[推一下!][举报/报错]  +
+
文库分类:其他文库小说作者:一色一凛文章状态:连载中最后更新:2023-09-11全文长度:193373字
+ +
+ +
+ + + + + + +
+ +
本作已动画化(含OVA/剧场版)
+
+ + 作品Tags:奇幻 异能 战斗 恋爱 龙傲天
+ 作品热度:C级,当前热度上升指数为:A级

+ 最近章节:
第二卷 插图

+ + + 内容简介:
  在技能的优劣决定一切的世界,担任城门守卫的斐特因为自己持有的技能《暴食》是个只会让肚子变饿的没用能力,而被迫过著最低阶的生活。不过在他偶然解决掉侵入城内的盗贼后,世界彻底改变。没错,斐特持有的技能《暴食》,其实隐藏著能夺取杀害对象的技能和能力值的惊人力量。就这样,原本只能像只蝼蚁苟活下去的斐特,命运悄悄地,也迅速地开始转动——
+
+ +
+
+ + +
+
+ +
+阅读 + +
+ +
+收藏 + +
+ +
+支持 + +
+ +
+专栏 + +
+
+
+ +
+ + +
+
+ 《暴食狂战士 唯有我突破了所谓「等级」的概念》小说TXT、UMD、JAR电子书下载 + + + + +
+
+ + +
+
+ +
+ + + + + + + +






+
+ +





+
+ +
+ +
+ +
+ +
+
用户登录
+
+
+ + +
    + +
  • wdpm
  • + +
  • 普通会员
  • +
  • 新手上路
  • + +
+ +
+ + +
+
+
+ + +
+
我的书架
+
+ + + + + + +
文章名称最新章节
+ +
+
+ + +
+
小说搜索
+
+ +
+
    +
  • 条  件:
  • +
  • 关键字: + +
  • +
  •        +
  • +
+
+ +
+
+ +
+
轻小说大赏
+
+ +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
《暴食狂战士 唯有我突破了所谓「等级」的概念》吐槽吧,不吐不快![查看全部吐槽] +
+ 如果你发现本小说更新滞后(请附上新资源链接或网盘),或非版权下架而缺章少节、无法下载的情况,请立即通知管理员,我们将尽快跟进。
+
[顶]欢迎加入文库千人Q群:56102032、854859149、385413858 + 回复(-)/查看(-)轻小说文库2010-05-09
[顶]欢迎海内外用户或无QQ的用户加入文库Telegram用户群回复(-)/查看(-)轻小说文库2022-06-08
[顶]小说资源交流专贴(好书齐分享)回复(-)/查看(-)轻小说文库2022-07-19
战力膨胀过头了吧回复(0)/查看(73)天书奇谭10-17 18:12
這是七龍珠吧,套路超像回复(2)/查看(74)doraemon178810-17 09:55
剧情挺无聊的,,,回复(0)/查看(242)克拉丽丝『梦幻蕾亚』10-07 19:17
用AI翻译了web回复(1)/查看(338)overtake10-05 18:04
自己跟B站教程做的epub链接:https://pan.baidu.com/s/1gKC910... + 回复(0)/查看(257)zyktdsb10-04 13:26
动漫第一集刚看完,感觉跟流水线的厕纸一样,有没有看过的简单...回复(0)/查看(491)ctropl10-02 21:22
小說翻譯挺慢的回复(0)/查看(232)chongxi09-15 19:39
这小说我记得几年前就出了回复(1)/查看(511)yy566709-14 10:51
日式魔幻世界爽吃?回复(0)/查看(252)MED墨杜萨09-13 11:37
看简介可以拿来批判资本家回复(2)/查看(470)ricardo33109-12 23:42
+ +
+ + +
+ + + + + + + + + + + + + + +
发表书评:
标题
内容

发帖+1积分 + 严禁恶意灌水刷分
违者积分清零或锁定账号
请勿发表攻击性言论
严禁任何涉政治帖子 + 删无赦
剧透建议在标题处标明[剧透] +
+ +
  +
+
+ +
+
+
+
+ +
+ + +
+ 轻小说文库所有内容均收集自其他网站,本站不参与组织扫图、翻译、录入等工作。
网站仅为写作爱好者及日语翻译学习交流提供试阅,如果你喜欢该作品,请联系相关出版机构购买正版!
+ + Copyright (c) 2015 www.wenku8.com. All rights reserved.
+ + + + + + + + + +
+ + \ No newline at end of file diff --git a/analyze/wenku8/chapter_sample.html b/analyze/wenku8/chapter_sample.html new file mode 100644 index 0000000..799ca2c --- /dev/null +++ b/analyze/wenku8/chapter_sample.html @@ -0,0 +1,802 @@ + + + + + 第一卷 第6话 往哈特家的后门-暴食狂战士 唯有我突破了所谓「等级」的概念-其他文库-轻小说文库 + + + + + + + + +
+ +
+
+ + +
+ + +
+ +
第一卷 第6话 往哈特家的后门
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
    本文来自 轻小说文库(http://www.wenku8.com)
+     我一回到王都圣法特,就为了换取打倒魔物的赏金来到兑换所。
+
+     只见壮硕的武人们你推我挤,偶尔还听见怒骂声传来。似乎是为了交换的条件和柜台人员起了争执。
+
+     感觉被那种人缠上会很麻烦。我畏缩起身子排进队伍中。
+
+     前方一名体格壮硕的男子转过头来,从头到脚仔细打量了我后,不屑地哼笑一声。似乎是看了我这身打扮,以为我是小队中打杂的喽啰吧。
+
+     这对现在的我而言反而正好。
+
+     就算我交给柜台人员大量魔物器官,也只会被认为是来帮忙办杂事的吧,不会被多问什么。相信即使看到我这次带来的38对哥布林耳朵,也不会被吓到才对。
+
+     「下一位,请。」
+
+     喔,轮到我了。我把放在地板的麻布袋放到柜台上。由于只是个小袋子,已经被哥布林耳朵塞得鼓鼓。
+
+     「让我看看……哇,猎了好多呢。是组大型队伍去狩猎吗?」
+
+     「咦、啊,没错,大家一起努力卯起劲来猎呢……是真的喔。」
+
+     我拚命在脑海中思考根本不存在的虚构队伍,回应柜台人员。多么空虚啊我……竟然得编造虚构队伍的丰功伟业。
+
+     『笑死人啦。』
+
+     「烦耶!」
+
+     糟糕,听不见格里德声音的柜台人员以困惑的眼神看著我。这也难怪,毕竟我和他说著说著却突然被嫌烦。虽然我是在对格里德说,柜台人员肯定觉得是自己被骂吧。
+
+     「对不起,什么事都没有。」
+
+     我努力陪笑,勉强渡过难关──大概啦。
+
+     走出兑换所的我松了口气。在和柜台人员聊天的过程中,我得知大部分的狩猎都顶多一天杀十只左右。理由是若持续猎杀同种类的魔物,将会累积一种称为「仇恨」的恨意,变得更容易被魔物盯上。
+
+     这么说来,我刚才猎哥布林的时候,它们越到后来越视我为弒亲仇敌般……难怪啊。
+
+     看来之后换钱要配合其他武人,最多带十只的量来换就好,其他的就放弃吧。毕竟要是每次都带那么多魔物器官来,无论如何都会遭人怀疑。可惜归可惜,也只能这样做了。
+
+     我看了换完钱后,装有三枚银币和八十枚铜币的袋子。
+
+     已经超过我五年来辛辛苦苦存到的──两枚银币。
+
+     而且还只花短短半天就赚到了。
+
+     「我这五年来到底……」
+
+     就像这样。当我越接近正常人的生活,就不得不理解到自己至今为止待在一个多么扭曲的环境中。
+
+     这么一想,就涌上对拉法尔他们的怒火。你只是比垃圾还不如的废物,所以根本没资格生气……他们就是这样大放厥词的家伙。
+
+     咕噜噜……
+
+     一想起拉法尔他们,刚才靠著哥布林填饱的馋虫又开始大合唱,简直在诉说著还想不够,还想吃啊。
+
+     现在还太早。再说还有罗琪希大人的事。
+
+     这已经不只是我的问题了。
+
+     好啦,现在该拿这笔钱怎么办……对了!
+
+     看了身上缝缝补补的破衣,我想到了花钱的好选择。
+
+     *
+
+     『真是人要衣装呢。』
+
+     「吵死啦!」
+
+     原本骯脏的我们如今变得乾净清洁。我去服饰店花了两枚银币,买了一套品质还算高的衣服。
+
+     再来花了五十枚铜币替黑剑格里德买剑鞘,且顺便加了十枚铜币请人清洗掉剑身上的油渍。
+
+     这下就算去到圣骑士居住的地区,也不会带给守卫们不佳的印象了吧。
+
+     无论从哪个角度看,现在的我都是普通人。
+
+     靠著猎杀哥布林赚来的钱还有剩,午餐稍微吃点好料吧。
+
+     我意气风发地朝餐饮店林立的大街走去。虽然去位于后街那间常光顾的酒吧也行,但偶尔也想转换心情,去不同的店用餐。
+
+     这是条在王都内拥有最多餐饮店的街。路上来往的行人相对较多,一停下脚步可能就得被人潮推著走了。
+
+     好啦,该去哪间店好呢?要大快朵颐的话,果然还是该吃肉吧。睽违五年的肉到底会是什么滋味,光想就让我兴奋不已,口水直流。
+
+     格里德见状,透过《读心》对我说:
+
+     『不过就是肉,你这家伙也太夸张了吧?』
+
+     「你在说啥啊!肉!是肉耶!?」
+
+     『哼,本大爷是武器,不懂什么是食欲。比起这个,往后你可得好好清理战斗中脏掉的本大爷啊。这对本大爷来说可是和你吃饭一样重要啊。』
+
+     「我知道啦,刚才不就花了十枚铜币替你清洁了吗?」
+
+     『真要本大爷说的话,那点程度你不学著自己弄,以后可就头痛啦。』
+
+     的确……假如每次都拜托打铁店整备武器,花费也不是笔小数目。加上若有机会去王都外连续狩猎好几天都不回来,黑剑格里德自然必须由我亲自保养。
+
+     虽说是维护,但这把黑剑的剑刃上没有一点缺口,所以只是擦拭掉沾在剑身上的血和油脂啦。
+
+     本来我打算放著不管,格里德却一副千百个不愿意。不断透过《读心》嚷嚷著快替他清理乾净。
+
+     格里德和其他武器不同,拥有心灵。换句话说他像人类一样,身体骯脏不清洁时会感到不适。当我得知格里德的心情,不由得思考他在那个武器摊贩沾满油渍,被当成废弃品对待时,心中究竟充斥著什么样的情绪。
+
+     就算我想知道而开口问,倔强的格里德肯定不会告诉我。
+
+     「知道啦。吃完饭就去买保养道具吧。」
+
+     『喔喔!斐特你终于理解本大爷有多重要了啊。可得像对待宝石一样,细心替本大爷保养喔。』
+
+     「真的是把很臭屁的武器耶。」
+
+     『这就是本大爷,格里德大爷啦。』
+
+     看来保养时他肯定会像个唠叨的小姑一样,东一句「磨磨这里」,西一句「这边没弄乾净」之类的。假如他太啰嗦,看我去井里提冷水泼他,这样或许能让他冷静一点,不那么唠叨。
+
+     关于帮格里德保养方面的话题暂且说到这,肚子实在饿得咕噜叫了,还是来吃午餐吧。正好眼前的店飘出烤肉香,受不了了,就选那间店吧。
+
+     当我打算进入餐饮店时,撞到一对亲子档。
+
+     应该说我有点松懈,毫无预警之下被从旁一撞。而留著杂乱胡渣的父亲竟吼起跌坐在地的我。
+
+     「走路看路啊你这废物!滚开!」
+
+     「你说什么!」
+
+     明明自己撞上来,那是什么态度啊?当我要加以反驳,亲子档却理都不理我,打算离开现场。被牵著的年幼女儿则是默不吭声。
+
+     心想太没道理的我气得朝逃跑的亲子档伸手。
+
+     就在此时,我摸到女儿的手,《读心》也跟著发动。
+
+     (救命……谁来……救救我……)
+
+     一瞬之间虽没能清楚读取,但那名少女的确在求助。
+
+     为什么!?难道他们不是父女吗?
+
+     仔细一看,男子和少女的确长得不像。难道那孩子是遭诱拐!?
+
+     总而言之,我对背对我径自离去的男子使用《鉴定》。
+
+     卡锡穆•布勒库 Lv15
+
+     体力:920
+
+     力气:900
+
+     魔力:670
+
+     精神:500
+
+     敏捷:950
+
+     技能:
+
+     咦?没有技能。怪了,这不可能啊。
+
+     技能是神赐予的力量,不论任何人都一定会拥有一种,或许只是我没读取清楚吧?我再度施展《鉴定》,但结果还是一样。
+
+     当我纳闷之际,格里德的声音透过《读心》传来。
+
+     『那男的大概透过隐藏技能把自己的技能藏起来,才会看不到吧。不过从能力值的体力和力气等方面来判断,看得出那男的是武人啦。问题是他藏起来的技能是啥。好啦,你要怎么做?』
+
+     「这还用问吗。」
+
+     硬是拉著少女的手的男子即将混入人群中。
+
+     表情僵硬,连声音都发不出的少女。既然我得知这件事,就不能坐视不管。
+
+     「唉~饭先别吃了,追吧。」
+
+     『喔喔!你打算救人?』
+
+     「没办法吧,我不能当没看到。」
+
+     『既然是你自己决定的,本大爷没打算多说啥。不过你要小心,那男的露出的是杀人犯的眼神。你可千万别对杀人不眨眼的敌人产生同情心啊。』
+
+     「……知道了。」
+
+     我已经杀死过一个人。虽说是潜进城内的坏蛋,夺走他人性命果然不是件多舒服的事。我想我大概一辈子都忘不了那个盗贼瞪著我断气的模样吧。
+
+     可是我并不感到后悔。
+
+     假如我当时放走那个盗贼,罗琪希大人肯定会遭其他圣骑士群起围剿。我听说圣骑士之间的竞争相当激烈。我无论如何都想避免让罗琪希大人这种为民著想的人因此偏离出人头地的康庄大道。
+
+     所以像我这样的废物即便弄脏自己的手,也替能成为罗琪希大人的力量感到高兴。
+
+     我没打算成为正义使者,也不可能成为。但至少看到眼前有人受苦,还是会想出手拯救。事情就是这么简单。
+
+     下定决心的我保持一定距离,尾随在两人之后。
+
+     当我跟踪了一段时间,男子在商业区内一处建有多间仓库的地方停下脚步。这里保管著从王都外搬运进来的货物,而只见男子带著少女走进一间外墙破破烂烂,应该没在使用的仓库中。
+
+     「那边就是他的住处吗?」
+
+     『谁晓得。或许他和人约在那边,打算把少女卖掉,不然就打算非礼她啊。』
+
+     「无论哪一种都糟透了吧!得加快脚步!」
+
+     我握紧黑剑格里德,静静接近仓库。周围没有人影,我贴在仓库的老旧墙壁边,从破窗悄悄观察里头的样子。
+
+     男子用铁项圈套住少女,以生锈的铁链将她像狗一般系在柱子上。这下不会错了,她是被绑来的。
+
+     少女似乎怕到发不出声音。嘲笑她的男子开口说:
+
+     「小鬼只要吓吓就吭不出声啦,真是件轻松的工作啊。」
+
+     这么说完的男子甩了少女一巴掌。力道似乎相当强劲,声音都在整间仓库里回荡著。
+
+     「像你这种孤儿存不存在都无所谓啦。反正一定是太没用,才被爸妈拋弃了吧。」
+
+     少女一听,脸色瞬间大变。
+
+     「哈!说中了吗。到底是啥垃圾技能,说来听听啊。啥!?听不到啦!」
+
+     盯著地面的少女掉下泪,但仍然因为恐惧发不出声。
+
+     那孩子也是无用之才吗……和不久之前的我一样,面临尽管对自己的力量绝望,还是得苟活下去的处境。
+
+     不,硬是被人掳走,且即将遭到某种毒手的少女现在碰上更糟的状况。
+
+     我克制住想马上冲出去救她的冲动,静静等待机会到来。
+
+     男子持续虐待少女,并说出种种恶毒的话。
+
+     「高兴吧,有个像你这种社会败类也派得上用场的地方。从今以后你就能成为某位圣骑士的玩具活下去,看,值得高兴对吧?」
+
+     少女一听便哭著不断摇头。
+
+     男子不悦咋舌,又甩了她巴掌。
+
+     「喂,你还真不老实耶。要是不好好听话,去到那边可会马上被杀喔。像之前的家伙可是连一星期都撑不过啊。虽然这样我又有钱能赚,没差就是了。」
+
+     接著出脚踹少女的肚子。超乎预料的冲击令她整个人蜷缩在地上。
+
+     看不下去的我打算把黑剑格里德拔出鞘,但是──
+
+     『等等斐特!再忍忍啊!』
+
+     「可是……」
+
+     格里德透过《读心》,阻止了就要冲出去的我。
+
+     再那样下去不行。继续坐视不管,那个少女的内心将会残留不可抹灭的伤疤。
+
+     然而,格里德依然要我别动。
+
+     『别感情用事,会没命的。你的能力值确实比对手高一点,但论战斗经验可是对方远远胜过你。这话的意思你应该懂才对。』
+
+     「我知道啦……要我冷静对吧。」
+
+     明明技术差,还凭情绪乱挥剑的话根本不会赢,格里德说得对极了。
+
+     我让心情平静下来,环顾起仓库内部。
+
+     虽说没被使用,仍能看到许多疑似废弃货物的大木箱层层堆起。能否利用那些木箱制造死角来靠近呢?
+
+     在我思考之际,男子有了动静。只见他尽情痛殴少女一会后便转身离开仓库。或许他有其他事要办,机会只有现在了。
+
+     我从破窗缓缓潜入仓库内,然后笔直朝被锁链绑住的少女冲去。
+
+     她听见我的脚步声,以为男子一下就回来了,头也不抬地发起抖来。
+
+     首先砍断锁炼,让少女恢复自由吧。
+
+     我把黑剑格里德拔出剑鞘,瞄准捆绑的锁炼。锋利无比的黑剑轻而易举就砍断了生锈的铁炼。
+
+     好,解决一个问题了!我出声喊了依然不停颤抖的少女:
+
+     「已经不要紧了喔。」
+
+     「…………」
+
+     听到我的声音抬起头的少女一脸惊讶。在盯著我瞧了好一会后,似乎理解到我不是那名男子的同伙。证据就是她彻底松了口气……流下高兴的泪水。
+
+     看来遭到诱拐的冲击让她还发不出声来。
+
+     「来,趁现在离开这里吧。」
+
+     我拉住少女的手,打算拉她起身。不过刚刚还显得安心的少女表情突然一变,这副充满恐惧的表情到底是?
+
+     少女没有看我,而是恐惧地看向我背后。
+
+     我跟著她的视线转过头,本该已经走出仓库的绑架犯就站在眼前。
+
+     该死!我马上明白自己中计了。男子早知我尾随在后,才故意假装离开仓库。
+
+     男子边不怀好意地笑,边朝我和少女走来。
+
+     「偶尔会有正义感作祟的蠢货跟踪我。不过呢,只要在拐来的小鬼面前杀了这些家伙,就都会乖乖听话啦。你根本只是飞蛾扑火呀。」
+
+     这么说完,男子拔出佩带的单手剑并摆出了中段的架式。光是如此就已造成难以形容的压力。
+
+     这就是格里德所说的战斗经验差距吗。
+
+     「怎么啦?看你腿抖成那副德性,哈哈哈!」
+
+     我举起黑剑格里德牵制步步逼近的男子。毕竟我后方可是有位怕得动弹不得的少女啊。
+
+     继续让对方靠近的话,我就得边保护少女边和男子交手。不能再制造更多不利的状况了。
+
+     可是若鲁莽往前冲,只会正中对方下怀。
+
+     别焦急啊我。但是不快做出决定,敌人可不会等我。而就在这时,格里德透过《读心》对我说:
+
+     『斐特,带著少女退到后面的货物堆去。』
+
+     直到刚才我都从窗户观察著仓库,知道随意堆在那的木箱如今摇摇欲坠,而且是更加远离出口的地方。乍听之下虽不禁怀疑,但我马上就明白格里德的意图了。
+
+     再来就看能不能成功……但不实际去做也不会知道。
+
+     绑架犯看我举剑的姿势,肯定察觉到我是个缺乏战斗经验、等级又低的弱者。不过就让我好好利用他的自大吧。
+
+     对于被拉法尔等人折磨五年的我来说,要装得跟废物一样比呼吸还简单。天啊,虽然越想越空虚,但现在顾不了那么多了。
+
+     『时机等本大爷下令。好,开始吧。』
+
+     「嗯,动手吧。」
+
+     边回想起当那该死守卫时的事,我牵起少女的手退往货物堆,装得一副拚命寻找藏身之处的样子。
+
+     来啊,赶快咬饵上钩吧!
+
+     相信看在男子眼中,我一定是惊慌失措,在恐惧下行动的吧。此时他露出一脸瞧不起我的表情。
+
+     「怎么?刚才的气势去哪啦?想救那没用小鬼的精神去哪啦?敢来碍我的事就别想活著离开,看我把你碎尸万段。」
+
+     对畏惧者施以恐吓,削弱他们反抗的手段。这就是拉法尔他们常用的技俩。这种家伙会做的事果然都没两样啊。
+
+     因此我对那男子的心境瞭若指掌。
+
+     他绝对会追来。
+
+     「逃到那种地方也没用,死心吧!」
+
+     我又逃往货物堆的更深处。通道越来越窄,来到一处没有退路的狭窄死胡同。
+
+     那家伙会如何看待这个状况?
+
+     脚步声缓缓靠近。右手拿著铁制单手剑的男子,脸上则是一副丝毫不认为自己会输的表情。
+
+     「已经没地方逃啦。」
+
+     男子像在逼迫似地一步又一步逐渐靠近。差不多了吧──我要少女尽可能退往后方。结果这时,格里德透过《读心》对我下令。
+
+     『就是现在,斐特!』
+
+     我双手高高举起黑剑。
+
+     男子见状,露出彷佛理解一切的笑容。
+
+     「难道你打算弄垮这堆货物来活埋我?可是这样一来,你和小鬼也会被活埋,是吓到脑子都不清楚了吗?」
+
+     「我就知道你会这么认为。」
+
+     不再多说的我全力冲向男子。这是仅此一次的赌注,失败就没有第二次了。
+
+     不放慢速度,就这样挥出高举的黑剑劈向男子。
+
+     (插图009)
+
+     好!上钩了!
+
+     这里是被货物堆包围,无处可逃的地方。所以我猜想若我拚死一搏使出容易破解的大动作攻击,对方就会用剑接招。
+
+     果不其然,男子没有闪躲,而选择接招。没错,他接下了我用连铁炼都能轻易砍断的黑剑使出的劈砍。
+
+     「什么!?不可能!!!」
+
+     男子拿著的铁制单手剑像奶油般碎裂喷溅。然后黑剑就这样顺势从男子肩头斜斜劈下。
+
+     喷出大量鲜血的男子往后一仰,倒在骯脏的仓库地板上。
+
+     我走到也开始吐血的男子身旁,确认一件令我在意的事──就是关于打算买少女的圣骑士。我无论如何都想知道会做这种残酷行为的圣骑士是谁。
+
+     「给我说,委托你掳人的圣骑士是谁?」
+
+     男子即便濒临死亡边缘,也坚决不说出口。
+
+     「说!是谁叫你干的!」
+
+     我用黑剑刺进伤口逼问。男子的表情虽因痛苦而扭曲,仍顽强抵抗。
+
+     啧!既然如此,只能去摸男子,靠《读心》去得知他的内心啊。当我正要伸出手时,终于忍受不住痛苦的男子说出了令人作呕的名字。
+
+     「赫德……赫德•布雷立库……」
+
+     赫德!?布雷立库家的次子!?
+
+     虽然平时就公然干些残忍的事,没想到私下竟然做出更丧尽天良的勾当!
+
+     到底已经有几个孩子遭赫德下毒手?我打算继续逼问,但男子却因失血过多断了气。
+
+     《发动暴食技能》
+
+     《累积体力+920、力气+900、魔力+670、精神+500、敏捷+950到能力值》
+
+     《技能追加隐蔽、单手剑技》
+
+     喔喔!如同格里德说的,真的持有隐蔽技能耶。他就是用这个藏起单手剑技的技能吗。
+
+     我用《鉴定》技能调查了获得的技能。
+
+     隐蔽:能让所持技能瞒过鉴定技能。
+
+     单手剑技:提升单手剑的攻击力。能使用招式《灵敏利刃(Sharp Edge)》。
+
+     隐蔽如同预料,单手剑技则似乎能用被称为「招式」的绝技。我一问格里德,得到的答案是技巧类的技能都一定拥有一种强力的绝技。我也试著鉴定了《灵敏利刃》──这是一招能藉由反手砍造成两次攻击的绝技。
+
+     假如在我出手前,男子先施展这招《灵敏利刃》,情况可就十分不乐观了。恐怕我现在人不会站在这里。
+
+     战斗有时也看运气,这次幸好女神是眷顾我。
+
+     好啦,继续在这里久待似乎很危险。毕竟死亡的男子打算把少女卖给赫德,现在我可不能被布雷立库家的王八蛋发现。
+
+     被找到的话,那些人肯定会把今天擅自翘掉守卫工作的我凌虐至死。如今我的能力值还太低,没有办法对抗他们。
+
+     我带著少女急忙离开仓库。然后从仓库区朝人多的闹区走去。现在尽量混进人多的地方比较能安心。
+
+     抬头往天空一望,太阳已开始下山,而我的肚子也彷佛回过神来似的咕噜作响。
+
+     并非出于暴食技能的空腹感,毕竟我才刚吞噬了绑架犯的灵魂。这是因为我午餐没吃,我的身体才抗议著快点送食物进来。
+
+     当我心想找个地方吃东西时,听见一阵可爱的咕噜声。
+
+     往声音响起的方向一看,不正是被我救出的少女羞红著脸,手压著肚子吗。
+
+     大概是松了口气后,肚子也跟著饿起来吧。
+
+     「好!哥哥请你吃东西吧。」
+
+     少女一扫刚才的阴沉,露出开朗的表情。
+
+     终于笑了吗。我本来还担心被掳走这件事会对她造成严重的创伤,但或许是我杞人忧天了。能够露出这种表情至少是不要紧了。
+
+     那么就照当初的预定,吃肉去吧。
+
+     若在这个闹区里,店可说任我挑选。当我抽动鼻头分辨气味,一股诱人的香气钻进鼻孔。这是……炖牛肉!
+
+     这道菜的话,小孩也有办法吃。决定了,就挑这间吧。
+
+     我牵著少女的手打开店门。这似乎是间颇受欢迎的餐厅,里头坐了许多人。
+
+     桌子区……可惜没空位。那么柜台区……喔!正好空了两个位置啊。
+
+     我们趁还没被人占走前迅速坐下。这时店员拿了菜单过来。
+
+     「请问要点什么呢?这边的是本日推荐餐点。」
+
+     今天似乎有进到品质良好的渔获,因此店员强力推荐海鲜。
+
+     坐在旁边位置的人正好就在吃那道餐点,看上去的确美味。似乎是不错的选择,但是……
+
+     「麻烦来炖牛肉和面包,两人份。」
+
+     「好的,马上来。」
+
+     果然不能舍弃最初的目标。这孩子也因为期盼能吃到炖牛肉而双眼发亮,我不能辜负她的期待。
+
+     当我和少女一起笑眯眯地引颈期盼,装著大量肉块的炖牛肉和刚出炉的面包端过来了。看起来好好吃!!
+
+     我口水都快滴下来了。至于坐在旁边的少女看来是忍不住,已经口水直流。
+
+     「难道这是你第一次吃肉?」
+
+     少女边擦口水边点头。这么说来,这孩子也是因为拥有没用的技能,遭到双亲拋弃的孤儿。连在当守卫时的我都吃不起肉,这个孤儿当然不可能吃得到。
+
+     盯著我的脸的她似乎是在意能不能开动。都来到这里了,我当然不会说不准,尽管吃吧。
+
+     「来,开动吧,今天你很努力了喔。」
+
+     我轻轻拍少女的背,她便反握起汤匙默默开始吃。
+
+     只见她眨眼间就吃光炖牛肉和面包。或许是肚子吃饱,身心都放松下来了吧,少女便嚎啕大哭了起来。
+
+     太好了,她终于发得出声音了啊。
+
+     我想美味的食物肯定有令人幸福的力量。每吃一口这碗炖牛肉,也令我涌现明天要好好努力的念头。
+
+     欢乐的时光总是过得特别快。不能继续拖到太晚。
+
+     我问身为孤儿的少女有没有地方可回,结果竟然是我住的贫民窟内的破旧孤儿院,超近的耶!
+
+     「既然这样,我们一起走到半路吧。」
+
+     「嗯!」
+
+     走出店外后,我们从商业区往住宅区移动。然后回到住宅区中的穷人群聚生活的贫民窟。
+
+     先将少女送回孤儿院吧。
+
+     走在实在称不上平坦的道路上,我发现周遭逐渐变得明亮。啊,原来是阴沉夜空的云散开了。
+
+     照射在这条没有路灯的街道上的月光感觉真美,也好温暖呢。
+
+     「来,马上就到孤儿院了……嗯?怎么啦?」
+
+     「……」
+
+     少女忽然沉默不语。原本以为她已恢复精神,难道是突然想起被绑架犯掳走时的事吗?
+
+     不过少女一反我的担忧,微笑著说:
+
+     「谢谢你喔,大哥哥!」
+
+     「……」
+
+     这次换我哑口无言。糟糕,这或许是我头一次被人感谢。
+
+     总觉得有点难为情,但感觉并不坏。
+
+     我不禁松了口气。原来我这种人也能多少派上用场──偶尔这么想也没关系吧。
+
+     能看到孤儿院啦。喔?修女们正在外面,好像在找谁似的。恐怕就是在找我带回来的这位少女吧。
+
+     所以我从后方轻推了她一把。
+
+     「已经不要紧了吧。接下来你自己走回去喔。」
+
+     「大哥哥不跟我一起来吗?」
+
+     「是啊。要跟你说再见了,保重喔。」
+
+     我的任务早在很久前就结束了。虽然这是个弱者几乎无力反抗的世界,但若想存活下去,只能靠自己的力量。
+
+     我觉得……只有这项坚持说什么都不能放弃。
+
+     大概是我的心意传达到了,离开我身边的少女一个人往前走去。她的身影看上去就像当初离开故乡村庄时的我。那是父亲因病去世后,在村庄中失去居所的我仅存的一条路。
+
+     虽是条前途黯淡的路,也只能继续走下去。
+
+     修女们见到少女,泪眼汪汪地将她搂进怀中。结果刚才还显得若无其事的少女表情一崩,放声大哭起来。
+
+     看来她还哭不够。现在就尽情哭泣吧。若能化为明日的经验,就更该那么做。
+
+     祝她日后能过得平安幸福。
+
+     我趁修女还没发现我之前离开现场。在回家途中,格里德透过《读心》对我说:
+
+     『怎么啦?瞧你一脸彷佛干了不合个性的事啊。』
+
+     「吵死啦!才不是好吗!」
+
+     我只是看著少女,回想起年幼时的自己罢了。
+
+     我已经无法回到父母坟墓所在,那个把我当成米虫而赶走我的故乡了。可能的话当然想回去扫墓,但应该会吃闭门羹吧。
+
+     我一辈子都忘不了在病魔折磨下逐渐流逝生命的父亲握著我的手,直到最后一刻都在替我往后的人生担心的模样。这些年来,我有过著能向父亲自豪的日子吗?
+
+     「我只是心想日后要走的路还很长啦。」
+
+     『废话,你才刚要起步啊。话先说在前头,你要走的路可长的喔。』
+
+     「嗯,首先得去罗琪希大人那边重新就职呢。不得不和她的父亲见面让我真的很紧张。」
+
+     『哈哈哈!你现在紧张又能怎样?明天中午才要碰面不是吗?』
+
+     「对方可是王都内屈指可数的名门的当家耶。对我而言根本是高高在上……甚至比天还高的人啊。你又不必在意,说得真轻松耶。」
+
+     『当然吧,毕竟本大爷是武器啊。』
+
+     是是是,反正格里德这种无机物不会懂啦。长年来对圣骑士根深蒂固的恐惧,不是能轻易抹灭的。
+
+     就算如今明白自己能透过暴食技能变强,这点仍然没变。若是罗琪希大人的父亲,我想会是位好人,但要和他面对面的话实在难免紧张啊。
+
+     呼~或许是我累了吧。今天早点睡好了。
+
+     今天真的发生太多事。竟在短短一天内做了从歼灭哥布林到解决绑架犯。
+
+     肯定是这股疲惫让我东想西想。明明应该相信罗琪希大人,现在我却连这点都做不到,根本烂人一个。
+
+     我一推开破屋的家门,整个人便往稻草堆成的床上扑去。
+
+     果然累过头了,眨眼间便失去了意识。
+
+     *
+
+     隔天,睡到接近中午的我慌慌张张起身准备。我三两下整理完行头,赶往罗琪希大人等著的圣骑士区入口。
+
+     和其他的地区不同,圣骑士区的周遭由高耸墙壁围绕,甚至令我产生这里也有座城堡的错觉。
+
+     我一和守卫报上姓名就轻易获准入内了。似乎是罗琪希大人有事先告知。
+
+     至于我是不是本人就得请她亲自确认。为此如今有两名士兵在左右两侧跟著我走,简直活像我干了坏事遭到逮捕啊。
+
+     至于我被带到的豪宅,只能说不愧是王都内的五大名门之一。
+
+     沿途走到这所见的宅邸根本小巫见大巫。甚至大到我都觉得怎么比较都没意义。
+
+     跟在身旁的其中一名士兵走进豪宅领地内,穿越庭院。
+
+     过了一会,身穿白色洋装的女子走了过来。真是位美人啊。
+
+     「来了吗,我正等著你呢。」
+
+     听到声音,我才发现女子就是罗琪希大人。由于平时只在守卫换班时才碰面,所以我只看过她穿白色轻铠的模样。身穿洋装的她判若两人,美不胜收。
+
+     确认我是本人后,士兵们便告辞离开。
+
+     现在就只剩下我们。而或许是我傻楞楞地张嘴盯著她瞧吧──
+
+     「怎么了吗?」
+
+     罗琪希大人讶异问。
+
+     「因为大人您实在太美,所以我……看傻眼了,对不起。」
+
+     她一听也脸颊泛红,然后轻咳了几声。
+
+     「果、果然偶尔该穿穿洋装呢。还有你才令我看傻了呢。来,这边请吧。」
+
+     明明是间大豪宅,却相当安静。不见任何佣人的踪影,甚至有种死寂的氛围。
+
+     我边望著整理得十分完善的草皮,边跟在罗琪希大人身后。真的安静过头了。
+
+     只听得见风声拂过。
+
+     我追著散发寂寥气息的背影前进。
+
+     来到豪宅面前往右转。咦?不进去里面吗?
+
+     尽管想问,这气氛也让我开不了口。
+
+     接著再往前走一会,我才总算清楚这股寂寥的氛围原因何在。
+
+     「这是……」
+
+     我无法再多说什么。
+
+     罗琪希大人看了我的反应后温柔微笑。
+
+     然后她蹲下身子,伸手摸了看似冰冷的墓碑。
+
+     「父亲大人,我决定从今天起雇用他。往后哈特家会变得更热闹喔。」
+
+     罗琪希大人对仍未反应过来的我说:
+
+     「五天前,父亲大人在比此地更南边的迦利亚丧命了。」
+
+     「迦利亚不是……」
+
+     我记得是被魔物占据的大陆。而且据说凶猛的程度是王都周遭的魔物根本无法相提并论。
+
+     圣骑士的重要职责便是抑止从那边往王国进军的魔物。也因为如此,王国不惜赐予高高在上的地位和大量钱财给圣骑士们。
+
+     然而,我实在难以想像会有什么魔物能让王国五大名家之一的当家命丧黄泉。
+
+     彷佛猜测到我的不安的罗琪希大人说:
+
+     「死因并非魔物。迦利亚还有『那个』啊。」
+
+     听她这么一说,我想到一个可能。经常被拿来和洪水、地震、海啸等相提并论的生物。
+
+     就是活生生的天灾──天龙。据说无论持有多强大的力量,都无法阻止它。由于压倒性的强悍,甚至有人将其视为神之使徒来信仰崇拜。
+
+     一旦被天龙盯上就没戏唱,只能做好死亡的觉悟。
+
+     「听说父亲大人率领的军队全军覆没。万万没想到天龙竟会去到那么远离巢穴的外缘,毕竟这几千年来从未有过这种记录。」
+
+     天龙的巢穴位于迦利亚中央,据说它不会去到迦利亚的国境边界。可是偏偏就在这次失准了。要说理由的话,真的只能说不走运。
+
+     遗族们愿不愿意接受就又是另外一回事了。
+
+     「今日上午才总算告一段落喔。包含父亲大人的丧礼等等,可说忙得我手忙脚乱。家业也由我继承,这下我正式成了哈特家的当家。」
+
+     面对在这种时刻仍展现乐观的罗琪希大人,我只能低低垂著头。
+
+     我完全没注意到。明明她在守卫换班时的表情一如往常,背后却发生了这种事。我竟一点都不晓得。
+
+     明明身处这种状况下,罗琪希大人仍为我著想,把我叫来这里。
+
+     相较之下,我一心以为要接受罗琪希大人的父亲面试,只想著该如何蒙混自己的力量来过关……
+
+     罗琪希大人,对不起,我实在……
+
+     「别垂头丧气的,让我们一起振兴哈特家吧。你愿意吗?」
+
+     「是的,十分乐意!」
+
+     从这天起,我成为了哈特家的佣人。 +
    最新最全的日本动漫轻小说 轻小说文库(http://www.wenku8.com) 为你一网打尽!
+
+ +
+ +
+ + + + + + +
+ + + diff --git a/analyze/wenku8/id_content.html b/analyze/wenku8/id_content.html new file mode 100644 index 0000000..4b903a9 --- /dev/null +++ b/analyze/wenku8/id_content.html @@ -0,0 +1,505 @@ + + + + + Title + + +
+
+ + + + + + + + + + + + + + +
+ + + + + +
暴食狂战士 唯有我突破了所谓「等级」的概念[推一下!][举报/报错]  +
+
文库分类:其他文库小说作者:一色一凛文章状态:连载中最后更新:2023-09-11全文长度:193373字
+ +
+ +
+ + + + + + +
+ +
本作已动画化(含OVA/剧场版)
+
+ + 作品Tags:奇幻 异能 战斗 恋爱 龙傲天
+ 作品热度:C级,当前热度上升指数为:A级

+ 最近章节:
第二卷 插图

+ + + 内容简介:
  在技能的优劣决定一切的世界,担任城门守卫的斐特因为自己持有的技能《暴食》是个只会让肚子变饿的没用能力,而被迫过著最低阶的生活。不过在他偶然解决掉侵入城内的盗贼后,世界彻底改变。没错,斐特持有的技能《暴食》,其实隐藏著能夺取杀害对象的技能和能力值的惊人力量。就这样,原本只能像只蝼蚁苟活下去的斐特,命运悄悄地,也迅速地开始转动——
+
+ +
+
+ + +
+
+ +
+阅读 + +
+ +
+收藏 + +
+ +
+支持 + +
+ +
+专栏 + +
+
+
+ +
+ + +
+
+ 《暴食狂战士 唯有我突破了所谓「等级」的概念》小说TXT、UMD、JAR电子书下载 + + + + +
+
+ + +
+
+ +
+ + + + + + + +






+
+ +





+
+ +
+ +
+ +
+ +
+
用户登录
+
+
+ + +
    + +
  • wdpm
  • + +
  • 普通会员
  • +
  • 新手上路
  • + +
+ +
+ + +
+
+
+ + +
+
我的书架
+
+ + + + + + +
文章名称最新章节
+ +
+
+ + +
+
小说搜索
+
+ +
+
    +
  • 条  件:
  • +
  • 关键字: + +
  • +
  •        +
  • +
+
+ +
+
+ +
+
轻小说大赏
+
+ +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
《暴食狂战士 唯有我突破了所谓「等级」的概念》吐槽吧,不吐不快![查看全部吐槽] +
+ 如果你发现本小说更新滞后(请附上新资源链接或网盘),或非版权下架而缺章少节、无法下载的情况,请立即通知管理员,我们将尽快跟进。
+
[顶]欢迎加入文库千人Q群:56102032、854859149、385413858 + 回复(-)/查看(-)轻小说文库2010-05-09
[顶]欢迎海内外用户或无QQ的用户加入文库Telegram用户群回复(-)/查看(-)轻小说文库2022-06-08
[顶]小说资源交流专贴(好书齐分享)回复(-)/查看(-)轻小说文库2022-07-19
战力膨胀过头了吧回复(0)/查看(73)天书奇谭10-17 18:12
這是七龍珠吧,套路超像回复(2)/查看(74)doraemon178810-17 09:55
剧情挺无聊的,,,回复(0)/查看(242)克拉丽丝『梦幻蕾亚』10-07 19:17
用AI翻译了web回复(1)/查看(338)overtake10-05 18:04
自己跟B站教程做的epub链接:https://pan.baidu.com/s/1gKC910... + 回复(0)/查看(257)zyktdsb10-04 13:26
动漫第一集刚看完,感觉跟流水线的厕纸一样,有没有看过的简单...回复(0)/查看(491)ctropl10-02 21:22
小說翻譯挺慢的回复(0)/查看(232)chongxi09-15 19:39
这小说我记得几年前就出了回复(1)/查看(511)yy566709-14 10:51
日式魔幻世界爽吃?回复(0)/查看(252)MED墨杜萨09-13 11:37
看简介可以拿来批判资本家回复(2)/查看(470)ricardo33109-12 23:42
+ +
+ + +
+ + + + + + + + + + + + + + +
发表书评:
标题
内容

发帖+1积分 + 严禁恶意灌水刷分
违者积分清零或锁定账号
请勿发表攻击性言论
严禁任何涉政治帖子 + 删无赦
剧透建议在标题处标明[剧透] +
+ +
  +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/analyze/wenku8/toc.html b/analyze/wenku8/toc.html new file mode 100644 index 0000000..30c232a --- /dev/null +++ b/analyze/wenku8/toc.html @@ -0,0 +1,143 @@ + + + + + Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
第一卷
第1话 无用之才第2话 蠢蠢欲动的暴食技能第3话 技能考察第4话 贪婪的黑剑
第5话 大快朵颐第6话 往哈特家的后门第7话 沉溺饥饿第8话 饥饿强化
第9话 狼吞虎咽第10话 第一位阶第11话 暂时的休息第12话 酒吧内的传闻
第13话 罗琪希的视察第14话 潜伏月夜下的凶骸第15话 邪刻纹的少女第16话 偷吃
第17话 呼唤恸哭的疯狗第18话 暴食技能大快朵颐第19话 贪婪的一击第20话 名为约定的誓约
第21话 岔路第22话 苍穹蓝天第23话 该完成的事第24话 第二位阶
第25话 各自启程番外篇 罗琪希和斐特后记插图
第二卷
第1话 没有乡愁的故乡第2话 与过往的邂逅第3话 静止的村子第4话 黑镰收割
第5话 拳头的重量第6话 愤怒的少女第7话 受管理的都市第8话 灭亡的沙漠
第9话 接踵而来的冲击第10话 缠绕沙尘的巨臂第11话 血红雷鸣第12话 武人穆库罗
第13话 蛮横不讲理的圣骑士第14话 暴食与愤怒第15话 黄昏的老骑士第16话 剑圣的毕生绝学
第17话 期望的共斗第18话 统治死亡的都市第19话 师徒之力第20话 净化之光
第21话 新的可能第22话 取回的尊严第23话 玛茵的委托第24话 绿之亚人
第25话 黑斧乱舞第26话 忘却之村第27话 障壁的机天使第28话 唤醒的力量
第29话 第三位阶第30话 于迦利亚境内番外篇 罗琪希的远征插图
+ + \ No newline at end of file diff --git "a/analyze/wenku8/\346\217\222\345\233\276.html" "b/analyze/wenku8/\346\217\222\345\233\276.html" new file mode 100644 index 0000000..a26b673 --- /dev/null +++ "b/analyze/wenku8/\346\217\222\345\233\276.html" @@ -0,0 +1,156 @@ + + + + + 第一卷 插图-暴食狂战士 唯有我突破了所谓「等级」的概念-其他文库-轻小说文库 + + + + + + + + +
+ +
+
+ + +
+ + +
+ +
第一卷 插图
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
    本文来自 轻小说文库(http://www.wenku8.com)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
    最新最全的日本动漫轻小说 轻小说文库(http://www.wenku8.com) 为你一网打尽!
+
+ +
+ +
+ + + + + + +
+ + + diff --git a/playground/fix-linovelib/403.html b/playground/fix-linovelib/403.html deleted file mode 100644 index e37d798..0000000 --- a/playground/fix-linovelib/403.html +++ /dev/null @@ -1,67 +0,0 @@ - - -Just a moment... - - - - - - - -
-
- -
-
- - - \ No newline at end of file diff --git a/playground/fix-linovelib/index.py b/playground/fix-linovelib/index.py deleted file mode 100644 index 66ad428..0000000 --- a/playground/fix-linovelib/index.py +++ /dev/null @@ -1,33 +0,0 @@ -import cloudscraper -import requests - -headers = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - # 'Accept': 'image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', - # 'Referer': 'https://w.linovelib.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46' -} - -# Accept: -# image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 - -# Accept-Encoding: -# gzip, deflate, br - -# Accept-Language: -# zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 - -# img_path= "https://img3.readpai.com/3/3279/167340/196667.jpg" -page_url = "https://w.linovelib.com/novel/3279.html" - -# resp = requests.get(page_url, headers=headers, ) -scraper = cloudscraper.create_scraper(delay=10, browser=headers) -resp = scraper.get(page_url) - -print(resp.text) -print(resp.status_code) - -# 200 Just a moment... 这种怎么攻略? - diff --git a/playground/fix-linovelib/index_2.py b/playground/fix-linovelib/index_2.py deleted file mode 100644 index 1145ebf..0000000 --- a/playground/fix-linovelib/index_2.py +++ /dev/null @@ -1,6 +0,0 @@ -from bs4 import BeautifulSoup -import cloudscraper -scraper = cloudscraper.create_scraper(delay=10, browser={'custom': 'ScraperBot/1.0',}) -url = 'https://w.linovelib.com/novel/3279.html' -req = scraper.get(url) -print(req) \ No newline at end of file diff --git a/src/linovelib2epub/linovel.py b/src/linovelib2epub/linovel.py index cf6b88b..2c4fb80 100644 --- a/src/linovelib2epub/linovel.py +++ b/src/linovelib2epub/linovel.py @@ -4,14 +4,13 @@ import shutil import time import urllib.parse -from enum import Enum, auto - -import uuid +from enum import Enum from pathlib import Path -from typing import Optional, Union, Dict, Any, List, cast, Set +from typing import Optional, Union, Dict, Any, List, cast -from ebooklib import epub +import uuid from PIL import Image +from ebooklib import epub from ebooklib.epub import EpubItem, EpubBook, EpubHtml from rich import print as rich_print from rich.prompt import Confirm @@ -22,6 +21,7 @@ from .models import LightNovel, LightNovelVolume, LightNovelImage from .spider import ASYNCIO, LinovelibMobileSpider # type: ignore[attr-defined] from .spider.masiro_spider import MasiroSpider +from .spider.wenku8_spider import Wenku8Spider from .utils import (create_folder_if_not_exists, random_useragent, read_pkg_resource, sanitize_pathname) @@ -303,6 +303,7 @@ class TargetSite(Enum): LINOVELIB_MOBILE = 'linovelib_mobile' LINOVELIB_WEB = 'linovelib_web' MASIRO = 'masiro' + WENKU8 = 'wenku8' class Linovelib2Epub: @@ -333,6 +334,7 @@ def __init__(self, site_to_base_url = { TargetSite.LINOVELIB_MOBILE: 'https://w.linovelib.com', TargetSite.MASIRO: 'https://masiro.me', + TargetSite.WENKU8: 'https://www.wenku8.net', } # user override base_url, or use fallback detection base_url = site_to_base_url[self.target_site] @@ -369,6 +371,7 @@ def __init__(self, site_to_spider = { TargetSite.LINOVELIB_MOBILE: LinovelibMobileSpider, TargetSite.MASIRO: MasiroSpider, + TargetSite.WENKU8: Wenku8Spider, } self._spider = site_to_spider[self.target_site](spider_settings=self.spider_settings) diff --git a/src/linovelib2epub/models.py b/src/linovelib2epub/models.py index 8668d34..77663f6 100644 --- a/src/linovelib2epub/models.py +++ b/src/linovelib2epub/models.py @@ -7,7 +7,7 @@ class ImageDuplicateCheckingStrategy: def is_duplicate(self, url_1, url_2): - return False + return url_1 == url_2 class LinovelibMobileImageDuplicateCheckingStrategy(ImageDuplicateCheckingStrategy): @@ -16,6 +16,7 @@ def is_duplicate(self, url_1, url_2): # https://linovelib-img.zezefans.com/3/3843/206654/227245.jpg => /3/3843/206654/227245.jpg # https://img3.readpai.com/3/3843/206654/227245.jpg => /3/3843/206654/227245.jpg # linovelib 这种实际上应该也算重复,但是从地址来看,无法感知是否重复。 + # 因此这里需要更加严格的重复判断 path_1 = urlparse(url_1).path path_2 = urlparse(url_2).path @@ -42,7 +43,7 @@ def is_duplicate(self, url_1, url_2): @dataclass class LightNovelImage: - # example: http://example.com/path/to or http://example.com + # example: http://example.com no path, host only site_base_url: str = '' # 这个图片从属html页面的原始地址 @@ -87,7 +88,7 @@ def filename(self): @property def local_relative_path(self): - # derived property from book_id, volume_id + # derived property from self if self.is_book_cover: local_relative_path = f'{self.hostname}/{self.book_id}/{self.filename}' else: diff --git a/src/linovelib2epub/spider/base_spider.py b/src/linovelib2epub/spider/base_spider.py index f70f446..16529fd 100644 --- a/src/linovelib2epub/spider/base_spider.py +++ b/src/linovelib2epub/spider/base_spider.py @@ -1,6 +1,7 @@ import asyncio import os import pickle +import re import time from abc import ABC, abstractmethod from multiprocessing import Pool @@ -11,10 +12,12 @@ import aiohttp as aiohttp import requests from aiohttp import ClientSession +from bs4 import BeautifulSoup from requests.exceptions import ProxyError +from ..exceptions import LinovelibException from ..logger import Logger -from ..models import LightNovel, LightNovelImage +from ..models import LightNovel, LightNovelImage, LightNovelVolume, LightNovelChapter from ..utils import (check_image_integrity, create_folder_if_not_exists, is_async, is_valid_image_url) @@ -213,3 +216,164 @@ def _process_image_download(self, novel: LightNovel) -> None: def _save_novel_pickle(self, novel): with open(self.spider_settings['novel_pickle_path'], 'wb') as fp: pickle.dump(novel, fp) + + async def download_pages(self, session, page_url_set: set): + + self.logger.info(f'page url set = {len(page_url_set)}') + + url_to_page = {url: 'NOT_DOWNLOAD_READY' for url in page_url_set} + + async with session: + tasks = {asyncio.create_task(self._download_page(session, url), name=url) for url in page_url_set} + pending: set = tasks + succeed_count = 0 + + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.ALL_COMPLETED) + # Note: This does not raise TimeoutError! Futures that aren't done when the timeout occurs + # are returned in the second set + + # 1. succeed => normal result in done(# HAPPY CASE) + # 2. Timeout => No TimeoutError, put timeout tasks in pending(SAD CASE(need retry)) + # 3 Other Exception before timeout => (SAD CASE(need retry) + + for done_task in done: + exception = done_task.exception() + task_url = done_task.get_name() + + if exception is None: + url_to_page[task_url] = done_task.result() + succeed_count += 1 + else: + # [TEST]make connect=.1 to reach this branch, should retry all the urls that entered this case + self.logger.info(f'Exception: {type(exception)}') + self.logger.info(f'FAIL: {task_url}; should retry this url.') + pending.add(asyncio.create_task(self._download_page(session, task_url), name=task_url)) + + self.logger.info(f'SUCCEED_COUNT: {succeed_count}') + self.logger.info(f'[NEXT TURN]Pending task count: {len(pending)}') + + # ASSERTION: make sure data is ok. + for page_content in url_to_page.values(): + if page_content == "NOT_DOWNLOAD_READY": + raise LinovelibException('如果这个断言被触发,那么证明代码逻辑有问题。青春猪头少年不会梦到奇奇怪怪的BUG。') + + return url_to_page + + async def _download_page(self, session, url) -> str | None: + # use semaphore to control concurrency + # todo add a setting property named FETCH_CHAPTER_CONCURRENCY_LEVEL + max_concurrency = 2 + sem = asyncio.Semaphore(max_concurrency) + + async with sem: + timeout = aiohttp.ClientTimeout(total=30, connect=15) # per request timeout + async with session.get(url, headers=self.request_headers(), timeout=timeout) as resp: + if resp.status == 200: + self.logger.info(f'page {url} 200 => ok.') + return await resp.text() + elif resp.status == 404: + # maybe 404 etc. Now ignore it, don't raise error to avoid retry dead loop + # 404 is considered as success => don't retry + self.logger.error(f'page {url} 404 => skip it.') + pass + else: + # 429 too many requests => should retry + # 503 Service Unavailable => should retry + # ...... => should retry + self.logger.error(f'page {url} {resp.status} => should retry.') + raise LinovelibException(f'fetch page url {url} failed with error status {resp.status}.') + + async def fetch_chapters(self, session, catalog_list, book): + """ + A basic implementation for crawling chapters. + Please consider overriding `extract_body_content()` and `download_pages` in subclass instance. + :param session: + :param catalog_list: + :param book: + :return: + """ + page_url_set = {chapter["chapter_url"] for volume_dict in catalog_list for chapter in volume_dict['chapters']} + url_to_page = await self.download_pages(session, page_url_set) + + # Main goals: + # 1. extract body and update dict + # 2. update image src to local file path in body content, + # 3. generate illustration_dict. + + for url, page in url_to_page.items(): + url_to_page[url] = self.extract_body_content(page) + + volume_id = 0 + for volume_dict in catalog_list: + volume_id += 1 + new_volume = LightNovelVolume(volume_id=volume_id) + new_volume.title = volume_dict['volume_title'] + + self.logger.info(f'volume: {volume_dict["volume_title"]}') + + chapter_id = -1 + chapter_list = [] # store chapters + for chapter in volume_dict['chapters']: + chapter_id += 1 + chapter_title = chapter["chapter_title"] + + light_novel_chapter = LightNovelChapter(chapter_id=chapter_id) + light_novel_chapter.title = chapter_title + chapter_illustrations: List[LightNovelImage] = [] + + self.logger.info(f'chapter : {chapter_title}') + + chapter_url = chapter['chapter_url'] + chapter_body = url_to_page[chapter_url] + + # one page per chapter + images_soup = BeautifulSoup(chapter_body, 'lxml').find_all('img') + for _, image in enumerate(images_soup): + # Images src analysis: + # https://i.ibb.co/1fRfdhs/6f9fbd2762d0f7039cfafb8d0bfa513d2797c5a0.jpg + # https://masiro.moe/data/attachment/forum/202103/07/173827oqkmqhcbylyytty9.jpg => 526 status code + # https://www.masiro.me/images/encode/fy-221114012533-99Qz.jpg + + # 可能为站内链接,也可能是站外链接。因为url没有固定格式 + # 这里我们需要自定义一个中间的文件夹名称,用于分割不同的爬虫实例。 + # 为了让文件夹名称更加可读和具有语义,这里使用 bookid-volumeid 作为隔离。 + + # 举例,例如 bookid为 875,volume_id 取本地自增id(例如3),那么 875-3 就是结果。 + # 最后,将这个分隔符和图片原来的文件名拼接,得到 875-3/fy-221114012533-99Qz.jpg 这样格式的链接。 + # 更加具体地,为 XXXX/masiro.me/875-3/fy-221114012533-99Qz.jpg + + remote_src = image.get("src") + src_value = re.search('(?<= src=").*?(?=")', str(image)) + + light_novel_image = LightNovelImage(site_base_url=self.spider_settings["base_url"], + related_page_url=chapter_url, + remote_src=remote_src, + chapter_id=chapter_id, + volume_id=volume_id, + book_id=self.spider_settings['book_id']) + + image_local_src = f'{self.spider_settings["image_download_folder"]}/{light_novel_image.local_relative_path}' + new_image = str(image).replace(str(src_value.group()), image_local_src) + chapter_body = chapter_body.replace(str(image), new_image) + chapter_illustrations.append(light_novel_image) + + light_novel_chapter.content = chapter_body + light_novel_chapter.illustrations = chapter_illustrations + chapter_list.append(light_novel_chapter) + + for chapter in chapter_list: + new_volume.add_chapter(cid=chapter.chapter_id, title=chapter.title, content=chapter.content, + illustrations=chapter.illustrations) + + book.add_volume(vid=new_volume.volume_id, title=new_volume.title, chapters=new_volume.chapters) + + book.mark_volumes_content_ready() + + def extract_body_content(self, page): + """ + return the whole html page content as a chapter of one volume, you need to override it to extract what you need. + :param page: + :return: + """ + return page diff --git a/src/linovelib2epub/spider/masiro_spider.py b/src/linovelib2epub/spider/masiro_spider.py index 0d61517..4962300 100644 --- a/src/linovelib2epub/spider/masiro_spider.py +++ b/src/linovelib2epub/spider/masiro_spider.py @@ -14,7 +14,7 @@ from rich.prompt import Confirm from linovelib2epub.logger import Logger -from linovelib2epub.models import LightNovel, LightNovelVolume, LightNovelChapter, LightNovelImage +from linovelib2epub.models import LightNovel, LightNovelImage from linovelib2epub.spider import BaseNovelWebsiteSpider from linovelib2epub.utils import aiohttp_get_with_retry, aiohttp_post_with_retry from .config import env_settings @@ -153,7 +153,7 @@ async def _crawl_book_basic_info_with_catalog(self, if quote == 0: # 1 self.logger.info("当前所有卷都是免费积分或你已经购买,直接执行下载。") - await self._continue_fetch(session, final_catalog_list, new_novel) + await self.fetch_chapters(session, final_catalog_list, new_novel) return new_novel else: # 2 @@ -180,7 +180,7 @@ async def _crawl_book_basic_info_with_catalog(self, # batch payments await self._pay_chapters(session, login_info, chapter_to_pay) - await self._continue_fetch(session, final_catalog_list, new_novel) + await self.fetch_chapters(session, final_catalog_list, new_novel) return new_novel else: @@ -256,159 +256,6 @@ async def _pay_chapter(self, session, login_info, chapter_id, chapter_cost): except Exception as e: raise e - async def _continue_fetch(self, session, catalog_list, book): - page_url_set = {chapter["chapter_url"] for volume_dict in catalog_list for chapter in volume_dict['chapters']} - url_to_page = await self._download_page_urls(session, page_url_set) - - # Main goals: - # 1. extract body and update dict - # 2. update image src to local file path in body content, - # 3. generate illustration_dict. - - for url, page in url_to_page.items(): - url_to_page[url] = self._extract_body_content(page) - - volume_id = 0 - for volume_dict in catalog_list: - volume_id += 1 - new_volume = LightNovelVolume(volume_id=volume_id) - new_volume.title = volume_dict['volume_title'] - - self.logger.info(f'volume: {volume_dict["volume_title"]}') - - chapter_id = -1 - chapter_list = [] # store chapters - for chapter in volume_dict['chapters']: - chapter_id += 1 - chapter_title = chapter["chapter_title"] - - light_novel_chapter = LightNovelChapter(chapter_id=chapter_id) - light_novel_chapter.title = chapter_title - chapter_illustrations: List[LightNovelImage] = [] - - self.logger.info(f'chapter : {chapter_title}') - - chapter_url = chapter['chapter_url'] - chapter_body = url_to_page[chapter_url] - - # one page per chapter - images_soup = BeautifulSoup(chapter_body, 'lxml').find_all('img') - for _, image in enumerate(images_soup): - # Images src analysis: - # https://i.ibb.co/1fRfdhs/6f9fbd2762d0f7039cfafb8d0bfa513d2797c5a0.jpg - # https://masiro.moe/data/attachment/forum/202103/07/173827oqkmqhcbylyytty9.jpg => 526 status code - # https://www.masiro.me/images/encode/fy-221114012533-99Qz.jpg - - # 可能为站内链接,也可能是站外链接。因为url没有固定格式 - # 这里我们需要自定义一个中间的文件夹名称,用于分割不同的爬虫实例。 - # 为了让文件夹名称更加可读和具有语义,这里使用 bookid-volumeid 作为隔离。 - - # 举例,例如 bookid为 875,volume_id 取本地自增id(例如3),那么 875-3 就是结果。 - # 最后,将这个分隔符和图片原来的文件名拼接,得到 875-3/fy-221114012533-99Qz.jpg 这样格式的链接。 - # 更加具体地,为 XXXX/masiro.me/875-3/fy-221114012533-99Qz.jpg - - remote_src = image.get("src") - src_value = re.search('(?<= src=").*?(?=")', str(image)) - - light_novel_image = LightNovelImage(site_base_url=self.spider_settings["base_url"], - related_page_url=chapter_url, - remote_src=remote_src, - chapter_id=chapter_id, - volume_id=volume_id, - book_id=self.spider_settings['book_id']) - - image_local_src = f'{self.spider_settings["image_download_folder"]}/{light_novel_image.local_relative_path}' - new_image = str(image).replace(str(src_value.group()), image_local_src) - chapter_body = chapter_body.replace(str(image), new_image) - chapter_illustrations.append(light_novel_image) - - light_novel_chapter.content = chapter_body - light_novel_chapter.illustrations = chapter_illustrations - chapter_list.append(light_novel_chapter) - - for chapter in chapter_list: - new_volume.add_chapter(cid=chapter.chapter_id, title=chapter.title, content=chapter.content, - illustrations=chapter.illustrations) - - book.add_volume(vid=new_volume.volume_id, title=new_volume.title, chapters=new_volume.chapters) - - book.mark_volumes_content_ready() - - def _extract_body_content(self, page: str): - """ - :param page: - :return: - """ - html_content = BeautifulSoup(page, 'lxml') - body_content = html_content.find('div', {'class': 'nvl-content'}).prettify() - return body_content - - async def _download_page_urls(self, session, page_url_set: set): - - self.logger.info(f'page url set = {len(page_url_set)}') - - url_to_page = {url: 'NOT_DOWNLOAD_READY' for url in page_url_set} - - async with session: - tasks = {asyncio.create_task(self._download_page(session, url), name=url) for url in page_url_set} - pending: set = tasks - succeed_count = 0 - - while pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.ALL_COMPLETED) - # Note: This does not raise TimeoutError! Futures that aren't done when the timeout occurs - # are returned in the second set - - # 1. succeed => normal result in done(# HAPPY CASE) - # 2. Timeout => No TimeoutError, put timeout tasks in pending(SAD CASE(need retry)) - # 3 Other Exception before timeout => (SAD CASE(need retry) - - for done_task in done: - exception = done_task.exception() - task_url = done_task.get_name() - - if exception is None: - url_to_page[task_url] = done_task.result() - succeed_count += 1 - else: - # [TEST]make connect=.1 to reach this branch, should retry all the urls that entered this case - self.logger.info(f'Exception: {type(exception)}') - self.logger.info(f'FAIL: {task_url}; should retry this url.') - pending.add(asyncio.create_task(self._download_page(session, task_url), name=task_url)) - - self.logger.info(f'SUCCEED_COUNT: {succeed_count}') - self.logger.info(f'[NEXT TURN]Pending task count: {len(pending)}') - - # ASSERTION: make sure data is ok. - for page_content in url_to_page.values(): - if page_content == "NOT_DOWNLOAD_READY": - raise LinovelibException('如果这个断言被触发,那么证明代码逻辑有问题。青春猪头少年不会梦到奇奇怪怪的BUG。') - - return url_to_page - - async def _download_page(self, session, url) -> str | None: - # use semaphore to control concurrency - max_concurrency = 2 - sem = asyncio.Semaphore(max_concurrency) - - async with sem: - timeout = aiohttp.ClientTimeout(total=30, connect=15) # per request timeout - async with session.get(url, headers=self.request_headers(), timeout=timeout) as resp: - if resp.status == 200: - self.logger.info(f'page {url} 200 => ok.') - return await resp.text() - elif resp.status == 404: - # maybe 404 etc. Now ignore it, don't raise error to avoid retry dead loop - # 404 is considered as success => don't retry - self.logger.error(f'page {url} 404 => skip it.') - pass - else: - # 429 too many requests => should retry - # 503 Service Unavailable => should retry - # ...... => should retry - self.logger.error(f'page {url} {resp.status} => should retry.') - raise LinovelibException(f'fetch page url {url} failed with error status {resp.status}.') - def _convert_to_catalog_list(self, html_text) -> List: """ input example: @@ -579,3 +426,12 @@ async def _masiro_get_token(self, login_info: MasiroLoginInfo, session): self.logger.debug(f'token: {token}') login_info.token = token + + def extract_body_content(self, page: str): + """ + :param page: + :return: + """ + html_content = BeautifulSoup(page, 'lxml') + body_content = html_content.find('div', {'class': 'nvl-content'}).prettify() + return body_content diff --git a/src/linovelib2epub/spider/wenku8_spider.py b/src/linovelib2epub/spider/wenku8_spider.py new file mode 100644 index 0000000..82cbb73 --- /dev/null +++ b/src/linovelib2epub/spider/wenku8_spider.py @@ -0,0 +1,205 @@ +import asyncio +import re +from typing import Dict, Any, List + +import aiohttp +import inquirer +from bs4 import BeautifulSoup + +from linovelib2epub.logger import Logger +from linovelib2epub.models import LightNovel, LightNovelImage +from linovelib2epub.spider import BaseNovelWebsiteSpider +from linovelib2epub.utils import aiohttp_get_with_retry + + +class Wenku8Spider(BaseNovelWebsiteSpider): + + def __init__(self, spider_settings: Dict[str, Any]): + super().__init__(spider_settings) + self.logger = Logger(logger_name=type(self).__name__, + log_filename=self.spider_settings["log_filename"]).get_logger() + self._catalog_url = "" + + def request_headers(self) -> Dict[str, Any]: + return { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.57' + } + + def fetch(self) -> LightNovel: + novel = asyncio.run(self._fetch()) + return novel + + async def _fetch(self) -> LightNovel: + jar = aiohttp.CookieJar(unsafe=True) + conn = aiohttp.TCPConnector(ssl=False) + trust_env = False if self.spider_settings["disable_proxy"] else True + timeout = aiohttp.ClientTimeout(total=30, connect=15) + + async with aiohttp.ClientSession(connector=conn, trust_env=trust_env, cookie_jar=jar, + timeout=timeout) as session: + novel = await self._fetch_basic_info(session) + await self._fetch_catalog_content(session, novel) + return novel + + async def _fetch_basic_info(self, session) -> LightNovel: + # index url + # https://www.wenku8.net/book/2961.htm + book_id = self.spider_settings["book_id"] + book_index_url = f"https://www.wenku8.net/book/{book_id}.htm" + page_text = await aiohttp_get_with_retry(session, book_index_url, headers=self.request_headers()) + + soup = BeautifulSoup(page_text, 'lxml') + title = soup.select_one("#content table:nth-child(1) span b").text + cover_src = soup.select_one("#content table img")['src'] + # 说作者:一色一凛 + author_text = soup.select_one("#content table:nth-child(1) tr:nth-child(2) td:nth-child(2)").text + author = re.sub(r"小说作者:\s*", "", author_text) + # desc + # nth-of-type 不会选择内层的table,区别于nth-of-child() + desc = soup.select("#content table:nth-of-type(2) td:nth-child(2) span")[-1].text + + # catalog url example https://www.wenku8.net/novel/2/2961/index.htm + catalog_url = soup.select_one("legend + div > a")['href'] + self._catalog_url = catalog_url + + new_novel = LightNovel() + new_novel.book_id = self.spider_settings["book_id"] + new_novel.author = author + new_novel.book_title = title + new_novel.book_cover = LightNovelImage(site_base_url=self.spider_settings["base_url"], + related_page_url=book_index_url, + remote_src=cover_src, + book_id=self.spider_settings["book_id"], + is_book_cover=True) + new_novel.description = desc + new_novel.mark_basic_info_ready() + + return new_novel + + async def _fetch_catalog_content(self, session, novel): + catalog_url = self._catalog_url + catalog_html = await aiohttp_get_with_retry(session, catalog_url, self.request_headers()) + + catalog_list = self._convert_to_catalog_list(catalog_html) + if self.spider_settings['select_volume_mode']: + catalog_list = self._handle_select_volume(catalog_list) + + await self.fetch_chapters(session, catalog_list, novel) + + def _convert_to_catalog_list(self, catalog_html) -> List: + # goal => + # [{vid:1,volume_title: "XX", chapters:[{dict},{dict},...] + + # => volume title + # 第一卷 + + # => chapter title + # 第1话 无用之才 + # 第2话 蠢蠢欲动的暴食技能 + # ...... + # 后记 + # 插图 ---> move this chapter to first index in this volume array + + # => volume title + # 第二卷 + + soup = BeautifulSoup(catalog_html, 'lxml') + catalog_items = soup.find('table').find_all('td') + + catalog_list = [] + current_volume = [] + current_volume_title = "" + volume_index = 0 + + for idx, catalog_item in enumerate(catalog_items): + catalog_item_text = catalog_item.text + item_css_class = catalog_item['class'] + + # is volume title + if 'vcss' in item_css_class: + volume_index += 1 + + # reset current_* variables + current_volume_title = catalog_item_text + current_volume = [] + + catalog_list.append({ + 'vid': volume_index, + 'volume_title': current_volume_title, + 'chapters': current_volume + }) + # is chapter + elif 'ccss' in item_css_class: + href = catalog_item.find("a")["href"] + # https://www.wenku8.net/novel/2/2961/index.htm + 146006.htm => https://www.wenku8.net/novel/2/2961/146006.htm + chapter_url = f'{self._catalog_url.rsplit("/", 1)[0]}/{href}' + + new_chapter = { + 'chapter_url': chapter_url, + 'chapter_title': catalog_item_text + } + if catalog_item_text == '插图': + current_volume.insert(0, new_chapter) + else: + current_volume.append(new_chapter) + else: + pass + + return catalog_list + + def _handle_select_volume(self, catalog_list): + def _reduce_catalog_by_selection(catalog_list, selection_array): + return [volume for volume in catalog_list if volume['vid'] in selection_array] + + def _get_volume_choices(catalog_list): + """ + [(volume_title,vid),(volume_title,vid),...] + + :param catalog_list: + :return: + """ + return [(volume['volume_title'], volume['vid']) for volume in catalog_list] + + # step 1: need to show UI for user to select one or more volumes, + # step 2: then reduce the whole catalog_list to a reduced_catalog_list based on user selection + # UI show + question_name = 'Selecting volumes' + question_description = "Which volumes you want to download?(select one or multiple volumes)" + # [(volume_title,vid),(volume_title,vid),...] + volume_choices = _get_volume_choices(catalog_list) + questions = [ + inquirer.Checkbox(question_name, + message=question_description, + choices=volume_choices, ), + ] + # user input + # answers: {'Selecting volumes': [3, 6]} + answers = inquirer.prompt(questions) + catalog_list = _reduce_catalog_by_selection(catalog_list, answers[question_name]) + return catalog_list + + def extract_body_content(self, page: str): + """ + :param page: + :return: + """ + html_content = BeautifulSoup(page, 'lxml') + content_body = html_content.select_one('#content') + + # remove all contentdp div + contentdps = content_body.select("#contentdp") + for element in contentdps: + element.decompose() + + #     我一回到王都圣法特,就为了换取打倒魔物的赏金来到兑换所。
+ #
+ #     只见壮硕的武人们你推我挤,偶尔还听见怒骂声传来。似乎是为了交换的条件和柜台人员起了争执。
+ #
+ + # maybe we should remove nbsp and br, re-wrap it with a `p` container + + return content_body.prettify() +