ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Selenium动态数据采集实战:从技术选型到合规避坑

2026/9/20 11:45:36 拓冰建站 浏览量
Selenium动态数据采集实战:从技术选型到合规避坑 1. 数据采集项目的整体设计与技术选型1.1 为什么选择Selenium而不是Requests做公开数据采集这行十来年我踩过最多的坑不是代码写不出来而是选错了工具。很多人一上来就用Requests加BeautifulSoup觉得轻量、快、代码少。这个组合确实好用但它有个致命前提目标页面的数据是直接写在HTML源码里的。一旦页面靠JavaScript动态渲染Requests拿到的就只是一具空壳里面什么都没有。我这次要处理的是一个药品监管类公开信息平台。这类平台的特点是数据量大、分页复杂、查询条件多而且页面内容基本靠前端异步加载。你右键查看网页源代码会发现表格区域是空的真正的数据藏在XHR请求或者JS渲染之后。这种情况下Requests直接出局。Selenium的核心价值在于它驱动的是真实浏览器。浏览器怎么渲染它就怎么拿。页面上的元素只要肉眼能看到Selenium就能定位到。这省去了逆向分析接口的大量时间尤其适合那些前端逻辑复杂、加密参数多的场景。当然Selenium不是没有代价。它慢资源占用高稳定性受网络和浏览器版本影响。所以我的原则是能逆向接口就逆向逆向成本太高才上Selenium。这个项目属于后者。1.2 整体架构拆解整个采集流程我分成四层驱动层Selenium WebDriver Chrome浏览器负责页面加载和交互定位层通过CSS选择器和XPath定位元素提取目标数据调度层控制翻页、条件筛选、异常重试存储层数据清洗后写入CSV或数据库这个分层的好处是职责清晰。驱动层出问题就查浏览器和驱动版本定位层出问题就查页面结构变化调度层出问题就查逻辑判断。排查效率比一坨代码高得多。1.3 反爬机制的常见类型与应对思路公开信息平台的反爬手段我归纳下来主要有这么几类反爬类型表现形式应对思路请求头检测拒绝无User-Agent或异常UA的请求使用真实浏览器天然携带完整请求头频率限制短时间内大量请求被封IP随机延时、控制并发、分时段采集动态渲染数据不在HTML源码中Selenium等待JS执行完成元素混淆类名随机化、动态生成用相对定位、文本定位替代固定类名验证码弹出图形或滑块验证降低频率避免触发必要时人工介入页面跳转检测到异常后跳转空白页监控URL变化及时重试Selenium天然规避了第一类和第三类问题因为它是真实浏览器在操作。需要重点处理的是频率控制和元素定位的稳定性。注意任何采集行为都必须遵守目标网站的robots协议和服务条款仅采集公开、非敏感、允许访问的数据。本文所有技术讨论均基于合规前提。2. 环境搭建与核心细节解析2.1 Python环境与Selenium安装环境这块我不想讲太多废话但确实有人卡在这一步。我的建议是直接用Anaconda或者官方Python安装包版本选3.9到3.11之间太新的版本有时候第三方库兼容性跟不上。安装Selenium就一行命令pip install selenium但光装Selenium还不够你还需要浏览器驱动。以前要手动下载chromedriver版本还得和Chrome对上非常麻烦。现在推荐用webdriver-manager它会自动帮你匹配驱动版本pip install webdriver-manager代码里这样用from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)这样就不用操心驱动版本问题了。我实测下来这个方案在Windows、macOS、Linux上都能跑通。2.2 浏览器启动参数配置默认启动的Chrome会带一堆东西扩展、通知栏、自动化提示条。这些不仅影响速度还可能干扰元素定位。我通常会加一组启动参数from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) 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)逐个解释一下--disable-gpu在部分Linux服务器上避免GPU渲染问题--no-sandbox容器环境下必须加否则Chrome起不来--disable-dev-shm-usage避免共享内存不足导致崩溃--window-size固定窗口大小保证元素布局一致--disable-blink-featuresAutomationControlled去掉部分自动化特征excludeSwitches和useAutomationExtension隐藏自动化提示条这些参数不是每个都必需但加上之后稳定性明显提升。尤其是--disable-dev-shm-usage我在服务器上跑的时候不加这个跑十几分钟就崩一次。2.3 元素定位策略的选择Selenium提供了八种定位方式但实际常用的就四种ID、CSS选择器、XPath、文本定位。我的优先级是ID CSS选择器 XPath 文本定位。ID最稳定但动态页面里往往没有ID。CSS选择器性能好、语法简洁适合结构清晰的页面。XPath功能最强支持轴定位和文本匹配但性能稍差而且容易因为页面微调而失效。文本定位适合按钮点击比如下一页这种。这个项目里表格数据的定位我用CSS选择器翻页按钮用XPath文本匹配。举个例子# 定位表格所有行 rows driver.find_elements(By.CSS_SELECTOR, table tbody tr) # 定位下一页按钮 next_btn driver.find_element(By.XPATH, //a[contains(text(),下一页)])用contains(text(),下一页)而不是精确匹配是因为按钮文字可能带空格或者后缀精确匹配容易翻车。2.4 等待机制显式等待优于隐式等待新手最容易犯的错就是用time.sleep()。这个方法是死等不管页面加载完没完时间到了才继续。页面快的时候浪费时间页面慢的时候又不够。Selenium提供了两种等待隐式等待和显式等待。隐式等待是全局设置driver.implicitly_wait(10)表示所有元素查找最多等10秒。它简单但不够灵活而且和显式等待混用会有奇怪的行为。显式等待是针对某个条件等待推荐用这个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, 15) table wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, table tbody tr)))presence_of_element_located表示元素出现在DOM中就返回不要求可见。如果要求可见用visibility_of_element_located。这两个的区别很关键有些元素在DOM里但被隐藏用presence能拿到用visibility就会超时。我一般用presence拿数据用visibility做点击前的判断。3. 实操过程与核心环节实现3.1 页面加载与初始等待第一步是打开目标页面。这里有个细节driver.get()默认会等页面onload事件触发但现代前端页面往往是异步加载onload触发时数据还没渲染完。所以get之后必须加显式等待。driver.get(目标页面URL) wait WebDriverWait(driver, 20) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, table tbody tr)))如果页面有查询表单还需要先填条件再点查询。比如按日期筛选date_input wait.until(EC.presence_of_element_located((By.ID, startDate))) date_input.clear() date_input.send_keys(2024-01-01) search_btn driver.find_element(By.CSS_SELECTOR, button.search-btn) search_btn.click() # 等待结果刷新 wait.until(EC.staleness_of(rows[0])) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, table tbody tr)))staleness_of是等待元素失效用来判断旧数据已经被替换。这个技巧很实用因为点击查询后旧表格不会立刻消失直接等新元素可能出现时序问题。3.2 数据提取与清洗表格数据的提取我习惯先拿到所有行再逐行解析单元格rows driver.find_elements(By.CSS_SELECTOR, table tbody tr) data [] for row in rows: cells row.find_elements(By.TAG_NAME, td) if len(cells) 5: continue record { name: cells[0].text.strip(), code: cells[1].text.strip(), date: cells[2].text.strip(), status: cells[3].text.strip(), remark: cells[4].text.strip() } data.append(record)这里有几个经验点len(cells) 5的判断是为了跳过空行或者合并行.text拿的是可见文本如果单元格里有隐藏元素用.get_attribute(textContent)更保险.strip()去掉首尾空白避免后续处理出问题如果某个字段需要从链接里拿比如详情页地址link cells[0].find_element(By.TAG_NAME, a).get_attribute(href)3.3 翻页逻辑的实现翻页是采集的核心环节。常见的有两种页码按钮和下一页按钮。页码按钮适合知道总页数的情况for page in range(1, total_pages 1): # 提取当前页数据 extract_data() if page total_pages: next_page_btn wait.until(EC.element_to_be_clickable( (By.XPATH, f//a[text(){page 1}]) )) driver.execute_script(arguments[0].scrollIntoView();, next_page_btn) next_page_btn.click() wait.until(EC.staleness_of(rows[0])) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, table tbody tr)))scrollIntoView是必须的因为按钮可能在页面底部不滚动到可视区域点击会报错。element_to_be_clickable比presence更严格确保按钮可点击。下一页按钮的逻辑类似但需要判断是否还有下一页try: next_btn driver.find_element(By.XPATH, //a[contains(text(),下一页) and not(disabled)]) next_btn.click() except NoSuchElementException: break用not(disabled)排除禁用状态用try-except处理最后一页。3.4 随机延时与频率控制这是合规采集的关键。不加延时地连续请求既容易触发限制也不符合负责任采集的原则。import random import time def random_delay(min_sec2, max_sec5): time.sleep(random.uniform(min_sec, max_sec))每次翻页后调用一次。延时范围我一般设2到5秒具体看目标站点的响应速度。如果页面本身加载就慢延时可以短一点如果响应很快延时就要长一点。更精细的做法是模拟人类操作节奏翻页前停顿一下滚动页面再点击。这样比机械式点击更自然。3.5 数据存储采集到的数据先存内存最后统一写入CSVimport csv with open(output.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[name, code, date, status, remark]) writer.writeheader() writer.writerows(data)utf-8-sig是为了Excel打开不乱码。如果数据量大建议边采集边写入避免内存溢出。数据库的话SQLite适合小规模MySQL或PostgreSQL适合大规模。4. 常见问题与排查技巧实录4.1 元素定位失败的排查思路元素定位失败是最常见的问题报错通常是NoSuchElementException或TimeoutException。排查顺序我总结成一张表现象可能原因排查方法找不到元素页面未加载完加显式等待检查等待条件找不到元素元素在iframe中driver.switch_to.frame()切换找不到元素元素在Shadow DOM中用JS执行shadowRoot查询找到但点击无效元素被遮挡滚动到可视区域或用JS点击找到但文本为空元素隐藏改用textContent属性定位时好时坏类名动态变化改用相对定位或文本定位iframe这个问题特别隐蔽。有些平台把表格嵌在iframe里你直接找table永远找不到。判断方法是在控制台输入window.frames.length大于0就说明有iframe。iframe driver.find_element(By.TAG_NAME, iframe) driver.switch_to.frame(iframe) # 操作完记得切回来 driver.switch_to.default_content()4.2 页面结构变化的应对公开平台的页面结构会不定期调整今天能用的选择器明天可能就失效。我的应对策略是选择器尽量用稳定的属性比如name、>SELECTORS { table_rows: table tbody tr, next_btn: //a[contains(text(),下一页)], search_btn: button.search-btn }这样页面一变只改配置就行不用翻遍代码。4.3 浏览器崩溃与内存泄漏长时间运行SeleniumChrome可能崩溃或者内存暴涨。我的处理方式每采集N页重启一次浏览器定期清理缓存和cookies用try-except包裹主循环崩溃后自动重启def safe_quit(driver): try: driver.quit() except Exception: pass # 每50页重启 if page % 50 0: safe_quit(driver) driver create_driver()重启后需要重新导航到目标页面所以要把当前进度存下来重启后从断点继续。4.4 验证码与访问限制的处理如果采集频率控制得当一般不会触发验证码。但万一触发了我的建议是立即停止当前任务不要硬刚延长延时降低采集速度分时段采集避开高峰期必要时人工介入处理千万不要试图用自动化手段绕过验证码这既不合规技术上也不稳定。负责任的做法是尊重目标站点的访问规则。提示采集前先看目标站点的robots.txt和服务条款确认允许采集的范围和频率。这是从业者的基本素养。4.5 数据去重与增量采集重复采集是浪费资源。我的做法是给每条记录生成唯一标识比如用编号加日期做哈希import hashlib def make_id(record): raw f{record[code]}_{record[date]} return hashlib.md5(raw.encode()).hexdigest()采集前先加载已有数据的ID集合遇到重复就跳过。增量采集的话记录上次采集的最大日期下次只采这个日期之后的。5. 性能优化与稳定性提升5.1 无头模式的使用与取舍无头模式headless不显示浏览器界面速度快、资源占用低适合服务器环境options.add_argument(--headlessnew) options.add_argument(--disable-gpu)但无头模式有个坑某些页面会检测无头特征返回不同的内容。如果发现无头模式下数据不对就切回有头模式。我一般先用有头模式调试确认逻辑没问题再切无头跑批量。5.2 并发采集的边界Selenium本身不适合高并发每个实例都是一个完整浏览器开多了内存扛不住。如果确实需要提速我的建议是控制在3到5个实例不要更多每个实例独立管理互不干扰配合代理池分散请求注意合规使用但说实话对于大多数公开数据采集任务单实例加合理延时就够了。追求速度而牺牲稳定性最后往往得不偿失。5.3 日志与监控跑批量任务一定要有日志否则出了问题都不知道哪一步挂的import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(spider.log, encodingutf-8), logging.StreamHandler() ] )关键节点都打日志开始采集、每页完成、异常捕获、任务结束。这样出问题能快速定位。6. 合规采集的经验与边界6.1 采集范围的自我约束技术能力不等于可以随意采集。我给自己定的规矩是只采集公开可见的数据不碰需要登录才能看的内容不采集个人隐私信息比如姓名、电话、身份证号控制采集频率不对目标站点造成压力数据仅用于学习研究不用于商业用途这些不是法律条文是从业者的自我约束。技术本身中性但使用技术的人要有底线。6.2 数据使用的注意事项采集到的数据怎么用同样重要。我的原则是不公开传播原始数据不用于任何可能损害他人利益的场景分析结果只做宏观统计不做个体画像6.3 技术学习的正确姿势学Selenium和爬虫最好的方式是拿自己的博客、公开的测试站点练手。比如用Selenium自动化测试自己的网站或者采集公开的天气数据、新闻标题。这些场景既安全又能练到技术。我见过太多人一上来就盯着大平台结果要么被封要么踩线。技术学习是个长期过程稳扎稳打比急功近利强得多。7. 完整代码结构与关键片段7.1 项目目录组织project/ ├── config.py # 配置URL、选择器、延时参数 ├── driver_factory.py # 浏览器创建与销毁 ├── extractor.py # 数据提取逻辑 ├── paginator.py # 翻页控制 ├── storage.py # 数据存储 ├── main.py # 主流程 └── spider.log # 日志分模块的好处是每个文件职责单一改起来不牵连。比如页面结构变了只改config.py存储格式变了只改storage.py。7.2 主流程代码import logging from config import SELECTORS, BASE_URL from driver_factory import create_driver, safe_quit from extractor import extract_table from paginator import go_next_page from storage import save_to_csv logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) def main(): driver create_driver() all_data [] try: driver.get(BASE_URL) page 1 while True: logging.info(f正在采集第 {page} 页) records extract_table(driver) all_data.extend(records) logging.info(f第 {page} 页采集 {len(records)} 条) if not go_next_page(driver): logging.info(已到最后一页) break page 1 if page % 50 0: safe_quit(driver) driver create_driver() driver.get(BASE_URL) save_to_csv(all_data, output.csv) logging.info(f采集完成共 {len(all_data)} 条) except Exception as e: logging.error(f采集异常: {e}) finally: safe_quit(driver) if __name__ __main__: main()7.3 关键函数实现翻页函数from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import NoSuchElementException, TimeoutException import random import time def go_next_page(driver): try: next_btn driver.find_element( By.XPATH, //a[contains(text(),下一页) and not(disabled)] ) driver.execute_script(arguments[0].scrollIntoView();, next_btn) time.sleep(random.uniform(2, 5)) next_btn.click() wait WebDriverWait(driver, 15) wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, table tbody tr) )) return True except (NoSuchElementException, TimeoutException): return False这个函数返回布尔值主流程根据返回值决定是否继续。用not(disabled)排除禁用按钮用try-except处理最后一页。数据提取函数def extract_table(driver): rows driver.find_elements(By.CSS_SELECTOR, table tbody tr) records [] for row in rows: cells row.find_elements(By.TAG_NAME, td) if len(cells) 5: continue records.append({ name: cells[0].text.strip(), code: cells[1].text.strip(), date: cells[2].text.strip(), status: cells[3].text.strip(), remark: cells[4].text.strip() }) return records8. 从Selenium到更优方案的思考8.1 Selenium的局限性用久了Selenium你会越来越清楚它的边界。慢是最大的问题一个页面加载加渲染动辄好几秒。资源占用也高一个Chrome实例几百兆内存。稳定性受浏览器版本、驱动版本、网络环境影响维护成本不低。8.2 什么时候该换方案如果目标页面的数据接口可以逆向那就用Requests或httpx直接请求接口速度快十倍不止。如果页面是静态的用BeautifulSoup或lxml解析轻量高效。如果需要处理复杂的JS渲染但不想开浏览器可以研究Playwright它在某些场景下比Selenium更现代。但逆向接口需要分析加密参数、签名算法学习曲线陡峭。对于不想深入前端逆向的开发者Selenium仍然是最友好的选择。8.3 技术选型的决策树我的决策逻辑是这样的页面数据在HTML源码里吗是用Requests BeautifulSoup数据靠JS渲染但接口简单吗是逆向接口用Requests接口有复杂加密逆向成本高吗是用Selenium或Playwright需要模拟复杂交互拖拽、滑块吗是用Selenium这个决策树帮我省了大量时间避免在错误的工具上死磕。9. 实操心得与避坑清单9.1 我踩过的那些坑坑一忘记切换iframe。找了半天元素找不到最后发现表格在iframe里。这个坑我踩过不止一次现在养成了习惯定位失败先查iframe。坑二用time.sleep代替显式等待。早期代码里全是sleep跑起来又慢又不稳。改成显式等待后速度和稳定性都上了一个台阶。坑三忽略页面加载时序。点击查询后立刻提取数据拿到的还是旧数据。后来学会用staleness_of判断旧元素失效问题解决。坑四不控制频率被封。年轻时候不懂事连续快速请求结果被限制访问。现在老老实实加延时再也没出过问题。坑五选择器写死类名。页面一改版类名变了代码全废。现在尽量用相对定位和文本定位抗变化能力强很多。9.2 避坑清单定位失败先查iframe和Shadow DOM等待用显式等待不用sleep翻页后等旧元素失效再提取选择器用稳定属性不用随机类名控制频率加随机延时长时间运行定期重启浏览器关键节点打日志方便排查采集前看robots协议和服务条款数据存储用utf-8-sig避免乱码增量采集避免重复劳动9.3 给新手的建议如果你刚接触Selenium我的建议是先拿简单的静态页面练手熟悉基本API。然后找一个有分页的公开列表页练习翻页和数据提取。最后再挑战动态渲染的复杂页面。不要一上来就盯着大平台那些反爬机制复杂容易打击信心。从简单场景入手循序渐进你会发现Selenium其实没那么难。另外多看看官方文档。Selenium的文档写得很清楚很多问题文档里都有答案。遇到报错先看异常信息再搜索最后才问人。这个习惯能帮你快速成长。最后说一句技术是工具合规是底线。用技术做有价值的事比用技术炫技重要得多。