1. 项目背景与核心价值
最近帮朋友抢演唱会门票时,发现大麦网的演出信息分散在不同页面,手动收集费时费力。作为一个常年和数据打交道的开发者,我决定用Python写个爬虫自动抓取这些数据。这个项目不仅解决了实际需求,更重要的是形成了一套可复用的代码框架,能快速适配其他演出场次的数据采集。
市面上虽然有不少爬虫教程,但针对大麦网这种动态加载且反爬机制完善的商业网站,很少有完整可用的解决方案。本文将分享从环境搭建到数据存储的全流程,重点解决三个核心问题:如何绕过动态加载、如何处理登录验证、如何设计可复用的爬取逻辑。代码已通过实际测试,可稳定获取演唱会名称、场次、票价、售票状态等关键信息。
提示:商业网站爬虫需遵守robots.txt协议,建议设置合理爬取间隔(本方案默认3秒/页),避免对服务器造成压力。
2. 技术选型与工具链搭建
2.1 为什么选择Selenium+BeautifulSoup组合
对比Scrapy等框架,Selenium的最大优势是能模拟真实浏览器行为。大麦网大量使用JavaScript动态加载数据,普通请求无法获取完整DOM树。通过ChromeDriver控制浏览器,可以完美解决这个问题。而BeautifulSoup则用于解析已加载的HTML,比纯Selenium的定位方式更灵活。
环境准备清单:
- Python 3.8+(兼容async/await语法)
- Chrome浏览器(版本需与驱动匹配)
- ChromeDriver(建议最新稳定版)
- 核心库:selenium==4.1.0 / beautifulsoup4==4.11.1 / pandas==1.5.0
安装验证代码:
python复制from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.damai.cn")
assert "大麦网" in driver.title
driver.quit()
2.2 反爬对抗策略设计
大麦网的反爬机制主要包含:
- 用户行为检测(鼠标轨迹、点击频率)
- 指纹识别(WebGL渲染、字体列表)
- IP频次限制
应对方案:
python复制options = webdriver.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_argument("user-agent=Mozilla/5.0...")
3. 核心爬取流程实现
3.1 页面导航与等待策略
大麦网的演出列表采用懒加载,需要滚动触发数据请求。这里采用显式等待+滚动组合操作:
python复制from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def load_full_page(driver, url):
driver.get(url)
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2) # 关键间隔
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "items"))
)
3.2 数据解析关键技巧
演出信息分布在多个DOM层级,需要多层定位:
- 先用CSS选择器获取卡片容器:
.search-items .items - 遍历每个演出卡片:
.items li - 提取关键字段:
- 演出名:
.items li a的title属性 - 时间:
.items li .time的data-time属性 - 票价:
.items li .price span的text
- 演出名:
特殊字段处理示例:
python复制def parse_price(element):
try:
return element.find('span', class_='price').get_text().strip()
except AttributeError:
return "票价待定"
4. 数据存储与代码复用设计
4.1 结构化数据存储
使用Pandas进行数据清洗后存储为CSV:
python复制import pandas as pd
def save_to_csv(data_list, filename):
df = pd.DataFrame(data_list, columns=[
'演出名称', '演出时间', '演出地点',
'最低票价', '最高票价', '售票状态'
])
df['爬取时间'] = pd.Timestamp.now()
df.to_csv(filename, index=False, encoding='utf_8_sig')
4.2 模块化设计实现复用
将核心功能拆分为独立模块:
code复制damai_crawler/
├── __init__.py
├── browser.py # 浏览器控制
├── parser.py # 数据解析
├── storage.py # 数据存储
└── config.py # 全局配置
关键复用接口设计:
python复制class DamaiCrawler:
def __init__(self, headless=False):
self.driver = init_browser(headless)
def crawl(self, keyword=None, max_pages=5):
"""支持按关键词搜索和分页控制"""
pass
def export(self, format='csv'):
"""支持多种导出格式"""
pass
5. 实战中的避坑指南
5.1 常见异常处理
- 元素定位失效:增加try-catch块和重试机制
python复制from retrying import retry
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def safe_find(driver, by, value):
return driver.find_element(by, value)
- 验证码触发:自动识别或人工介入方案
python复制if "verify" in driver.current_url:
input("请手动完成验证码后按回车继续...")
5.2 性能优化技巧
- 启用浏览器缓存:
options.add_argument("--disk-cache-dir=./cache") - 并行化改造:使用
concurrent.futures.ThreadPoolExecutor - 智能限流:根据响应时间动态调整请求间隔
6. 完整代码实现
核心爬取流程整合版:
python复制# damai_spider.py
import time
import pandas as pd
from selenium import webdriver
from bs4 import BeautifulSoup
class DamaiSpider:
def __init__(self):
self.options = webdriver.ChromeOptions()
self._setup_options()
self.driver = webdriver.Chrome(options=self.options)
def _setup_options(self):
"""配置浏览器选项"""
self.options.add_argument("--disable-blink-features=AutomationControlled")
self.options.add_argument("user-agent=Mozilla/5.0...")
def crawl_events(self, url, pages=3):
"""主爬取方法"""
all_events = []
try:
for page in range(1, pages+1):
page_url = f"{url}?page={page}"
self.driver.get(page_url)
self._scroll_page()
events = self._parse_events()
all_events.extend(events)
time.sleep(3) # 礼貌爬取
finally:
self.driver.quit()
return all_events
def _scroll_page(self):
"""滚动加载完整页面"""
last_height = self.driver.execute_script("return document.body.scrollHeight")
while True:
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
new_height = self.driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
def _parse_events(self):
"""解析演出数据"""
soup = BeautifulSoup(self.driver.page_source, 'html.parser')
events = []
for item in soup.select('.search-items .items li'):
try:
events.append({
'name': item.select_one('a').get('title'),
'time': item.select_one('.time').text,
'venue': item.select_one('.place').text,
'price': item.select_one('.price').text.strip(),
'status': item.select_one('.status').text
})
except Exception as e:
print(f"解析异常: {e}")
return events
if __name__ == "__main__":
spider = DamaiSpider()
data = spider.crawl_events("https://search.damai.cn/search.html")
pd.DataFrame(data).to_csv("damai_events.csv", index=False)
7. 扩展应用场景
这套框架只需简单修改即可适配:
- 其他票务平台:猫眼/票星球修改选择器即可
- 动态价格监控:添加定时任务和价格波动报警
- 抢票脚本扩展:集成到自动化抢票系统
我在实际使用中发现,将爬取间隔设置为3-5秒、使用住宅代理IP、定期更换User-Agent组合使用,可以稳定运行数周不被封禁。对于需要登录的场景,建议使用cookies持久化方案而非自动登录。
