☰
owllook 小说榜单爬虫实战:基于 Ruia 异步框架爬取起点与纵横排行榜数据
2026/10/4 15:50:21 网站建设 项目流程
  • 后端
  • 搜索引擎
  • 网页爬虫

【免费下载链接】owllook

owllook-小说搜索引擎

项目地址:https://gitcode.com/gh_mirrors/ow/owllook
点击查看免费下载

owllook 是一款开源的"小说搜索引擎",其 owllook/spiders 目录承载着整个项目的榜单与书籍数据采集层。本文聚焦其中专门负责"小说榜单爬虫"的实现,围绕官方文档 owllook/spiders/README.md 中记录的起点中文网(qidian)榜单抓取方案,结合仓库内真实源码(排名爬虫、全站小说爬虫、书籍详情爬虫、定时调度与代理中间件),系统讲解如何用异步爬虫框架Ruia完成榜单数据的抓取、解析、清洗与入库,并给出可直接复用的分类参数表与运行方式。

阅读完本文,你将掌握:起点榜单 URL 的分类参数含义与构造方式、Ruia 中Item/TextField/AttrField/HtmlField的字段提取与clean_*清洗方法、榜单 Top10 截取与 MongoDBupsert入库的完整链路,以及如何通过定时任务把榜单数据持续同步进 owllook 的检索体系。

一、榜单爬虫在 owllook 中的定位

owllook 的整体数据流是"爬虫采集 → 结构化入库 → 搜索引擎召回 → 页面展示"。榜单爬虫负责持续抓取各大小说站的排名数据,为首页榜单、小说热度排序等场景提供原始素材,属于整个项目的数据采集入口之一。从源码结构看,owllook/spiders 目录内的爬虫按数据形态分为两类:

  • 榜单/排名爬虫:抓取榜单页的排名列表,产出"榜单标题 + 书名 Top N + 榜单入口链接";
  • 书籍信息爬虫:抓取全站小说列表页与单本书详情页,产出书名、作者、封面、简介、最新章节等结构化字段。

榜单爬虫的代表实现是 qidian_ranking.py(起点榜单)与 zh_ranking.py(纵横人气榜单),二者共用同一套 Ruia 异步框架与同一张 MongoDB 集合novels_ranking,而官方 README 记录的正是起点的榜单抓取方案,接下来以此为线索展开。

二、起点榜单目标 URL 与分类参数全表

owllook/spiders/README.md 明确给出起点的榜单入口为:

http://r.qidian.com/?chn=-1

chn参数决定榜单展示的小说分类,完整参数表如下(该表直接继承自项目文档,是编写起点榜单爬虫的关键配置依据):

参数 URL分类说明
http://r.qidian.com/?chn=-1全部分类
http://r.qidian.com/?chn=21玄幻
http://r.qidian.com/?chn=1奇幻
http://r.qidian.com/?chn=2武侠
http://r.qidian.com/?chn=22仙侠
http://r.qidian.com/?chn=4都市
http://r.qidian.com/?chn=15职场
http://r.qidian.com/?chn=6军事
http://r.qidian.com/?chn=5历史
http://r.qidian.com/?chn=7游戏
http://r.qidian.com/?chn=8体育
http://r.qidian.com/?chn=9科幻
http://r.qidian.com/?chn=10灵异
http://r.qidian.com/?chn=12二次元

2.1 分类参数在源码中的落地

这份参数表并非孤立存在,它被完整落到了 qidian_ranking.py 的QidianRankingSpider中:

  • start_urls通过列表推导式批量生成 14 个分类榜单 URL:[f"https://www.qidian.com/rank/?chn={key}" for key in [-1, 21, 1, 2, 22, 4, 15, 6, 5, 7, 8, 9, 10, 12]],注意生产代码使用的域名是www.qidian.com/rank/,README 中记录的是早期入口r.qidian.com,二者chn参数语义一致,抓取时以当前源码为准;
  • qidian_type字典将chn参数值映射为中文分类名,解析结果最终会写入数据库的type字段,例如'-1': '全部类别'、'21': '玄幻';
  • concurrency = 3控制并发请求数为 3,避免对榜单接口造成过大压力。

也就是说,若要扩展榜单分类,只需修改start_urls与qidian_type两处即可,参数表与代码一一对应,方便维护。

三、榜单爬虫的 Ruia 实现剖析

