手把手教你用Python爬取京东评论和微博数据(含Selenium登录技巧)
实战进阶:Python数据采集的深度策略与工程化实践
在数据驱动的决策时代,获取第一手的用户反馈和市场声音,已成为产品迭代、品牌监测和趋势洞察的核心能力。对于具备一定Python基础的开发者而言,从公开的互联网平台采集数据,不再仅仅是“写个脚本”那么简单,它更像是一场与平台反爬机制、动态渲染技术和数据安全边界的精妙博弈。无论是分析电商平台上某款笔记本电脑的真实口碑,还是追踪社交媒体上特定话题的舆论风向,都需要一套兼顾效率、稳定与合规的技术方案。
本文将聚焦于两个极具代表性的实战场景:电商评论与社交媒体内容采集。我们将超越简单的requests库请求,深入探讨如何应对动态加载的JavaScript数据、如何优雅地处理需要登录验证的复杂页面,并分享在工程化实践中积累的避坑经验与性能优化技巧。我们的目标不仅是“爬取数据”,更是构建一套可维护、可扩展、抗干扰的数据采集系统。
1. 动态数据抓取:从接口分析到高效采集
面对现代Web应用,直接解析HTML源码往往徒劳无功,因为大量数据是通过Ajax或Fetch API动态加载的。以主流电商平台的商品评论为例,其内容通常以JSON格式通过后端接口提供。
1.1 逆向工程:定位核心数据接口
动手写代码前,最关键的步骤是使用浏览器的开发者工具进行网络抓包分析。打开目标商品页(例如某款热销电子产品),滚动到评论区域,观察网络面板(Network)中新增的请求。
提示:在Network面板中,使用
XHR或Fetch过滤器能快速定位到数据接口请求,重点关注请求的URL、参数(Payload/Query String Parameters)和请求头(Headers)。
一个典型的评论接口请求可能如下所示:
GET https://api.example.com/comment/productPageComments.action?
productId=100011483893&
score=0&
sortType=5&
page=0&
pageSize=10&
isShadowSku=0
关键参数解析:
productId: 商品唯一标识,通常可在商品页URL或页面源码中找到。page&pageSize: 控制分页。sortType: 评论排序方式(如按时间、按好评度)。score: 筛选特定评分(0表示全部,3表示好评等)。
1.2 构建稳健的请求模块
一旦确定了接口和参数规律,我们就可以用requests库构建采集脚本。核心在于模拟真实的浏览器请求,包括必要的请求头。
import requests
import time
import random
import pandas as pd
from typing import List, Dict
class ProductCommentCrawler:
def __init__(self, product_id: str):
self.product_id = product_id
self.base_url = "https://api.example.com/comment/productPageComments.action"
self.session = requests.Session()
# 设置关键请求头,User-Agent和Referer尤为重要
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': f'https://item.jd.com/{product_id}.html', # 模拟从商品页跳转
'Accept': 'application/json, text/javascript, */*; q=0.01',
}
self.session.headers.update(self.headers)
def fetch_comments_by_page(self, page: int, page_size: int = 10) -> List[Dict]:
"""获取单页评论数据"""
params = {
'productId': self.product_id,
'score': 0,
'sortType': 5,
'page': page,
'pageSize': page_size,
'isShadowSku': 0,
'fold': 1
}
try:
response = self.session.get(self.base_url, params=params, timeout=10)
response.raise_for_status() # 检查HTTP错误
data = response.json()
# 实际接口返回结构需根据实际情况调整
comments = data.get('comments', [])
return comments
except requests.exceptions.RequestException as e:
print(f"请求第{page}页失败: {e}")
return []
except ValueError as e:
print(f"解析JSON失败第{page}页: {e}")
return []
def crawl(self, max_pages: int = 10, delay: float = 1.5) -> pd.DataFrame:
"""执行多页爬取,并返回DataFrame"""
all_comments = []
for page in range(max_pages):
print(f"正在爬取第 {page + 1} 页...")
comments = self.fetch_comments_by_page(page)
if not comments:
print(f"第 {page + 1} 页无数据或获取失败,可能已爬完。")
break
all_comments.extend(comments)
# 随机延迟,避免请求过于频繁
time.sleep(delay + random.uniform(0, 0.5))
# 将数据转换为DataFrame便于处理
df = pd.DataFrame(all_comments)
# 清洗和选择需要的字段
if not df.empty:
df = df[['content', 'creationTime', 'score', 'userNickName']].copy()
df['creationTime'] = pd.to_datetime(df['creationTime'])
return df
# 使用示例
if __name__ == '__main__':
crawler = ProductCommentCrawler(product_id="100011483893")
comment_df = crawler.crawl(max_pages=15)
print(f"共爬取到 {len(comment_df)} 条评论")
# 保存到CSV
comment_df.to_csv('product_comments.csv', index=False, encoding='utf-8-sig')
关键点解析:
- 使用Session:保持会话,可以自动管理cookies,在某些场景下更接近真实浏览器行为。
- 设置请求头:
User-Agent和Referer是绕过基础反爬的关键。Accept头声明接受JSON格式,有时也是必需的。 - 错误处理:网络请求充满不确定性,必须用
try...except包裹,并处理JSONDecodeError。 - 遵守Robots协议与延迟:在请求间添加随机延迟(
time.sleep)是基本的网络礼仪,也是对目标服务器资源的尊重,能有效降低IP被封禁的风险。
1.3 数据清洗与结构化存储
原始JSON数据往往包含冗余信息。我们需要提取核心字段并进行清洗。例如,评论内容中可能包含HTML标签、特殊字符或无关的换行符。
import re
def clean_comment_text(text: str) -> str:
"""清洗评论文本"""
if not isinstance(text, str):
return ''
# 移除HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 移除多余的空白字符(包括换行、连续空格)
text = re.sub(r'\s+', ' ', text).strip()
# 其他自定义清洗规则...
return text
# 应用清洗函数
comment_df['clean_content'] = comment_df['content'].apply(clean_comment_text)
对于存储,除了CSV,根据数据量和后续使用场景,可以考虑SQLite(轻量级)、PostgreSQL或MongoDB(非结构化数据友好)。
2. 模拟登录与复杂交互:Selenium的进阶应用
当目标数据位于需要登录才能访问的页面(如社交媒体个人主页),或者页面交互极其复杂(如下拉加载、点击展开)时,Selenium这类浏览器自动化工具便成为首选。
2.1 Selenium环境配置与反检测策略
首先确保安装selenium库和对应的浏览器驱动(如ChromeDriver)。
pip install selenium
一个基础的、但增加了反检测能力的Selenium启动配置如下:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
def create_stealth_driver(driver_path: str):
"""创建一个经过基本反检测处理的Chrome驱动"""
chrome_options = Options()
# 添加常用选项以更像真实用户
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 禁用自动化控制标志
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
service = Service(executable_path=driver_path)
driver = webdriver.Chrome(service=service, options=chrome_options)
# 执行CDP命令,覆盖navigator.webdriver属性
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
window.chrome = { runtime: {} };
'''
})
return driver
2.2 实战:模拟登录与Cookie管理
以需要扫码或密码登录的网站为例,手动登录一次并保存Cookie,后续会话可直接加载Cookie,避免每次运行都需人工干预,这是自动化采集的常用技巧。
步骤一:手动登录并保存Cookie
def login_and_save_cookie(url, login_selector, save_path='cookies.pkl'):
"""手动登录后保存cookies"""
driver = create_stealth_driver('chromedriver')
driver.get(url)
print("请手动完成登录...")
# 等待用户手动登录,例如扫码
input("登录完成后,按回车键继续...")
# 获取并保存cookies
cookies = driver.get_cookies()
import pickle
with open(save_path, 'wb') as f:
pickle.dump(cookies, f)
print(f"Cookies已保存至 {save_path}")
driver.quit()
return cookies
步骤二:使用保存的Cookie恢复会话
def create_driver_with_cookies(cookie_path, driver_path='chromedriver'):
"""创建驱动并加载已有cookies"""
driver = create_stealth_driver(driver_path)
driver.get('https://www.zhihu.com') # 先访问域名
# 加载cookies
import pickle
with open(cookie_path, 'rb') as f:
cookies = pickle.load(f)
for cookie in cookies:
# 添加前可能需要删除'expiry'字段,因为其格式可能导致错误
if 'expiry' in cookie:
# 处理过期时间,确保是整数
cookie['expiry'] = int(cookie['expiry'])
try:
driver.add_cookie(cookie)
except Exception as e:
print(f"添加cookie失败: {cookie.get('name')}, 错误: {e}")
# 刷新页面,使cookies生效
driver.refresh()
# 显式等待,确认登录成功(例如出现用户头像元素)
try:
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "img.Avatar"))
)
print("Cookie登录成功!")
except:
print("Cookie可能已失效,需要重新登录。")
return driver
2.3 定位元素与智能等待
Selenium爬虫的稳定性极大程度依赖于元素定位和等待策略。绝对避免使用time.sleep进行固定等待,而应使用显式等待。
def fetch_weibo_content(driver, user_id, max_pages=5):
"""爬取某微博用户的前N页内容"""
base_url = f"https://weibo.com/u/{user_id}"
driver.get(base_url)
data_list = []
for page in range(1, max_pages + 1):
print(f"处理第 {page} 页")
# 显式等待微博卡片加载
try:
weibo_cards = WebDriverWait(driver, 15).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.WB_cardwrap"))
)
except:
print("等待微博卡片超时,可能页面结构已变或需要滚动。")
break
for card in weibo_cards:
try:
# 使用相对定位,在card元素内查找,更稳定
content_elem = card.find_element(By.CSS_SELECTOR, "div.WB_text.W_f14")
time_elem = card.find_element(By.CSS_SELECTOR, "a.S_txt2[node-type='feed_list_item_date']")
# 获取转发、评论、点赞数
repost = card.find_element(By.CSS_SELECTOR, "span[node-type='forward_btn_text']").text
comment = card.find_element(By.CSS_SELECTOR, "span[node-type='comment_btn_text']").text
like = card.find_element(By.CSS_SELECTOR, "span[node-type='like_status']").text
weibo_data = {
'time': time_elem.get_attribute('title') or time_elem.text,
'content': content_elem.text.strip(),
'repost': repost,
'comment': comment,
'like': like
}
data_list.append(weibo_data)
print(f" 获取微博: {weibo_data['time'][:10]}...")
except Exception as e:
# 某条微博解析失败,记录日志并继续
print(f" 解析单条微博时出错: {e}")
continue
# 尝试点击“下一页”或滚动加载更多
try:
next_button = driver.find_element(By.XPATH, "//a[@class='page next S_txt1 S_line1']")
next_button.click()
# 等待下一页内容加载
time.sleep(2) # 此处可结合显式等待优化
except:
# 如果没有找到分页按钮,尝试模拟滚动加载
print("尝试滚动加载...")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(3) # 等待新内容加载
return data_list
注意:社交媒体网站的前端结构变更频繁,上述CSS选择器(如
.WB_cardwrap)可能需要根据实际情况调整。定期检查并更新定位策略是维护此类爬虫的必要工作。
3. 工程化实践:构建健壮的数据采集管道
将零散的脚本升级为工程化项目,需要考虑模块化、配置化、错误恢复、日志监控和分布式扩展。
3.1 配置管理与模块化设计
将爬虫参数(如URL、请求头、CSS选择器)抽取到配置文件(如config.yaml或config.py)中。
# config.py
class Config:
# 京东评论爬虫配置
JD_COMMENT_API = "https://club.jd.com/comment/productPageComments.action"
JD_HEADERS = {
'User-Agent': '...',
'Referer': 'https://item.jd.com/'
}
# 微博爬虫配置
WEIBO_USER_ID = '2803301701'
WEIBO_CSS_SELECTORS = {
'card': 'div.WB_cardwrap',
'content': 'div.WB_text.W_f14',
'time': "a.S_txt2[node-type='feed_list_item_date']"
}
# 通用配置
REQUEST_DELAY = (1, 3) # 随机延迟范围
MAX_RETRIES = 3
LOG_LEVEL = 'INFO'
3.2 实现重试机制与异常处理
网络不稳定、目标服务器临时故障是常态。为关键请求添加重试逻辑至关重要。
import logging
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout)),
before_sleep=lambda retry_state: logger.warning(f"第{retry_state.attempt_number}次重试,异常: {retry_state.outcome.exception()}")
)
def fetch_with_retry(session, url, params=None, **kwargs):
"""带重试机制的请求函数"""
response = session.get(url, params=params, timeout=15, **kwargs)
response.raise_for_status()
return response
3.3 数据存储与增量更新
对于持续监控任务,只采集新数据能极大节省资源。通常基于数据的唯一ID或发布时间来实现增量采集。
import sqlite3
from contextlib import contextmanager
@contextmanager
def get_db_connection(db_path='data.db'):
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
def setup_database():
"""初始化数据库,创建表"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS weibo_posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
weibo_id TEXT UNIQUE, -- 微博唯一ID,用于去重
content TEXT,
publish_time DATETIME,
repost_count INTEGER,
comment_count INTEGER,
like_count INTEGER,
crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
def save_weibo_data_if_new(data_list):
"""保存微博数据,忽略已存在的记录"""
with get_db_connection() as conn:
cursor = conn.cursor()
for data in data_list:
# 假设data中包含从接口或页面解析出的唯一weibo_id
weibo_id = data['id']
cursor.execute('SELECT 1 FROM weibo_posts WHERE weibo_id = ?', (weibo_id,))
if cursor.fetchone():
logger.info(f"微博 {weibo_id} 已存在,跳过。")
continue
cursor.execute('''
INSERT INTO weibo_posts (weibo_id, content, publish_time, repost_count, comment_count, like_count)
VALUES (?, ?, ?, ?, ?, ?)
''', (weibo_id, data['content'], data['publish_time'], data['repost'], data['comment'], data['like']))
conn.commit()
logger.info(f"成功插入 {len(data_list)} 条新微博记录。")
4. 法律、伦理与性能边界探讨
技术能力的提升必须与法律意识和伦理规范同行。数据采集活动始终运行在合规的钢丝上。
4.1 合规性自查清单
在启动任何爬虫项目前,请务必审视以下几点:
- Robots协议:检查目标网站的
robots.txt文件(如https://www.example.com/robots.txt),尊重其规定的禁止爬取目录。 - 服务条款:仔细阅读网站的《用户协议》或《服务条款》,明确禁止自动化访问的条款。
- 数据用途:确保采集的数据用于个人学习、研究或合法的商业分析,不得用于非法售卖、侵犯隐私、发起攻击或进行不正当竞争。
- 访问频率:严格控制请求速率,添加显著的延迟,避免对目标服务器造成拒绝服务攻击(DoS)压力。
- 个人信息:绝对避免爬取和存储用户的个人敏感信息(如手机号、身份证号、详细地址等)。对于公开的评论和博文,也需谨慎处理。
4.2 应对反爬策略的合理边界
平台的反爬措施在不断升级,从简单的User-Agent检查到复杂的行为指纹识别。我们的应对策略应保持在合理的技术对抗范围内:
| 反爬手段 | 常见表现 | 合理应对策略 | 风险提示 |
|---|---|---|---|
| 请求头校验 | 检查User-Agent, Referer, Accept-Language等 |
使用常见浏览器的完整请求头 | 低风险,基础操作 |
| IP频率限制 | 同一IP短时间内请求过多返回429或封禁 | 使用代理IP池、严格遵守请求延迟 | 中等风险,需成本投入 |
| Cookie/Session验证 | 需要登录或验证码 | 模拟登录获取Cookie、使用已登录的Session | 中等风险,需维护账号 |
| JavaScript动态渲染 | 数据由JS加载,源码中不见 | 使用Selenium/Puppeteer或直接调用接口 | 中等风险,资源消耗大 |
| 行为指纹检测 | 检测鼠标轨迹、浏览器指纹、WebDriver特征 | 使用undetected-chromedriver等反检测库、模拟人类操作 |
高风险,技术门槛高,易失效 |
提示:当反爬措施过于严格时,应首先考虑目标数据是否有官方API提供,或者是否可以通过合作、购买等合法渠道获取。技术手段不应是首选,更不应是唯一手段。
4.3 性能优化与资源管理
当采集任务量大时,性能成为瓶颈。Selenium尤其消耗资源。
- 无头模式:在无GUI的服务器上运行时,使用
chrome_options.add_argument('--headless')。 - 资源限制:关闭图片加载、禁用CSS等非必要资源以加速。
chrome_options.add_argument('--blink-settings=imagesEnabled=false')
prefs = {"profile.managed_default_content_settings.images": 2}
chrome_options.add_experimental_option("prefs", prefs)
- 并发控制:对于I/O密集型的网络请求(如调用API),可以使用
asyncio+aiohttp或concurrent.futures.ThreadPoolExecutor实现并发,但务必控制并发数,避免对目标服务器造成冲击。 - Selenium连接复用:对于需要多次操作同一页面的任务,尽量复用
driver实例,避免频繁启动和关闭浏览器。
在项目后期,我倾向于将稳定的API采集任务与需要浏览器交互的复杂任务分离。API部分用异步框架实现高并发、高效率的采集;而Selenium则作为处理登录、验证码和极端动态页面的“特种部队”,按需调用,这样能在稳定性和效率之间取得更好的平衡。数据采集就像一场持久战,耐心、细致和对规则的尊重,远比炫技的代码更重要。
更多推荐


所有评论(0)