
1. 项目概述与核心目标最近在做一个医药数据相关的分析项目需要获取药师帮平台上的药品信息和价格数据。药师帮作为国内知名的医药B2B平台汇聚了大量的药品、器械和供应商信息对于市场研究来说是个宝库。但直接手动去一个个页面复制粘贴效率实在太低而且数据量一大根本没法操作。所以用Python写个爬虫来自动化这个数据采集过程就成了最直接有效的解决方案。这个项目的核心目标很明确自动化登录药师帮绕过其登录验证码然后模拟用户搜索行为批量抓取指定药品的列表数据并将结果规整地保存到CSV文件中。听起来步骤清晰但实操起来每一步都可能遇到“拦路虎”尤其是那个动态验证码和复杂的页面交互。接下来我就把自己从环境搭建到最终数据落地的完整过程以及中间踩过的坑和总结的技巧详细拆解一遍。2. 环境准备与工具选型工欲善其事必先利其器。在开始写代码之前选择合适的工具链至关重要。这个项目涉及到浏览器自动化、验证码处理和数据处理工具选型直接决定了开发的效率和最终脚本的稳定性。2.1 Python环境与核心库首先确保你的Python环境是3.8或更高版本这是目前大多数库稳定支持的基础。我习惯使用conda或venv创建独立的虚拟环境避免包版本冲突。核心库的选择基于项目需求Selenium: 这是浏览器自动化的不二之选。药师帮的页面大量使用了JavaScript动态加载内容单纯的requests库无法获取到渲染后的数据。Selenium可以驱动真实的浏览器如Chrome完美模拟人的点击、输入、滚动等操作。Pandas: 数据处理和CSV文件读写的利器。用它来清洗、整理爬取到的数据然后导出为CSV比用Python内置的csv模块要方便和强大得多。WebDriver Manager: 一个非常实用的工具。它能够自动下载和管理与你的浏览器版本匹配的ChromeDriver省去了手动查找、下载和配置PATH的麻烦。安装命令很简单pip install selenium pandas webdriver-manager2.2 浏览器与驱动管理我选择Chrome作为自动化浏览器因为它用户基数大Selenium对其支持也最成熟稳定。这里的关键是浏览器版本与ChromeDriver驱动版本的匹配版本不匹配是导致Selenium报错的最常见原因之一。以前我们需要手动去ChromeDriver官网下载对应版本现在用webdriver-manager就省心多了。它在脚本运行时会自动检查本地Chrome版本并下载匹配的驱动代码如下所示。但这里有个细节有些公司的网络环境可能无法直接访问Google的下载服务器导致自动下载失败。我的经验是可以预先在能正常访问的网络环境下运行一次让webdriver-manager把驱动缓存到本地或者手动下载一个驱动并指定路径。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # 自动管理ChromeDriver推荐在网络通畅时使用 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) # 如果自动下载失败可以手动指定驱动路径备用方案 # service Service(executable_pathr你的本地驱动路径/chromedriver.exe) # driver webdriver.Chrome(serviceservice)2.3 验证码处理思路预研登录药师帮最大的挑战就是验证码。根据我的测试药师帮的登录验证码通常是动态生成的图形验证码数字字母混合或滑块验证码。完全通用的全自动识别方案成本高且不稳定对于定向爬虫项目来说性价比低。因此我采用了更务实高效的策略半自动处理。具体思路是当Selenium打开登录页面并填充账号密码后程序会暂停并提示用户手动在浏览器中完成验证码的识别和输入。确认登录成功后脚本再继续执行后续的爬取任务。这样既绕过了复杂的技术识别难题又保证了登录的成功率。我们只需要确保Selenium打开的浏览器窗口是可见的并且焦点就在验证码输入框附近即可。3. 核心爬虫逻辑设计与实现环境准备好后我们来构建爬虫的核心骨架。整个流程可以分解为几个清晰的步骤启动浏览器、访问网站、执行登录、进行搜索、解析数据、翻页循环、保存结果。3.1 浏览器实例化与基础配置直接启动一个“裸”的Chrome浏览器可能会遇到一些问题比如被网站检测到是自动化工具或者浏览器行为与真人差异太大。因此我们需要进行一些配置来让浏览器实例更“像人”。from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from webdriver_manager.chrome import ChromeDriverManager import time def init_driver(): chrome_options Options() # 1. 禁用“Chrome正受到自动测试软件控制”的提示栏 chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) # 2. 尝试绕过WebDriver检测部分网站会检测navigator.webdriver属性 chrome_options.add_argument(--disable-blink-featuresAutomationControlled) # 3. 使用常规用户数据目录避免每次打开都是全新空白会话 # chrome_options.add_argument(r--user-data-dirC:\Users\YourName\AppData\Local\Google\Chrome\User Data) # chrome_options.add_argument(--profile-directoryDefault) # 4. 可选无头模式不显示浏览器界面调试阶段建议关闭 # chrome_options.add_argument(--headless) # 5. 设置一些常规参数模拟真实浏览器 chrome_options.add_argument(--start-maximized) # 启动时最大化窗口 chrome_options.add_argument(--disable-infobars) chrome_options.add_argument(--disable-dev-shm-usage) chrome_options.add_argument(--no-sandbox) service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionschrome_options) # 执行CDP命令进一步覆盖可能被检测的JS属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); }) return driver注意上述配置中的--user-data-dir参数非常有用。如果你已经用Chrome手动登录过药师帮指定这个路径可以让Selenium直接加载已登录的会话完全跳过登录和验证码环节这是最稳定高效的方式。你需要将路径中的YourName替换成你电脑的用户名。首次使用前请确保所有Chrome窗口已关闭。3.2 登录流程的自动化与手动干预如果无法使用已登录的会话我们就需要模拟登录流程。这里的关键是精准定位页面元素。def login_yaoshibang(driver, username, password): login_url https://www.yaoshibang.com/login # 请替换为实际登录页URL driver.get(login_url) time.sleep(3) # 等待页面加载更好的做法是使用WebDriverWait # 定位账号密码输入框需要实际查看页面HTML结构 # 使用F12开发者工具查看元素的id、name、class或XPath try: # 示例定位方式实际需调整 user_input driver.find_element(By.ID, username) # 或 By.NAME, By.XPATH pwd_input driver.find_element(By.ID, password) user_input.clear() user_input.send_keys(username) time.sleep(1) pwd_input.clear() pwd_input.send_keys(password) time.sleep(2) print(账号密码已填充请在浏览器中手动完成验证码输入并点击登录...) print(登录成功后请回到控制台按回车继续程序。) # 程序暂停等待用户手动操作 input(手动登录完成后按回车键继续...) # 可以增加一个检测比如检查页面是否跳转到首页或出现登录成功元素 # WebDriverWait(driver, 30).until(EC.url_contains(home)) print(登录成功检测通过继续执行。) except Exception as e: print(f登录元素定位失败: {e}) driver.save_screenshot(login_error.png) # 出错时截图 return False return True实操心得time.sleep是简单的等待但在网络不稳定时容易出错。更健壮的做法是使用WebDriverWait配合expected_conditionsEC来等待特定元素出现、可点击或可见。例如等待登录按钮出现后再点击。但在等待验证码输入这个环节由于需要人工干预简单的input()暂停和提示是最可靠的。3.3 搜索与数据列表页解析登录成功后就可以进行药品搜索了。我们需要分析药师帮搜索结果的页面结构。进入搜索页/在首页搜索框输入同样需要定位搜索输入框和搜索按钮。解析列表数据打开开发者工具F12查看一个药品条目的HTML结构。通常一个条目会包含在一个div或li标签中内部有药品名称、规格、厂家、价格等子元素这些信息通常被包裹在特定的class或标签里。提取数据使用Selenium的find_elements注意是复数先找到当前页所有商品条目的容器然后循环每个容器在其内部使用find_element单数来提取具体的文本信息。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def search_and_parse(driver, keyword): data_list [] try: # 1. 定位搜索框并输入关键词 search_box WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, input.search-input)) # 示例CSS选择器 ) search_box.clear() search_box.send_keys(keyword) time.sleep(1) # 2. 定位搜索按钮并点击 search_btn driver.find_element(By.CSS_SELECTOR, button.search-btn) search_btn.click() # 3. 等待搜索结果列表加载 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CLASS_NAME, goods-item)) # 等待商品条目出现 ) time.sleep(2) # 再给一点时间让内容完全渲染 # 4. 开始解析当前页数据 # 假设每个商品项都有 classgoods-item items driver.find_elements(By.CLASS_NAME, goods-item) print(f当前页找到 {len(items)} 个商品项。) for item in items: try: # 在单个item内查找具体信息选择器需要根据实际页面调整 name_elem item.find_element(By.CSS_SELECTOR, .goods-name a) name name_elem.text.strip() # 获取href属性可能是详情页链接 detail_link name_elem.get_attribute(href) spec item.find_element(By.CLASS_NAME, goods-spec).text.strip() manufacturer item.find_element(By.CLASS_NAME, goods-manufacturer).text.strip() price item.find_element(By.CLASS_NAME, goods-price).text.strip() data_list.append({ 药品名称: name, 规格: spec, 生产厂家: manufacturer, 参考价格: price, 详情页链接: detail_link }) except Exception as e: # 某个字段提取失败记录日志并继续下一个商品 print(f解析单个商品时出错: {e}) continue except Exception as e: print(f搜索或解析过程发生错误: {e}) driver.save_screenshot(search_error.png) return data_list3.4 翻页逻辑的实现搜索结果通常有多页。我们需要实现自动翻页直到抓取完所有数据或达到设定的页数限制。翻页的关键是定位“下一页”按钮并判断是否可点击。常见的翻页元素可能是带有“下一页”文本的a标签或button也可能是一个右箭头图标。def crawl_multiple_pages(driver, keyword, max_pages5): all_data [] current_page 1 while current_page max_pages: print(f正在抓取第 {current_page} 页...) page_data search_and_parse(driver, keyword) # 注意这里需要优化search_and_parse不应每次都执行搜索而是解析当前页 all_data.extend(page_data) # 尝试翻到下一页 try: # 查找下一页按钮选择器需根据实际页面确定 # 例如可能是 classnext-page 的链接或者包含“下一页”文本的a标签 next_button driver.find_element(By.XPATH, //a[contains(text(),下一页)]) # 或者 next_button driver.find_element(By.CSS_SELECTOR, .pagination .next:not(.disabled)) # 检查按钮是否可点击是否被禁用 if disabled in next_button.get_attribute(class): print(已到达最后一页停止翻页。) break next_button.click() # 等待新页面加载完成 WebDriverWait(driver, 15).until( EC.stale_of(next_button) # 等待旧元素失效表示页面已开始刷新 ) time.sleep(3) # 等待新页面数据加载可结合EC优化 current_page 1 except Exception as e: print(f翻页失败或已是最后一页: {e}) break print(f抓取结束共获取 {len(all_data)} 条数据。) return all_data重要提示上面的search_and_parse函数在翻页循环中需要调整。它不应该每次都执行搜索操作而应该只负责解析当前浏览器已经打开的那个搜索结果页。因此在翻页循环中第一次调用前执行搜索之后每次翻页后调用它来解析新加载的页面内容。最好将函数拆分为perform_search(keyword)和parse_current_page()。4. 数据存储与导出为CSV抓取到的数据是Python字典列表用Pandas处理并导出为CSV非常方便。CSV格式通用可以用Excel直接打开也便于后续导入数据库或进行数据分析。import pandas as pd from datetime import datetime def save_to_csv(data_list, keyword): if not data_list: print(没有数据可保存。) return # 使用Pandas DataFrame df pd.DataFrame(data_list) # 生成文件名包含关键词和时间戳避免重复 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename f药师帮_{keyword}_数据_{timestamp}.csv # 保存为CSV设置编码为UTF-8-sig以兼容Excel中文显示 df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f数据已成功保存到文件: {filename}) print(f数据维度: {df.shape[0]} 行 x {df.shape[1]} 列) # 简单预览前几行数据 print(\n数据预览:) print(df.head())注意事项编码问题在Windows系统下用Excel直接打开UTF-8编码的CSV可能会中文乱码。使用utf-8-sig编码可以在文件开头添加BOM字节顺序标记让Excel正确识别。数据去重在翻页爬取时有时可能会因为网络延迟或页面刷新导致重复抓取同一页的数据。可以在保存前根据唯一标识如药品名称规格厂家进行去重。增量爬取如果是定期爬取可以将新爬取的数据与历史CSV文件合并并标记爬取日期。5. 完整脚本组装与优雅退出将上述所有函数模块组装起来并加入异常处理和资源清理形成一个完整的、健壮的脚本。import time import pandas as pd 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 selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from webdriver_manager.chrome import ChromeDriverManager def main(): driver None try: # 1. 初始化浏览器 print(正在初始化浏览器驱动...) driver init_driver() # 2. 尝试登录或使用已有会话 # 如果你配置了user-data-dir可以跳过此步直接访问需要登录后才能看的页面 # driver.get(https://www.yaoshibang.com/user/center) # 直接访问个人中心测试登录状态 # 如果需要模拟登录调用登录函数 # if not login_yaoshibang(driver, 你的账号, 你的密码): # print(登录失败程序退出。) # return # 3. 执行搜索和爬取 search_keyword 阿莫西林胶囊 # 替换为你想搜索的药品 max_pages_to_crawl 3 # 控制爬取页数避免请求过多 print(f开始爬取关键词 {search_keyword} 最大页数: {max_pages_to_crawl}) all_data crawl_multiple_pages(driver, search_keyword, max_pages_to_crawl) # 4. 保存数据 save_to_csv(all_data, search_keyword) print(\n爬虫任务执行完毕) except Exception as e: print(f程序运行过程中发生未预期错误: {e}) import traceback traceback.print_exc() # 打印详细的错误堆栈便于调试 finally: # 5. 无论成功与否最终都关闭浏览器 if driver: print(正在关闭浏览器...) driver.quit() print(浏览器已关闭。) if __name__ __main__: main()6. 常见问题排查与实战技巧在实际操作中你几乎一定会遇到下面这些问题。这里我把解决方案和思路整理出来希望能帮你节省大量调试时间。6.1 元素定位失败NoSuchElementException这是Selenium脚本中最常见的错误。原因和解决办法如下页面尚未加载完成元素还没出现你就去定位它。解决用WebDriverWait配合EC进行显式等待代替固定的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 # 等待最多10秒直到元素出现 element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, myElement)) ) # 或者等待元素可点击 button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CLASS_NAME, my-btn)) )元素在iframe或shadow DOM内部Selenium不能直接定位到这些隔离环境内的元素。解决对于iframe需要使用driver.switch_to.frame(frame_reference)切换到对应的frame后才能定位其中的元素。操作完后用driver.switch_to.default_content()切回来。选择器写错了或页面结构变了网站的HTML结构可能更新。解决重新用开发者工具检查元素使用更稳定的属性如>chrome_options.add_argument(f--proxy-serverhttp://your-proxy-ip:port)终极方案逆向分析接口如果浏览器自动化被严重封锁可以考虑用浏览器开发者工具的“网络Network”选项卡找到数据加载的AJAX接口通常是XHR或Fetch请求然后尝试用requests库直接调用这些接口。这通常需要处理加密参数和Cookie难度较大但效率最高也最隐蔽。6.3 验证码处理进阶思考对于本项目的手动输入验证码方案如何更友好自动截图并提示在需要输入验证码时程序可以自动对验证码区域进行截图并保存到本地然后在控制台打印出图片的路径让用户去查看。captcha_elem driver.find_element(By.ID, captcha_image) captcha_elem.screenshot(captcha.png) print(f验证码已截图至 captcha.png 请查看并输入。)集成打码平台如果项目需要全自动化可以考虑付费的打码平台如超级鹰、图鉴等。这些平台提供API你上传验证码图片它们返回识别结果。这会产生额外成本且识别率并非100%。机器学习方案高阶对于固定的验证码样式可以尝试用机器学习库如pytesseract做OCR识别简单的文字验证码或用OpenCVCNN训练模型识别滑块缺口位置。这需要大量的数据收集、标注和模型训练投入产出比需要仔细评估。6.4 数据抓取不全或错位动态滚动加载很多现代网站是滚动到底部时加载更多数据。Selenium需要模拟滚动操作。last_height driver.execute_script(return document.body.scrollHeight) while True: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) # 等待新内容加载 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: break last_height new_height数据在JavaScript变量中有时数据并不直接写在HTML里而是由JS脚本动态渲染。Selenium可以执行JS来获取这些变量。data driver.execute_script(return window.appData.productList;) # 假设数据在这个JS变量里解析逻辑不健壮页面结构可能微调或者某些商品缺少某个字段。你的解析代码必须有足够的容错能力使用try...except包裹每个字段的提取并记录解析失败的日志便于后续调整选择器。6.5 性能优化与稳定性设置页面加载超时避免因某个资源加载过慢导致脚本卡死。driver.set_page_load_timeout(30) # 30秒后超时 driver.set_script_timeout(30)复用浏览器会话如前所述使用--user-data-dir可以避免每次重新登录大幅提升效率。合理控制请求频率在翻页和点击操作之间加入适当的、随机的延迟既是对目标网站的尊重也能降低被封风险。做好异常恢复脚本可能因为网络波动、弹窗等原因中断。可以考虑将爬取进度如当前页码保存到文件或数据库当脚本重启时可以从断点继续而不是从头开始。整个项目从零到一跑通最关键的不是代码有多复杂而是对目标网站行为的细致观察、对Selenium API的熟练运用以及面对各种反爬机制时灵活的问题解决思路。希望这份详细的拆解和实录能帮你顺利“搞定”药师帮以及类似的动态网页爬取需求。记住爬虫的代码是死的但网站是活的保持学习和调整的心态最重要。