杰瑞科技汇

Python线程如何共享session数据?

在 Python 的标准库中,threading 模块(用于线程)和 requests 库(用于 Session 对象)是两个独立但经常协同工作的概念。 它们解决的是不同层面的问题。

Python线程如何共享session数据?-图1
(图片来源网络,侵删)
  • 线程:是并发编程的基础,它允许你的程序在单个进程内同时执行多个任务(代码片段),这对于处理 I/O 密集型任务(如网络请求、文件读写)尤其有用,因为当一个线程等待 I/O 时,CPU 可以切换到其他线程去执行任务,从而提高程序的效率和响应速度。
  • Session:是HTTP 通信中的一个概念,它代表了一个持久化的连接,可以让你在多个请求之间共享某些数据,cookies、headers(身份认证信息等),使用 Session 可以避免在每次请求时都重新建立 TCP 连接(即“握手”过程),从而显著提高性能。

下面,我们将分三部分来详细解释:

  1. Python 线程 (threading 模块)
  2. Python Session (requests.Session 对象)
  3. 线程与 Session 的协同工作

Python 线程 (threading 模块)

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

为什么使用线程?

  • 提高响应速度:对于有用户界面的程序,可以使用一个线程来响应用户输入,另一个线程来执行耗时任务,防止界面卡死。
  • 利用多核 CPU:对于 CPU 密集型任务,可以使用多线程(或多进程)来并行计算,充分利用多核处理器的性能。
  • 简化 I/O 操作:对于网络请求、数据库查询等 I/O 密集型任务,使用多线程可以避免在等待 I/O 时阻塞整个程序。

threading 模块基本用法

threading 模块是 Python 的标准库,提供了丰富的线程管理功能。

核心概念:

Python线程如何共享session数据?-图2
(图片来源网络,侵删)
  • Thread:用于创建和管理线程。
  • target:线程要执行的函数。
  • args:传递给目标函数的位置参数(元组)。
  • start():启动线程,此时线程进入“就绪”状态,等待 CPU 调度。
  • join():主线程等待子线程执行完毕后再继续执行,这常用于确保所有子任务都完成后,主线程再进行下一步操作。

示例:

import threading
import time
def worker(num):
    """这是一个线程要执行的函数"""
    print(f"Worker {num} is starting...")
    time.sleep(2)  # 模拟耗时操作,比如网络请求或文件读写
    print(f"Worker {num} has finished.")
if __name__ == "__main__":
    threads = []
    # 创建并启动 5 个线程
    for i in range(5):
        # 创建线程对象
        t = threading.Thread(target=worker, args=(i,))
        threads.append(t)
        # 启动线程
        t.start()
    # 主线程等待所有子线程完成
    for t in threads:
        t.join()
    print("All threads have finished. Main thread continues.")

输出结果(顺序可能略有不同):

Worker 0 is starting...
Worker 1 is starting!
Worker 2 is starting!
Worker 3 is starting!
Worker 4 is starting!
... (等待 2 秒) ...
Worker 0 has finished.
Worker 1 has finished.
Worker 2 has finished.
Worker 3 has finished.
Worker 4 has finished.
All threads have finished. Main thread continues.

可以看到,主线程启动了 5 个工作线程后,并没有立即退出,而是通过 join() 等待它们全部完成。


Python Session (requests.Session 对象)

在 Web 开发和爬虫领域,Session 是一个非常实用的概念,它不是一个 Python 内置对象,而是 requests 库提供的一个高级接口。

Python线程如何共享session数据?-图3
(图片来源网络,侵删)

为什么使用 Session?

  1. 保持会话状态:这是最重要的功能,很多网站(如需要登录的网站)通过 Cookie 来识别用户身份,当你使用 Session 对象发起第一个请求并登录后,Session 会自动保存服务器返回的 Cookie,后续所有通过该 Session 发起的请求都会自动带上这个 Cookie,从而维持登录状态。
  2. 性能提升Session 对象会保持一个底层的 TCP 连接(requests 默认使用 urllib3 库,它实现了连接池),当你使用同一个 Session 发起多个请求到同一个域名时,可以复用这个连接,避免了每次请求都进行 TCP 三次握手和四次挥手,大大降低了延迟。

requests.Session 基本用法

核心概念:

  • requests.Session():创建一个 Session 对象。
  • session.get() / session.post():通过 Session 对象发起请求,用法与 requests.get() / requests.post() 完全相同。
  • 自动处理 CookieSession 会自动处理和管理 Cookie。

示例:

