一、理解数据源:结构化的混乱
在开始编写提取代码之前,我们必须理解携程页面数据的构成。现代网页大量使用JavaScript动态渲染,数据通常以JSON格式直接嵌入在HTML中,而非简单的静态文本。
- 价格信息:通常不在静态HTML里,而是通过AJAX请求从后端API获取的一个JSON数据包。这个数据包结构复杂,包含原价、现价、折扣、房型、是否含早餐等大量信息。
- 评分与评论摘要 :如总评分、分项评分(位置、卫生、服务、设施)、评论总数等,常以嵌套的JSON对象形式存在于页面的
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);"><script></font>标签内。 - 评论内容:与价格类似,评论详情也是通过单独的API接口异步加载的,返回标准的JSON格式,其中包含了用户昵称、评论内容、评分、入住时间、有用数等字段。
因此,我们的提取策略主要分为两种:
- 方案A:解析内嵌JSON - 从初始的HTML页面中,通过正则表达式或HTML解析器找到包含酒店概要信息的JSON字符串。
- 方案B:直接请求API接口 - 通过浏览器开发者工具(F12)找到获取价格和评论数据的真实API地址,用爬虫直接模拟这些请求。
本文将重点介绍方案A,因为它更通用,且在一次请求中能获取到除详细评论外的大部分关键信息。
二、技术栈与准备工作
我们将使用以下Python库:
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">requests</font>:用于发送HTTP请求,获取网页内容。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">json</font>:用于解析JSON字符串。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">re</font>:正则表达式库,用于精准匹配和提取文本中的JSON块。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">pandas</font>:用于最终数据的整理和存储。
三、实战代码:三步提取关键信息
假设我们已经通过爬虫获取到了一个杭州酒店列表页或详情页的HTML内容,存储于变量 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">html_content</font> 中。
步骤一:定位并提取核心JSON数据块
经过对携程页面的分析,我们发现酒店的核心数据被包含在一个特定的 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);"><script></font> 标签中,其类型为 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">application/json</font>,并且有一个特定的ID。我们可以使用正则表达式来精准抓取它。
plain
import re
import json
import pandas as pd
import requests
from typing import Dict, Any
# 代理配置
proxyHost = "www.16yun.cn"
proxyPort = "5445"
proxyUser = "16QMSOML"
proxyPass = "280651"
# 构建代理字典
proxies = {
"http": f"http://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}",
"https": f"https://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}"
}
# 请求头配置
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1"
}
def get_html_with_proxy(url: str) -> str:
"""
使用代理发送请求获取HTML内容
参数:
url: 目标URL
返回:
HTML内容字符串
"""
try:
response = requests.get(
url,
proxies=proxies,
headers=headers,
timeout=10,
verify=False # 注意:在生产环境中建议设置为True
)
response.raise_for_status() # 如果状态码不是200,抛出异常
return response.text
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return ""
def extract_hotel_json(html_content: str) -> Dict[str, Any]:
"""
从携程酒店HTML页面中提取包含酒店关键信息的JSON数据。
参数:
html_content: 网页的HTML内容字符串
返回:
解析后的Python字典,如果提取失败则返回空字典。
"""
# 使用正则表达式匹配特定的JSON数据块
# 此模式是示例,实际模式需根据携程页面结构调整
pattern = r'<script id="json-lg" type="application/json">(.*?)</script>'
match = re.search(pattern, html_content, re.DOTALL)
if match:
json_str = match.group(1)
try:
# 解析JSON字符串
hotel_data = json.loads(json_str)
return hotel_data
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
return {}
else:
print("未找到匹配的JSON数据块。")
return {}
# 示例使用 - 更新后的版本
def main_example():
"""
使用代理获取网页内容的示例
"""
# 示例URL - 请替换为实际的携程酒店URL
example_url = "https://hotels.ctrip.com/hotels/detail/123456"
# 使用代理获取HTML内容
html_content = get_html_with_proxy(example_url)
if html_content:
# 提取JSON数据
hotel_json = extract_hotel_json(html_content)
if hotel_json:
print("成功提取酒店JSON数据")
# 这里可以继续处理提取到的数据
print(f"提取到数据键: {list(hotel_json.keys())}")
else:
print("未能提取到有效的JSON数据")
else:
print("未能获取网页内容")
# 批量处理多个URL的函数
def batch_process_hotels(url_list: list, delay: float = 2) -> list:
"""
批量处理多个酒店URL
参数:
url_list: URL列表
delay: 请求延迟(秒),避免请求过快
返回:
提取到的酒店数据列表
"""
import time
all_hotel_data = []
for i, url in enumerate(url_list):
print(f"正在处理第 {i+1}/{len(url_list)} 个URL: {url}")
html_content = get_html_with_proxy(url)
if html_content:
hotel_data = extract_hotel_json(html_content)
if hotel_data:
hotel_data['source_url'] = url # 添加来源URL
all_hotel_data.append(hotel_data)
# 添加延迟,避免请求过于频繁
if i < len(url_list) - 1: # 最后一个不需要延迟
time.sleep(delay)
return all_hotel_data
if __name__ == "__main__":
# 运行示例
main_example()
# 批量处理示例
# hotel_urls = [
# "https://hotels.ctrip.com/hotels/detail/123456",
# "https://hotels.ctrip.com/hotels/detail/789012",
# # 添加更多URL...
# ]
# results = batch_process_hotels(hotel_urls)
# print(f"成功处理 {len(results)} 个酒店数据")
步骤二:从JSON中解析价格、评分与评论摘要
成功提取到顶层JSON后,我们需要像剥洋葱一样,一层层地找到目标数据。这需要对JSON结构有清晰的了解。
plain
def parse_key_info(hotel_data: Dict[str, Any]) -> Dict[str, Any]:
"""
从酒店JSON数据中解析出价格、评分和评论摘要等关键信息。
参数:
hotel_data: 从extract_hotel_json函数得到的字典
返回:
包含提取出的关键信息的新字典。
"""
key_info = {}
try:
# 1. 提取酒店基本信息
key_info['酒店名称'] = hotel_data.get('pageConfig', {}).get('pageData', {}).get('baseInfo', {}).get('hotelName')
key_info['酒店ID'] = hotel_data.get('pageConfig', {}).get('pageData', {}).get('baseInfo', {}).get('hotelId')
# 2. 提取价格信息 - 路径非常复杂,需要仔细分析
# 注意:价格信息可能因房型而异,这里取第一个可用房型的价格
room_types = hotel_data.get('pageConfig', {}).get('pageData', {}).get('roomTypes', [])
if room_types:
first_room = room_types[0]
key_info['房型'] = first_room.get('roomName')
# 价格可能在priceInfo或avgPrice等字段中
price_info = first_room.get('priceInfo', {})
key_info['最低价格'] = price_info.get('avgPrice', price_info.get('price'))
key_info['货币'] = price_info.get('currencyCode', 'CNY')
else:
key_info['房型'] = None
key_info['最低价格'] = None
key_info['货币'] = 'CNY'
# 3. 提取评分信息
review_summary = hotel_data.get('pageConfig', {}).get('pageData', {}).get('reviewSummary', {})
key_info['综合评分'] = review_summary.get('score')
key_info['点评数量'] = review_summary.get('count')
# 细分评分
sub_scores = review_summary.get('subScores', {})
key_info['位置评分'] = sub_scores.get('position')
key_info['设施评分'] = sub_scores.get('facility')
key_info['服务评分'] = sub_scores.get('service')
key_info['卫生评分'] = sub_scores.get('sanitation')
# 4. 提取评论标签摘要(例如: "好评" "环境优雅" 等)
keyword_summary = review_summary.get('keywordSummary', [])
key_info['评论标签'] = ', '.join([kw.get('keyword') for kw in keyword_summary[:3]]) # 取前三个标签
except Exception as e:
print(f"解析关键信息时出错: {e}")
return key_info
# 示例使用
# key_info = parse_key_info(hotel_json)
# print(key_info)
步骤三:整合与数据存储
现在,我们可以将上述功能整合到一个流程中,并处理多个酒店的数据,最后使用Pandas保存到CSV文件。
plain
def main(hotel_urls):
"""
主函数,处理多个酒店URL,提取信息并保存。
"""
all_hotels_info = []
for url in hotel_urls:
print(f"正在处理: {url}")
# 1. 获取页面HTML(此处需自行添加headers等反爬措施)
# response = requests.get(url, headers=headers)
# html_content = response.text
# 为演示,我们假设html_content已获取
html_content = "" # 这里应该是实际的HTML内容
# 2. 提取JSON
hotel_json = extract_hotel_json(html_content)
if not hotel_json:
continue
# 3. 解析关键信息
hotel_info = parse_key_info(hotel_json)
hotel_info['数据来源URL'] = url # 记录来源
all_hotels_info.append(hotel_info)
# 4. 转换为DataFrame并保存
df = pd.DataFrame(all_hotels_info)
# 数据清洗:确保价格是数值型
df['最低价格'] = pd.to_numeric(df['最低价格'], errors='coerce')
# 保存到CSV文件
output_filename = 'hangzhou_hotels_key_info.csv'
df.to_csv(output_filename, index=False, encoding='utf-8-sig')
print(f"数据已成功保存到 {output_filename}")
print(f"共处理了 {len(df)} 家酒店的信息。")
return df
# 假设的URL列表
# hotel_urls = [
# 'https://hotels.ctrip.com/hotels/detail/123456',
# 'https://hotels.ctrip.com/hotels/detail/789012',
# # ... 更多杭州酒店详情页URL
# ]
# df_result = main(hotel_urls)
四、注意事项与进阶优化
- 反爬虫策略 :携程有严格的反爬机制。务必设置合理的请求头(User-Agent, Referer),使用代理IP池,并添加请求间隔(如
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">time.sleep</font>)。 - 代码健壮性 :网页结构可能随时变动。上述代码中的JSON路径是示例,实际应用中需要您使用浏览器开发者工具重新分析并调整路径。应添加大量的
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">try-except</font>块来处理可能缺失的字段。 - 处理分页评论:若要获取全部评论,需要找到评论API,并构造分页参数进行循环请求。这通常涉及分析Network选项卡中的XHR请求。
- 法律与伦理:本文内容仅用于技术交流和学习。大规模爬取商业网站数据可能违反其服务条款,请务必控制爬取速度和数据量,尊重网站权益,并将数据用于合法合规的分析研究。
五、结语
通过本文介绍的技术流程,我们成功地将非结构化的网页HTML,转化为了包含价格、评分和评论摘要的结构化数据。这套"提取-解析-存储"的方法论,不仅适用于携程,经过调整后也可应用于其他OTA平台(如飞猪、Booking.com)的数据处理。掌握了这项数据提炼技术,您就拥有了将互联网上海量、杂乱的公开信息,转化为高质量、可操作商业洞察的金钥匙。