ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python+Django爬虫实战:起点中文网Top500数据提取系统完整教程

Python+Django爬虫实战:起点中文网Top500数据提取系统完整教程 如果你正在纠结毕设选题或者刷到过“基于pythondjango的起点中文网Top500小说数据提取”这类标题很可能你也想做一个“既有点技术含量又能在有限时间搞定”的Web系统。这个项目我完整做过也帮好几个学弟学妹调过代码今天把从技术选型、爬虫设计、数据入库到页面展示的整个链路盘一遍。重点不是贴一段代码就完事而是把每一步“为什么这么做”讲透让你不只是抄个源码而是真正能接住答辩时老师抛出的各种问题。这个项目能做的事其实很直白用Python爬虫从起点中文网拉取Top500榜单数据清洗后存进数据库再用Django搭一个网页让用户可以按分类、字数、推荐量等维度浏览和筛选小说榜单。适合的人群很明确——正在找计算机、软件工程、大数据相关毕设题目的学生以及想练手爬虫加Web开发整合能力的Python初学者。全文我会带着源码级别细节和实际踩坑记录来写读完你不仅能复现还能说出几个让老师眼前一亮的“项目亮点”。1. 项目整体设计与技术选型背后的思路1.1 为什么数据源偏偏选了起点Top500做毕设选数据源第一原则是“稳定公开、字段丰富、自带价值”。“起点中文网Top500”这三个条件全占。榜单页是公开页面不需要登录没有复杂加密爬虫难度刚好卡在大学毕设的舒适区字段又足够丰富——书名、作者、分类、字数、状态、周推荐、总推荐、简介这些信息既有展示价值又能支撑后续的数据分析和可视化。选这个数据源还有一个隐藏优势榜单本身就是有排序逻辑的数据天然适合做“数据提取”和“数据管理”的叙事主线。你说自己做了数据提取结果只爬了个静态页面老师会觉得太浅但如果你能把榜单数据拉下来、清洗、入库、再做成可筛选可排序的网页整个“数据生命周期”就闭环了这比单纯写一个爬虫要有说服力得多。另外起点还有其他榜单比如月票榜、女频榜、新书榜只要改一下URL和字段映射就能把项目扩展成“多榜单数据管理系统”。这个扩展方向在答辩时非常加分因为它展示的是你理解了架构而不是背熟了一个页面。数据规模也不要只看“500条”——虽然500不算大但一旦你把它做成一个通用爬虫框架换目标、加字段、增量更新都是顺手的事“大数据”的思维就体现在这套可扩展的数据链路里。1.2 技术栈选型为什么是requests加Django而不是“全家桶”先说爬虫部分。Python爬虫常见方案有requests、Scrapy、Selenium。我做这个项目时用的是requests加BeautifulSoup没有上Scrapy也没有上Selenium。原因很实在目标页面是静态渲染的榜单不涉及点击和滚动加载数据量500条requests的并发和异步优势完全用不上Scrapy的工程化结构对这个体量的项目属于“杀鸡用牛刀”而且你要在答辩时解释清楚整个爬虫框架细节难度也更高。Selenium就更重了还得装浏览器驱动反而容易把自己坑进去。Web框架选Django而不是Flask主要看两点第一Django自带ORM、Admin后台、模板引擎、表单处理几乎把项目骨架搭好了你在毕设周期内能把精力集中在核心逻辑上第二答辩的时候Django可以说“自带完整的MTV架构”这句话对很多老师来说是实打实的加分项。Flask虽然轻巧灵活但很多功能要自己插拔组合对新手而言反而容易漏东西。数据库的选择上本地开发和演示阶段我用的是SQLite零配置、文件型数据库、Django默认支持非常省心。如果你想让项目显得更“工业化”也可以换MySQLDjango只要改一下settings里的数据库配置和数据导入脚本其他代码基本不用动。这里提醒一句如果没有特殊需求不要为了“显得专业”强行上Redis、MongoDB之类技术选型要讲逻辑能用一句话说清楚“为什么用它”才是最好的。前端部分我选了Bootstrap加Django模板渲染没有做前后端分离。原因很简单项目核心价值在后端数据链路前端用模板渲染配合简单CSS就可以达到很好的展示效果还不用处理跨域和异步接口问题。等你想加可视化了再在模板里引入ECharts前后端不分离也能流畅搞定。1.3 整体架构与数据流向一张图看懂项目闭环整个项目可以拆成四个环节数据提取、数据存储、数据服务、数据展示。数据提取由独立的爬虫脚本完成爬取结果先清洗成结构化记录数据存储通过Django ORM写入数据库数据服务由Django的视图函数提供——接收前端请求、查询数据库、组织上下文数据展示由模板渲染完成输出表格、筛选控件和分页按钮。这四个环节的职责边界要划分清楚这是毕设项目中体现“工程思维”的关键。爬虫脚本不依赖Django的视图它只面向数据库视图不写任何SQL只通过ORM模型类操作数据模板只负责展示不处理业务逻辑。这样划分的好处是你想换一个数据源只改爬虫脚本和字段映射你想换一种展示效果只改模板和CSS你想加一个API接口给App用视图层加一个JSON响应就行整个项目的内聚性和可维护性都在线。数据流向具体来说爬虫通过requests请求榜单页面得到HTML后用BeautifulSoup按CSS选择器提取小说字段经过清洗和类型转换调用Django ORM的update_or_create写入数据库用户访问网站时视图从数据库检索数据按请求参数过滤和排序然后交给模板渲染成表格返回浏览器。整个过程就是“爬—洗—存—查—显”每个词都能对应到一个明确的技术实现点。2. 核心模块拆解爬虫、数据清洗、模型与页面交互2.1 爬虫模块的细节设计不只是一个requests加BeautifulSoup爬虫模块是整个项目“数据提取”的核心也是最容易被老师追问的部分。先看请求头起点对异常UAUser-Agent有一定概率拦截所以必须设置一个完整的浏览器标识包括User-Agent、Accept、Accept-Language甚至Referer。我的做法是构造一个request_headers字典每次发送请求都带上这样的目的不是“攻击”而是模拟正常浏览器访问降低请求被拒绝的概率。榜单分页也要先看清楚页面结构。起点Top500这种榜单通常有两种翻页方式一种是传统的分页链接一种是下拉加载或AJAX请求。我建议你先用浏览器开发者工具看Network面板确认数据是直接渲染在HTML里还是通过接口返回。如果是接口返回JSON爬虫就变成requests请求对应接口然后解析JSON反而比解析HTML更简单稳定。我当时遇到的是多分页HTML处理方法很简单——在URL中动态替换页码参数循环请求每一页控制每页条数最后汇总所有页面的数据。字段解析是爬虫的核心细节。我列出实际用到的字段和解析思路书名通常在一个带超链接的h4或p标签里作者一般在书名后面的第二个链接中简介是class为intro的段落分类和状态在小说信息区的链接或标签中字数和推荐量则需要从带单位的文本里提取数字。这里有个容易踩的坑字数和推荐量在页面上通常显示为“123.45万”或“1.2亿”直接取文本会出现非数字字符必须在清洗阶段转换成整数。目录爬取策略方面我用的是每本书只存两条最新章节信息用来显示“最近更新”而不是把整本书的章节目录全爬下来。这样既保留了数据的新鲜感又不会把爬虫复杂度和请求量翻几倍。如果你想让项目数据量更大、内容更“大数据”可以考虑单独爬取书籍详情页的完本字数、总收藏、总点击等指标这会让分析维度更丰富但对应也要处理更多页面的请求逻辑。爬虫的运行频率也要控制好。我是在脚本里加了一个随机延时函数每次请求之间睡眠2到3秒页面多时适当延长。具体的延时策略是在一个范围内随机取值目的是让行为更像人工操作也避免给目标站点造成压力。批量抓取时还要设置超时和重试机制requests的timeout参数我习惯设为10秒遇到网络异常用try-except捕获后重试三次连续失败就跳过当前页打印日志不让整个任务中断在某个坏页面上。2.2 数据清洗与入库策略别让脏数据毁掉你的页面爬到原始数据只是第一步清洗才是真正决定页面是否美观、数据是否可信的关键。我在清洗阶段做了四件事第一是把“字数”和“推荐数”里的“万字”“亿”“万”等字符去掉统一换算成整数存储第二是把简介前后空格和多余空白符去掉长度超过一定范围时截断避免页面排版被超长简介撑破第三是分类文本归一化把“玄幻”“都市”“历史”等不同来源的标签统一成标准格式第四是去重用书名加作者作为唯一条件避免重复爬取导致的重复记录。去重和更新的策略我建议用update_or_create而不是get_or_create。两者的区别在名字里能看出来get_or_create是“有就不动没有就新建”适合一次性导入update_or_create是“有就更新没有就插入”适合重复爬取时保持数据最新。榜单数据是会变动的同一本书昨天的推荐数和今天不一样如果用get_or_create历史数据就永远是第一次爬取时的样子看起来非常假。我用update_or_create以title和author作为查询条件每次爬完更新其余字段榜单就一直保持“新鲜”。关于批量入库我之前也考虑过bulk_create它确实是批量插入性能最优的方案。但当时的使用场景是“每次抓完一批更新已有数据”bulk_create对更新场景支持不好而且它绕过了Django模型的一些校验和信号机制。所以我选择了逐条update_or_create。实际跑下来500条数据入库也就几秒钟时间性能完全不是问题。如果你以后要爬数万条数据再考虑把更新逻辑改成批量update加小批量insert的组合现阶段不要为了“优化”牺牲代码的清晰度。2.3 Django数据模型设计字段是数据提取的“最终落点”数据模型的设计决定了后续查询和展示的灵活性。我定义的模型叫Novel字段包括标题、作者、分类、简介、字数、状态、周推荐、总推荐、最近更新章节、来源URL、创建时间和更新时间。这里重点说几个字段的选择理由。字数、周推荐、总推荐这三个字段用IntegerField而不是把原文本直接存成字符串。为什么因为用户后续要“按推荐量排序”和“按字数筛选”如果字段是字符串排序结果会是按字典序出现“99999大于9999999”这种逻辑错误用整数存Django的order_by和filter才能正确工作。简介字段用TextField因为简介可能超过CharField的默认长度限制TextField没有长度限制配合截断逻辑使用更安全。书名和作者用CharField加max_length200足够来源URL也用CharField存因为URL通常是规范字符串不需要长文本类型。在Meta里我设置了ordering [-total_recommend]让默认查询按总推荐量降序返回。这样做的好处是列表页打开就是Top500的真实顺序不需要每次手动order_by。同时我设置了db_indexTrue在有高频率查询的字段上比如分类和总推荐量。索引的作用是让数据库在排序和筛选时不用全表扫描。500条数据加不加索引几乎没有感知差别但我还是加了一是养成规范习惯二是万一以后数据量上来了这个设计能直接兜住。2.4 页面交互与榜单展示逻辑筛选、排序、分页不能少页面交互这块我实现了三个核心功能分类筛选、排序切换、分页。分类筛选用的是Django的filter(categoryxxx)排序切换是通过GET参数sort控制order_by的字段分页用的是Django自带的Paginator。这三个功能看起来简单但它们共同构成了“用户能对500条数据进行有效操作”的完整体验也正好对应了“数据管理”这个毕设主题。一个容易被忽略的细节是组合条件查询。用户选择了“玄幻”分类后如果还想按“字数”排序不能只处理sort参数否则分类条件就丢了。我的做法是在视图函数里先构造一个filters {}字典然后把非空的筛选参数放进去再统一用Novel.objects.filter(**filters).order_by(sort_field)这样多个筛选条件可以叠加URL参数也不会相互覆盖。如果你用Django的Q对象也可以实现类似效果但对于当前字段量filter(**filters)更直白。分页时我用Paginator(queryset, per_page15)每页显示15条。都问Django分页器会计算总页数和当前页数据模板中只需要遍历page_obj.object_list再根据page_obj.has_previous和page_obj.has_next渲染上一页下一页按钮。这里有坑要注意当用户点击第二页时URL要保留之前的分类和排序参数否则翻页后筛选状态就丢了。我的处理方式是手动拼接分页链接把request.GET中除了page以外的参数一并带上。3. 实操全过程从创建Django工程到页面跑通3.1 环境准备与工程初始化一次把Django项目骨架搭好动手之前需要确认本机Python环境。推荐使用Python 3.8及以上如果你用的是Windows安装Python时记得勾选“Add Python to PATH”否则后续命令行里执行python可能报错。装好Python后建议用虚拟环境隔离项目依赖。虚拟环境的核心价值是让不同项目之间的依赖不互相影响命令也简单先执行python -m venv venvWindows下运行激活脚本Linux和macOS下用source激活。在虚拟环境激活状态下执行pip install django requests beautifulsoup4 lxml这就是本项目全部核心依赖。lxml是BeautifulSoup的解析器比Python自带的html.parser快很多处理大页面体感更明显。装完后在目标目录下创建工程和应用。创建应用的名称我建议叫novels语义清晰后面写代码时不会自己都绕晕。创建工程和应用之后还要在settings.py里把novels添加到INSTALLED_APPS列表同时我推荐把LANGUAGE_CODE设置为zh-hansTIME_ZONE设置为Asia/ShanghaiUSE_TZ设置为False。这些配置能让Admin后台显示中文且时间字段保存的是本地时间而不是UTC时间。新手很容易忽略这个配置最后展示时间总是差了8小时查半天查不出原因。3.2 爬虫脚本与数据入库实操核心代码一次讲清楚爬虫脚本我单独放在项目根目录下的spider.py里不放在Django应用内部。它的结构是设置Django启动环境定义请求头和辅助函数解析HTML并构造字段字典调用Novel模型入库最后打印统计信息。关键代码是这一段我加注释方便你直接理解import os import time import random import requests from bs4 import BeautifulSoup # 让爬虫脚本能独立使用Django ORM os.environ.setdefault(DJANGO_SETTINGS_MODULE, novel_project.settings) import django django.setup() from novels.models import Novel # 必须在django.setup()之后导入模型 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.6,en;q0.4, } def parse_page(html): soup BeautifulSoup(html, lxml) books [] for item in soup.select(.book-list li): # 具体选择器以实际页面为准 title_node item.select_one(.name) author_node item.select_one(.author) intro_node item.select_one(.intro) ... # 其余字段按实际结构提取 books.append({ title: title_node.get_text(stripTrue), author: author_node.get_text(stripTrue), text: intro_node.get_text(stripTrue) if intro_node else , }) return books def clean_number(raw): # 把 1.23万/4567/1.2亿 统一转成整数 if not raw: return 0 raw raw.strip() if 万 in raw: return int(float(raw.replace(万, )) * 10000) if 亿 in raw: return int(float(raw.replace(亿, )) * 100000000) return int(raw)入库的核心是update_or_create我称它为“爬虫和Django握手”的关键一行title和author作为唯一匹配条件其他字段作为更新数据。每次爬完全部页面后打印新增和更新的记录数。这样你可以直观看到脚本到底干了多少活也方便排查某些书籍是否解析失败。运行爬虫脚本时直接在项目根目录执行python脚本会自动读取settings配置并连接数据库。如果你中途想清空重爬可以在Django shell里执行删除语句Django执行查询删除对象非常简单一行代码就能搞定注意删除前确认你已经导出或备份了需要的数据。3.3 视图、路由与模板的实现把数据变成用户能看的页面视图函数是连接数据库和模板的桥梁。我写了两个视图一个是小说列表页支持分类筛选、排序和分页一个是小说详情页展示该书的完整信息。列表页的核心逻辑是接收GET参数、构造查询条件、执行排序、实例化分页器、把数据塞进上下文。一个简洁的列表视图可以这样写def novel_list(request): filter_kwargs {} sort_field -total_recommend category request.GET.get(category, ) if category: filter_kwargs[category] category sort request.GET.get(sort, ) if sort in [title, author, word_count, -word_count, total_recommend, -total_recommend]: sort_field sort book_list Novel.objects.filter(**filter_kwargs).order_by(sort_field) paginator Paginator(book_list, 15) page_number request.GET.get(page) page_obj paginator.get_page(page_number) categories Novel.objects.values_list(category, flatTrue).distinct() return render(request, novels/list.html, { page_obj: page_obj, categories: categories, current_category: category, current_sort: sort, })这里有个细节我对sort字段做了白名单校验。为什么不直接信任用户的sort参数因为order_by接受的是字段名字符串如果允许任意值用户传一个不存在的字段会直接报错传一个数据库敏感字段虽然在当前场景风险不大但养成“永远不要盲目信任用户输入”的习惯对写代码的人来说很重要。路由配置同样简单清晰列表页挂在path(, views.novel_list)和path(novels/, views.novel_list)上详情页挂在path(novel/, views.novel_detail)上。Django捕捉URL参数用的是尖括号语法如果以后要支持按书名跳转可以改成str转换器。模板部分我用Bootstrap写了一个响应式表格表头展示书名、作者、分类、字数、状态、周推荐、总推荐表格上方放分类筛选下拉框和排序下拉框提交时用GET表单这样用户的选择会拼在URL里同时保留分页参数。分页按钮组放在表格底部点击下一页时用JavaScript从当前URL参数字符串里替换page的值然后再跳转。字体和间距用Bootstrap默认样式即可不必把时间花在花哨的CSS上。3.4 数据库初始化与页面测试跑通“处处不漏”的完整流程一切代码写完后先执行python manage.py makemigrations再执行python manage.py migrate。这两条命令分别负责生成迁移文件和把迁移应用到数据库缺一不可。新手容易漏掉makemigrations就跑去migrate结果根本不会建表。顺便说一句如果你改了模型字段一定要重新执行这两条命令Django的ORM不会自动同步数据库结构。接下来把爬虫脚本跑一遍数据库里就会写入500条左右记录。验证写入是否成功可以在Django shell里查询记录总数或者直接登录Admin后台查看。然后启动开发服务器python manage.py runserver浏览器访问本地地址看到列表页就说明基本跑通了。测试时重点关注几件事第一页面默认是否按总推荐量降序排列第二切到玄幻分类再按字数排序筛选条件是否生效第三翻到第二页时分类和排序参数是否还在URL里第四详情页能不能正常打开简介和来源URL是否显示正常。把这几条走一遍才是真正意义上的“全链路验收”。如果发现某个字段大量为0或为空说明爬虫的解析选择器可能写错了回到解析模块去修比在视图层打补丁更合理。4. 常见问题排查与项目落地避坑实录4.1 爬虫类问题请求被拒、编码乱码、字段解析失败爬虫是翻车率最高的模块我在调试时遇到的第一类问题就是“请求被拒”。表现得非常直接HTML里什么书都没有只有提示“访问异常”之类的文字。排查方法很顺手先看返回状态码如果是403几乎可以确定是请求头不够“真人”接着可以把headers里所有字段补全重点看User-Agent和Accept-Language。还有一个小技巧是带着Cookie去请求——你先在浏览器里打开榜单页从开发者工具里复制Cookie字符串加到headers里不过要注意Cookie会过期演示时常带常新。第二类是乱码。我在Windows下用requests请求中文网站时遇到过返回的response.text打印出来全是问号。解决方法是在拿到响应后手动指定编码requests会猜测编码但猜测未必正确。最稳妥的是直接根据页面charset设置response.encoding比如utf-8或gbk然后再取text。起点页面本身就是UTF-8所以设置response.encoding utf-8基本能解决。第三类是字段解析不完整症状是某些行的作者或简介为空多半是页面结构调整或某些书籍缺少对应的DOM节点。我采取的兜底方案是用try-except包住每个字段的提取提取失败就给默认值并打一条日志记录绝不因为单本书的问题中断整个抓取流程。4.2 Django与数据库问题中文乱码、ORM查询报错、迁移不同步Django后台出现中文乱码主要集中在两个地方一个是数据库写入乱码另一个是页面渲染乱码。数据库写入乱码在MySQL上比较常见通常是因为数据库或表的字符集不是utf8mb4需要在创建数据库时显式指定字符集。SQLite则几乎不会遇到这个编码问题。页面渲染乱码比数据库乱码更少见如果你已经在模板里加了 还乱码检查一下是不是视图返回的响应头被手动设置了错误编码。ORM查询报错集中在两类一是字段名写错Django里模型字段名和数据库列名不完全一样报错信息里只要看清“Unknown column”或“no such column”后面跟的是哪个字段对照模型定义就能定位。二是查询条件类型不匹配比如给IntegerField传了一个字符串Django会尝试隐式转换转换失败就会抛ValueError。我的经验是遇到ORM报错不要慌先把报错信息里提到的模型和字段对一遍大多数问题都在这一层。还有一个坑是迁移不同步。你改了模型执行makemigrations时提示“No changes detected”但数据库里根本没有新字段。这种情况通常是忘了把APP注册到INSTALLED_APPSDjango不会扫描未注册的APP下的模型。遇到这种问题先检查APP注册再重新执行makemigrations基本都能解决。4.3 页面性能与大数据量场景从表格卡顿到分页索引的优化思路有朋友问过我“数据量一大页面表格就卡顿怎么办”这个项目虽然只有500条但网吧式的大数据量表格卡顿问题其实是同一个思路。很多人的第一反应是加缓存其实第一步应该看是不是把全部数据一次性查出来了。500条数据一次渲染可能感觉不明显数据量到5000、50000的时候页面延迟会非常直观。解决表格卡顿的关键是“不要让数据库把所有数据都吐给模板”。分页是最基础的优化手段Django内置Paginator就可以让每页只查询15条数据浏览器只渲染15行这个改动对性能的改善是最立竿见影的。我在项目里把每页设为15条既保证了阅读体验又给页面留下了大量白边空间。第二个优化点是给查询加索引。如果你的用户频繁按某个字段排序和筛选建议给该字段加上db_indexTrue。实现上ORM的查询条件会变成数据库的WHERE和ORDER BY没有索引时是文件排序有索引后是索引排序在数据量增大时差别很明显。第三个优化点是避免N1查询这个在Django中对应的就是select_related和prefetch_related。如果Novel表关联了其他外键表在列表页查询时要一次性用join把关联数据带出来否则每渲染一行都要额外执行一次数据库查询页面会明显变慢。4.4 部署、演示与答辩让项目“拿得出手”的几个关键动作答辩前的部署和演示环节很多人会吃亏。我的建议是不要依赖本地IDE运行演示而是准备一个可以离线启动的流程。如果答辩教室网络不稳定爬虫演练可能翻车我的做法是提前把数据抓全并写入数据库答辩时只演示页面功能爬虫部分用代码和运行日志做佐证。这样做不是为了投机取巧而是因为毕设答辩的核心是让评审看见你的系统能稳定工作。数据导出也是一个好用的加分项。我在项目里加了CSV导出功能视图返回一个HttpResponse设置Content-Type为text/csv把查询结果逐行写入浏览器点击按钮就能直接下载Excel能打开的CSV文件。这个功能实现成本很低但在答辩时展示“数据的可迁移性”很有效。答辩老师常问的问题我总结下来集中在这几个为什么选择这个技术栈、爬虫遇到反爬怎么处理、数据更新策略是什么、Web框架和爬虫如何衔接、有没有考虑过数据量增长到几百万条之后的架构变化。这些问题没有标准答案但如果你能按项目实际设计来答再带出扩展思考比如引入消息队列、分布式爬虫、增加数据分析模块通常都会被认可。从搭建骨架到数据入库再到页面渲染和性能优化这个项目的每一步都可以作为你毕设解说词里的素材。我个人实际操作下来的体会是这个项目最值得做的地方不是“爬虫加网站”这个组合本身而是它把数据提取的完整链路——从源头抓取、清洗、持久化到对外服务——真正走通了。很多人学完爬虫只会打印到控制台学完Django只会做TodoList而这个小项目把两件事捏成了一个整体你的知识也在这个过程中完成了串联。最后再分享一个小技巧调试爬虫解析字段时不要一遍遍打印整个HTML先用BeautifulSoup的select_one方法把目标元素选出来打印它的get_text()结果确认目标字段的文本是什么形态再决定怎么清洗。大部分解析问题不是“代码不会写”而是“不知道页面长什么样”先把页面结构摸清楚爬虫写起来会顺畅得多。这个项目后续扩展的方向也很多——加上ECharts的推荐量分布图改成DRF加Vue的前后端分离或者把数据源扩展到其他榜单源都是值得尝试的路子祝你把这段毕设之路走得比想象中更顺。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进