3步搞定苹果8上市价格数据爬虫保姆级教程 3步搞定苹果8上市价格数据爬虫保姆级教程 还在对着文档发呆,敲了半小时代码却跑不通一个请求?看了一堆教程还是不会写项目,问题往往出在环境配置和请求细节上。别急,这篇保姆级教程直接给你一套能跑通的实战方案,专治各种“代码看着简单,一动手就报错”的疑难杂症。 我们今天要做的,是一个基于 Python 的数据采集小项目。目标很明确:模拟浏览器行为,抓取苹果官网历史页面中关于 iPhone 8 上市时的价格信息。虽然 iPhone 8 早已退市,但其历史页面或相关电商归档数据依然具有极高的分析价值。通过这个项目,你将掌握 HTTP 请求、HTML 解析、数据清洗以及异常处理的全流程。 项目目标与需求分析 在动手写代码前,先明确我们要解决什么问题。很多新手喜欢上来就写代码,结果发现抓下来的数据全是乱码,或者因为反爬机制被封 IP。 我们的核心目标有三个: 稳定获取数据:能够成功请求目标 URL,并返回正确的 HTML 内容。 精准解析信息:从复杂的 HTML 结构中,准确提取出“iPhone 8”和对应的“上市价格”字段。 数据标准化存储:将提取出的散乱文本转化为结构化的 JSON 或 CSV 文件,方便后续使用 Pandas 进行数据分析。 为什么选苹果8上市价格作为切入点?因为苹果官网的结构相对规范,且其历史数据往往有固定的归档路径,适合作为入门级的“靶子”。更重要的是,通过追踪特定商品的价格变迁,你能体会到数据清洗在真实业务中的重要性——原始数据往往是脏的、碎的,甚至带有营销话术,我们需要从中剥离出纯粹的数值。 目录结构与依赖安装 工程化的第一步,是搭建清晰的项目结构。不要把所有代码扔在一个 main.py 里,那是初级脚本,不是项目。 apple-price-crawler/ ├── config.py # 配置管理 ├── crawler.py # 核心抓取逻辑 ├── parser.py # 数据解析逻辑 ├── utils.py # 通用工具函数 ├── data/ # 数据存储目录 │ └── .gitkeep ├── requirements.txt # 依赖包列表 └── main.py # 程序入口 关键依赖包说明: requests: 用于发送 HTTP 请求,比 urllib 更简洁。 BeautifulSoup4: HTML 解析的神器,处理嵌套标签比正则表达式靠谱得多。 lxml: 作为 BeautifulSoup 的解析引擎,速度比默认的 html.parser 快数倍。 pandas: 用于后续的数据整理和导出,虽然抓取阶段可选,但为了项目完整性,建议引入。 在终端执行以下命令安装依赖: pip install requests beautifulsoup4 lxml pandas 建议创建虚拟环境(venv),避免污染全局 Python 环境。这是职业开发者的基本素养,也是避免“在我机器上能跑”尴尬的关键。 核心代码实现 1. 配置管理 (config.py) 硬编码 URL 和 Headers 是大忌。我们将可变参数抽离出来。 import os # 目标 URL,此处假设有一个归档页面或特定商品页 # 实际开发中,请替换为真实可访问的历史数据源 URL TARGET_URL = https://www.apple.com/shop/buy-iphone/iphone-8 # 模拟浏览器 Headers,防止被识别为爬虫 HEADERS = { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36, Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8, Accept-Language: zh-CN,zh;q=0.9,en;q=0.8, Referer: https://www.apple.com/ } # 数据保存路径 DATA_DIR = os.path.join(os.path.dirname(__file__), data) 2. 网络请求模块 (crawler.py) 这里涉及一个常见的坑:重试机制。网络波动或服务器限流是常态,一次失败不代表永远失败。 import requests import time import logging # 配置日志,比 print 更专业 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def fetch_html(url, headers, max_retries=3, timeout=10): 获取 HTML 内容,包含重试机制 for i in range(max_retries): try: logging.info(f正在请求: {url} (尝试 {i+1}/{max_retries})) response = requests.get(url, headers=headers, timeout=timeout) # 状态码检查 if response.status_code == 200: # 设置编码,防止中文乱码 response.encoding = 'utf-8' return response.text else: logging.warning(f请求失败,状态码: {response.status_code}) except requests.exceptions.RequestException as e: logging.error(f请求异常: {e}) time.sleep(2 * (i + 1)) # 指数退避,避免频繁重试 return None 逐行讲解关键点: timeout=10:必须设置超时时间,否则网络卡住时程序会无限等待。 time.sleep(2 * (i + 1)):这是指数退避策略。第一次失败等2秒,第二次等4秒,第三次等6秒。这样既能给服务器喘息机会,又能提高成功率。 3. 数据解析模块 (parser.py) 这是最容易出错的地方。苹果官网的 DOM 结构经常变动,选择器(Selector)必须写得稳健。 from bs4 import BeautifulSoup import re def parse_price_info(html_content): 解析 HTML,提取 iPhone 8 的价格信息 if not html_content: return [] soup = BeautifulSoup(html_content, 'lxml') results = [] # 模拟查找逻辑:寻找包含 iPhone 8 文本的容器 # 注意:实际选择器需根据真实网页结构调整,此处为示例逻辑 # 假设价格通常在 span class=price 标签内,且附近有关于 iPhone 8 的描述 # 策略:先找所有可能的价格元素,再关联上下文 price_elements = soup.find_all(span, class_=price) for elem in price_elements: # 获取价格文本 price_text = elem.get_text(strip=True) # 简单验证:是否包含数字和货币符号 if re.search(r'[\$¥][\d,.]+', price_text): # 获取父级容器,尝试获取商品名称 parent = elem.find_parent(div, class_=product-item) if parent: name_elem = parent.find(h3) product_name = name_elem.get_text(strip=True) if name_elem else Unknown # 过滤:只保留包含 iPhone 8 的记录 if iPhone 8 in product_name: # 清洗价格文本,去除货币符号,保留数字 clean_price = re.sub(r'[^\d.]', '', price_text) results.append({ product: product_name, price: float(clean_price) if clean_price else 0.0, currency: CNY if ¥ in price_text else USD }) return results 避坑指南: 不要依赖 ID 选择器,ID 在动态页面中极易重复或改变。优先使用 Class 组合或标签层级。 re.sub(r'[^\d.]', '', price_text) 这行代码至关重要。原始数据可能是 “$699.00” 或 “¥5888”,直接转 float 会报错。必须先用正则提取纯数字。 4. 主程序入口 (main.py) 串联所有模块,并处理数据持久化。 import pandas as pd import json import os from config import TARGET_URL, HEADERS, DATA_DIR from crawler import fetch_html from parser import parse_price_info def main(): # 确保数据目录存在 if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) # 1. 抓取 html = fetch_html(TARGET_URL, HEADERS) if not html: print(抓取失败,程序退出) return # 2. 解析 data_list = parse_price_info(html) if not data_list: print(未解析到有效数据,请检查网页结构或选择器) return # 3. 存储 # 方式一:JSON json_file = os.path.join(DATA_DIR, iphone8_price.json) with open(json_file, 'w', encoding='utf-8') as f: json.dump(data_list, f, ensure_ascii=False, indent=4) # 方式二:CSV df = pd.DataFrame(data_list) csv_file = os.path.join(DATA_DIR, iphone8_price.csv) df.to_csv(csv_file, index=False, encoding='utf-8-sig') print(f成功保存 {len(data_list)} 条记录至 {json_file}) if __name__ == __main__: main() 运行与测试 代码写完不等于功能正常。在本地运行前,请先用浏览器开发者工具(F12)检查目标页面的网络请求和 DOM 结构。 运行脚本: python main.py 观察日志: 如果看到 WARNING 或 ERROR,根据日志信息排查。常见错误包括: ConnectionRefusedError:检查网络或代理设置。 403 Forbidden:Headers 不够真实,或被 WAF 拦截。尝试更换 User-Agent 或增加 Cookie。 404 Not Found:URL 失效,需重新寻找有效数据源。 验证数据: 打开 data/iphone8_price.csv,检查价格列是否为数值型,产品名是否准确。如果发现价格全是 0 或 NaN,说明 parser.py 中的正则表达式或选择器需要调整。 调试技巧: 在 parse_price_info 函数中,临时添加一行 print(soup.prettify()[:1000]) 打印前 1000 个字符的格式化 HTML,肉眼观察结构,比盲目猜测选择器高效得多。 优化扩展 基础功能跑通后,项目才算真正落地。以下几个方向可以显著提升项目的工程价值: 异步并发请求: 如果目标数据分布在多个页面(如不同颜色、不同存储容量的 iPhone 8 页面),使用 aiohttp 和 asyncio 可以将效率提升 5-10 倍。对于单页面抓取,同步即可,无需过度设计。 代理 IP 池: 高频抓取必然触发风控。集成代理池(如 Scrapy-Proxy-Pool)是生产环境的标配。在 crawler.py 中,将 headers 和 proxies 参数化,支持动态切换。 数据存储升级: 当数据量达到万级时,CSV 文件的读写性能会下降。建议接入 SQLite 或 PostgreSQL。使用 SQLAlchemy ORM 可以屏蔽底层数据库差异,便于后期迁移。 数据可视化: 既然抓到了历史价格,不妨用 matplotlib 画一张时间序列折线图,直观展示 iPhone 8 上市初期到退市期间的价格波动。这会让你的项目报告更具说服力。 监控与报警: 如果这是一个长期运行的项目(如监控竞品价格),需要加入定时任务(Cron 或 Celery Beat),并在数据抓取失败或价格异常波动时,通过邮件或企业微信发送报警通知。 小结 从环境搭建到数据落盘,这套流程看似简单,实则涵盖了数据采集项目的核心要素:稳定性、准确性、可维护性。 很多教程只教你怎么“抓”,却不教你怎么“稳”。在实际工作中,数据质量远比抓取速度重要。一个能稳定运行半年不出错的小爬虫,比一个炫技但三天两头报错的“高并发框架”更有价值。 回顾一下我们做的关键决策: 使用 虚拟环境 隔离依赖,避免冲突。 使用 指数退避 重试机制,提升健壮性。 使用 正则清洗 原始数据,确保数值可用性。 使用 结构化存储(JSON/CSV),方便后续分析。 技术栈没有高低之分,关键在于解决实际问题。当你面对一个全新的数据源时,不要急于复制粘贴代码,而是先理解其 HTML 结构和反爬策略,再针对性地编写解析逻辑。 你更常用哪种写法?是倾向于用 Scrapy 框架快速搭建,还是像这样用 Requests + BeautifulSoup 手写轻量级脚本?评论区交流你的实战经验,分享你在反爬对抗中遇到的最头疼的坑。