以典型的、高度仿真的户外租赁平台网页结构和 API为例进行深度采集实战演示
发布时间:2026/9/26 10:30:26来源:尧图网络
㊗️本期内容已收录至专栏《Python爬虫实战》持续完善知识体系与项目实战建议先订阅收藏后续查阅更方便㊙️本期爬虫难度指数⭐⭐⭐⭐☆高级福利一次订阅后专栏内的所有文章可永久免费看持续更新中保底1000(篇)硬核实战内容。全文目录 开篇语0️⃣ 前言Preface1️⃣ 摘要Abstract2️⃣ 背景与需求Why3️⃣ 合规与注意事项必写4️⃣ 技术选型与整体流程What/How5️⃣ 环境准备与依赖安装可复现6️⃣ 核心实现请求层Fetcher7️⃣ 核心实现解析层Parser8️⃣ 数据存储与导出Storage9️⃣ 运行方式与结果展示必写 常见问题与排错强烈建议写1️⃣1️⃣ 进阶优化可选但加分1️⃣2️⃣ 总结与延伸阅读 文末✅ 专栏持续更新中建议收藏 订阅✅ 互动征集✅ 免责声明 开篇语哈喽各位小伙伴们你们好呀我是【喵手】。运营社区 C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO欢迎大家常来逛逛一起学习一起进步我长期专注Python 爬虫工程化实战主理专栏 《Python爬虫实战》从采集策略到反爬对抗从数据清洗到分布式调度持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”让数据价值真正做到——抓得到、洗得净、用得上。专栏食用指南建议收藏✅ 入门基础环境搭建 / 请求与解析 / 数据落库✅ 进阶提升登录鉴权 / 动态渲染 / 反爬对抗✅ 工程实战异步并发 / 分布式调度 / 监控与容错✅ 项目落地数据治理 / 可视化分析 / 场景化应用专栏推广时间如果你想系统学爬虫而不是碎片化东拼西凑欢迎订阅专栏《Python爬虫实战》一次订阅后专栏内的所有文章可永久免费阅读持续更新中。订阅后更新会优先推送按目录学习更高效0️⃣ 前言Preface一句话说明本文将手把手带你使用 Python 及requestslxml编写一个面向对象的高级爬虫框架定向抓取户外装备租赁门店的核心信息最终产出规范化的 CSV 数据报表和 SQLite 数据库文件。读完能获得什么掌握一套工业级的爬虫项目代码架构请求、解析、存储分离高内聚低耦合。精通应对复杂带参查询如装备—地区—租赁周期 联合筛选的抓取策略。学会如何优雅地处理异常、容错兜底以及数据清洗让你告别“一跑就崩”的脚本小字辈。1️⃣ 摘要Abstract一句话说明针对户外租赁市场的碎片化信息本方案采用静态页面解析与接口请求相结合的混合模式利用 Python 提取门店名、营业状态等 6 大核心字段并输出为本地化结构数据outdoor_rentals_data.csv。读完能获得什么洞悉反爬虫机制的常见套路及规避手段如动态 UA、重试退避策略。获得一份可以直接运行、极具扩展性的高质量 Python 爬虫源码。建立合法合规的采集边界意识明确“能抓什么”与“绝对不能碰什么”。2️⃣ 背景与需求Why为什么要爬这几年不管是川西徒步还是市郊精致露营Glamping户外活动简直火得一塌糊涂。但对于新手或者临时组局的玩家来说“买装备太贵且吃灰租装备找不到好店”是一个巨大的痛点。目前市面上的租赁门店信息散落在各类本地生活服务平台、垂直论坛甚至是社交媒体的帖子里。通过爬虫技术我们可以实现信息聚合建立一个区域性的“装备租赁黄页”进一步还能进行数据分析比如分析“哪个地区的帐篷租赁最紧俏”、“押金免赔的门店占比多少”为商业决策或个人出行提供数据支撑。目标站点与目标字段我们假设目标是一个综合性的户外生活服务网页例如https://www.mock-outdoor-rentals.com/search。明确的字段清单如下门店名 (store_name)提供租赁服务的商户名称。装备类别 (equipment_category)如帐篷、睡袋、雪具、登山杖等。地区 (region)门店所在的省市区用于区域筛选。租赁方式 (rental_method)按天租/按周租/免押金信用租等。营业状态 (operating_status)营业中/休息中/已关停。链接 (detail_link)门店或该装备的详情页 URL。实战重点聚焦由于我们需要精确到“装备—地区—租赁周期”这往往意味着需要在请求时构造特定的 URL 参数Query String或表单数据。同时坚决不采集任何用户的租赁订单、支付流水等隐私数据这是技术人的底线。3️⃣ 合规与注意事项必写在代码世界里裸奔是危险的。作为资深爬虫爱好者我们第一课永远是“学会踩刹车”。robots.txt 基本说明动手前永远先访问https://target-site.com/robots.txt。这个文件是网站所有者给爬虫立的规矩。如果里面写了Disallow: /search/rentals/作为遵纪守法的开发者我们应该尊重。本文代码仅作为技术验证和开放数据获取的演示。频率控制、不要攻击式并发不要把爬虫写成 DDOS 攻击器人家服务器也是要花钱买带宽的。我们在代码里必须加上随机休眠time.sleep将请求频率控制在人类正常浏览的极限范围内比如每页间隔 2-5 秒。数据边界我们只采集公开的商业信息门店名、状态、方式。不采集敏感信息坚决不绕过付费墙不破解人家的登录限制获取内部闭门数据。用技术收集公开资料是本事用技术搞破坏是犯法。4️⃣ 技术选型与整体流程What/How抓取模式混合模式静态 HTML 解析为主结合 API 筛选在这个实战中我们通常面临的是列表页带有很多过滤参数往往是通过 API 异步加载或者 URL 拼接而详情页则是传统的静态 HTML。整体数据流转图谱发起采集 (Fetcher)➡️提取解析 (Parser)➡️数据清洗 (Cleaner)➡️本地存储 (Storage)采集层构造带条件的 GET 请求伪装成浏览器。解析层从返回的杂乱网页中提取我们要的 6 个字段。清洗层把“营业中(10:00-22:00)”清洗成标准的布尔或分类状态去除首尾空格。存储层落盘为 CSV 文件同时存入 SQLite 防止数据丢失。为什么选requestslxml不用 Scrapy 是因为对于这种中小型垂直信息抓取Scrapy 显得太重了杀鸡焉用牛刀不用 Playwright/Selenium 是因为没必要为了渲染几个简单的 DOM 节点去跑个无头浏览器太吃内存且慢。requests负责轻量级网络通讯lxml底层是 C 语言实现XPath 解析速度极快这两者结合是性价比最高的“黄金搭档”。5️⃣ 环境准备与依赖安装可复现好马配好鞍先把环境搞定。建议使用虚拟环境保持你的系统 Python 干净。Python 版本强烈建议 Python 3.9 或以上版本我们需要用到一些新的类型提示特性代码更优雅。安装核心依赖包# 激活你的虚拟环境后执行pipinstallrequests lxml pandas fake-useragent tenacity(说明pandas用于最终优雅地导出 CSVfake-useragent用于随机生成浏览器头防封tenacity是一个非常牛的重试库让你的爬虫自带“不死鸟”属性。)项目结构推荐outdoor_crawler/ ├── main.py # 爬虫启动入口 ├── config.py # 配置文件URL、参数字典等 ├── fetcher.py # 网络请求模块 ├── parser.py # HTML/JSON解析模块 ├── storage.py # 数据存储模块 ├── logs/ # 运行日志存放处 └── data/ # 输出结果CSV/SQLite6️⃣ 核心实现请求层Fetcher请求层的核心目标只有一个把 HTML 源码稳定、安全地拿回来。这里有很多门道你直接用裸的requests.get()很大可能会被直接 403 拒绝访问。我们需要进行“精装修”。# fetcher.pyimportrequestsimporttimeimportrandomfromfake_useragentimportUserAgentfromtenacityimportretry,stop_after_attempt,wait_exponentialimportlogging logging.basicConfig(levellogging.INFO,format%(asctime)s - %(levelname)s - %(message)s)classBaseFetcher:def__init__(self):# 维持一个 Session 可以保持一定的连接池甚至保留必要的 Cookieself.sessionrequests.Session()self.uaUserAgent()def_get_headers(self):动态生成高质量的 Headersreturn{User-Agent:self.ua.random,Accept:text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,en;q0.8,# Referer 很关键假装我们是从首页或者百度点进来的Referer:https://www.mock-outdoor-rentals.com/}# 使用 tenacity 实现优雅的重试失败后等待2秒、4秒、8秒...最多试3次retry(stopstop_after_attempt(3),waitwait_exponential(multiplier2,min2,max10))deffetch(self,url,paramsNone): 发起 GET 请求获取页面源码 headersself._get_headers()try:# timeout 务必要加防止目标服务器假死导致爬虫永久卡住responseself.session.get(url,headersheaders,paramsparams,timeout15)# 检查 HTTP 状态码如果不是 200主动抛出异常触发重试response.raise_for_status()# 友善的频率控制每次请求完随机睡 1 到 3 秒time.sleep(random.uniform(1.0,3.0))# 解决可能的中文乱码问题如果有charset声明requests通常能处理这里加一层保险response.encodingresponse.apparent_encodingreturnresponse.textexceptrequests.exceptions.RequestExceptionase:logging.error(f抓取失败 URL:{url}, 参数:{params}, 错误:{e})raise# 抛出异常让 retry 捕获实战重点解读当我们进行“装备—地区—租赁周期”筛选时本质上就是往fetch里的params传字典。比如params{region: sichuan, equip: tent, cycle: week}requests会自动帮你拼成?regionsichuanequiptentcycleweek非常省心。7️⃣ 核心实现解析层Parser拿到 HTML 字符串后面对一堆杂乱的标签我们需要精准定位。我个人强烈推崇lxml配合 XPath。XPath 就像是网页 DOM 树里的 GPS 导航只要结构在找数据一抓一个准。# parser.pyfromlxmlimportetreeimportloggingclassRentalParser:def__init__(self,html_content):# 将 HTML 字符串解析为可使用 XPath 遍历的 DOM 树对象self.treeetree.HTML(html_content)def_safe_extract(self,element,xpath_rule,default未知): 容错提取器网页结构很容易变或者某些门店没填字段 如果没有取到值要优雅地返回默认值而不是抛出 IndexError 导致爬虫崩溃。 try:resultelement.xpath(xpath_rule)ifresult:# 去除提取出的字符串首尾的空白符和换行returnstr(result[0]).strip()returndefaultexceptExceptionase:logging.warning(f解析出错:{e})returndefaultdefparse_list_page(self): 解析列表页获取当前页所有门店的基础信息和详情页链接 items_data[]# 假设每一个租赁门店卡片都在 class 为 rental-card-item 的 div 里# 这里使用虚拟的 XPath 结构做演示nodesself.tree.xpath(//div[contains(class, rental-card-item)])ifnotnodes:logging.warning(未匹配到任何门店卡片请检查 XPath 或页面是否渲染为空)returnitems_datafornodeinnodes:# 详情页链接相对路径要拼接为绝对路径这里假设提取出来就是绝对的detail_linkself._safe_extract(node,.//a[classstore-link]/href,default)# 如果没拿到链接这条数据意义不大直接跳过ifnotdetail_link:continueitem{store_name:self._safe_extract(node,.//h3[classtitle]/text()),equipment_category:self._safe_extract(node,.//span[classtag-equip]/text()),region:self._safe_extract(node,.//div[classlocation]/text()),rental_method:self._safe_extract(node,.//div[classrent-type]/text()),operating_status:self._safe_extract(node,.//span[contains(class, status)]/text()),detail_link:detail_link}items_data.append(item)returnitems_data解析层关键点节点遍历一定要先用大的 XPath//div[classrental-card-item]框住一个个独立的区块Node然后再在区块内用相对路径.开头进行二次提取。如果你直接全局提取//h3/text()最后各个字段的列表长度可能对不上导致数据错乱张冠李戴。缺失容错_safe_extract极其重要。现实中的网页又脏又乱有的店名缺标签直接用[0]会触发IndexError整个程序直接宕机。8️⃣ 数据存储与导出Storage抓到的数据就像刚挖出来的原油必须妥善装桶。我们采用双保险一边用 SQLite 存方便断点续传和后续 SQL 分析一边导出一份给业务人员看的英文命名 CSV。# storage.pyimportsqlite3importpandasaspdimporthashlibimportosclassDataStorage:def__init__(self,db_pathdata/outdoor_rentals.db):os.makedirs(os.path.dirname(db_path),exist_okTrue)self.db_pathdb_path self.connsqlite3.connect(self.db_path)self.cursorself.conn.cursor()self._init_db()self.data_buffer[]# 内存缓冲区用于最后导出 CSVdef_init_db(self):初始化表结构# url_hash 作为主键完美解决重复抓取的问题去重策略self.cursor.execute( CREATE TABLE IF NOT EXISTS rentals ( url_hash TEXT PRIMARY KEY, store_name TEXT, equipment_category TEXT, region TEXT, rental_method TEXT, operating_status TEXT, detail_link TEXT ) )self.conn.commit()def_generate_hash(self,url):URL唯一性摘要计算returnhashlib.md5(url.encode(utf-8)).hexdigest()defsave_item(self,item):单条数据入库url_hashself._generate_hash(item[detail_link])try:self.cursor.execute( INSERT INTO rentals (url_hash, store_name, equipment_category, region, rental_method, operating_status, detail_link) VALUES (?, ?, ?, ?, ?, ?, ?) ,(url_hash,item[store_name],item[equipment_category],item[region],item[rental_method],item[operating_status],item[detail_link]))self.conn.commit()self.data_buffer.append(item)logging.info(f 数据入库成功:{item[store_name]})exceptsqlite3.IntegrityError:logging.debug(f⚠️ 数据已存在跳过去重:{item[store_name]})exceptExceptionase:logging.error(f数据库写入异常:{e})defexport_to_csv(self,filenameoutdoor_rentals_data.csv):使用 Pandas 将缓冲区数据导出为规范的 CSVifnotself.data_buffer:logging.warning(内存中没有新数据跳过 CSV 导出。)returndfpd.DataFrame(self.data_buffer)export_pathos.path.join(data,filename)# 英文表头映射df.rename(columns{store_name:Store Name,equipment_category:Equipment Category,region:Region,rental_method:Rental Method,operating_status:Operating Status,detail_link:Detail Link},inplaceTrue)# utf-8-sig 可以防止在 Excel 中打开出现中文乱码df.to_csv(export_path,indexFalse,encodingutf-8-sig)logging.info(f CSV 数据报表已生成:{export_path})defclose(self):self.conn.close()9️⃣ 运行方式与结果展示必写把上面三个积木块组装起来我们就得到了完整的主程序main.py。# main.pyfromfetcherimportBaseFetcherfromparserimportRentalParserfromstorageimportDataStorageimportloggingdefmain():# 1. 初始化各层组件fetcherBaseFetcher()storageDataStorage()# 2. 构造查询参数 (实战重点装备-地区-租赁周期筛选)# 假设这是我们要爬取的三组不同组合search_tasks[{region:sichuan,category:tent,cycle:weekend},{region:xinjiang,category:sleeping_bag,cycle:week},{region:yunnan,category:hiking_poles,cycle:daily}]base_urlhttps://www.mock-outdoor-rentals.com/searchtry:fortaskinsearch_tasks:logging.info(f 开始采集任务:{task})# 第一步请求源数据html_sourcefetcher.fetch(base_url,paramstask)ifhtml_source:# 第二步解析提取字段parserRentalParser(html_source)itemsparser.parse_list_page()# 第三步清洗与存储foriteminitems:# 可以在这里做一些简单的清洗比如过滤掉已经关门的店if关停initem[operating_status]:continuestorage.save_item(item)exceptKeyboardInterrupt:logging.info( 收到手动终止信号正在保存数据退出...)finally:# 第四步兜底导出与资源释放storage.export_to_csv(outdoor_rentals_data.csv)storage.close()logging.info( 爬虫任务全部结束)if__name____main__:main()运行方式打开终端Terminal在项目根目录下输入python main.py结果展示预览控制台会打印出带时间戳的运行日志。在data/目录下会生成outdoor_rentals_data.csv内容类似如下Store NameEquipment CategoryRegionRental MethodOperating StatusDetail Link极客露营(成都店)牧高笛隧道帐篷四川-成都市周末套餐/免押营业中https://…/store/101寻野户外装备库黑冰G1000睡袋新疆-乌鲁木齐按周租赁/需押金营业中https://…/store/205云端漫步碳纤维登山杖云南-大理市按天租赁休息中(仅周末营业)https://…/store/308 常见问题与排错强烈建议写老实说爬虫写出来第一遍就能完美跑到底的概率比买彩票中奖还低。遇到报错千万别慌下面这些坑我都帮你踩过遇到 HTTP 403 (Forbidden) 或 429 (Too Many Requests) 怎么办现象返回的内容说你访问太快或者要求填验证码。对策首先增加time.sleep()的时间。其次检查 Headers 是否带上了常见的参数User-Agent, Referer。如果还是不行说明 IP 被封了需要引入代理池Proxy Pool在requests.get里加上proxies{http: http://IP:PORT}。HTML 抓回来了但打开一看是“空壳”找不到数据怎么办现象print(html_source)发现里面只有div idapp/div没有你想要的店名。对策这是典型的动态渲染网页Vue/React。目标数据实际上是浏览器通过 AJAX 接口悄悄拉回来的 JSON。解决办法按F12打开开发者工具进入Network (网络)标签页选择Fetch/XHR重新刷新页面找到返回数据的那个真实的 API 接口。把爬虫目标改为抓这个接口此时parser.py里不再用 XPath而是直接用 Python 的json模块解析字典。解析时频繁报错IndexError或者抓出来的全是空字符串对策网页的前端小哥可能改版了导致你的 XPath 规则失效。不要过度依赖浏览器插件生成的绝对路径比如/html/body/div[2]/ul/li[1]/h3一旦加个广告位全盘崩溃。坚持使用相对特征定位//h3[classtitle]。抓下来的中文变成一堆是ç乱码怎么搞对策编码没对上。在response.text之前强行指定编码response.encoding utf-8或者试图让他自己猜response.encoding response.apparent_encoding。1️⃣1️⃣ 进阶优化可选但加分当你能顺利抓下来几百条数据后你可能想抓全网几万条数据。这时候前面的基础代码就需要“鸟枪换炮”了。并发加速协程单线程requests是一发一收太慢了。进阶可以把requests替换为aiohttp配合asyncio。利用异步非阻塞 I/O同时发出去上百个请求效率能提升 10 倍以上。但切记并发越高被封越快必须配高级代理。断点续跑增量抓取万一电脑断电总不能从头重抓。我们上文的 SQLiteurl_hash主键已经具备了一定的去重防写入能力更高级的做法是在每次发请求前先查一下这个 URL 或者带有过滤条件的特征在数据库里有没有有的话直接continue跳过网络请求省时省力。日志与监控爬虫跑在后台总得知道健康度。可以使用logging模块配合飞书/钉钉的 Webhook 机器人。在except捕获到大量重试失败时发送报警信息到手机上“老板爬虫被封了快来切 IP”1️⃣2️⃣ 总结与延伸阅读复盘总结在这篇硬核指南中我们从零到一构建了一个面向对象、职责分明的户外装备租赁信息采集系统。我们不仅实现了对核心业务字段门店、分类、地区、租赁方式的精准提取更重要的是掌握了处理网络超时重试、应对 DOM 缺失的容错机制以及 SQLite 和 CSV 的双规落盘策略。下一步可以做什么如果你对爬虫彻底上瘾了我强烈建议你去了解以下两个技术栈Playwright如果目标站点不仅数据是通过异步加载的还带有一大堆复杂的防篡改签名如逆向 JS 极其困难直接上 Playwright。它能调起一个真实的 Chrome 浏览器模拟人类点击堪称“降维打击”。Scrapy 框架如果你的目标不仅是这一个网站而是要采集十几个户外平台的百万级数据去学习 Scrapy。它是 Python 爬虫界不可撼动的王者自带并发、管道和中间件体系。希望这份详细的代码指南能帮你扫清障碍写代码是个不断踩坑又爬出来的过程享受它吧期待看到你生成的精美数据报表出炉的那一刻如果有哪里卡住了随时回来找我交流。 文末好啦以上就是本期的全部内容啦如果你在实践过程中遇到任何疑问欢迎在评论区留言交流我看到都会尽量回复咱们下期见小伙伴们在批阅的过程中如果觉得文章不错欢迎点赞、收藏、关注哦三连就是对我写作道路上最好的鼓励与支持❤️✅ 专栏持续更新中建议收藏 订阅墙裂推荐订阅专栏 《Python爬虫实战》本专栏秉承着以“入门 → 进阶 → 工程化 → 项目落地”的路线持续更新争取让每一期内容都做到✅ 讲得清楚原理✅ 跑得起来代码✅ 用得上场景✅ 扛得住工程化想系统提升的小伙伴强烈建议先订阅专栏 《Python爬虫实战》再按目录大纲顺序学习效率十倍上升✅ 互动征集想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战评论区留言告诉我你的需求我会优先安排实现(更新)哒~⭐️ 若喜欢我就请关注我叭更新不迷路⭐️ 若对你有用就请点赞支持一下叭给我一点点动力⭐️ 若有疑问就请评论留言告诉我叭我会补坑 更新迭代✅ 免责声明本文爬虫思路、相关技术和代码仅用于学习参考对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。使用或者参考本项目即表示您已阅读并同意以下条款合法使用 不得将本项目用于任何违法、违规或侵犯他人权益的行为包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。风险自负 任何因使用本项目而产生的法律责任、技术风险或经济损失由使用者自行承担项目作者不承担任何形式的责任。禁止滥用 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。使用或者参考本项目即视为同意上述条款,即 “谁使用谁负责” 。如不同意请立即停止使用并删除本项目。
网站建设高端定制企业官网