Ruia 是 owllook 使用的轻量异步爬虫框架,其核心思想是:用Item声明字段提取规则(CSS 选择器),用Spider声明抓取入口与解析逻辑,配合asyncio实现并发抓取。qidian_ranking.py 与 zh_ranking.py 都遵循这一模式。

3.1 榜单 Item:四级 CSS 选择器定位

起点榜单页的 Item 定义如下(见 qidian_ranking.py):

class RankingItem(Item): target_item = TextField(css_select='.rank-list') ranking_title = TextField(css_select='h3.wrap-title') more = AttrField(css_select='h3>a.more', attr='href') book_list = HtmlField(css_select='div.book-list>ul>li', many=True)
  • TextField:提取元素的文本内容,如榜单标题;
  • AttrField:提取元素的属性值,如href属性,这里用attr='href'取到"查看更多"的链接;
  • HtmlField(css_select=..., many=True):批量提取多个元素的 HTML 片段,这里把每个div.book-list>ul>li(榜单中的一本书)整体截取出来,交给下一级 Item 二次解析。

与起点对应的纵横榜单 Item(见 zh_ranking.py)结构几乎一致:div.rank_i_p_list作为榜单容器、div.rank_i_p_tit作为榜单标题、div.rank_i_p_list>div.rank_i_li批量提取每本书。可见 owllook 用"两级 Item + CSS 选择器"的组合,把不同站点的榜单页统一抽象成了同构的数据结构。

3.2 二级 Item:书名提取与容错

榜单页中每本书的书名可能出现在两个位置(置顶书名与普通书名),因此第二级 Item 同时声明两个字段并做兜底(见 qidian_ranking.py):

class NameItem(Item): top_name = TextField(css_select='h4', default='') other_name = TextField(css_select='a.name', default='')

default=''保证字段缺失时不报错,解析时用item_data.top_name or item_data.other_name优先取置顶书名,取不到再回退到普通书名——这是典型的健壮性写法。

3.3 clean_* 清洗钩子:数据标准化

Ruia 允许为字段定义clean_<字段名>异步方法做后处理。起点榜单里用到了两个清洗钩子:

async def clean_ranking_title(self, ranking_title): if isinstance(ranking_title, list): return ranking_title[0].text else: return str(ranking_title).split('榜')[0] + '榜' async def clean_more(self, more): return "https:" + more
  • clean_ranking_title:兼容"返回列表"与"返回字符串"两种情况,并把标题统一规整为"XX榜"格式;
  • clean_more:页面返回的"查看更多"链接缺少协议头,统一补全为https:绝对地址。

纵横榜单的RankingItem则没有额外的清洗逻辑,字段直接使用,说明清洗钩子是按需添加的,当页面数据结构不稳定时,这里就是兜底修正的最佳位置。

3.4 parse 解析:榜单 Top10 截取与结果组装

榜单页通常包含多个榜单区块,parse方法对每个区块取排名前十的书籍(见 qidian_ranking.py):

async for item in RankingItem.get_items(html=res.html): each_book_list = [] # 只取排名前十的书籍数据 for index, value in enumerate(item.book_list[:10]): item_data = await NameItem.get_item(html=value) name = item_data.top_name or item_data.other_name each_book_list.append({'num': index + 1, 'name': name}) data = { 'title': item.ranking_title, 'more': item.more, 'book_list': each_book_list, 'updated_at': time.strftime("%Y-%m-%d %X", time.localtime()), } result.append(data)

res_dic中除了data榜单数据外,还附带了target_url(本次抓取的榜单 URL)、type(由qidian_type映射出的中文分类名)与spider(固定为"qidian"),这些元信息用于区分数据来源。纵横版(zh_ranking.py)的逻辑相同,只是type固定为"人气榜单"、spider为"zongheng"。

3.5 save 入库:MongoDB upsert 幂等写入

榜单结果通过save方法写入 MongoDB 的novels_ranking集合(见 qidian_ranking.py):

async def save(self, res_dic): try: motor_db = MotorBaseOld().db await motor_db.novels_ranking.update_one( {'target_url': res_dic['target_url']}, {'$set': { 'data': res_dic['data'], 'spider': res_dic['spider'], 'type': res_dic['type'], 'finished_at': time.strftime("%Y-%m-%d %X", time.localtime()) }}, upsert=True) except Exception as e: self.logger.exception(e)

