Python爬虫实战:突破验证码与登录,高效采集药师帮药品数据 1. 项目概述从“药师帮”看数据采集的实战价值最近在整理医药行业的数据发现“药师帮”这个平台的信息非常关键。它作为国内领先的医药B2B平台汇聚了海量的药品信息、价格动态和供应商数据对于市场分析、竞品调研乃至学术研究都有不小的参考价值。不过手动去一个个页面复制粘贴显然不现实这时候用Python写个爬虫就成了最直接高效的解决方案。这个项目就是要搞定药师帮网站的数据抓取把我们需要的信息结构化地保存下来。听起来好像就是个普通的爬虫但做过的人都知道这类成熟的商业平台往往防护严密登录验证、动态加载、反爬机制一个不少。特别是验证码几乎是绕不开的坎。所以这次我们不只讲怎么发请求、解析HTML更要重点解决如何突破这些常见的访问障碍尤其是验证码识别和模拟登录的问题。最终目标是把爬取到的数据比如药品名称、规格、厂家、价格、库存等规整地存进CSV文件里方便后续用Excel或Pandas做分析。无论你是想学习如何处理复杂的登录场景还是需要获取特定领域的商业数据这个案例都能提供一套完整的、可复现的思路和代码。我们会用到requests、selenium这些库也会探讨一些应对反爬的策略。下面我就把这次实战中的关键步骤、踩过的坑以及解决方案详细拆解一遍。2. 核心需求解析与技术选型2.1 目标网站分析与核心挑战药师帮yaoshang.com是一个需要登录才能访问大部分内容的平台这直接决定了我们爬虫的基本形态必须是一个能模拟用户登录行为的“浏览器”。经过初步探查其核心挑战主要集中在以下几点登录与会话维持网站采用账号密码登录成功后依赖Cookies或Session来维持登录状态。爬虫必须能成功完成登录流程并妥善管理这些凭证以便后续的页面抓取。验证码识别登录或频繁访问时极大概率会触发验证码。可能是常见的数字字母图片验证码也可能是更复杂的滑块、点选等交互式验证码。这是本项目需要攻克的首要技术难点。动态内容加载药品列表、详情信息很可能不是一次性加载在初始HTML中的而是通过JavaScript发起Ajax请求从后端接口动态获取JSON数据。这意味着简单的requests.get()可能拿不到完整数据。反爬虫机制除了验证码网站可能还设有请求头校验、访问频率限制、IP封禁等常规反爬措施。爬虫需要表现得像一个“好人”。数据结构化提取与存储最终我们需要从复杂的HTML或JSON中精准提取出目标字段并以表格形式如CSV持久化存储保证数据的整洁和可用性。2.2 技术栈选型与理由针对上述挑战我选择了以下技术组合并解释一下为什么这么选核心请求库RequestsSelenium双引擎Requests轻量级效率高用于处理那些不需要执行JS的简单请求例如提交登录表单如果验证码能解决、访问静态接口。它是我们获取数据的主力。Selenium浏览器自动化工具。当网站内容严重依赖JavaScript渲染或者验证码交互复杂到必须用真实浏览器环境触发时Selenium就派上用场了。我们可以用它来打开浏览器完成登录包括处理验证码获取渲染后的页面源码或Cookies再交给Requests去高效爬取。这是一种“混合模式”。为什么不只用Selenium因为Selenium驱动真实浏览器资源消耗大、速度慢不适合大规模数据抓取。它更适合完成“打开门”这个动作。为什么不只用Requests因为对于动态加载和复杂验证码纯Requests模拟的难度和成本可能更高。验证码处理ddddocr/Selenium手动过码 / 打码平台ddddocr一个开源且效果不错的通用验证码识别库。对于简单的数字字母图片验证码可以尝试用它本地识别成本最低。Selenium手动过码在开发调试阶段或者验证码过于复杂时最稳妥的方法是让程序在出现验证码时暂停我们手动输入然后程序继续。这保证了登录的成功率。第三方打码平台如果项目需要全自动化且验证码识别率要求高可以接入专业的打码平台API。这会产生费用但稳定性和识别率有保障。本项目为演示原理优先采用前两种方式。数据解析BeautifulSoup4/lxml/ 直接处理JSONBeautifulSoup4HTML解析神器语法友好适合从复杂的HTML标签树中提取数据。如果数据直接通过Ajax接口返回为JSON格式那最简单直接用json()方法解析即可效率最高。数据存储csv模块Python标准库中的csv模块简单可靠足以将提取出的数据列表写入到CSV文件中。CSV格式通用便于用Excel、Numbers或Pandas进行下一步分析。其他辅助工具time/random用于在请求间插入随机延时模拟真人操作避免因请求过快被封。Fake-Useragent用于生成随机的、真实的浏览器User-Agent字符串伪装请求头。注意在开始任何爬虫项目前请务必仔细阅读目标网站的robots.txt文件和服务条款尊重网站的版权和数据所有权合理控制爬取频率避免对目标网站服务器造成过大压力。本案例仅用于技术学习与交流。3. 环境准备与核心工具配置3.1 Python环境与库安装首先确保你有一个Python 3.7以上的环境。然后我们通过pip安装所需的第三方库。建议创建一个虚拟环境来管理依赖。# 安装核心请求与解析库 pip install requests selenium beautifulsoup4 # 安装验证码识别库 (可选用于简单图形验证码) pip install ddddocr # 安装随机User-Agent生成库 pip install fake-useragent # 安装用于解析动态JSON路径的库 (可选如果数据在复杂的JSON中) # pip install jsonpath-ng3.2 Selenium与浏览器驱动配置Selenium需要对应的浏览器驱动才能工作。这里以最常用的Chrome浏览器为例。查看Chrome版本打开Chrome在地址栏输入chrome://version/查看“Google Chrome”后面的版本号例如120.0.6099.110。下载ChromeDriver访问 ChromeDriver官网 或国内镜像站下载与你的Chrome浏览器主版本号完全相同的驱动例如Chrome 120.x就找120.x.x.x版本的驱动。放置驱动将下载的chromedriver.exeWindows或chromedriverMac/Linux文件放在一个目录下并将该目录添加到系统的PATH环境变量中。更简单的方法是直接把它放在Python的安装目录下和python.exe同级或者放在项目根目录然后在代码中指定其路径。一个更省事的方法是使用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)3.3 项目结构设计在开始编码前规划一个清晰的项目结构有助于代码管理。建议如下pharmacist-helper-spider/ ├── config.py # 配置文件存放URL、账号、密码等敏感信息 ├── main.py # 主程序入口 ├── core/ │ ├── login.py # 登录模块处理验证码和会话获取 │ ├── spider.py # 核心爬取逻辑 │ └── utils.py # 工具函数如请求头生成、延时处理 ├── data/ # 存放爬取到的CSV数据 └── logs/ # 存放日志文件可选将账号密码等敏感信息放在config.py中并用.gitignore忽略它避免泄露。4. 核心环节一模拟登录与验证码攻克登录是获取数据权限的第一步也是最容易卡住的一步。我们设计一个稳健的登录流程。4.1 手动分析登录流程首先手动在浏览器中打开药师帮登录页用开发者工具F12的“网络”(Network)选项卡监控登录过程。找到登录表单提交的URL通常是/login或/api/login这样的接口。查看提交的请求方法POST和请求体Form Data或Payload里面通常包含username、password、captcha验证码等字段还可能有一个隐藏的csrf_token。查看响应登录成功后是跳转还是返回一个包含token/session的JSON。假设我们分析发现登录接口是https://www.yaoshang.com/api/loginPOST方法需要phone、password、captcha三个参数。4.2 实现登录模块我们创建一个login.py文件核心任务是获取一个有效的登录会话Requests的Session对象。方案A使用Selenium处理复杂登录推荐用于绕过复杂验证码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 import time from config import USERNAME, PASSWORD def login_with_selenium(): 使用Selenium模拟浏览器登录获取Cookies。 此方法能处理任何在浏览器中可见的登录交互包括复杂验证码。 options webdriver.ChromeOptions() # 可选无头模式不显示浏览器窗口 # options.add_argument(--headless) # 可选禁用一些自动化特征降低被检测风险 options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, \webdriver\, {get: () undefined}) }) try: driver.get(https://www.yaoshang.com/login) # 替换为实际登录页URL wait WebDriverWait(driver, 10) # 1. 定位并输入用户名 username_input wait.until(EC.presence_of_element_located((By.NAME, phone))) # 根据实际元素名修改 username_input.send_keys(USERNAME) time.sleep(1) # 模拟人操作间隔 # 2. 定位并输入密码 password_input driver.find_element(By.NAME, password) password_input.send_keys(PASSWORD) time.sleep(1) # 3. 处理验证码 - 这里是关键 # 情况1简单图片验证码可以尝试自动识别 # captcha_element driver.find_element(By.XPATH, //img[classcaptcha-img]) # captcha_src captcha_element.get_attribute(src) # # 下载图片并用ddddocr识别此处省略识别代码 # # captcha_text recognize_captcha(captcha_src) # # driver.find_element(By.NAME, captcha).send_keys(captcha_text) # 情况2复杂验证码或自动识别失败采用手动输入开发调试用 print(【请手动完成验证码验证完成后在控制台按回车继续...】) input() # 程序暂停等待用户手动输入验证码并点击登录后按回车 # 4. 点击登录按钮 login_button driver.find_element(By.XPATH, //button[typesubmit]) login_button.click() time.sleep(3) # 等待登录完成和页面跳转 # 5. 检查是否登录成功例如检查是否跳转到首页或出现用户菜单 # if dashboard in driver.current_url: # print(登录成功) # else: # print(登录可能失败请检查。) # 6. 获取登录后的Cookies这是最关键的一步 cookies driver.get_cookies() # 将Selenium格式的cookies转换为Requests可用的字典格式 cookies_dict {cookie[name]: cookie[value] for cookie in cookies} print(f成功获取Cookies: {list(cookies_dict.keys())}) return cookies_dict except Exception as e: print(f登录过程中出现错误: {e}) return None finally: driver.quit() # 关闭浏览器释放资源 # 将获取的cookies用于创建Requests Session import requests def create_session_from_cookies(cookies_dict): session requests.Session() # 将cookies添加到session中 for name, value in cookies_dict.items(): session.cookies.set(name, value) # 更新session的请求头使其更像浏览器 session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Referer: https://www.yaoshang.com/, Accept-Language: zh-CN,zh;q0.9, }) return session方案B纯Requests登录如果验证码可破解如果验证码是简单的图片且你能找到获取验证码图片的独立接口可以尝试纯Requests方案。import requests from ddddocr import DdddOcr from config import LOGIN_URL, USERNAME, PASSWORD def login_with_requests(): session requests.Session() # 1. 首先访问登录页获取可能需要的token或初始cookies resp_init session.get(https://www.yaoshang.com/login) # 可能需要从resp_init.text中解析出csrf_token (使用BeautifulSoup) # 2. 获取验证码图片 # 假设验证码图片URL是固定的或者通过某个接口获取 captcha_url https://www.yaoshang.com/api/captcha captcha_resp session.get(captcha_url) with open(captcha.png, wb) as f: f.write(captcha_resp.content) # 3. 识别验证码 ocr DdddOcr() with open(captcha.png, rb) as f: image_bytes f.read() captcha_code ocr.classification(image_bytes) print(f识别出的验证码为: {captcha_code}) # 4. 构造登录数据 login_data { phone: USERNAME, password: PASSWORD, captcha: captcha_code, # csrf_token: csrf_token, # 如果有的话 } # 5. 提交登录 login_resp session.post(LOGIN_URL, datalogin_data) # 检查响应判断是否登录成功 if login_resp.status_code 200 and success in login_resp.text: print(Requests登录成功) return session else: print(f登录失败响应: {login_resp.text[:200]}) return None实操心得对于药师帮这类商业平台方案ASelenium获取Cookies Requests爬取的稳定性远高于方案B。手动过验证码虽然需要人工干预但在数据量不是特别巨大的情况下登录一次获取的Cookies可能能维持一段时间如几小时的会话足够完成一次爬取任务。这是性价比最高的方式。全自动识别验证码的投入产出比在初期往往不高。5. 核心环节二页面爬取与数据解析策略成功登录并获得有效Session后就可以开始爬取目标数据了。我们需要分析数据在页面中是如何呈现的。5.1 分析数据加载方式打开药师帮的商品列表页或搜索页同样使用开发者工具的“网络”选项卡。观察请求滚动页面时是否出现了新的XHR/Fetch请求这些请求的URL和响应内容通常是JSON很可能包含了我们需要的结构化数据。这是最理想的情况因为JSON数据无需解析HTML直接提取即可。检查响应如果滚动没有新请求数据可能直接渲染在初始HTML中。这时就需要用BeautifulSoup来解析HTML。假设我们发现了一个搜索接口GET https://www.yaoshang.com/api/search?keyword阿莫西林page1返回JSON数据。5.2 实现核心爬取逻辑我们创建spider.py编写爬取和解析函数。import requests import time import random from bs4 import BeautifulSoup import json from urllib.parse import quote from core.utils import get_random_ua # 假设有一个生成随机User-Agent的工具函数 class YaoshangSpider: def __init__(self, session): 初始化爬虫传入已登录的requests.Session对象。 self.session session self.base_url https://www.yaoshang.com self.data_list [] # 用于存储爬取到的所有数据 def fetch_via_api(self, keyword, max_pages5): 通过分析出的API接口爬取数据。 :param keyword: 搜索关键词 :param max_pages: 最大爬取页数 for page in range(1, max_pages 1): print(f正在爬取关键词『{keyword}』第 {page} 页...) # 构造API URL注意关键词需要URL编码 encoded_keyword quote(keyword) api_url f{self.base_url}/api/search?keyword{encoded_keyword}page{page}size20 try: # 使用已有的登录session发送请求 resp self.session.get(api_url, timeout10) resp.raise_for_status() # 如果状态码不是200抛出异常 # 解析JSON响应 data resp.json() # 根据实际JSON结构提取商品列表例如 data[list] items data.get(list, []) if not items: print(f第 {page} 页无数据可能已到末页。) break for item in items: # 提取所需字段这里字段名是假设的需要根据实际响应调整 product_info { 商品ID: item.get(productId), 商品名称: item.get(productName), 通用名: item.get(commonName), 规格: item.get(spec), 厂家: item.get(manufacturer), 参考价格: item.get(price), 库存状态: item.get(stockStatus), 最小起批量: item.get(minOrder), 供应商: item.get(supplierName), 爬取时间: time.strftime(%Y-%m-%d %H:%M:%S) } self.data_list.append(product_info) print(f 已添加: {product_info[商品名称][:20]}...) # 随机延时模拟人工操作避免被封 time.sleep(random.uniform(1.5, 3.5)) # 检查是否还有下一页根据接口返回的字段判断如 data[hasNext] # if not data.get(hasNext, True): # break except requests.exceptions.RequestException as e: print(f请求第 {page} 页时发生错误: {e}) break except json.JSONDecodeError as e: print(f解析第 {page} 页JSON时发生错误: {e}) print(f响应文本: {resp.text[:500]}) break def fetch_via_html(self, start_url): 如果需要从HTML页面解析数据当没有直接API时。 resp self.session.get(start_url) soup BeautifulSoup(resp.text, lxml) # 使用BeautifulSoup根据实际HTML结构定位商品元素 # 例如商品项可能在 div classproduct-item 里 product_items soup.find_all(div, class_product-item) for item in product_items: # 提取信息这里的选择器是示例必须根据实际网站调整 name_elem item.find(a, class_product-name) price_elem item.find(span, class_price) # ... 其他字段 product_info { 商品名称: name_elem.text.strip() if name_elem else , 价格: price_elem.text.strip() if price_elem else , # ... } self.data_list.append(product_info) # 查找下一页链接如果有 # next_page soup.find(a, text下一页) # if next_page: # next_url self.base_url next_page[href] # self.fetch_via_html(next_url) # 递归或循环爬取 def get_data(self): 返回爬取到的所有数据。 return self.data_list5.3 应对反爬策略请求头伪装确保Session的headers包含User-Agent,Referer,Accept-Language等常见浏览器头。使用fake-useragent库轮换User-Agent。请求频率控制在每次请求后使用time.sleep(random.uniform(a, b))进行随机延时。对于重要网站建议将b设置为3秒以上。IP代理池如果爬取量非常大或触发了IP限制需要考虑使用代理IP。可以购买付费代理服务或自建代理池。在requests中通过proxies参数使用。proxies { http: http://your-proxy-ip:port, https: http://your-proxy-ip:port, } resp session.get(url, proxiesproxies)错误重试机制对于网络超时等临时性错误可以编写重试逻辑。import tenacity # 一个很好的重试库 tenacity.retry(stoptenacity.stop_after_attempt(3), waittenacity.wait_fixed(2)) def safe_request(url): return session.get(url)6. 核心环节三数据存储与CSV导出爬取到的数据存储在内存的列表里最后需要持久化到硬盘。CSV是最简单通用的选择。import csv import os from datetime import datetime class DataExporter: staticmethod def save_to_csv(data_list, filename_prefixyaoshang_data): 将数据列表保存为CSV文件。 :param data_list: 列表其中每个元素是一个字典 :param filename_prefix: 文件名前缀 if not data_list: print(没有数据可保存。) return # 生成带时间戳的文件名避免覆盖 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fdata/{filename_prefix}_{timestamp}.csv os.makedirs(data, exist_okTrue) # 确保data目录存在 # 获取字典的所有键作为CSV的表头 fieldnames data_list[0].keys() try: with open(filename, w, newline, encodingutf-8-sig) as csvfile: # utf-8-sig支持Excel直接打开显示中文 writer csv.DictWriter(csvfile, fieldnamesfieldnames) writer.writeheader() writer.writerows(data_list) print(f数据已成功保存至: {filename} 共 {len(data_list)} 条记录。) except IOError as e: print(f写入CSV文件时出错: {e}) except Exception as e: print(f保存数据时发生未知错误: {e}) staticmethod def append_to_csv(data_list, filenameyaoshang_data_latest.csv): 追加数据到现有CSV文件如果文件存在且结构相同。 # 实现逻辑类似区别在于打开模式用 a且如果文件不存在或为空时才写入表头。 pass注意事项使用utf-8-sig编码而不是utf-8是为了在Windows系统的Excel中打开CSV时中文字符能正常显示而不会变成乱码。这是一个非常实用的小技巧。7. 主程序串联与完整流程最后我们在main.py中将所有模块串联起来形成一个完整的、可执行的流程。from core.login import login_with_selenium, create_session_from_cookies from core.spider import YaoshangSpider from core.exporter import DataExporter import time def main(): print( 药师帮数据爬虫启动 ) # 第一步登录获取有效Cookies print(\n1. 正在尝试登录...) cookies login_with_selenium() # 这里会弹出浏览器需要手动过验证码 if not cookies: print(登录失败程序退出。) return # 第二步用Cookies创建持久化会话 session create_session_from_cookies(cookies) print(2. 登录会话创建成功。) # 第三步初始化爬虫 spider YaoshangSpider(session) # 第四步执行爬取任务可以爬取多个关键词 search_keywords [阿莫西林, 布洛芬, 连花清瘟] # 示例关键词 for keyword in search_keywords: print(f\n3. 开始爬取关键词: {keyword}) spider.fetch_via_api(keyword, max_pages3) # 每个关键词爬3页 time.sleep(random.uniform(5, 10)) # 不同关键词间长间隔 # 第五步获取并保存数据 all_data spider.get_data() print(f\n4. 爬取结束共获取 {len(all_data)} 条商品数据。) if all_data: DataExporter.save_to_csv(all_data) print(5. 数据保存完成。) else: print(5. 未获取到任何数据。) print(\n 程序执行完毕 ) if __name__ __main__: main()8. 常见问题与排查技巧实录在实际运行中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。8.1 登录相关问题问题Selenium打开的浏览器被检测到是自动化工具。现象页面有提示或者无法正常加载登录元素。解决使用options.add_experimental_option和execute_cdp_cmd来隐藏WebDriver特征代码中已体现。也可以尝试使用undetected-chromedriver这个第三方库它专门用于规避检测。问题手动输入验证码后程序还是提示登录失败。排查检查输入账号密码和点击登录按钮的代码定位的元素是否正确ID、Class、XPath可能随网站更新而变化。用driver.save_screenshot(debug.png)截图看看页面状态。检查登录成功后的判断条件。有时登录后不是直接跳转而是弹出一个提示框。可以尝试等待某个只有登录后才出现的元素出现如用户头像wait.until(EC.presence_of_element_located((By.CLASS_NAME, user-avatar)))。获取Cookies的时机可能太早。在点击登录后用WebDriverWait显式等待几秒确保登录流程完全走完再获取Cookies。问题通过API登录方案B总是返回验证码错误。解决这通常意味着验证码识别失败或者网站有更复杂的校验如一次性的token。强烈建议放弃纯Requests方案改用Selenium获取Cookies的方案A。这是绕过复杂登录验证最有效的方法。8.2 爬取数据问题问题session.get(api_url)返回的状态码是403或404。排查403 Forbidden最可能的原因是请求头不完整或被识别为爬虫。检查Session的headers确保包含Referer、Origin等。尝试更新User-Agent。404 Not FoundAPI接口URL可能不对或者需要特定的参数。再次用浏览器开发者工具仔细核对真实的请求URL和参数。Cookies失效登录获取的Cookies可能有过期时间。如果爬取时间过长中间可能需要重新登录。可以在代码中加入对特定错误响应如跳转到登录页的判断触发重新登录流程。问题API返回的数据是空的但浏览器能看到数据。排查检查请求参数是否齐全。有些接口需要pageSize,sortType,timestamp等额外参数。检查请求方法。有些接口可能是POST而不是GET。尝试用Session直接访问一个在浏览器中能正常看到数据的完整URL看看返回什么。可能是接口有鉴权你的Session权限不足比如只能看部分数据。问题爬了几页后就被封IP了。解决首要措施大幅增加请求间隔时间。将time.sleep的随机范围调大例如random.uniform(5, 15)。使用代理IP这是解决IP封锁的根本方法。搭建或购买代理IP池在请求时轮换使用。模拟更真实的行为除了延时还可以模拟鼠标移动、随机滚动页面在Selenium中等。8.3 数据解析与存储问题问题BeautifulSoup找不到元素返回空列表。排查确认页面已加载完成如果是用Requests获取的HTML可能页面内容是JS动态生成的。这时必须用Selenium获取渲染后的driver.page_source。检查选择器网站的HTML结构可能已经改变。使用浏览器的“检查”功能重新确认目标元素的选择器XPath或CSS Selector。优先使用相对稳定、有语义化的属性如>