import requests
# 1. 创建一个 Session 对象
session = requests.Session()
# 2. 发起一个登录请求 (假设登录接口会设置 Cookie)
# login_url = "https://example.com/api/login"
# login_data = {"username": "test", "password": "123456"}
# response = session.post(login_url, data=login_data)
# print("Login status:", response.status_code)
# 3. 发起一个需要登录才能访问的请求
# profile_url = "https://example.com/api/profile"
# response = session.get(profile_url)
# print("Profile page:", response.text)
# 为了演示,我们用一个简单的例子代替
# 第一次请求,服务器可能会返回一个 Set-Cookie 头
print("--- Making first request ---")
response1 = session.get("http://httpbin.org/cookies/set/sessionid/123456")
print("Response 1 Cookies:", response1.cookies.get_dict()) # 应该能看到 sessionid
# 第二次请求,Session 会自动带上第一次获取的 Cookie
print("\n--- Making second request ---")
response2 = session.get("http://httpbin.org/cookies")
print("Response 2 Cookies:", response2.json()) # 会看到 sessionid 被发送回来了
# 关闭 Session,释放连接池资源
session.close()

在这个例子中,我们访问 httpbin.org 这个测试网站,它提供了一个 /cookies/set 接口,可以让我们设置一个 Cookie,第二次请求 /cookies 时,Session 自动将第一次设置的 sessionid 发送了回去,证明了会话的保持。


线程与 Session 的协同工作

我们把这两个概念结合起来,一个常见的场景是:在多线程环境中,使用 Session 对象并发地爬取一个需要登录的网站。

这里有一个非常重要的陷阱需要了解:requests.Session 对象是线程不安全的。

陷阱:Session 的线程不安全性

如果你在多个线程中共享同一个 Session 对象,并发的读写操作可能会导致数据混乱,

  • Cookie 被一个线程的请求覆盖,导致另一个线程的认证失败。
  • 连接池的状态被破坏,导致请求失败或性能下降。

正确的协同工作方式

为了避免线程安全问题,最佳实践是每个线程都创建自己的 Session 对象

为什么这是可行的?

  1. 隔离性:每个线程有自己的 Session,就有自己的 Cookie 存储和连接池,一个线程的请求不会影响另一个线程。
  2. 资源共享:虽然 Session 对象本身不共享,但登录所需的用户名、密码等共享数据可以作为参数传递给每个线程的函数。
  3. 资源效率:虽然每个线程都有自己的连接池,但对于现代操作系统来说,为每个线程维护一个小的连接池开销并不大,远比处理因共享 Session 导致的混乱要好。

示例:多线程爬取需要登录的网站

import threading
import time
import requests
# 共享的登录凭证
USERNAME = "test_user"
PASSWORD = "test_password"
def fetch_profile(session, user_id):
    """
    每个线程执行的任务:使用自己的 session 登录并获取用户信息
    """
    try:
        # 1. 使用自己的 session 进行登录
        print(f"Thread {threading.get_ident()}: Logging in for user {user_id}...")
        login_url = "http://httpbin.org/post" # 这里用 httpbin 的 post 接口模拟登录
        login_data = {"username": USERNAME, "password": PASSWORD, "user_id": user_id}
        # 登录请求
        response = session.post(login_url, data=login_data)
        response.raise_for_status() # 如果请求失败则抛出异常
        # 2. 登录成功后,获取个人资料页面
        print(f"Thread {threading.get_ident()}: Fetching profile...")
        profile_url = "http://httpbin.org/get"
        profile_response = session.get(profile_url)
        profile_response.raise_for_status()
        print(f"Thread {threading.get_ident()}: Successfully fetched profile. Status: {profile_response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"Thread {threading.get_ident()}: An error occurred: {e}")
if __name__ == "__main__":
    NUM_THREADS = 5
    # 创建并启动多个线程
    threads = []
    for i in range(NUM_THREADS):
        # --- 关键点:每个线程都创建自己的 Session ---
        session = requests.Session()
        # 创建线程,并将 session 和用户ID作为参数传递
        # args 必须是一个元组
        t = threading.Thread(target=fetch_profile, args=(session, i))
        threads.append(t)
        t.start()
    # 主线程等待所有子线程完成
    for t in threads:
        t.join()
    print("\nAll threads have finished.")

代码解析:

  1. requests.Session() 在循环内for i in range(NUM_THREADS): 循环的每一次迭代,我们都创建了一个全新的 session 对象,这个对象随后被传递给新创建的线程。
  2. 线程隔离:线程 A 拿到 session_A,线程 B 拿到 session_B,它们各自管理自己的 Cookie 和连接池,互不干扰。
  3. 传递参数target=fetch_profile 指定了线程要执行的函数,args=(session, i) 将每个线程专属的 session 和一个 user_id 传递给了 fetch_profile 函数。
特性 Python 线程 (threading) Python Session (requests.Session)
目的 实现并发,提高程序效率和响应速度。 管理和复用 HTTP 连接,保持会话状态(如 Cookie)。
领域 操作系统层面的并发编程。 应用层面的 HTTP 通信。
核心对象 threading.Thread requests.Session
线程安全 - 不安全,不能在多线程间直接共享。
协同方式 每个线程创建自己的 Session 对象,以避免数据竞争,保证并发请求的独立性和正确性。

线程是舞台,Session 是演员,每个演员(线程)都应该有自己的道具和剧本(Session 对象),这样他们才能在舞台上(并发环境中)独立、正确地表演,而不会互相干扰。

分享:
扫描分享到社交APP
上一篇
下一篇