ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Selenium实战:从零爬取豆瓣Top250,掌握浏览器自动化核心技巧

Selenium实战:从零爬取豆瓣Top250,掌握浏览器自动化核心技巧 说实话我一开始是有点抗拒用Selenium去爬豆瓣Top250的。毕竟这个页面是服务端渲染的老牌页面直接requests加BeautifulSoup三分钟就能拿下来何必动用一台真实浏览器。但后来带过几个新人发现大家普遍卡在登录弹窗、翻页失效、等待时间踩不准这些地方我才意识到豆瓣Top250其实是把Selenium核心知识点练熟的好靶场。它足够简单——分页规律、元素结构几年都不怎么大变又足够真实——不定时弹登录框、偶尔加载慢半拍、访问频繁了还会被限流。在这样一个“你知道正确答案”的页面里正好可以验证自己对Selenium每一步的理解到底对不对。这篇文章就是我完整走一遍的实战复盘包含可以直接运行的代码以及我在里面踩过的坑、想通的逻辑。适合刚学完Python基础、想写第一个完整爬虫的朋友也适合已经会用requests但想补上“浏览器自动化”这块拼图的人。1. 为什么拿Selenium啃豆瓣Top250而不是直接上requests1.1 这个项目覆盖了Selenium的核心用法很多人问爬个静态页面有必要上Selenium吗真没必要但作为练习项目它的价值恰好在于“对照实验”。你清楚知道页面上有哪些元素、翻页参数是怎么变的所以代码一旦报错你能很快判断是定位写错了、等待没给足还是网站改了结构。这种“已知答案调试未知问题”的过程就是学习自动化测试和爬虫最舒服的路径。整个项目会覆盖Selenium的几块核心能力浏览器驱动配置、元素定位、显式等待、翻页处理、异常捕捉、数据抽取。这几块能力几乎所有的浏览器自动化项目都用得上。不管是以后去爬需要登录的站点、处理无限滚动页面还是用Playwright做自动化测试思维模型完全一致。1.2 requests方案和Selenium方案的区别先放一张对比表方便你理解为什么有人喜欢requests、有人离不开Selenium。维度requests BeautifulSoupSelenium请求方式直接模拟HTTP请求驱动真实浏览器加载页面速度快250条几秒内完成慢页面渲染、资源加载都有耗时反爬对抗需要手动维护Headers、Cookie像真实用户操作但也会被识别无头特征动态内容拿不到JS渲染后的数据能拿到完整渲染后的DOM交互能力无法模拟点击、输入、滑动可以模拟几乎一切用户操作对豆瓣Top250来说requests确实够用。但注意这个页面的列表数据是包含在首屏HTML里的所以requests能轻松拿到。如果网站哪天把列表改成异步加载或者点击“展开更多”才会出现数据requests方案就立刻失效Selenium的改动成本就要小很多。1.3 为什么最后还是推荐Selenium走一遍新手写爬虫最常见的问题是“不知道怎么判断页面加载完了没有”。requests只要拿到HTML就算完事但真实浏览器里页面是边加载边渲染的元素出现有先后顺序。Selenium的核心价值就是逼着你学会“等待”——等条件成立再继续操作而不是蒙头sleep。另外Selenium会暴露一个新手经常忽略的事实网站不是一个稳定的接口它是一个会弹窗、会变慢、会抽风的应用。你在Selenium里学会try/except、学会失败重试这套代码防御能力放到任何爬虫项目里都是通用的。2. 环境准备浏览器驱动版本匹配这一关就能卡掉一半新人2.1 最少要装哪些东西Python环境就不多说了建议3.8以上。核心依赖只有一个pip install seleniumChrome浏览器建议用最新稳定版。这个项目本质上只是调用浏览器去访问页面所以Chrome版本越新和驱动的兼容性就越容易处理。2.2 浏览器驱动的版本陷阱这是新手栽跟头最多的地方。Selenium要控制浏览器需要ChromeDriver而ChromeDriver必须和浏览器版本匹配。版本差多了启动时会报类似session not created、unknown error: cannot find Chrome binary这样的错误。现在的Selenium从4.6版本开始内置了Selenium Manager可以自动去下载匹配的驱动省掉了很多麻烦。但我个人的习惯仍然是手动下载驱动放到一个固定目录然后用Service来指定路径。原因很简单Selenium Manager在部分公司内网环境下会下载失败手动管理更可控。from selenium.webdriver.chrome.service import Service service Service(rD:\chromedriver\chromedriver.exe) driver webdriver.Chrome(serviceservice, optionsoptions)注意老代码里常见的executable_pathr...写法在Selenium 4.0之后已经标记为过期新项目就别用了。2.3 headless模式到底开不开无头模式就是浏览器在后台运行、不弹窗。好处是节省资源适合部署在服务器上坏处是部分反爬系统会识别无头浏览器的特征导致页面表现异样。我的建议是第一次调试时不要开headless把浏览器窗口摆在眼前亲眼看它一页一页翻过去这样你能立刻发现弹窗有没有出现、元素有没有加载。等代码稳定之后再改成headless模式跑批量任务。如果你确实需要headless新版Chrome建议用这个参数options.add_argument(--headlessnew)同时可以加上这些配置降低被识别的概率options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False)--window-size这个参数很多人忽略不带的话headless模式默认窗口尺寸是800x600部分元素会被判定为不可见导致读取失败。先固定成1920x1080能少很多麻烦。3. 抓取代码逐段拆解显式等待、稳健定位、start参数翻页3.1 等待策略不要用time.sleep这是Selenium最容易被用错的地方。新手喜欢在driver.get(url)后面直接time.sleep(2)等两秒再抓。这个写法的问题在于网络好时白白等2秒网络差时2秒压根不够页面没加载完就去定位元素照样报错。正确做法是显式等待让代码等到“条件成立”才继续执行from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ol.grid_view)))上面这段的意思是最多等10秒直到页面里出现ol.grid_view这个列表容器。如果10秒内没出现就抛出Timeout异常。这就是“显式等待”的含义——等的是条件不是时间。3.2 定位策略宁可条件写稳一点豆瓣Top250每部电影的信息都在ol.grid_view li里这是一个很清晰的重复区块。建议用它来圈定范围再在每部电影的内部去定位具体字段避免把整个页面搅在一起处理。核心字段的定位方式排名item.find_element(By.CSS_SELECTOR, em).text片名item.find_elements(By.CSS_SELECTOR, span.title)第一个是中文名第二个是外文名/别名有的电影只有一个所以要用find_elements再取列表导演/主演/年份/国家/类型item.find_element(By.CSS_SELECTOR, p).text再按字符拆分评分item.find_element(By.CSS_SELECTOR, span.rating_num).text评价人数item.find_elements(By.CSS_SELECTOR, div.star span)最末一个span里包含“人评价”一句话简介item.find_elements(By.CSS_SELECTOR, span.inq)注意有的电影没有必须判空3.3 翻页逻辑直接用start参数而不是点击“后页”豆瓣Top250的URL非常有规律https://movie.douban.com/top250?start0filter https://movie.douban.com/top250?start25filter https://movie.douban.com/top250?start50filter每页25条10页刚好250条。循环10次就行for page in range(10): url fhttps://movie.douban.com/top250?start{page * 25}filter driver.get(url) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ol.grid_view))) items driver.find_elements(By.CSS_SELECTOR, ol.grid_view li) # 解析 items为什么不用点击页面上的“后页”我实测下来点击的方式依赖“后页”按钮可见、未被遮挡、可点击任何一个条件不满足都会失效。而直接改URL再driver.get等价于手动在地址栏输入地址再回车是最稳的翻页方式。我个人后期做数据回填时还喜欢把这种情况做成函数传一个start值就抓一页逻辑更清爽。3.4 完整解析代码下面这段是解析单页所有电影的代码def parse_page(driver, page): url fhttps://movie.douban.com/top250?start{page * 25}filter driver.get(url) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ol.grid_view))) movies [] items driver.find_elements(By.CSS_SELECTOR, ol.grid_view li) for item in items: # 排名有时前面有空格顺手strip rank item.find_element(By.CSS_SELECTOR, em).text.strip() # 片名第一个span.title是中文名第二个是外文名/别名 title_tags item.find_elements(By.CSS_SELECTOR, span.title) title_cn title_tags[0].text.strip() if title_tags else title_other title_tags[1].text.strip() if len(title_tags) 1 else # 导演等信息 info_text item.find_element(By.CSS_SELECTOR, p).text.strip() # 评分 rating item.find_element(By.CSS_SELECTOR, span.rating_num).text.strip() # 评价人数 star_spans item.find_elements(By.CSS_SELECTOR, div.star span) rating_people star_spans[-1].text.strip() if star_spans else # 一句话简介可能没有 inq_tags item.find_elements(By.CSS_SELECTOR, span.inq) inq inq_tags[0].text.strip() if inq_tags else movies.append({ rank: rank, title_cn: title_cn, title_other: title_other, info_text: info_text, rating: rating, rating_people: rating_people, inq: inq }) return moviesinfo_text的原始格式一般是这样的导演: 弗兰克·德拉邦特 Frank Darabont / 主演: 蒂姆·罗宾斯 Tim Robbins / ... / 1994 / 美国 / 犯罪 剧情年份、国家、类型都在后半段用/分割后取对应位置即可。但要注意有的老电影没有“主演”信息直接是导演: XXX / 上映年份 / 国家 / 类型所以不要硬性按固定下标取建议先按/切分再逐个字段去判断里面有没有“年份数字”“国家关键词”。我写这个项目时是用正则去提取年份import re year_match re.search(r(19\d{2}|20\d{2}), info_text) year year_match.group(0) if year_match else 这样比固定下标稳妥得多。4. 实测中的稳定性问题登录弹窗、元素遮挡、超时重试4.1 登录弹窗怎么处理豆瓣有个让人头大的习惯连续访问几页之后会弹一个“登录豆瓣”的模态框有时候还会把页面遮住导致后续操作定位不到元素。弹窗这玩意儿并不是每次都出现所以不能用程序流程去硬等。我的做法是每页开始先“尝试关一次弹窗”关不掉也无所谓不中断主流程def close_modal_if_present(driver): try: close_btn driver.find_element(By.CSS_SELECTOR, .modal-dialog .btn-close) close_btn.click() except Exception: pass有些弹窗没有关闭按钮或者按钮被其他浮层挡住这时候可以模拟按Esc键把它关掉from selenium.webdriver.common.keys import Keys driver.find_element(By.TAG_NAME, body).send_keys(Keys.ESCAPE)如果页面因为弹窗变成了登录页最简单的识别方式是看当前URL有没有包含login。有的话直接重新driver.get回到原地址if login in driver.current_url: driver.get(https://movie.douban.com/top250?start0filter)4.2 元素不在视口内滚动与可见性问题豆瓣Top250这个页面本身不需要滚动就能加载全部25条但如果你以后去爬其他网站会遇到“元素在页面底部直接读取/点击失败”的情况。热词里有人提到“Selenium 网页左右滑动”“左右滚动可见”本质都是同一类问题——元素不在视口内浏览器认为它不可见、不可交互。通用解法是先滚动到目标元素所在位置target driver.find_element(By.CSS_SELECTOR, 某个选择器) driver.execute_script(arguments[0].scrollIntoView(true);, target)如果页面的数据是懒加载的滚动到页面底部还会触发新的数据加载这也是无限滚动类页面的标准处理方式。熟练掌握scrollIntoView就能应对绝大部分“元素看不见”的报错。4.3 网络波动与超时重试真实爬虫里网络波动导致driver.get半天加载不出来是家常便饭。WebDriverWait会等10秒但10秒内页面只加载了一半依然拿不到数据。我给这个项目加了一个简单的重试函数def fetch_with_retry(driver, url, max_retries3): for attempt in range(max_retries): try: driver.set_page_load_timeout(15) driver.get(url) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ol.grid_view))) return True except Exception: if attempt max_retries - 1: return False time.sleep(2) return False运行结果里如果哪一页返回False记录下来最后再单独补抓即可。不要为了省事重试太多次连续失败说明大概率触发了限流这时候停下来比硬刚更明智。4.4 频率控制别把自己爬成攻击者Selenium开着真实浏览器看起来比requests更接近真实用户但豆网的限流策略不是摆设。我在实测中发现如果每页间隔0.5秒以内连续抓大约第4页开始就极易触发登录弹窗严重时还会出现“请求过多”的提示。解决方案很朴素随机延时import random import time time.sleep(random.uniform(1, 3))每页之间随机休息1到3秒全程也就多花30秒但稳定性提升非常明显。这种“让行为更像人”的思路比任何技术对抗都重要。另外说一句边界问题微信公众号付费内容、朋友圈这类私有数据本来就不该去爬也不属于公开页面的范畴。爬虫要做的是在公开信息、合理频率、尊重版权的前提下做自动化采集这一点守住底线然后谈技术才有意义。5. 数据落地与项目升级CSV存储、可视化与Playwright迁移思路5.1 存成CSVExcel打开不乱码数据抓到手之后第一件事是落盘。我习惯用CSV格式因为它足够通用Excel、pandas都能直接读。这里有个小坑直接用默认的utf-8编码写CSVExcel打开会乱码要用utf-8-sig。import csv def save_to_csv(movies, filenamedouban_top250.csv): # utf-8-sig 是重点Excel才不会乱码 with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([排名, 片名, 外文名, 年份, 国家/地区, 类型, 评分, 评价人数, 一句话简介]) for m in movies: writer.writerow([ m[rank], m[title_cn], m[title_other], m[year], m[country], m[genre], m[rating], m[rating_people], m[inq] ])如果你有pandas直接这样更省事import pandas as pd df pd.DataFrame(movies) df.to_csv(douban_top250.csv, indexFalse, encodingutf-8-sig)5.2 简单看一眼数据分布数据落盘之后顺手做个可视化这个项目才更有意思。比如用matplotlib画一下评分分布能直观看到豆瓣Top250的评分基本都集中在8.5到9.5分这个区间import matplotlib.pyplot as plt df[rating] df[rating].astype(float) df[rating].hist(bins20) plt.title(豆瓣Top250评分分布) plt.xlabel(评分) plt.ylabel(数量) plt.show()这种“爬下来之后还能干什么”的延伸会让整个项目从单纯的爬虫练习变成一次完整的数据分析流程。5.3 从Selenium到Playwright新项目还选谁最近这两年Playwright越来越流行热词里也能看到“playwright爬取抖音评论区”的需求。它和Selenium最大的区别是内置了auto-wait很多时候你不需要自己写WebDriverWait元素还没出现时它会自动等待另外Playwright的API设计更现代支持隔离的浏览器上下文在做多账号场景时天然有优势。但我不建议新手一上来就学Playwright。原因很简单Selenium的教程和踩坑文档数量是Playwright没法比的而且Selenium的等待、定位、异常恢复这套心法恰恰能在Playwright里面原样复用。你先用Selenium把“等待是什么”“为什么元素会找不到”这些问题搞明白再切换到Playwright会发现只是把find_element换成了page.locator思维完全不用变。小建议跑完这个项目之后再上手Playwright去爬一次上面的榜单感受一下两者在代码写法上的差异。有了对比你才能真正理解为什么有些人执着于Selenium为什么有些人跳去玩Playwright。最后再分享一点个人经验。用Selenium写爬虫真正决定脚本能不能长期跑的不是定位写得多花哨而是等待、重试、异常恢复这三件事。我见过太多人把代码写完跑一次成功就收进收藏夹过两周再跑发现豆网改了一行class脚本直接崩。爬虫天生就是脆弱的工程保持“页面变了马上能改”的心态比追求一次性写完美重要得多。项目跑完后建议你再改几个地方试试把start参数翻页改成点击翻页感受一下差异把等待时间改成随机延时观察什么时候会触发登录弹窗把存储格式改成Excel体验一下不同编码的坑。这几项改动每一项都会让Selenium的理解更扎实。
返回列表