木鸟短租网爬虫课设:requests+BeautifulSoup+pandas全流程实战

发布时间:2026/9/17 6:01:12
木鸟短租网爬虫课设:requests+BeautifulSoup+pandas全流程实战 简介面向大学生数据采集与预处理课程设计的完整项目资料以木鸟短租网为实战对象内含爬虫源码和课程设计报告重点展示requests、BeautifulSoup、Scrapy、Selenium等技术的应用覆盖数据采集、反爬应对、清洗与预处理全流程。压缩包共35个文件含19张png图片页面截图、流程图等及组成Word报告的xml/rels组件整体仅4.85MB报告正文可编辑方便吸收改造。已有1755人学习下载适合课程设计、毕业设计或爬虫入门参考。项目从需求分析、架构设计到编码调试均有详细记录模块划分清晰排错思路完整能帮助读者快速搭建同类爬虫项目并提升数据处理能力。1. 木鸟短租网课设里的爬虫难点不在爬而在稳很多人把爬虫课设写成了「抓一个静态页面就交差」但放到木鸟短租网这个目标上事情要麻烦得多城市分站入口不统一、列表页走瀑布流加载、详情页的价格和面积混在中文文本里需要二次清洗。这套课程设计之所以值得拆是因为它把数据采集和预处理连成了一整条链路——requests 拿到 HTML、BeautifulSoup 抽字段、pandas 做清洗最后产出的 CSV 可以直接画价格分布图。对正在做数据采集课设、或者想从教材示例走向真实站点的人来说源码里的采集节奏控制、字段兜底和数据标准化思路都能直接抄进自己的项目。整套源码加课程设计报告的结构也能帮你在答辩时把「为什么这样设计」讲清楚。2. 短租平台的信息结构拆解与采集技术选型2.1 木鸟短租网的页面层级与字段清单木鸟短租网的站点结构是典型的三层城市分站首页进入区域列表再进入单个房源详情页。列表页在浏览器里看到的是瀑布流实际网络请求有两种可能一种是把前 N 条房源直接渲染进 HTML另一种是滚动后通过 XHR 接口加载后续数据。这套源码选择的是直接分析初始 HTML因为详情页的完整数据在 HTML 里都有不需要引入无头浏览器去模拟滚动。采集之前先把字段清单定下来否则边爬边想字段清洗阶段会很痛苦。参照源码里的字段定义按下面这张表规划字段来源页面示例值目标类型city分站入口 URLbeijingstrtitle列表页/详情页近地铁温馨大床房strlink列表页/fangyuan/12345.htmlstrprice详情页369/晚intarea详情页55平米floatlayout详情页1室1卫strrent_type详情页整租strmax_guests详情页可住2人intscore详情页4.8分floatcomments详情页126条点评int这套课设的合理之处在于把字段定义和解析逻辑分开字段清单确定后爬虫里只按字段名提取最后写 CSV 时用同一个字段列表列顺序就自动统一了。后面做数据分析时这个字段清单就是 DataFrame 的初始列名省掉大量对齐工作。2.2 为什么选 requests BeautifulSoup而不上 ScrapyScrapy 性能好、扩展多但课设场景里有个现实问题评分老师看的是代码能不能读懂、关键逻辑够不够清楚。Scrapy 的 pipeline、middleware、ItemLoader 这些概念在课设报告里的解释成本很高而且中间件配置一旦出错调试难度对新手不友好。这套源码选择 requests BeautifulSoup 组合就两条主线requests 负责拿页面BeautifulSoup 负责抽字段全程同步代码断点容易打。Selenium 也不是首选。房源详情数据在 HTML 里都有只有列表页的增量加载需要额外处理。如果列表页确实要滚动加载优先分析 XHR 接口参数而不是启动浏览器实例。原因很实际Selenium 启动浏览器实例的内存开销大驱动版本不匹配属于高频翻车点在课设里给自己加险不值得。我一般会定一个降级策略先用 requests 试静态 HTML遇到拿不到的字段再降级到 Selenium不要一上来就全链路浏览器模拟。2.3 robots.txt、采集频率与合规边界课程设计需要遵守目标站的 robots.txt这不是教条而是爬虫工程师的基本素养。正式采集前先请求目标站的 robots.txt把禁止采集的路径排除掉。这套源码遵守了常规约束只采集公开的房源展示信息不碰用户隐私数据不采集登录后才可见的内容。合规边界记三条一是频率上做限速不对目标服务器造成压力二是数据用途限定在教学演示不做商业化转售三是如果目标站返回明确的反爬响应应该停止采集而不是死磕绕过方案。这几条写进课程设计报告本身就是加分项。3. requests 会话管理、列表页解析与详情页字段抽取3.1 用 Session 维持连接与请求头构造采集的第一步是建立会话。使用 requests.Session 而不是裸 requests.get原因有两点一是 Session 会维持 Cookie某些站点的列表页可能先写 Cookie 再返回数据两次独立请求会漏二是 Session 内部复用 TCP 连接对高频采集更友好。源码中请求头的构造大致如下import requests HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.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.9, Referer: https://www.muniao.com/ } session requests.Session() session.headers.update(HEADERS)这里的 Headers 值得写全Referer 告诉服务端请求是从站内跳转来的避免直接被识别为外部爬虫User-Agent 保持与主流浏览器一致是成本最低的反爬规避手段。Session 创建后统一用session.get()发请求Cookie 和连接复用都在会话内完成比每次新建请求对象要规范。3.2 列表页的 BeautifulSoup 定位与翻页参数说明列表页拿到 HTML 后用 BeautifulSoup 的find_all定位房源卡片。关键是把卡片容器的 class 或 data 属性找准建议先存一份 HTML 到本地用浏览器 DevTools 确认选择器再写死这比反复跑程序验证高效得多。from bs4 import BeautifulSoup def parse_list(html, city): soup BeautifulSoup(html, html.parser) items [] cards soup.select(div.house-list div.house-item) for card in cards: a_tag card.select_one(a.house-title) link a_tag.get(href) title a_tag.get_text(stripTrue) if not link: continue items.append({ city: city, title: title, link: link, detail_url: https://www.muniao.com link }) return items参数要点说明select里的house-list、house-item是示例 class实际以抓下来的真实 HTML 为准。真实站点的类名多半带业务前缀或哈希后缀正因如此用真实 HTML 验证选择器这一步不能省a_tag.get(href)取出的是相对路径拼接detail_url时补了站点域名漏掉这一步会导致后续详情页请求全部 404if not link: continue是必要的兜底部分卡片可能是广告位或已下架房源没有链接就跳过不影响采集主流程。翻页处理分两种情况。普通分页直接循环页码参数即可瀑布流加载则在浏览器开发者工具里找到 XHR 请求通常是list?cityxxxoffset20这类接口返回 JSON 或 HTML 片段。处理方式一样用session.get()请求接口解析新增条目直到返回条目数为 0。3.3 详情页字段抽取的兜底写法详情页是数据的主战场。价格、面积、可住人数这些字段经常混在一段中文文本里抽取时用「选择器定位 正则二次清洗」的组合。源码里详情解析的核心函数结构如下import re def parse_detail(html): soup BeautifulSoup(html, html.parser) data {} # 价格从 369/晚 中提取 369 price_text soup.select_one(span.price-num) if price_text: match re.search(r(\d), price_text.get_text()) data[price] int(match.group(1)) if match else None # 面积从 55平米 提取 55.0 area_text soup.select_one(div.house-area) data[area] float(re.search(r(\d\.?\d*), area_text.get_text()).group(1)) if area_text else None # 可住人数从 可住2人 提取 2 guest_text soup.select_one(div.house-guest) match re.search(r(\d), guest_text.get_text()) if guest_text else None data[max_guests] int(match.group(1)) if match else None # 租赁方式 type_text soup.select_one(div.house-type) data[rent_type] type_text.get_text(stripTrue) if type_text else 未知 return data这个函数有两个值得直接抄走的设计。第一是每个字段都做了两层保护先判断节点是否存在再正则取值避免某个房源页面结构异常导致整个采集进程崩溃。第二是取不到值的字段统一落为 None而不是跳过该房源——记录一行缺失数据比静默漏掉一行数据更容易在清洗阶段定位问题。如果自己改这个函数建议把re.search的 pattern 提取为模块级常量后面调整正则比在函数里逐个翻要方便。采集主循环里每抓完一个详情页就追加写入一行并用 set 记录已访问的 URL防止列表页重复出现同一房源。数据写入用csv.DictWriter字段顺序就是 2.1 节里的字段清单这样一个 CSV 爬完就能直接交给 pandas不再需要手工调整列。4. 反爬信号识别、指数退避重试与断点续采4.1 被反爬拦截时的典型信号与排查方向课设过程中最怕的不是网站结构复杂而是程序还在跑数据却全是错的。判断是否被反爬看下面几个信号信号现象排查方法HTTP 状态码403、418、503 频繁出现检查 response.status_code写日志验证码跳转HTML 里出现 verify、captcha 关键词保存响应文本搜索关键词字段大面积为空详情页抽取结果全是 None打印原始 HTML确认是否被替换成反爬页请求全部超时Timeout 异常频率升高降低并发加大延时上面这张表在课程设计报告的「问题与解决」章节里几乎可以原样引用。如果遇到 403第一个排查点永远是请求头不齐全其次是访问频率过快。如果确认是限流唯一正确的处理是退避等待而不是换代理硬闯。课设场景下一旦被识别处理成本就会很高也会给目标站点带来不必要的负担按流程停采才合理。不要为了完成进度尝试绕过验证码机制必要时直接降级为只用本地已有数据完成报告。4.2 限速、随机延时与指数退避重试采集质量的核心指标是数据完整而不是速度快。源码里通过random.uniform生成随机延时避免固定间隔被流量特征识别对偶发的网络超时用指数退避重试来恢复。参考实现import random import time from requests.exceptions import RequestException def fetch_with_retry(session, url, max_retries3, base_delay2.0): for attempt in range(max_retries): try: resp session.get(url, timeout15) if resp.status_code 200: return resp.text elif resp.status_code in (403, 418): # 被反爬拦截继续重试没有意义 raise RuntimeError(fblocked by anti-crawler: {resp.status_code}) else: print(fhttp {resp.status_code}, retry {attempt 1}) except RequestException as exc: print(frequest error: {exc}, retry {attempt 1}) # 指数退避2s, 4s, 8s delay base_delay * (2 ** attempt) random.uniform(0, 1) time.sleep(delay) return None参数含义拆开说base_delay2.0是第一次重试前的等待基数每次重试翻倍第三次等待约 8 秒timeout15给每个请求 15 秒上限避免某个房源页卡死整条链路重试次数 3 次是采集场景默认值超过就返回 None由上层决定是否记录失败日志。末尾的random.uniform(0, 1)很关键——纯指数退避的等待时间是确定值目标站点把每两次请求的时间间隔取出来对比能发现整齐的等比数列规律加一点随机抖动后重试间隔就不再可预测。4.3 断点续采中断不等于重来一个集中采集任务可能跑几十分钟甚至几个小时中途断电、断网、进程被杀都很常见。源码里用了一个小设计把这个问题解决掉在本地维护一份visited.txt每成功采完一个详情页就把 URL 追加写入。重启后先加载这个文件构建 visited 集合跳过已完成的 URL。核心逻辑简短但完整import os visited_file visited.txt def load_visited(): if not os.path.exists(visited_file): return set() with open(visited_file, encodingutf-8) as f: return {line.strip() for line in f if line.strip()} visited load_visited() # 采集某个详情页成功后执行 visited.add(detail_url) with open(visited_file, a, encodingutf-8) as f: f.write(detail_url \n)这个技巧对采集流程的价值在于失败重试解决的是单次请求失败visited 文件解决的是整个任务中断。两个机制配合起来数据采集才能稳定推进。在课设报告里把「中断 3 次后任务恢复且没有重复数据」作为测试结果写进去能明显体现对工程细节的把握。注意课设提交前删掉 visited.txt 和缓存文件保证运行环境干净。5. pandas 数据清洗从采集原始数据到标准化字段表5.1 读入原始数据、去重与空值盘点爬虫输出的 CSV 是半成品直接做分析会踩坑重复记录、字符串数字混在一列、分类字段叫法不统一。先从读数据开始import pandas as pd df pd.read_csv(muniao_raw.csv, encodingutf-8-sig) print(原始行数:, len(df)) print(重复行数:, df.duplicated(subset[link]).sum()) df df.drop_duplicates(subset[link]) df df.dropna(subset[title, link]) print(去重后行数:, len(df)) print(df.isna().sum())这段代码的要点用link作为去重键而不是整行去重因为整行去重无法处理「同一条房源在不同时间抓了两遍、价格字段有变动」的情况dropna(subset[title, link])只删除关键字段缺失的记录其他字段的缺失留给下一步单独处理。isna().sum()输出每一列的缺失数量把它写进课设报告用来展示清洗前数据质量问题的证据。5.2 价格、面积与可住人数的字符串转数值详情页解析已经把大部分字段抽成数值但人工核对时仍可能有漏网的非标准文本比如价格写成「价格面议」面积出现「约55平米」。清洗阶段统一用正则处理import re def clean_price(value): if pd.isna(value): return None match re.search(r\d, str(value)) return int(match.group()) if match else None def clean_area(value): if pd.isna(value): return None match re.search(r\d\.?\d*, str(value)) return float(match.group()) if match else None df[price] df[price].apply(clean_price) df[area] df[area].apply(clean_area) df[max_guests] df[max_guests].apply(clean_price) print(df[[price, area, max_guests]].describe())apply把函数逐行作用到 Series正则在这里是兜底能匹配解析阶段漏掉的中文夹杂文本。describe()快速看出价格分布是否合理比如最小值 0、最大值 99999这类明显异常交给下一步的区间过滤。如果某一列在describe()里 count 数明显小于总行数说明该列有缺失值需要回到 5.1 节的缺失表中对照确认。5.3 租赁方式与城市字段的分类标准化租赁方式字段在原始文本里可能出现「整租」「整栋」「单间」「床位」等叫法城市字段可能混着中文名和拼音。为了让后续分组统计不歧义用映射表做标准化rent_map { 整租: 整租, 整栋: 整栋, 单间: 单间, 合租: 单间, 床位: 单间, } df[rent_type] df[rent_type].map(rent_map).fillna(单间) city_map { 北京: beijing, beijing: beijing, 上海: shanghai, shanghai: shanghai, } df[city] df[city].map(city_map).fillna(df[city]) print(df.groupby([city, rent_type]).size())map的作用是把左侧原始叫法替换成右侧标准值fillna(单间)处理映射表里未覆盖的情况避免新增分类打乱后续分析维度。城市字段双写中英文映射是因为数据可能来自不同分站入口统一后groupby的结果就是一张标准透视表。把这张透视表放进课设报告的数据预处理结果部分比贴一大段代码更有说服力。6. 清洗结果的量化验证与课设报告自查清单6.1 清洗前后数据量的对比验证数据预处理做完要能回答「清洗到底改了什么」。最简单有效的做法是保存两份行数对比外加价格分布的统计cleaned df.dropna(subset[price, area]).query(1 price 5000) print(f清洗前: {len(df)} 行, 清洗后: {len(cleaned)} 行) print(cleaned.groupby(city)[price].agg([count, mean, median]))如果清洗前 2000 行、清洗后剩 1600 行这 400 行的去向要能说清多少是重复、多少是缺失、多少是价格异常被过滤每一项对应报告里清洗过程章节的一个小节。query里的价格区间过滤是用的最多的一条准则排除超过 5000 的记录避免短租平台的别墅房源把整体均价拉偏。agg([count, mean, median])一次算出每个城市的房源数量和价格中位数中位数比均值更抗异常值适合价格这类长尾分布字段。6.2 报告里值得放的三个数据证据第一个是采集过程日志的一小段截图包含每个请求的状态码和延时证明数据来源可靠第二个是describe()输出的数值字段统计表作为预处理依据第三个是清洗前后的行数对比和groupby结果作为预处理效果的量化证据。这三项都不需要额外写代码全部来自前面步骤的中间输出。把「做了哪些步骤」换成「数据从什么状态变成了什么状态」课设报告的含金量会明显提升。字段缺失分布表建议用df.isna().sum()的完整输出加一列缺失率计算方式就是缺失数除以总行数。6.3 一套可直接复用的课设自查顺序我的自查顺序是先确认采集量和目标房源量在一个量级再用df.isna().sum()看每一列的缺失分布然后跑一遍describe()检查数值范围最后用groupby看业务维度如城市、租赁方式的计数是否合理。把这套顺序写进报告的验证部分答辩时被问「怎么证明数据有效」直接指向describe输出即可。最后一次运行程序前记得把 visited.txt 和调试用的 HTML 缓存文件清掉保证提交的源码包和报告描述完全对应。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询