Python 爬虫:Selenium 自动化控制(Headless 模式 / 无痕浏览)

本文将聚焦 Selenium 的两大核心高级特性 ------Headless 模式无痕浏览,从环境搭建、基础操作到实战案例,全方位讲解如何利用 Selenium 构建高效、隐蔽的 Python 爬虫系统。无论是需要绕过反爬机制的商业数据采集,还是追求资源轻量化的服务器端自动化,掌握这些技术都将使你的爬虫能力实现质的飞跃。

一、环境准备:从零搭建 Selenium 开发环境

1.1 Python 环境配置

Selenium 支持 Python 3.7 + 版本,推荐使用 Python 3.9 + 以获得最佳兼容性。首先确保 Python 已安装:

复制代码
# 检查Python版本
python --version  # 或 python3 --version

若未安装,可从Python 官网下载对应系统版本,或通过包管理器安装:

复制代码
# Ubuntu/Debian
sudo apt update && sudo apt install python3 python3-pip

# macOS(使用Homebrew)
brew install python

# Windows
# 从官网下载安装包,勾选"Add Python to PATH"

1.2 Selenium 库与浏览器驱动安装

1.2.1 Selenium 安装

通过pip安装 Selenium 最新版:

复制代码
pip install selenium -U  # -U表示升级到最新版
1.2.2 浏览器驱动自动管理(Selenium Manager)

传统痛点 :手动下载 ChromeDriver/GeckoDriver,需严格匹配浏览器版本,且需配置环境变量。
解决方案 :Selenium 4.6.0 + 内置Selenium Manager,可自动检测浏览器版本并下载对应驱动,无需手动干预。

验证 Selenium Manager 是否生效:

复制代码
from selenium import webdriver

# 无需指定驱动路径,Selenium Manager自动处理
driver = webdriver.Chrome()  # 初始化Chrome浏览器
driver.get("https://www.baidu.com")
print(driver.title)  # 输出:百度一下,你就知道
driver.quit()

注意:若系统中未安装对应浏览器,Selenium Manager 会自动下载并缓存浏览器(仅支持 Chrome、Firefox、Edge)。

1.3 开发工具推荐

  • PyCharm:强大的 Python IDE,支持代码补全、调试和测试。
  • VS Code:轻量编辑器,配合 Python 插件和 Selenium 扩展,适合快速开发。
  • 浏览器开发者工具:F12 打开,用于分析页面结构、定位元素和监控网络请求。

二、Selenium 核心基础:从元素定位到页面交互

2.1 WebDriver 核心 API 解析

Selenium 通过WebDriver接口控制浏览器,核心类与方法如下:

类 / 方法 功能描述
webdriver.Chrome() 初始化 Chrome 浏览器实例
driver.get(url) 加载指定 URL
driver.title 获取当前页面标题
driver.page_source 获取页面 HTML 源码
driver.current_url 获取当前页面 URL
driver.quit() 关闭浏览器并释放资源
driver.close() 关闭当前标签页(不释放驱动进程)

2.2 元素定位策略(8 种方法详解)

准确定位页面元素是自动化的核心,Selenium 提供 8 种定位方式,优先级从高到低为:

