
PythonBeautifulSoup4实战携程数据爬取避坑指南去年做旅游数据分析项目时我需要爬取携程网的美食、景点和酒店信息。本以为用PythonBeautifulSoup4这种经典组合就能轻松搞定结果踩了无数坑。今天把这些血泪教训整理成文希望能帮你少走弯路。1. 请求头与反爬机制破解携程的反爬策略比想象中严密得多。第一次请求时我的爬虫直接被403拒之门外。经过反复测试发现这几个关键点必须包含的请求头字段headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://www.ctrip.com/, Accept-Language: zh-CN,zh;q0.9, X-Requested-With: XMLHttpRequest # 关键伪装字段 }常见坑点直接使用requests.get()不带headers会被立即封禁User-Agent过于简单会被识别如仅写Python-requests缺少Referer字段时部分接口返回空数据提示用浏览器开发者工具查看真实请求头尽量模拟得一模一样。我曾因为漏掉一个不起眼的Accept-Encoding字段导致返回乱码。2. 动态加载内容处理携程大量使用AJAX动态加载直接解析HTML会漏掉关键数据。比如酒店价格信息通常是通过接口异步加载的。解决方案# 查找隐藏的API接口 import json from urllib.parse import unquote def parse_hotel_price(hotel_id): api_url fhttps://m.ctrip.com/restapi/soa2/xxx.json # 实际接口需要分析 response requests.get(api_url, headersheaders) data json.loads(unquote(response.text)) return data[price]常见问题接口参数加密需要逆向JS代码接口返回数据被编码常见unicode/url编码频率限制必须加延迟3. BeautifulSoup4选择器优化页面结构经常变动是最大的痛点。上周还能用的选择器这周可能就失效了。分享几个稳定定位的技巧多属性组合选择# 不推荐 - 仅依赖class soup.find(div, class_rdetailbox) # 推荐 - 组合多个属性 soup.find(div, attrs{ class: rdetailbox, data-type: food-item })CSS选择器优先级[data-id]等自定义属性最稳定id属性次之但携程很少用最后才考虑class和标签名实测对比表选择方式稳定性示例data属性★★★★★[data-typefood]复合class★★★☆.module.food-list单一class★★.food-item标签名★div ul li4. 异常处理与日志记录我的第一个版本因为没有妥善处理异常跑了8小时结果因一个页面结构变化而崩溃。改进后的方案import logging from datetime import datetime logging.basicConfig( filenamefctrip_{datetime.now().strftime(%Y%m%d)}.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) def safe_extract(element, selector, defaultNone): try: target element.select_one(selector) return target.text.strip() if target else default except Exception as e: logging.warning(f提取失败: {selector} - {str(e)}) return default关键策略每个解析步骤都要有try-catch记录完整的错误上下文URL、参数等设置合理的默认值避免中断5. 数据存储优化最初我直接存JSON文件后来发现几个问题数据量大时文件臃肿难以增量更新无法快速查询改用SQLite后的存储方案import sqlite3 def init_db(): conn sqlite3.connect(ctrip_data.db) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS foods (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, city TEXT, price REAL, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP)) conn.commit() return conn插入数据时使用UPSERT语法避免重复def save_food(item, conn): c conn.cursor() c.execute(INSERT OR REPLACE INTO foods (name, city, price) VALUES (?,?,?), (item[name], item[city], item[price])) conn.commit()6. 分布式爬取策略当需要爬取多个城市数据时单线程效率太低。我的改进方案基于gevent的协程池from gevent import monkey; monkey.patch_all() import gevent from gevent.pool import Pool def crawl_city(city): # 具体爬取逻辑 pass cities [beijing, shanghai, guangzhou] pool Pool(5) # 并发5个城市 jobs [pool.spawn(crawl_city, city) for city in cities] gevent.joinall(jobs)注意事项控制并发数建议3-5个每个请求必须加随机延迟0.5-2秒使用代理IP池防止封禁7. 数据清洗技巧原始数据往往包含大量噪音需要特别处理价格字段清洗def clean_price(text): if not text: return 0.0 # 去除¥、起等字符 cleaned re.sub(r[^\d.], , text) try: return float(cleaned) except: return 0.0地址标准化area_mapping { 朝阳区: chaoyang, 海淀区: haidian # 其他映射关系... } def standardize_address(addr): for zh, en in area_mapping.items(): if zh in addr: return en return other经过这些优化后我的爬虫成功率从最初的30%提升到了95%。最深的体会是写爬虫就像打游击战必须随时准备应对网站的变化。建议每周检查一次核心选择器是否仍然有效保留足够的日志帮助快速定位问题。