Python爬虫实战:从小说网站数据采集到可视化分析全流程

发布时间:2026/7/15 5:55:54

Python爬虫实战:从小说网站数据采集到可视化分析全流程 1. 项目概述从数据到洞察一个完整的网文分析实战最近几年网络文学的数据量呈爆炸式增长每天都有海量的新作品上线。无论是作为读者想找一本好书还是作为内容创作者想了解市场风向亦或是作为研究者想分析文化趋势这些数据都蕴含着巨大的价值。但问题来了这些数据散落在各个小说网站的页面上我们怎么才能把它们系统地收集起来并从中提炼出有用的信息呢这就是我们今天要聊的实战项目用Python爬取小说网站数据并进行可视化分析。这不仅仅是一个简单的“爬虫画图”的练习而是一个从数据采集、清洗、存储到分析、展示的完整数据流水线构建过程。我做过不少类似的项目发现这套流程不仅能帮你深入理解Python在数据处理领域的应用其产出的结果——比如哪个题材最火、哪位作者上升势头最猛——对于实际决策也很有参考意义。整个项目会涉及几个核心环节首先我们需要选定一个目标网站比如17K小说网并分析其页面结构制定爬取策略。然后我们会用requests库模拟浏览器请求用BeautifulSoup或lxml解析HTML把小说名、作者、类型、点击量、简介等关键信息“抓”下来。接着这些原始数据往往比较杂乱需要用pandas进行清洗和整理并存到数据库比如轻量级的SQLite里。最后也是最能体现价值的一步就是用matplotlib、seaborn或者更炫酷的Pyecharts把数据变成直观的图表比如用柱状图看各类型小说的数量分布用折线图追踪作品的月票趋势甚至用词云图来展现热门题材的关键词。无论你是刚学完Python基础想找个综合项目练手还是数据分析的初学者想了解完整的数据处理流程甚至是需要做一个课程设计或毕业设计这个项目都能提供一个清晰的框架和丰富的实践点。接下来我就手把手带你走一遍这个流程并分享一些我爬取小说网站时踩过的坑和总结的技巧。2. 核心思路与技术选型为什么是这套组合拳在动手写代码之前花点时间想清楚“为什么这么做”至关重要。这能帮你避开很多后期的大坑。对于这个小说数据爬取与分析项目我们的核心目标是高效、稳定、合规地获取数据并清晰地呈现分析结果。围绕这个目标我选择了下面这套技术方案并逐一解释背后的考量。2.1 爬虫层Requests BeautifulSoup 的黄金搭档为什么不直接用Scrapy这样功能强大的框架对于小说网站这类结构相对固定、页面数量可控通常是列表页详情页的场景Requests和BeautifulSoup的组合更加轻量、灵活学习曲线也平缓得多。Requests库负责网络通信。它的API设计非常人性化几行代码就能完成GET/POST请求、添加请求头、处理Cookie等操作。对于小说网站我们主要使用GET请求来获取网页HTML内容。BeautifulSoup库负责解析HTML。小说网站的信息都嵌套在复杂的HTML标签里BeautifulSoup能让我们用类似find()、select()这样直观的方法像剥洋葱一样把小说标题、作者这些信息精准地提取出来。它的容错性也比直接使用正则表达式好得多。注意选择解析器时我推荐lxml。虽然需要额外安装pip install lxml但它的解析速度和稳定性远高于Python内置的html.parser在处理大量页面时优势明显。为什么需要考虑反爬这是实战和教程最大的区别。小说网站为了保护服务器资源和数据通常会设置一些反爬机制。最常见的有User-Agent检测检查请求是否来自真实的浏览器。解决方案很简单用Requests的headers参数模拟一个主流浏览器的UA即可。请求频率限制如果爬取速度太快IP可能会被暂时封禁。因此必须在爬虫代码中加入time.sleep()进行延时模拟人类浏览的间隔。一个比较安全的间隔是2-5秒。动态加载内容有些网站的数据是通过JavaScript异步加载的直接拿到的HTML里没有。这时需要分析网络请求找到真正的数据接口通常是XHR请求用Requests直接调用接口。如果接口参数复杂可以考虑使用Selenium这类自动化测试工具来模拟浏览器行为但会牺牲速度。2.2 数据处理与存储层Pandas SQLite 的轻量组合爬下来的原始数据是“脏”的比如会有空白字符、格式不统一、甚至缺失值。直接用来分析会出问题。Pandas库数据清洗和处理的瑞士军刀。我们可以把爬取的数据先放到Pandas的DataFrame里它提供了极其强大的数据操作功能。例如df.drop_duplicates()去除重复的小说条目。df.fillna()或df.dropna()处理缺失值比如没有作者信息的记录。df[column].str.strip()清除字符串两端的空格。df[clicks] pd.to_numeric(df[clicks], errorscoerce)将点击量字符串转换为数值类型便于后续计算。SQLite数据库为什么不用文件如CSV直接存储对于小型到中型项目SQLite是一个完美的选择。它是一个无需单独服务器进程的、文件式的数据库。使用Python内置的sqlite3模块或通过Pandas的to_sql方法可以轻松地将清洗后的DataFrame持久化到本地一个.db文件中。这带来了几个好处数据结构化存储方便用SQL进行复杂查询支持增量更新下次爬取时可以只更新新增或变动的数据而不用全部重爬为后续的Web可视化系统如用Django提供了直接的数据源。2.3 可视化层Matplotlib/Seaborn 与 Pyecharts 的取舍可视化是让数据说话的最后一环。这里有两个主流方向Matplotlib Seaborn这是Python科学计算领域的标准配置。Matplotlib功能强大且高度可定制但默认样式比较朴素。Seaborn基于Matplotlib提供了更美观的统计图形和更简洁的API。如果你需要生成静态的、用于报告或论文的出版级图表或者进行深入的统计分析这个组合是首选。它的学习资源也极其丰富。Pyecharts这是一个生成Echarts图表的Python库。Echarts是一个由百度开源的、非常强大的JavaScript可视化库。Pyecharts的优势在于能生成交互式的HTML图表。你可以用鼠标悬停查看数据详情、缩放、拖拽图表效果非常炫酷。如果你最终想将分析结果部署成一个简单的网页报告或者希望图表有更强的交互性Pyecharts是更好的选择。它和Web前端技术的结合也更顺畅。我的建议对于初学者可以从Matplotlib开始先掌握基本的条形图、折线图、散点图绘制。当需要更美观的统计图表时引入Seaborn。如果项目要求是生成一个可交互的HTML分析报告那么直接学习Pyecharts会更高效。在本项目中为了展示的多样性我们可以两者都用。3. 实战第一步爬虫的精细设计与实现理论说再多不如一行代码。我们现在就以一个典型的小说网站列表页和详情页为例来构建我们的爬虫。请注意以下代码示例仅用于教学演示实际爬取前请务必查看目标网站的robots.txt文件并尊重网站的爬虫协议控制爬取速度避免对对方服务器造成压力。3.1 环境准备与页面结构分析首先安装必要的库pip install requests beautifulsoup4 lxml pandas假设我们要爬取的目标是小说网站的“玄幻奇幻”分类列表页。第一步不是写代码而是手动打开浏览器使用开发者工具F12进行分析。分析列表页打开分类列表页例如https://www.example.com/fantasy/。查看网页源代码找到小说条目所在的HTML结构。通常每个小说会包裹在一个div或li标签里里面包含了书名可能是一个a标签链接到详情页、作者、简介片段等信息。我们需要找到这些标签的CSS选择器或类名如.book-item。分析详情页点击一本小说进入详情页。同样分析详情页的源代码找到小说完整简介、总点击量、月票数、收藏数等详细数据的HTML位置。这个分析过程是爬虫成功的关键。我习惯用浏览器的“检查”功能直接点击页面元素查看其对应的HTML代码和CSS路径。3.2 编写基础爬取函数下面我们编写一个爬取单页列表的函数。这里我们使用一个模拟的URL和HTML结构进行演示。import requests from bs4 import BeautifulSoup import pandas as pd import time import random def fetch_book_list_from_page(page_url): 从小说列表页爬取一页的基本书籍信息。 # 1. 设置请求头模拟浏览器 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } try: # 2. 发送GET请求 response requests.get(page_url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出异常 response.encoding response.apparent_encoding # 自动识别编码 except requests.RequestException as e: print(f请求页面失败: {page_url}, 错误: {e}) return [] # 3. 使用BeautifulSoup解析HTML soup BeautifulSoup(response.text, lxml) # 4. 假设每本小说都在 classbook-item 的div中 book_items soup.find_all(div, class_book-item) books_on_page [] for item in book_items: book_info {} # 提取书名和详情页链接 (假设在a classtitle标签里) title_tag item.find(a, class_title) if title_tag: book_info[title] title_tag.get_text(stripTrue) book_info[detail_url] title_tag.get(href) # 处理相对链接 if book_info[detail_url] and not book_info[detail_url].startswith(http): # 这里需要根据目标网站实际情况拼接基础URL book_info[detail_url] https://www.example.com book_info[detail_url] else: book_info[title] None book_info[detail_url] None # 提取作者 (假设在span classauthor标签里) author_tag item.find(span, class_author) book_info[author] author_tag.get_text(stripTrue) if author_tag else None # 提取简介片段 (假设在p classintro标签里) intro_tag item.find(p, class_intro) book_info[brief_intro] intro_tag.get_text(stripTrue) if intro_tag else None # 提取类型 (可能从页面URL或分类标签获取这里假设列表页就是一个分类) book_info[category] 玄幻奇幻 # 实际中可以从页面或URL解析 # 只有获取到有效书名和链接的才加入列表 if book_info[title] and book_info[detail_url]: books_on_page.append(book_info) print(f从 {page_url} 成功提取 {len(books_on_page)} 本书籍信息。) return books_on_page # 测试单页爬取 test_url https://www.example.com/fantasy/list_1.html page_books fetch_book_list_from_page(test_url) print(page_books[:2]) # 打印前两本看看3.3 深入详情页与处理翻页列表页通常只提供基本信息更详细的数据如总点击量、评分、完整简介在详情页。我们需要编写另一个函数来爬取详情页信息并与列表页信息合并。def fetch_book_detail(detail_url): 爬取单本小说的详情页信息。 headers {User-Agent: ...} # 同上 detail_info {} try: resp requests.get(detail_url, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding except requests.RequestException as e: print(f请求详情页失败: {detail_url}, 错误: {e}) return detail_info soup_detail BeautifulSoup(resp.text, lxml) # 假设完整简介在 div idfull-intro 里 full_intro_tag soup_detail.find(div, idfull-intro) detail_info[full_introduction] full_intro_tag.get_text(stripTrue) if full_intro_tag else None # 假设总点击量在 span classtotal-clicks 里 clicks_tag soup_detail.find(span, class_total-clicks) if clicks_tag: # 清洗数据例如“总点击1,234,567” - 1234567 clicks_text clicks_tag.get_text(stripTrue) import re numbers re.findall(r\d, clicks_text.replace(,, )) detail_info[total_clicks] int(numbers[0]) if numbers else 0 else: detail_info[total_clicks] 0 # 可以继续提取月票、收藏、字数等信息... # detail_info[monthly_tickets] ... # detail_info[word_count] ... # 非常重要随机延时避免请求过快 time.sleep(random.uniform(1, 3)) # 随机等待1-3秒 return detail_info现在我们需要一个主函数来协调整个爬取流程遍历多个列表页对每本书再爬取其详情页。def main_crawler(base_list_url, max_pages5): 主爬虫函数爬取多页列表及详情。 base_list_url: 列表页基础URL例如 https://www.example.com/fantasy/list_{}.html max_pages: 要爬取的列表页数量 all_books_data [] for page_num in range(1, max_pages 1): print(f正在爬取第 {page_num} 页列表...) list_url base_list_url.format(page_num) page_books fetch_book_list_from_page(list_url) for book in page_books: detail_url book.get(detail_url) if detail_url: print(f 正在爬取详情页: {book[title]}) detail_info fetch_book_detail(detail_url) # 合并列表页和详情页信息 book.update(detail_info) all_books_data.append(book) else: print(f 跳过 {book[title]}无详情页链接。) # 每爬完一页列表也做一个稍长的延时 time.sleep(random.uniform(2, 4)) print(f爬取完成共获取 {len(all_books_data)} 本书籍的完整信息。) return all_books_data # 假设网站列表页URL模式如此 base_url https://www.example.com/fantasy/list_{}.html books_data main_crawler(base_url, max_pages3) # 先爬3页试试 # 转换为DataFrame df_books pd.DataFrame(books_data) print(df_books.head()) print(df_books.info())3.4 数据清洗与存储爬取下来的DataFrame通常需要清洗。def clean_book_data(df): 清洗书籍数据DataFrame。 df_clean df.copy() # 1. 去除完全重复的行基于所有列 initial_count len(df_clean) df_clean df_clean.drop_duplicates() print(f去重: 从 {initial_count} 行减少到 {len(df_clean)} 行。) # 2. 处理缺失值 # 对于作者缺失的填充为‘未知’ df_clean[author].fillna(未知, inplaceTrue) # 对于点击量为0或缺失的可以视为0或者根据情况删除这里我们保留 df_clean[total_clicks].fillna(0, inplaceTrue) # 3. 统一格式确保点击量是整数类型 df_clean[total_clicks] pd.to_numeric(df_clean[total_clicks], errorscoerce).fillna(0).astype(int) # 4. 创建新特征例如根据点击量划分热度等级 # 使用分位数划分 click_bins [0, 1000, 10000, 100000, df_clean[total_clicks].max()] click_labels [冷门, 一般, 热门, 爆款] df_clean[popularity_level] pd.cut(df_clean[total_clicks], binsclick_bins, labelsclick_labels, include_lowestTrue) return df_clean df_cleaned clean_book_data(df_books) # 存储到CSV简单备份 df_cleaned.to_csv(novel_data_cleaned.csv, indexFalse, encodingutf-8-sig) # 存储到SQLite数据库更结构化便于后续复杂查询和Web应用调用 import sqlite3 conn sqlite3.connect(novels.db) df_cleaned.to_sql(books, conn, if_existsreplace, indexFalse) # replace模式会覆盖旧表 conn.close() print(数据已清洗并保存至 CSV 和 SQLite 数据库。)实操心得在爬虫开发中异常处理和日志记录至关重要。网络请求可能超时页面结构可能微调你的代码必须足够健壮。除了try...except建议将关键步骤如开始爬取某页、成功获取某本书、遇到错误打印出来或写入日志文件这样当程序运行几个小时中断时你也能知道断在哪里方便续爬。另外将爬取的数据分批、定时保存比如每爬完50本书存一次也是一个好习惯防止程序崩溃导致所有数据丢失。4. 数据可视化分析让数据开口说话数据已经规规矩矩地躺在数据库里了现在是时候让它们“可视化”直观地告诉我们一些故事了。我们将分别使用Matplotlib/Seaborn和Pyecharts来实现。4.1 使用 Matplotlib Seaborn 进行静态分析首先安装如果尚未安装pip install matplotlib seaborn假设我们想分析以下几个问题不同类别小说的数量分布。小说点击量的分布情况是否存在头部效应。热门小说简介的词云。import matplotlib.pyplot as plt import seaborn as sns from wordcloud import WordCloud, STOPWORDS import jieba # 用于中文分词 import numpy as np # 设置中文字体和图表样式 plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 sns.set_style(whitegrid) # 1. 各类型小说数量分布柱状图 plt.figure(figsize(12, 6)) category_counts df_cleaned[category].value_counts() # 如果类别太多只显示前10 top_categories category_counts.head(10) bars plt.bar(top_categories.index, top_categories.values) plt.title(各类型小说数量TOP10, fontsize16) plt.xlabel(小说类型, fontsize12) plt.ylabel(数量, fontsize12) plt.xticks(rotation45) # 旋转x轴标签避免重叠 # 在柱子上方显示数量 for bar in bars: height bar.get_height() plt.text(bar.get_x() bar.get_width()/2., height 0.5, f{int(height)}, hacenter, vabottom, fontsize9) plt.tight_layout() plt.show() # 2. 小说点击量分布直方图与箱线图 fig, axes plt.subplots(1, 2, figsize(15, 5)) # 直方图取对数坐标因为点击量可能跨度极大 axes[0].hist(np.log1p(df_cleaned[total_clicks]), bins30, edgecolorblack, alpha0.7) # log1p log(x1) axes[0].set_title(小说点击量分布对数刻度, fontsize14) axes[0].set_xlabel(log(点击量1), fontsize12) axes[0].set_ylabel(频数, fontsize12) # 箱线图按热度等级分组 # 先确保‘popularity_level’是分类有序类型 df_cleaned[popularity_level] pd.Categorical(df_cleaned[popularity_level], categoriesclick_labels, orderedTrue) sns.boxplot(xpopularity_level, ytotal_clicks, datadf_cleaned, axaxes[1], showfliersFalse) # 不显示异常值点 axes[1].set_yscale(log) # y轴使用对数刻度 axes[1].set_title(各热度等级小说点击量箱线图对数y轴, fontsize14) axes[1].set_xlabel(热度等级, fontsize12) axes[1].set_ylabel(点击量 (log), fontsize12) plt.tight_layout() plt.show() # 3. 生成词云分析热门小说的简介 print(正在生成词云...) # 将所有小说的简介合并成一个长文本 all_intros .join(df_cleaned[brief_intro].dropna().astype(str).tolist()) # 使用jieba进行中文分词 word_list jieba.lcut(all_intros) text_for_wordcloud .join(word_list) # 设置停用词去除无意义的词 stopwords set(STOPWORDS) stopwords.update([小说, 一个, 没有, 什么, 怎么, 因为, 所以, 这是, 还有, 可以]) # 添加自定义停用词 wordcloud WordCloud( font_pathsimhei.ttf, # 指定中文字体路径否则会乱码 width800, height400, background_colorwhite, stopwordsstopwords, max_words150, contour_width1, contour_colorsteelblue ).generate(text_for_wordcloud) plt.figure(figsize(12, 8)) plt.imshow(wordcloud, interpolationbilinear) plt.axis(off) plt.title(热门小说简介词云, fontsize18) plt.tight_layout() plt.show()4.2 使用 Pyecharts 创建交互式仪表板静态图表适合报告交互式图表更适合探索。我们用Pyecharts来创建一个包含多个图表的HTML页面。首先安装pip install pyechartsfrom pyecharts import options as opts from pyecharts.charts import Bar, Pie, Line, WordCloud, Page from pyecharts.globals import ThemeType # 准备数据 # 1. 各类型小说数量前10 category_counts_dict top_categories.to_dict() # 2. 假设我们还有按月份统计的“月票榜”数据这里模拟 # 在实际项目中这部分数据也需要从网站爬取通常是另一个榜单页面 months [1月, 2月, 3月, 4月, 5月, 6月] # 模拟三部热门小说每月的月票数 book_a_tickets [1200, 1500, 1800, 2200, 2500, 3000] book_b_tickets [800, 950, 1100, 1300, 1450, 1600] book_c_tickets [500, 700, 900, 1200, 1500, 2000] # 3. 词云数据从简介中提取高频词这里简化处理 # 实际应用中需要先分词并统计词频 word_freq [(玄幻, 156), (修炼, 120), (重生, 98), (系统, 95), (都市, 88), (无敌, 85), (仙界, 80), (穿越, 78), (强者, 75), (宇宙, 70)] # 创建图表 # 图表1条形图 - 小说类型分布 bar ( Bar(init_optsopts.InitOpts(themeThemeType.LIGHT, width100%, height400px)) .add_xaxis(list(category_counts_dict.keys())) .add_yaxis(作品数量, list(category_counts_dict.values()), color#5470c6) .set_global_opts( title_optsopts.TitleOpts(title各类型小说数量TOP10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate-15)), # 标签旋转 datazoom_opts[opts.DataZoomOpts()], # 添加数据区域缩放 toolbox_optsopts.ToolboxOpts(), # 添加工具箱可下载图片等 ) .set_series_opts( label_optsopts.LabelOpts(is_showTrue, positiontop), markpoint_optsopts.MarkPointOpts( data[ opts.MarkPointItem(type_max, name最大值), opts.MarkPointItem(type_min, name最小值), ] ), ) ) # 图表2折线图 - 热门小说月票趋势 line ( Line(init_optsopts.InitOpts(width100%, height400px)) .add_xaxis(months) .add_yaxis(《剑来》, book_a_tickets, is_smoothTrue, linestyle_optsopts.LineStyleOpts(width3)) .add_yaxis(《诡秘之主》, book_b_tickets, is_smoothTrue) .add_yaxis(《大奉打更人》, book_c_tickets, is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title热门小说月票趋势模拟数据), tooltip_optsopts.TooltipOpts(triggeraxis), legend_optsopts.LegendOpts(pos_top10%), toolbox_optsopts.ToolboxOpts(), ) ) # 图表3饼图 - 热度等级分布 pie_data df_cleaned[popularity_level].value_counts().items() pie ( Pie(init_optsopts.InitOpts(width50%, height400px)) .add( , [list(z) for z in pie_data], radius[30%, 75%], # 环形图 center[50%, 50%], ) .set_global_opts( title_optsopts.TitleOpts(title小说热度等级分布), legend_optsopts.LegendOpts(orientvertical, pos_top15%, pos_left2%), ) .set_series_opts( tooltip_optsopts.TooltipOpts( triggeritem, formatter{a} br/{b}: {c} ({d}%) ), label_optsopts.LabelOpts(formatter{b}: {d}%), ) ) # 图表4词云 wc ( WordCloud(init_optsopts.InitOpts(width100%, height400px)) .add( , word_freq, word_size_range[20, 80], shapecircle, # 词云形状可选diamond, triangle-forward等 ) .set_global_opts( title_optsopts.TitleOpts(title小说题材关键词云), toolbox_optsopts.ToolboxOpts(), ) ) # 将图表组合到一个页面中 page Page(layoutPage.SimplePageLayout) # 简单垂直布局 page.add(bar, line, pie, wc) # 渲染成HTML文件 page.render(novel_analysis_dashboard.html) print(交互式仪表板已生成请用浏览器打开 noval_analysis_dashboard.html 查看。)运行这段代码后会生成一个novel_analysis_dashboard.html文件。用浏览器打开它你会看到一个包含四个可交互图表的网页。你可以用鼠标悬停查看具体数值缩放图表甚至通过工具栏保存为图片。5. 常见问题与避坑指南实录在实际操作中你几乎一定会遇到下面这些问题。我把它们和解决方案整理出来希望能帮你节省大量调试时间。5.1 爬虫被屏蔽或返回异常数据问题代码运行一段时间后返回的状态码是403、404或者返回的HTML内容像是反爬提示页如“请验证”或空白页。排查与解决检查请求头确保User-Agent是有效的并且可以尝试添加Referer、Accept-Language等常见请求头使其更像浏览器。降低请求频率这是最可能的原因。立即增加time.sleep()的间隔时间比如从1秒增加到3-5秒并加入随机性random.uniform(2, 5)。使用IP代理如果网站对单一IP限制很严需要考虑使用代理IP池。有免费和付费的代理服务但免费代理往往不稳定。使用时代理格式proxies {http: http://your-proxy:port, https: https://your-proxy:port}然后在requests.get()中传入proxiesproxies。处理Cookie和Session有些网站需要登录或验证。你可以先用浏览器手动登录然后从开发者工具的Network标签中复制Cookie将其添加到请求头中。对于更复杂的交互可能需要使用requests.Session()来保持会话状态。解析动态内容如果所需数据不在初始HTML中而是在后续的JavaScript请求中加载的。你需要在开发者工具的“Network”面板中过滤XHR/Fetch请求找到真正返回数据的API接口。直接使用requests模拟调用这个API接口通常需要复制其请求头特别是可能有的X-Requested-With、Authorization等和请求参数可能是JSON格式。5.2 数据解析失败或提取为空问题BeautifulSoup的find或select方法返回None或空列表无法提取到数据。排查与解决确认选择器网站可能改版了。重新用开发者工具检查目标元素的HTML结构和CSS选择器是否还正确。有时类名是动态生成的包含随机字符串这时需要用部分匹配soup.find(div, class_lambda x: x and book-item in x)或用其他更稳定的属性如id、>

相关新闻