Python Selenium实战:淘宝天猫商品评论爬虫开发与反爬策略详解
1. 项目概述与核心价值
最近在做一个数据分析项目,需要一批真实的电商商品评论数据。直接找公开数据集,要么数据太旧,要么品类不符合需求。于是,我决定自己动手,用 Python 和 Selenium 写一个爬虫,直接从淘宝和天猫的商品详情页抓取评论。这听起来像是爬虫领域的“经典考题”,但实际操作起来,你会发现从登录、反爬绕过到数据解析,每一步都藏着不少细节和“坑”。今天,我就把这个项目的核心思路和关键代码拆解一遍,希望能帮你少走弯路,快速搭建一个稳定可用的电商评论采集工具。
这个爬虫的核心目标是:模拟真实用户浏览商品页、点击“评价”标签、滚动加载、并解析出结构化评论数据(如用户昵称、评价内容、购买属性、评价时间、追评等)。它特别适合需要小规模、定制化采集电商评论的研究者、数据分析师或电商运营人员。整个过程我们将完全在浏览器自动化框架 Selenium 的加持下完成,它强大的模拟能力是我们对抗现代电商网站复杂交互和反爬机制的关键。当然,我们也会严格遵守网站的 robots.txt 协议,控制请求频率,做一个有“道德”的爬虫。
2. 环境准备与核心工具选型
工欲善其事,必先利其器。在开始写代码之前,我们需要搭建好开发环境,并做出几个关键的技术选型。这些选择直接决定了后续开发的效率和爬虫的稳定性。
2.1 Python 与 Selenium 基础环境
首先,确保你安装了 Python(推荐 3.8 及以上版本)。然后,通过 pip 安装核心库:
pip install selenium
pip install pandas # 用于数据存储和整理
pip install lxml # 用于辅助解析 HTML,比内置的 html.parser 更快更稳定
Selenium 本身只是一个控制浏览器的 API,它需要一个浏览器驱动(WebDriver)才能工作。这里我强烈推荐使用 Chrome 浏览器 和对应的 ChromeDriver 。理由很简单:Chrome 的开发者工具(DevTools)功能强大,调试方便;社区资源丰富,遇到问题容易找到解决方案;并且其无头模式(Headless)非常成熟稳定。
驱动下载与配置要点 :
- 查看你本地 Chrome 浏览器的版本(在浏览器地址栏输入
chrome://version/)。 - 访问 ChromeDriver 的官方镜像站或国内可靠的镜像站,下载与你的 Chrome 主版本号完全一致的驱动版本。版本不匹配是新手最常见的报错原因之一。
- 将下载的
chromedriver.exe(Windows)或chromedriver(Mac/Linux)文件放在一个目录下,并将该目录添加到系统的环境变量PATH中。更简单的做法是,在代码中指定驱动的绝对路径。
注意 :淘宝/天猫的页面动态加载非常复杂,不建议使用无界面浏览器如 PhantomJS(已停止维护)或 Firefox 的 geckodriver 作为首选。ChromeDriver 在兼容性和性能上目前是最佳选择。
2.2 反爬策略的初步考量
在写第一行爬取代码前,我们必须正视反爬问题。淘宝、天猫这类顶级电商网站的反爬体系是立体而严密的,主要包括:
- 用户行为检测 :检测鼠标移动轨迹、点击速度、页面停留时间等是否像真人。
- 流量特征识别 :识别来自 Selenium 的自动化流量(某些特定的 JavaScript 变量或 WebDriver 特征)。
- 验证码与滑块 :在检测到异常或频繁访问时,触发登录验证或商品详情页的滑块验证。
- 请求频率限制 :短时间内过多请求会导致 IP 被暂时限制访问。
我们的核心策略是 “模拟真人,分散请求” 。Selenium 帮助我们完美模拟“点击”和“滚动”这两个关键真人行为。接下来,我们需要通过一些设置,让 Selenium 驱动的浏览器看起来更“像”一个普通浏览器,并规划合理的爬取节奏。
2.3 浏览器实例的优化配置
直接使用默认的 Selenium WebDriver 很容易被识别。下面这段初始化代码是项目的基石,每一行配置都有其目的:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
def create_stealth_driver():
chrome_options = Options()
# 1. 启用无头模式(根据需要开启)。调试阶段建议关闭,便于观察。
# chrome_options.add_argument('--headless')
# 2. 禁用自动化控制标志,这是最重要的反反爬措施之一
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
# 3. 添加其他参数,使浏览器环境更“自然”
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_argument('--no-sandbox') # 在Linux服务器上运行时可能需要
chrome_options.add_argument('--disable-dev-shm-usage') # 解决共享内存问题
chrome_options.add_argument('--disable-gpu') # 某些环境下需要
chrome_options.add_argument('--window-size=1920,1080') # 设置窗口大小
# 4. 可以尝试添加一个常见的User-Agent,但Selenium会自动管理,非必须
# chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...')
driver = webdriver.Chrome(options=chrome_options)
# 5. 执行CDP命令,覆盖 navigator.webdriver 属性
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
'''
})
return driver
关键配置解读 :
excludeSwitches和useAutomationExtension:这两个选项是隐藏 Chrome 顶部“正受到自动测试软件控制”提示的关键,也能移除一部分自动化特征。--disable-blink-features=AutomationControlled:这是较新的参数,用于禁用 Blink 引擎中可能暴露自动化的特性。- CDP 命令:通过 Chrome DevTools Protocol 在页面加载前注入 JavaScript,将
navigator.webdriver属性设置为undefined。这是绕过很多基于此属性检测的网站反爬的核心技巧。
3. 核心爬取流程与页面交互解析
环境准备好后,我们进入核心环节:如何定位到评论数据并把它“掏”出来。淘宝/天猫的商品评论是通过异步加载(Ajax)渲染的,并且采用了分页滚动加载模式,这意味着我们无法通过一个简单的 URL 获取所有评论,必须模拟用户的交互行为。
3.1 定位商品页与评论入口
首先,我们需要一个商品页的 URL。通常,它长这样: https://detail.tmall.com/item.htm?id=商品ID 或 https://item.taobao.com/item.htm?id=商品ID 。
def crawl_item_comments(driver, item_url):
driver.get(item_url)
time.sleep(3) # 等待页面初步加载,可替换为更智能的等待
# 尝试寻找并点击“评价”标签
try:
# 方案一:通过链接文本定位(常见)
comment_tab = driver.find_element(By.LINK_TEXT, "评价")
# 方案二:通过CSS选择器定位(更精确)
# comment_tab = driver.find_element(By.CSS_SELECTOR, "#J_TabBar > li.tm-selected > a")
comment_tab.click()
print("已点击评价标签")
time.sleep(2) # 等待评价内容区域加载
except Exception as e:
print(f"未找到评价标签或点击失败: {e}")
# 可以尝试其他定位方式或直接解析当前页是否已有评论
return []
这里有个大坑 :淘宝/天猫的页面结构并非一成不变,不同店铺、不同活动时期的页面模板可能有差异。“评价”标签的 ID 或类名可能会变。因此, 不能依赖绝对不变的定位器 。我的经验是准备 2-3 套定位方案,按优先级尝试,并做好异常捕获。优先使用 LINK_TEXT (“评价”这两个字一般不变),其次使用包含性选择器,例如 a[href*=\"#J_Reviews\"] 。
3.2 滚动加载与动态内容捕获
点击“评价”后,评论列表并不会全部加载,只会加载第一屏。我们需要模拟滚动到底部,触发加载更多。
def scroll_to_load_all(driver, max_scrolls=20):
last_height = driver.execute_script("return document.body.scrollHeight")
scroll_attempts = 0
while scroll_attempts < max_scrolls:
# 滚动到页面底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2.5) # 关键:等待新内容加载。时间太短加载不全,太长效率低。
# 计算新的滚动高度并与上一次对比
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
# 高度未变化,可能已加载完毕,或遇到了“点击加载更多”按钮
try:
load_more_btn = driver.find_element(By.CLASS_NAME, “load-more-btn-class”) # 类名需实际探查
load_more_btn.click()
time.sleep(2)
print(“点击了‘加载更多’按钮”)
except:
# 没有找到按钮,且高度不变,视为加载完成
print(“评论列表似乎已加载完毕。”)
break
last_height = new_height
scroll_attempts += 1
print(f“正在进行第 {scroll_attempts} 次滚动,当前页面高度:{new_height}”)
滚动策略的心得 :
- 等待时间(
time.sleep) :这是平衡成功率和效率的关键。2-3 秒是一个比较安全的范围。在服务器网络好的情况下可以尝试缩短,但低于 1.5 秒很容易因加载延迟导致判断失误。 绝对不要用固定次数的循环无脑滚动 ,一定要通过判断页面高度是否变化来决定是否继续。 - “加载更多”按钮 :有些页面不是无限滚动,而是有一个“点击加载更多”的按钮。我们的代码需要兼容这种情况。先尝试滚动触发,如果滚动失效再寻找并点击按钮。
- 最大滚动次数 :设置一个上限(如 20-30 次)防止因页面异常导致的无限循环。一个正常商品的评价,滚动 10 次左右基本能加载完前几百条。
3.3 评论数据解析与提取
当所有评论加载到页面后,我们就可以从 HTML 中解析数据了。这里使用 lxml 配合 Selenium 的 page_source 属性,效率比单纯用 Selenium 的 find_elements 高。
from lxml import etree
import pandas as pd
def parse_comments_from_page(driver):
html_source = driver.page_source
tree = etree.HTML(html_source)
comments_list = []
# 使用XPath定位所有评论条目块。这个XPath需要根据实际页面结构调整,以下是示例。
# 打开浏览器开发者工具,在元素面板观察评论块的结构是必须的步骤。
comment_blocks = tree.xpath('//div[@class="tm-rate-content"]/div[contains(@class, “rate-item”)]')
if not comment_blocks:
print(“未找到评论条目,可能XPath已失效,需要更新。”)
# 备选方案:尝试用Selenium直接找元素
comment_elements = driver.find_elements(By.CSS_SELECTOR, ‘.tm-rate-content .rate-item’)
# ... 然后遍历 comment_elements 提取数据
return comments_list
for block in comment_blocks:
try:
comment_data = {}
# 用户昵称
comment_data[‘user’] = block.xpath(‘.//div[@class=“rate-user-info”]/a/text()’)[0].strip() if block.xpath(‘.//div[@class=“rate-user-info”]/a/text()’) else ‘匿名用户’
# 评价内容
content_elem = block.xpath(‘.//div[@class=“tm-rate-content”]//div[@class=“rate-content”]/text()’)
comment_data[‘content’] = ‘’.join([c.strip() for c in content_elem]) if content_elem else ‘’
# 购买属性(如“颜色分类:黑色;尺码:XL”)
sku_info = block.xpath(‘.//div[@class=“rate-sku”]/text()’)
comment_data[‘sku’] = sku_info[0].strip() if sku_info else ‘’
# 评价时间
comment_data[‘time’] = block.xpath(‘.//div[@class=“rate-time”]/text()’)[0].strip() if block.xpath(‘.//div[@class=“rate-time”]/text()’) else ‘’
# 追评内容(如果存在)
append_elem = block.xpath(‘.//div[contains(@class, “rate-append”)]//div[@class=“rate-content”]/text()’)
comment_data[‘append_content’] = ‘’.join([c.strip() for c in append_elem]) if append_elem else ‘’
comments_list.append(comment_data)
except Exception as e:
print(f“解析单个评论时出错: {e},跳过此条。”)
continue
print(f“本轮解析出 {len(comments_list)} 条评论。”)
return comments_list
XPath 定位的实战技巧 :
- 不要复制绝对路径 :浏览器开发者工具复制的 XPath 往往是长串的、依赖于绝对位置的路径(如
/html/body/div[3]/div[2]/div[5]/...),页面稍作改动就会失效。 - 使用相对路径和属性定位 :像上面的例子,尽量寻找具有稳定
class或id的父级容器,然后使用相对路径(以.//开头)向下查找。contains(@class, “rate-item”)这样的写法比@class=“rate-item other-class”更健壮,因为它只要求类名包含“rate-item”即可。 - 准备多套方案 :将关键的 XPath 或 CSS 选择器定义为配置变量,一旦失效,只需修改配置,而不用翻遍代码。
4. 关键问题排查与稳定性优化
即使按照上述流程操作,在实际运行中你依然会遇到各种问题。这一部分是我踩过坑之后的经验总结,直接关系到爬虫能否长期稳定运行。
4.1 验证码与滑块破解策略
这是电商爬虫最大的拦路虎。当你的爬虫行为被系统判定为风险时,就会触发验证。
-
滑块验证 :淘宝的滑块验证非常复杂,其轨迹算法是加密的。对于个人开发者或小规模爬取, 不建议投入精力去逆向破解 。性价比最高的策略是 “绕开” 和 “降频” 。
- 绕开 :尝试不登录状态访问。很多商品详情页和评价列表对未登录用户是开放的,只是可能看不到全部评价(如只显示前几百条)。这对于很多分析场景已经足够。
- 降频 :这是根本。大幅降低爬取频率,在每次操作(如点击、滚动)之间加入随机延时(例如
time.sleep(random.uniform(2, 5))),模拟真人阅读的犹豫时间。一个商品爬完后,休眠更长时间(如10-30分钟)再爬下一个。 - 人工干预 :如果只是为了偶尔抓取少量数据,可以在触发滑块时,让程序暂停,并弹出当前浏览器窗口, 手动完成滑块验证 ,验证通过后程序再继续。这可以通过检测页面是否出现滑块元素来实现。
-
登录态维持 :如果必须登录,可以考虑使用 Selenium 手动登录一次后,保存 Cookies 。后续爬虫启动时,先加载 Cookies 再访问页面,可能在一段时间内保持登录状态,避免反复触发登录验证。
import pickle
import os
def save_cookies(driver, path=“cookies.pkl”):
with open(path, ‘wb’) as file:
pickle.dump(driver.get_cookies(), file)
def load_cookies(driver, path=“cookies.pkl”):
if os.path.exists(path):
driver.get(“https://www.taobao.com”) # 先访问域名
with open(path, ‘rb’) as file:
cookies = pickle.load(file)
for cookie in cookies:
# 注意:有些Cookie有‘expiry’字段,可能是浮点数,需要转成int
if ‘expiry’ in cookie:
cookie[‘expiry’] = int(cookie[‘expiry’])
try:
driver.add_cookie(cookie)
except Exception as e:
print(f“添加Cookie失败: {e}”)
print(“Cookies加载完毕。”)
else:
print(“未找到Cookie文件,需要手动登录。”)
4.2 元素定位失败与页面结构变化
“昨天还能跑,今天怎么就定位不到元素了?” 这是爬虫的日常。
- 健壮性编码 :
- 多用
try...except:对任何元素查找操作进行包裹,发生异常时记录日志并执行备用方案或优雅退出,而不是让整个程序崩溃。 - 使用显式等待(WebDriverWait) :代替固定的
time.sleep。显式等待会在指定时间内持续检测某个条件是否成立(如元素出现、可点击),成立则立即继续,这比死等固定时间更高效、更稳定。
- 多用
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
try:
# 等待最多10秒,直到“评价”标签出现并可点击
comment_tab = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.LINK_TEXT, “评价”))
)
comment_tab.click()
except TimeoutException:
print(“等待‘评价’标签超时,页面可能加载异常或结构已变。”)
# 尝试备用定位方案或退出
- 定期更新定位器 :将所有的 XPath 或 CSS 选择器集中管理在一个配置文件中。当爬虫失效时,第一时间去页面上重新分析结构,更新配置文件,而不是修改代码逻辑。
4.3 请求频率控制与IP管理
即使模拟得再像,过于密集的请求也会导致 IP 被暂时封锁。
- 随机化等待时间 :在所有关键操作(页面跳转、点击、滚动)后,使用随机延时。
import random def random_delay(min_sec=1, max_sec=4): time.sleep(random.uniform(min_sec, max_sec)) - 设置爬取间隔 :爬完一个商品后,休眠较长一段时间(如 5-15 分钟),再开始爬下一个。可以模拟人类浏览多个商品时的间隔。
- 使用代理IP(进阶) :对于大规模爬取,这是必须的。可以通过
chrome_options.add_argument(f‘--proxy-server=http://{proxy_ip}:{proxy_port}’)为 Selenium 配置代理。你需要有一个稳定的代理IP池,并编写IP切换和失效检测的逻辑。注意,免费代理大多不稳定,商用代理是更靠谱的选择。
4.4 数据存储与去重
爬取的数据需要妥善保存。
- 增量存储 :建议使用如 SQLite、MySQL 或直接写入 CSV/JSON 文件。每次运行爬虫时,可以先根据商品ID和评价时间判断是否已存在该条记录,实现去重。
- 结构化存储 :使用 Pandas DataFrame 在内存中整理数据,然后一次性写入文件,比逐条写入效率高得多。
def save_to_csv(comments_list, filename=“comments.csv”):
if not comments_list:
return
df = pd.DataFrame(comments_list)
# 如果文件已存在,则追加写入,并去重
try:
existing_df = pd.read_csv(filename)
combined_df = pd.concat([existing_df, df], ignore_index=True)
# 根据‘用户’和‘时间’去重(假设同一用户在同一时间不会发两条相同评价)
combined_df = combined_df.drop_duplicates(subset=[‘user’, ‘time’], keep=‘last’)
combined_df.to_csv(filename, index=False, encoding=‘utf_8_sig’) # 解决中文乱码
except FileNotFoundError:
# 文件不存在,直接创建
df.to_csv(filename, index=False, encoding=‘utf_8_sig’)
print(f“数据已保存/更新至 {filename}”)
5. 完整代码示例与执行流程
将上述模块组合起来,形成一个完整的、可运行的爬虫脚本框架。
# main.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from lxml import etree
import pandas as pd
import time
import random
def random_delay(a, b):
time.sleep(random.uniform(a, b))
def create_driver():
# ... (使用前面定义的 create_stealth_driver 函数) ...
pass
def crawl_single_item(driver, item_url):
print(f“开始爬取商品: {item_url}”)
driver.get(item_url)
random_delay(3, 5)
# 1. 定位并点击评价标签
try:
comment_tab = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.CSS_SELECTOR, “a[href*=‘#J_Reviews’]”))
)
driver.execute_script(“arguments[0].click();”, comment_tab) # 用JS点击有时更稳定
print(“成功点击评价标签”)
random_delay(2, 3)
except Exception as e:
print(f“定位评价标签失败: {e},尝试备用方案...”)
# 备用方案:直接查找评价内容区域,可能已默认展开
pass
# 2. 滚动加载所有评价
scroll_to_load_all(driver, max_scrolls=15)
# 3. 解析当前页面所有评价
comments = parse_comments_from_page(driver)
print(f“本次共爬取到 {len(comments)} 条评价。”)
return comments
def main():
driver = create_driver()
try:
# 可以在这里加载cookies以维持登录态
# load_cookies(driver)
# 商品URL列表
item_urls = [
“https://detail.tmall.com/item.htm?id=具体商品ID”,
# ... 更多商品
]
all_comments = []
for idx, url in enumerate(item_urls):
comments = crawl_single_item(driver, url)
all_comments.extend(comments)
# 保存当前批次数据,防止中途出错丢失
if comments:
save_to_csv(comments, f“batch_{idx}.csv”)
# 关键:爬取间隔,避免过快
if idx < len(item_urls) - 1:
wait_time = random.randint(300, 900) # 等待5到15分钟
print(f“等待 {wait_time//60} 分钟后爬取下一个商品...”)
time.sleep(wait_time)
finally:
driver.quit()
print(“浏览器已关闭。”)
# 合并所有批次数据
# ... 合并CSV文件的代码 ...
if __name__ == “__main__”:
main()
这个框架提供了一个清晰的流程。你需要根据实际页面的 HTML 结构,调整 parse_comments_from_page 函数中的 XPath,并可能需要完善 scroll_to_load_all 函数中对“加载更多”按钮的检测逻辑。
6. 法律与道德边界:做一个负责任的爬虫
最后,也是最重要的一点,我们必须讨论爬虫的合法性。技术本身无罪,但使用方式有边界。
- 尊重
robots.txt:在爬取任何网站前,访问其robots.txt文件(如https://www.taobao.com/robots.txt)。这个文件指明了网站允许和禁止爬虫访问的路径。虽然它不是法律文件,但遵守它是行业惯例和基本的尊重。 - 限制爬取速度 :本文反复强调的延时和间隔,不仅是技术需要,也是道德要求。你的爬虫不应该对目标网站的正常运营造成可感知的负担。
- 明确数据用途 :爬取的数据应仅用于个人学习、研究或分析, 不得用于商业售卖、恶意竞争或任何侵犯他人权益的行为 。公开使用或发表基于这些数据的研究时,应注意脱敏。
- 关注用户隐私 :评论数据中可能包含用户昵称等个人信息。在收集、存储和处理时,应有基本的隐私保护意识。
写一个能跑的爬虫是第一步,写一个稳定、健壮、可持续且合乎规范的爬虫,才是真正的挑战。希望这篇基于实战的详细拆解,能为你提供一条清晰的路径。剩下的,就是根据具体的目标页面去调试和优化那些定位器了。记住,耐心和细致的观察(多使用开发者工具)是你最好的调试工具。
更多推荐
所有评论(0)