1. ID 定位(By.ID

原理 :通过元素id属性定位,唯一性最高,推荐优先使用。
示例 :定位百度搜索框(id="kw"):

复制代码
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://www.baidu.com")
search_input = driver.find_element(By.ID, "kw")  # 获取搜索框元素
search_input.send_keys("Selenium Headless")  # 输入文本
2. Name 定位(By.NAME

原理 :通过元素name属性定位,适用于表单元素。
示例 :定位登录表单的用户名输入框(name="username"):

复制代码
username_input = driver.find_element(By.NAME, "username")
3. Class Name 定位(By.CLASS_NAME

原理 :通过元素class属性定位,注意 class 可能包含多个值(需完整匹配)。
示例 :定位 class 为"result-item"的搜索结果:

复制代码
result_items = driver.find_elements(By.CLASS_NAME, "result-item")  # 复数形式返回列表
4. Tag Name 定位(By.TAG_NAME

原理 :通过 HTML 标签名定位,适用于同类标签(如<a><div>)。
示例 :获取页面所有<a>标签:

复制代码
links = driver.find_elements(By.TAG_NAME, "a")
for link in links[:5]:  # 打印前5个链接文本
    print(link.text)

原理 :通过超链接文本完整匹配 定位。
示例:定位文本为 "新闻" 的链接:

复制代码
news_link = driver.find_element(By.LINK_TEXT, "新闻")
news_link.click()  # 点击链接

原理 :通过超链接文本部分匹配 定位,支持模糊匹配。
示例:定位包含 "地图" 的链接:

复制代码
map_link = driver.find_element(By.PARTIAL_LINK_TEXT, "地图")
7. CSS Selector 定位(By.CSS_SELECTOR

原理 :通过 CSS 选择器定位,灵活性最高,支持复杂规则。
常用语法

  • #id:匹配 id 属性
  • .class:匹配 class 属性
  • tag:匹配标签名
  • [attribute=value]:匹配属性值
  • parent > child:子元素选择器

示例 :定位百度搜索按钮(id="su",class="bg s_btn"):

复制代码
search_button = driver.find_element(By.CSS_SELECTOR, "#su")  # 或 .bg.s_btn
search_button.click()
8. XPath 定位(By.XPATH

原理 :通过 XML 路径语言定位,支持复杂层级和文本匹配,功能最强但性能略低。
常用语法

  • /html/body/div:绝对路径(不推荐)
  • //div[@id='content']:相对路径 + 属性
  • //text()[contains(.,'关键词')]:文本包含
  • //div[last()]:最后一个 div 元素

示例:定位百度搜索结果的第一个标题

复制代码
first_result = driver.find_element(By.XPATH, "//div[@id='content_left']//h3/a")
print(first_result.text)

2.3 常用页面操作方法

输入与点击
复制代码
# 输入文本
input_element.send_keys("文本内容")
# 清空输入
input_element.clear()
# 点击元素
button_element.click()
# 提交表单
form_element.submit()
下拉框选择

需使用Select类处理<select>标签:

复制代码
from selenium.webdriver.support.ui import Select

select_element = Select(driver.find_element(By.ID, "select"))
select_element.select_by_value("value1")  # 按value选择
select_element.select_by_visible_text("选项文本")  # 按文本选择
窗口与标签页切换
复制代码
# 获取所有窗口句柄
window_handles = driver.window_handles
# 切换到新窗口
driver.switch_to.window(window_handles[-1])
# 切换到iframe
driver.switch_to.frame("iframe_id")
# 返回主文档
driver.switch_to.default_content()

2.4 等待机制:解决动态加载问题

核心问题 :页面元素加载是异步的,直接定位可能导致NoSuchElementException
解决方案:三种等待机制确保元素加载完成:

1. 强制等待(time.sleep(n)

简单但不灵活,固定等待 n 秒:

复制代码
import time
time.sleep(3)  # 等待3秒
2. 隐式等待(driver.implicitly_wait(n)

全局设置,等待 n 秒内元素出现,超时则抛出异常:

复制代码
driver.implicitly_wait(10)  # 所有元素定位最多等待10秒
3. 显式等待(WebDriverWait

针对特定元素设置等待条件,更精准:

复制代码
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 等待10秒,直到元素可点击
wait = WebDriverWait(driver, 10)
clickable_element = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
clickable_element.click()

常用expected_conditions条件

  • presence_of_element_located:元素存在
  • visibility_of_element_located:元素可见
  • element_to_be_clickable:元素可点击
  • text_to_be_present_in_element:元素包含指定文本

三、Headless 模式深度解析:无界面爬虫的高效之道

3.1 Headless 模式原理与优势

Headless 模式 :无图形用户界面(GUI)的浏览器运行模式,所有操作在后台执行。
核心优势

  • 资源占用低:无需渲染页面,CPU 和内存占用减少 50%+
  • 运行速度快:页面加载时间缩短 30%-60%
  • 服务器友好:支持 Linux 服务器等无 GUI 环境
  • 隐蔽性强:降低被网站检测为爬虫的风险

适用场景:数据采集、自动化测试、服务器端渲染(SSR)页面爬取。

3.2 Chrome Headless 配置与使用

基础配置

Selenium 4.x 中 Chrome Headless 模式通过--headless=new参数启用(旧--headless已弃用):

复制代码
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

chrome_options = Options()
# 启用新Headless模式(Chrome 109+)
chrome_options.add_argument("--headless=new")
# 禁用GPU加速(可选,部分环境需要)
chrome_options.add_argument("--disable-gpu")
# 设置窗口大小(Headless模式默认窗口较小,需显式指定)
chrome_options.add_argument("--window-size=1920,1080")

# 初始化Headless Chrome
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://www.baidu.com")
print(f"页面标题:{driver.title}")  # 输出:页面标题:百度一下,你就知道
driver.quit()
高级优化配置

为进一步提升性能,可添加以下参数:

复制代码
# 禁用图片加载(提升速度)
chrome_options.add_argument("--blink-settings=imagesEnabled=false")
# 禁用JavaScript(根据需求选择)
chrome_options.add_argument("--disable-javascript")
# 禁用浏览器通知
chrome_options.add_argument("--disable-notifications")
# 隐藏滚动条
chrome_options.add_argument("--hide-scrollbars")
# 禁用扩展
chrome_options.add_argument("--disable-extensions")
# 设置User-Agent(伪装浏览器)
chrome_options.add_argument(
    "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
)

3.3 Firefox Headless 配置与使用

Firefox 通过options.headless = True启用 Headless 模式:

复制代码
from selenium import webdriver
from selenium.webdriver.firefox.options import Options

firefox_options = Options()
firefox_options.headless = True  # 启用Headless模式
firefox_options.add_argument("--width=1920")  # 设置宽度
firefox_options.add_argument("--height=1080")  # 设置高度

driver = webdriver.Firefox(options=firefox_options)
driver.get("https://www.baidu.com")
print(f"Firefox Headless模式页面标题:{driver.title}")
driver.quit()

3.4 Headless 模式性能对比测试

在相同硬件环境下,对百度首页加载性能进行测试:

模式 加载时间(秒) CPU 占用率 内存占用(MB)
常规模式 2.3 35% 380
Headless 模式 1.1 18% 150

结论:Headless 模式加载速度提升 52%,资源占用减少 50% 以上,适合大规模爬虫部署。

四、无痕浏览模式详解:隐私保护与环境隔离

4.1 无痕浏览原理与隐私保护机制

无痕浏览 (Incognito/Private Mode):浏览器不保存浏览历史、Cookie、缓存和表单数据,每次会话完全隔离。
核心价值

  • 环境纯净:避免历史数据影响爬取结果(如登录状态、个性化推荐)
  • 反爬规避:降低基于 Cookie 和本地存储的指纹追踪风险
  • 并发隔离:多线程爬虫可使用独立无痕窗口,避免会话冲突

4.2 各类浏览器无痕模式配置

Chrome 无痕模式

通过--incognito参数启用:

复制代码
chrome_options = Options()
chrome_options.add_argument("--incognito")  # 启用无痕模式
# 其他优化参数
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])  # 隐藏"受自动化控制"提示
chrome_options.add_argument("--disable-extensions")  # 禁用扩展

driver = webdriver.Chrome(options=chrome_options)
driver.get("https://www.baidu.com")
# 验证无痕模式:本地存储应为空
local_storage_size = driver.execute_script("return window.localStorage.length")
print(f"本地存储项数量:{local_storage_size}")  # 输出:0
driver.quit()
Firefox 隐私模式

通过-private参数启用:

复制代码
firefox_options = Options()
firefox_options.add_argument("-private")  # 启用隐私模式
driver = webdriver.Firefox(options=firefox_options)
driver.get("https://www.baidu.com")
driver.quit()

即使非无痕模式,也可手动清除会话数据:

复制代码
# 清除所有Cookie
driver.delete_all_cookies()
# 清除localStorage
driver.execute_script("window.localStorage.clear();")
# 清除sessionStorage
driver.execute_script("window.sessionStorage.clear();")

在无痕模式下注入必要 Cookie(如登录状态):

复制代码
# 注入Cookie
driver.add_cookie({
    "name": "sessionid",
    "value": "your_session_id",
    "domain": ".example.com",
    "path": "/"
})
# 刷新页面使Cookie生效
driver.refresh()

4.4 应用场景分析

场景 优势说明
多账号并发登录 每个无痕窗口独立会话,避免账号互踢
反爬检测规避 无历史数据,降低指纹识别风险
A/B 测试数据采集 确保每次访问为 "新用户",获取纯净结果
敏感操作模拟 操作完成后数据自动清除,提高安全性

五、高级技巧:反检测、多线程与错误处理

5.1 反检测策略:隐藏 Selenium 特征

网站通过检测window.navigator.webdriver属性识别自动化工具,需通过以下方法隐藏:

方法 1:设置excludeSwitches
复制代码
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
方法 2:通过 CDP 命令覆盖webdriver属性
复制代码
# 使用Chrome DevTools Protocol(CDP)注入脚本
driver.execute_cdp_cmd(
    "Page.addScriptToEvaluateOnNewDocument",
    {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            })
        """
    }
)
方法 3:随机 User-Agent 与指纹伪装
复制代码
from fake_useragent import UserAgent  # 需安装:pip install fake-useragent

ua = UserAgent()
chrome_options.add_argument(f"--user-agent={ua.random}")  # 随机User-Agent

5.2 多线程 / 分布式爬虫

使用threading模块实现多线程爬虫,结合 Headless 和无痕模式提高效率:

复制代码
import threading
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def crawl(url):
    chrome_options = Options()
    chrome_options.add_argument("--headless=new")
    chrome_options.add_argument("--incognito")
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(url)
    print(f"线程{threading.current_thread().name}:{driver.title}")
    driver.quit()

# 待爬取URL列表
urls = [
    "https://www.baidu.com",
    "https://www.bing.com",
    "https://www.google.com"
]

# 创建线程并启动
threads = []
for i, url in enumerate(urls):
    t = threading.Thread(target=crawl, args=(url,), name=f"Thread-{i}")
    threads.append(t)
    t.start()

# 等待所有线程完成
for t in threads:
    t.join()
print("所有爬虫任务完成")

5.3 错误处理与日志记录

异常处理框架
复制代码
from selenium.common.exceptions import (
    NoSuchElementException,
    TimeoutException,
    WebDriverException
)

def safe_crawl(url):
    try:
        driver = webdriver.Chrome(options=chrome_options)
        driver.get(url)
        # 核心爬取逻辑
        title = driver.title
        return {"url": url, "title": title, "status": "success"}
    except NoSuchElementException as e:
        return {"url": url, "error": f"元素未找到:{str(e)}", "status": "failed"}
    except TimeoutException:
        return {"url": url, "error": "页面加载超时", "status": "failed"}
    except WebDriverException as e:
        return {"url": url, "error": f"驱动错误:{str(e)}", "status": "failed"}
    finally:
        if 'driver' in locals():
            driver.quit()
日志记录

使用 Python 内置logging模块记录爬虫过程:

复制代码
import logging

logging.basicConfig(
    filename="selenium_crawl.log",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

logging.info("爬虫任务开始")
result = safe_crawl("https://www.baidu.com")
logging.info(f"爬取结果:{result}")

六、实战案例:从动态内容到反爬突破

案例一:动态内容加载网站爬取(以豆瓣电影为例)

目标 :爬取豆瓣电影 Top250 动态加载的电影名称和评分。
难点:页面通过 AJAX 加载更多内容,需模拟滚动到底部触发加载。

复制代码
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time

# 配置Headless模式
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=chrome_options)
driver.get("https://movie.douban.com/top250")

# 模拟滚动加载更多内容
def scroll_to_bottom():
    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

scroll_to_bottom()

# 提取电影信息
movies = driver.find_elements(By.CSS_SELECTOR, ".item")
for movie in movies[:10]:  # 打印前10部电影
    title = movie.find_element(By.CSS_SELECTOR, ".title").text
    rating = movie.find_element(By.CSS_SELECTOR, ".rating_num").text
    print(f"{title} - {rating}分")

driver.quit()

输出结果

复制代码
肖申克的救赎 The Shawshank Redemption - 9.7分
霸王别姬 - 9.6分
阿甘正传 Forrest Gump - 9.5分
...

案例二:模拟登录与数据提取(以 GitHub 为例)

目标 :使用无痕模式登录 GitHub,提取个人仓库列表。
关键步骤:输入账号密码、处理验证码(若有)、会话保持。

复制代码
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

chrome_options = Options()
chrome_options.add_argument("--incognito")  # 无痕模式
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])

driver = webdriver.Chrome(options=chrome_options)
driver.get("https://github.com/login")

# 输入账号密码
driver.find_element(By.ID, "login_field").send_keys("your_username")
driver.find_element(By.ID, "password").send_keys("your_password")
driver.find_element(By.NAME, "commit").click()

# 等待登录完成
WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".dashboard-sidebar"))
)

# 提取仓库列表
driver.get("https://github.com/your_username?tab=repositories")
repos = driver.find_elements(By.CSS_SELECTOR, ".source.repository-list-item")
for repo in repos[:5]:
    name = repo.find_element(By.CSS_SELECTOR, "a[itemprop='name codeRepository']").text
    desc = repo.find_element(By.CSS_SELECTOR, ".repo-description").text.strip() if repo.find_elements(By.CSS_SELECTOR, ".repo-description") else "无描述"
    print(f"仓库:{name}\n描述:{desc}\n---")

driver.quit()

案例三:反爬机制突破策略(以某电商平台为例)

目标 :爬取商品价格,突破基于webdriver检测的反爬。
解决方案:结合 Headless、无痕模式、CDP 隐藏特征和随机延迟。

复制代码
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
import random

chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--incognito")
chrome_options.add_argument(f"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15")
# 隐藏webdriver属性
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
driver = webdriver.Chrome(options=chrome_options)

# 使用CDP进一步隐藏特征
driver.execute_cdp_cmd(
    "Page.addScriptToEvaluateOnNewDocument",
    {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
            Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]});  # 模拟插件
            Object.defineProperty(navigator, 'languages', {get: () => ['zh-CN', 'zh']});  # 模拟语言
        """
    }
)

driver.get("https://example-mall.com/product/12345")
# 随机延迟,模拟人类操作
time.sleep(random.uniform(1, 3))

# 提取价格(假设反爬页面价格通过JS动态生成)
price = driver.execute_script("return document.querySelector('.price').innerText")
print(f"商品价格:{price}")

driver.quit()

七、常见问题与解决方案

7.1 元素定位失败问题

问题原因 解决方案
元素未加载完成 使用显式等待(WebDriverWait
动态 ID/Class 改用 CSS/XPath 相对定位,避免依赖动态属性
元素在 iframe 内 使用driver.switch_to.frame()切换 iframe
页面嵌套层级过深 简化 XPath,使用contains()等模糊匹配

7.2 浏览器版本兼容性问题

  • 问题 :Chrome 更新后驱动不兼容,报错session not created: This version of ChromeDriver only supports Chrome version XX
  • 解决方案:Selenium Manager 会自动处理版本匹配,确保 Selenium 版本≥4.6.0 即可。

7.3 性能优化技巧

  1. 禁用不必要资源:关闭图片、JS、CSS 加载(根据需求)。

  2. 复用浏览器实例:多任务共享一个 driver,减少启动开销。

  3. 限制并发数:根据服务器性能调整线程数,避免被封禁。

  4. 使用代理 IP :配合--proxy-server参数,避免 IP 被拉黑:

    复制代码
    chrome_options.add_argument("--proxy-server=http://123.45.67.89:8888")

八、总结与展望

核心知识点回顾

本文系统讲解了 Selenium 自动化控制的全流程,重点包括:

  • 环境搭建:Selenium Manager 自动驱动管理,告别手动配置。
  • 基础操作:8 种元素定位方法、页面交互与等待机制。
  • 高级特性:Headless 模式(无界面高效运行)与无痕浏览(隐私隔离)。
  • 实战技巧:反检测策略、多线程爬虫、错误处理与日志记录。
  • 案例驱动:动态内容爬取、模拟登录、反爬突破三大场景。

高级学习路径建议

  1. 深入 CDP 协议:通过 Chrome DevTools Protocol 实现网络拦截、性能分析等高级功能。
  2. Selenium Grid:分布式测试 / 爬取,支持多浏览器、多节点并行。
  3. 结合 Playwright:微软开源的自动化工具,性能优于 Selenium,支持更多高级 API。
  4. 爬虫框架整合:与 Scrapy 结合,实现 Selenium 处理动态页面 + Scrapy 处理数据存储。

Selenium 发展趋势

  • W3C 标准统一:各浏览器厂商逐步统一 WebDriver 实现,兼容性提升。
  • AI 驱动定位:结合计算机视觉(如 OpenCV)实现基于图像的元素定位,应对复杂反爬。
  • 更低资源占用:Headless 模式持续优化,未来可能与浏览器内核深度整合,进一步提升性能。

通过掌握 Selenium 的 Headless 模式和无痕浏览,你已具备构建高效、隐蔽爬虫系统的核心能力。在实际应用中,需始终遵守网站robots.txt协议,合理控制爬取频率,共同维护健康的网络数据生态。

相关推荐
q5673152315 小时前
解决爬虫IP限制:Selenium隧道代理完整解决方案
爬虫·tcp/ip·selenium
Czi.1 天前
无网络安装来自 GitHub 的 Python 包
开发语言·python·github
我叫黑大帅1 天前
从奶奶挑菜开始:手把手教你搞懂“TF-IDF”
人工智能·python·算法
深度学习入门1 天前
如何使用PyTorch搭建一个基础的神经网络并进行训练?
人工智能·pytorch·python·深度学习·神经网络·ai
nlog3n1 天前
LangChain Prompt管理核心:PromptTemplate与ChatPromptTemplate全解析
python·langchain·prompt
二闹1 天前
参数传进去后到底变不变?
后端·python
Juchecar1 天前
Windows环境解决uv安装包的“warning: Failed to hardlink files”
python
二闹1 天前
别再用错了!深入扒一扒Python里列表和元组那点事
后端·python
唐BiuBiu1 天前
【量化回测】backtracker整体架构和使用示例
python·backtrader·量化
Ratten1 天前
使用 PIL 实现图片的批量格式转换
python