几个值得注意的工程细节:

  • 以target_url作为查询键,同一分类榜单重复抓取时直接覆盖更新,配合upsert=True实现"不存在则插入、存在则更新"的幂等写入,避免重复记录;
  • updated_at(抓取开始时间)在parse阶段写入每条榜单数据,finished_at(入库完成时间)在save阶段写入文档外层,两者可以用于监控抓取耗时;
  • MotorBaseOld来自 owllook/database/mongodb/motorbase.py,是基于 Motor(MongoDB 异步驱动)封装的连接单例。

纵横榜单的入库逻辑与起点完全一致(zh_ranking.py),两条榜单数据汇入同一个novels_ranking集合,仅靠spider字段区分来源。

四、榜单数据的定时调度与接入方式

榜单是时效性数据,owllook 提供了两种定时刷新方案:

4.1 独立调度脚本

spider_console.py 是纯爬虫侧的调度入口:

schedule.every(60).minutes.do(start_spider) while True: schedule.run_pending() time.sleep(1)

每 60 分钟依次启动QidianRankingSpider与ZHRankingSpider,常驻运行,适合只想同步榜单数据的部署场景。

4.2 集成进项目主调度

scheduled_task.py 将榜单刷新纳入 owllook 主任务体系:

from owllook.spiders import QidianRankingSpider, ZHRankingSpider def start_spider(): QidianRankingSpider.start() ZHRankingSpider.start() def refresh_task(): schedule.every(CONFIG.SCHEDULED_DICT['SPIDER_INTERVAL']).minutes.do(start_spider) while True: schedule.run_pending() time.sleep(1)

刷新间隔由配置项SPIDER_INTERVAL控制。查 owllook/config/config.py 可知其默认值为 120(分钟),并支持通过环境变量覆盖:

SPIDER_INTERVAL=int(os.getenv('SPIDER_INTERVAL', 120)),

两种方式都基于schedule库 +time.sleep(1)的空转轮询实现,启动后即可按固定周期把最新榜单写进novels_ranking。

五、代理与请求可靠性中间件

榜单站点对高频请求较敏感,owllook 为爬虫设计了代理注入中间件,位于 middlewares.py:

@owl_middleware.request async def add_random_proxy(request): request.kwargs.update({'proxy': await update_proxy()}) request.request_config.update({'RETRY_FUNC': retry_func}) async def update_proxy(): proxy = await get_proxy_ip() if proxy: proxy = 'http://' + proxy else: proxy = None return proxy async def retry_func(request): proxy = await update_proxy() request.kwargs.update({'proxy': proxy}) return request

