- 后端
- 搜索引擎
- 网页爬虫
【免费下载链接】owllook
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=-1chn参数决定榜单展示的小说分类,完整参数表如下(该表直接继承自项目文档,是编写起点榜单爬虫的关键配置依据):
| 参数 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:" + moreclean_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 榜单爬虫核心链路回顾
整个榜单爬虫的完整调用链可归纳为:
start_urls按chn参数批量生成各分类榜单 URL;Spider并发请求(concurrency=3),可挂载 UA/代理中间件;parse用两级Item解析出"榜单标题 + 更多链接 + 书籍 Top10";clean_*钩子统一书名标题格式、补全协议头;save以target_url为键、upsert=True幂等写入novels_ranking;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-小说搜索引擎
相关推荐
时间表达式识别利器:fnlp如何精准解析中文复杂时间描述?
时间表达式识别利器:fnlp如何精准解析中文复杂时间描述? 在中文自然语言处理领域,时间表达式的识别与解析一直是开发者面临的重要挑战。fnlp作为一款专业的中文
NLP机器学习人工智能2025最强异步爬虫框架:Ruia异步Python爬虫实战指南 — 从入门到精通
2025最强异步爬虫框架:Ruia异步Python爬虫实战指南 — 从入门到精通 你还在为传统爬虫速度慢、资源占用高而烦恼吗?面对反爬机制束手无策?作为数据采集
Ruia:异步Python 3.6+ 网络爬虫微框架
Ruia:异步Python 3.6+ 网络爬虫微框架 Ruia 是一个基于 asyncio 的异步 Python 3.6+ 网络爬虫微框架,旨在使得网络爬取工作
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考