其工作方式为:每次请求前从代理池动态取一个代理 IP 注入request.kwargs['proxy'],并把自定义RETRY_FUNC写入请求配置,重试时同样换新代理,降低单 IP 连续请求被限制的风险。代理 IP 的获取实现在 spider_tools.py 的get_proxy_ip中,它向CONFIG.REMOTE_SERVER['proxy_server']发起 POST 请求取回代理地址,该服务地址在 config.py 中配置(默认http://0.0.0.0:8002/,即本地代理池服务,实际部署时需指向可用代理源)。

使用方式:榜单与书籍爬虫启动时传入该中间件,如QidianRankingSpider.start(middleware=middleware)(middleware为ruia_ua提供的 UA 中间件),全站书籍爬虫则组合传入[ua_middleware, owl_middleware](见 qidian_all_novels.py),同时拿到随机 UA 与随机代理。

六、从榜单到全量:配套的书籍数据爬虫

榜单解决"哪些书热门",书籍爬虫解决"这些书长什么样"。owllook 的爬虫体系以榜单为核心入口,同时配套了三类书籍数据爬虫,数据最终汇入all_novels(列表)与all_novels_info(详情)两个集合:

爬虫文件抓取目标产出集合
qidian_all_novels.py起点全站小说列表页all_novels
qidian_novel_info.py起点单本书详情页all_novels_info
zongheng_all_novels.py纵横全站小说列表页all_novels
zongheng_novel_info.py纵横单本书详情页all_novels_info
heiyan_novel_info.py黑岩单本书详情页all_novels_info

6.1 列表页爬虫:并发抓取 + 批量入库

qidian_all_novels.py 与 zongheng_all_novels.py 的结构相同:通过TextField/AttrField提取书名、作者、作者主页、分类、封面、简介等字段,clean_*钩子负责把相对协议补全为https:/http:绝对地址;parse中通过asyncio.ensure_future(self.save(res_dic))为每本书创建异步保存任务,再用asyncio.wait(tasks)汇总结果并打印"共 N 本小说,抓取成功 M 本"的统计信息;save内以novel_url + novel_name为查询键执行update_one(..., upsert=True)幂等入库。

值得留意的是二者在工程参数上的差异:起点列表爬虫设置了concurrency = 20、RETRIES = 15、TIMEOUT = 3(qidian_all_novels.py),纵横则开到concurrency = 60、RETRIES = 8(zongheng_all_novels.py),说明并发度与重试策略需要按目标站点承受能力单独调优。main入口按 100 页为一个批次批量生成start_urls并分页抓取,起点还使用uvloop.EventLoopPolicy()替换默认事件循环以提升 asyncio 性能(见 qidian_all_novels.py)。

6.2 详情页爬虫:两类数据源策略

详情爬虫存在两种典型的字段提取策略:

  • CSS 选择器策略(起点、纵横):如 qidian_novel_info.py 用TextField(css_select='.book-info>h1>em')取书名、AttrField(css_select='a#bookImg>img', attr='src')取封面;clean_status用'#'.join([i.text for i in status])把多个标签拼接成字符串;
  • Open Graph 元信息策略(黑岩):heiyan_novel_info.py 直接抓取页面<meta property="og:*">标签的content属性,例如og:title(书名)、og:novel:author(作者)、og:novel:latest_chapter_name(最新章节),信息密度更高、结构更稳定;clean_latest_chapter_time还会把"今天/昨日"这类相对时间替换成具体日期。

黑岩详情爬虫还配套了完整的单元测试 tests/test_heiyan_novel_info.py:测试用例内置了一段包含og:元信息标签的真实 HTML 片段,直接调用HYNovelInfoItem.get_item(html=HTML)并断言item_data.novel_name == '神仙微信群'。这个测试证明了两点:Item 的字段提取可以脱离网络独立验证;owllook 的爬虫开发流程把"页面解析"与"网络请求"彻底解耦,便于 CI 回归。

七、运行前提与实战要点小结

7.1 运行前提

  • 项目依赖 Ruia、ruia_ua、Motor(MongoDB 异步驱动)、schedule 等库,依赖清单见仓库根目录 Pipfile;
  • 需要可用的 MongoDB 服务,榜单数据写入novels_ranking集合;
  • 若启用 middlewares.py 的代理中间件,需先配置好代理池服务地址(CONFIG.REMOTE_SERVER['proxy_server'],见 config.py);
  • 目标站点页面结构可能随时间变化,README 中r.qidian.com的旧入口与源码中www.qidian.com/rank/的新入口并存,实际抓取时需以源码为准并注意遵守目标站点的访问规则。

7.2 榜单爬虫核心链路回顾

整个榜单爬虫的完整调用链可归纳为:

  1. start_urls按chn参数批量生成各分类榜单 URL;
  2. Spider并发请求(concurrency=3),可挂载 UA/代理中间件;
  3. parse用两级Item解析出"榜单标题 + 更多链接 + 书籍 Top10";
  4. clean_*钩子统一书名标题格式、补全协议头;
  5. save以target_url为键、upsert=True幂等写入novels_ranking;
  6. schedule定时任务按SPIDER_INTERVAL(默认 120 分钟)周期性重复 1~5 步。

7.3 可复用的扩展思路

  • 新增榜单站点:参考 zh_ranking.py 为纵横写的同构实现,只需定义目标页面的两级 Item CSS 选择器,并将save中的spider字段替换为站点标识即可;
  • 调整榜单分类:修改start_urls与qidian_type映射表;
  • 增强抓取稳定性:复用owl_middleware的随机代理与重试换 IP 逻辑,或按站点情况调整request_config中的RETRIES/DELAY/TIMEOUT。

至此,从 README 中一行 URL 参数表出发,你已经看到了 owllook 榜单爬虫从分类构造、异步解析、数据清洗到定时入库的完整工程实现,这份实现同样可以作为自建小说榜单聚合服务的参考蓝本。

  • 后端
  • 搜索引擎
  • 网页爬虫

【免费下载链接】owllook

owllook-小说搜索引擎

项目地址:https://gitcode.com/gh_mirrors/ow/owllook
点击查看免费下载

相关推荐

上一篇:EMQX 规则引擎 republish 动作的 Namespace 隔离修复:limit_selects_in_namespace 行为详解
下一篇:CAMEL-AI 多智能体框架入门指南:核心组件、生态体系与 5 分钟快速上手

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询