尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python爬取163邮箱收件箱:requests与selenium三种方案实战解析

Python爬取163邮箱收件箱:requests与selenium三种方案实战解析 做邮箱自动化第一步要过的坎永远是登录和列表获取。这篇东西我早想写了前阵子刚好有个业务账号要把 163 收件箱里的邮件批量导出归档手动翻页复制了两百多封之后实在受不了就花了半天时间把 urllib、requests、selenium 三条路全趟了一遍顺便把正文解析、附件下载、登录态复用这些细节一并处理掉。今天就把完整思路、可用代码和踩过的坑一起放出来给准备用 Python 爬取 163 收件箱列表及邮件内容的朋友做个参考。前提先说清楚以下所有操作针对的是你自己拥有合法访问权限的邮箱账号自动化处理个人邮箱内容没问题但做批量采集前建议先看一眼网易邮箱的服务条款别拿自动化去骚扰公共资源。1. 163 邮箱爬取的三个真实难点登录态、动态渲染与反爬限制1.1 为什么不能像爬静态网站那样直接 GET 首页很多人第一次写邮箱爬虫直觉是requests.get(https://mail.163.com/)然后从 HTML 里找邮件列表。真这么干你大概率拿到的是一个 163 邮箱的壳子页面里确实有一些script和link但邮件条目一条都看不到。原因在于 163 邮箱的收件箱属于典型的 SPA单页应用结构邮件列表数据是页面加载完成后通过异步请求从后端接口拉取的再用 JavaScript 动态渲染到 DOM 上。requests这种纯 HTTP 客户端只负责拿 HTML 源码并不执行 JavaScript所以拿到的永远是空壳。登录态是另一个绕不开的难点。163 邮箱的所有数据接口都要求携带有效的会话凭证Cookie 里的 SID即 session ID不带凭证请求任何接口返回的都是未登录提示或者直接 302 跳回登录页。第三个难点是反爬控制。邮件数据属于隐私数据网易的风控不是吃素的短时间高频请求会触发验证码、账号临时锁定或者对特定接口返回 429 Too Many Requests 限流错误。这一点在后面 requests 方案里我会专门讲处理策略。1.2 三种 Python 工具在面对同一难点时的不同位置先说结论方便你按自己情况选路工具能否执行 JS登录方式适用场景主要痛点urllib否Cookie 或模拟表单理解 HTTP 原理、无第三方依赖环境处理会话麻烦动态渲染页面拿不到列表requests否Session 保持 Cookie能抓到数据接口时的主力方案接口被加密或加参时排查成本高selenium是真实浏览器登录可扫码接口抓不到、验证码复杂时的兜底方案速度慢、资源占用高、页面改版易挂我自己实际的选型结果是接口能抓到就 requests抓不到或登录态太麻烦就 selenium 兜底urllib 用在排查请求细节和写最小验证脚本上。接下来按这三条路分别展开。2. 基础方案urllib 带你理解 Cookie 与请求的本质2.1 用 cookiejar 构建一个带会话的 openerurllib 方案虽然笨但对理解 HTTP 请求和会话机制非常有帮助。它的核心痛点是不能自动管理 Cookie所以需要借助http.cookiejar模块手动构造一个带 Cookie 管理能力的 opener。写一个最小可用的框架如下import http.cookiejar import urllib.request import urllib.parse import ssl # 忽略 SSL 证书验证403/证书报错时可临时开启 # ssl._create_default_https_context ssl._create_unverified_context cookie_jar http.cookiejar.CookieJar() opener urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) opener.addheaders [ (User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36), (Accept, text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8), (Connection, keep-alive), ] # 访问首页触发登录页跳转 resp opener.open(https://mail.163.com/, timeout10) print(resp.geturl()) print(resp.status)这个例子能干什么呢它能帮你确认两件事一是请求目标网站时的重定向过程resp.geturl()会显示最终落在哪个 URL二是 Cookie 在访问后有没有被种下来。但我要明确说如果你试图用 urllib 去模拟 163 邮箱的账号密码登录现在基本走不通。网易登录页面的密码加密、验证码、滑块验证等机制早已不是简单 POST 一个用户名密码就能过的这也是为什么我建议把这段代码的定位放在理解原理而不是生产可用。2.2 更现实的落地方式复制已登录 Cookie免去模拟登录urllib 方案里真正能落地的做法是先用浏览器手动登录 163 邮箱然后把登录后的 Cookie 字符串直接粘到代码里。这是所有方案里最简单、最稳定的方式因为它完全绕开了模拟登录的加密和验证码难题。操作路径如下用 Chrome 或 Edge 打开https://mail.163.com/并登录你的账号。按 F12 打开开发者工具切到 Network 面板刷新页面找到任意一个mail.163.com域名下的请求。在请求头的Request Headers里找到Cookie:字段整段复制。把 Cookie 字符串传给 urllib。import urllib.request cookie_str 你从浏览器复制的那一整段Cookie opener urllib.request.build_opener() opener.addheaders [ (User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36), (Cookie, cookie_str), ] resp opener.open(https://mail.163.com/, timeout10) html resp.read().decode(utf-8, errorsignore) print(len(html))跑完你会发现HTML 长度可能不小但真正关键的邮件列表数据还是不在里面或者只藏在某个script标签里的初始化数据中。这就是我前面说的动态渲染问题。2.3 urllib 方案的边界用 urllib 方案做 163 收件箱爬取能拿到什么程度取决于你是否愿意去手工分析页面里内嵌的初始数据。现在不少 Web 应用会把首屏数据以 JSON 的形式塞进script标签的全局变量里163 邮箱也不排除这种可能。你可以拿到 HTML 后做一步预处理import re # 提取内嵌的 JSON 数据变量名以实际页面为准 m re.search(rwindow\.__INITIAL_DATA__\s*\s*({.*?});, html, re.S) if m: init_data m.group(1) print(init_data[:2000])如果你的目标只是把列表数据抠出来这条路勉强能用。但它有两个致命问题一是页面改版后变量名和结构说变就变二是邮件分页、正文获取这类操作仍然需要构造复杂的接口请求靠正则抠数据完全不是长久之计。所以 urllib 方案我的定位是排查请求细节、写最小验证脚本、理解 Cookie 机制。真正做批量抓取请直接跳到后面的 requests 或 selenium 方案。3. 主力方案requests 会话保持 接口级数据提取3.1 先用 Session 搞定登录态requests 比 urllib 强在哪个地方核心就两点Session对象自动管理 Cookie以及代码写起来更简洁。用requests.Session()之后你不需要手动维护 CookieJar会话内的所有请求会自动携带上一次响应种下的 Cookie。登录态的处理和 urllib 方案思路一致推荐优先使用浏览器手动登录 复制 Cookie的方式import requests import json session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Referer: https://mail.163.com/, }) cookie_str 你从浏览器复制的那一整段Cookie # 方式一直接设置 Cookie 头简单粗暴但不推荐长期用 # session.headers.update({Cookie: cookie_str}) # 方式二解析 Cookie 字符串并逐条设置推荐后续用 session.cookies 更灵活 for item in cookie_str.split(; ): if in item: key, value item.split(, 1) session.cookies.set(key, value, domain.163.com)设置完成后随便请求一个 163 邮箱的页面验证一下登录态res session.get(https://mail.163.com/) print(res.status_code, res.url)如果响应码是 200 且返回的是邮箱主界面而不是登录页说明 Cookie 生效了。3.2 从 Network 面板找出真正的邮件列表接口这是 requests 方案里最核心、也最花时间的一步找到浏览器在展示收件箱时调用的数据接口。方法如下在已登录的浏览器里进入收件箱。打开开发者工具的 Network 面板勾选Fetch/XHR过滤条件。点击刷新按钮或手动翻页观察网络请求列表里mail.163.com域名下的接口。逐个点击请求在 Response 或 Preview 标签里找看起来是邮件列表的数据结构通常会有title、from、date、id之类的字段。163 邮箱早期版本的接口形式通常是https://mail.163.com/js6/s?funcmbox:listMessages这类带func参数的请求返回的是 JSONP 或 JS 变量赋值的文本。不同版本、不同入口163 免费邮、企业邮接口差异很大所以不要死记 URL要掌握抓包定位的方法。定位到接口之后把它复制成 cURL 命令Network 面板里右键请求选择 Copy as cURL然后通过curlconverter或在线工具转成 Python requests 代码再把 Cookie 替换成你脚本里的 session就能以最小成本复现所有参数。3.3 用 requests 请求接口并解析 JSON假设你通过抓包确认了列表接口的 URL 和参数一个典型的请求过程如下import requests import json import time session requests.Session() # ... 上面提到的 Cookie 设置代码 ... list_url https://mail.163.com/你的邮箱域名对应接口路径 params { # 以下参数是从抓包里复制的这里仅作示例 func: mbox:listMessages, sid: session.cookies.get(SID, domain.163.com), uid: 你的用户名163.com, start: 0, limit: 20, } resp session.post(list_url, dataparams) # 接口返回的可能是 JSON也可能是 JSONP 包裹的文本 text resp.text # 去掉 JSONP 包裹 if text.startswith(var ): json_str text.split(, 1)[1].strip().rstrip(;) elif text.startswith(callback(): json_str text[len(callback():-2] else: json_str text data json.loads(json_str) # 邮件列表一般在 data 里具体字段结构以抓包为准 messages data.get(var, {}).get(list, []) for msg in messages[:5]: print({ id: msg.get(id), from: msg.get(from), subject: msg.get(subject), date: msg.get(date), })跑通这一步你就算是拿到了收件箱列表的核心能力。后面的翻页本质上是改变start、limit这类分页参数循环请求即可。3.4 429 限流的处理重试、退避与降速接口跑通之后真正的敌人是限流。很多人在这一步直接撞上429 Too Many Requests尤其是一次性拉几百封邮件详情的时候。请求频率稍微高起来网易风控的响应就来了。我自己在实践中总结了一套处理策略核心就三个词降速、退避、随机化。import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session.mount(https://, HTTPAdapter( max_retriesRetry( total3, status_forcelist[429, 500, 502, 503], backoff_factor2, # 重试等待 2s, 4s, 8s ) )) # 每次请求之间加入随机延时 def safe_get(session, url, **kwargs): for attempt in range(3): try: time.sleep(random.uniform(1.5, 3.5)) resp session.get(url, **kwargs) if resp.status_code 429: print(f触发限流第 {attempt 1} 次重试) time.sleep(10 random.random() * 10) continue resp.raise_for_status() return resp except requests.RequestException as e: if attempt 2: raise time.sleep(5 * (attempt 1)) return None这里有一个细节很容易被忽略如果你用浏览器手动登录拿 Cookie 的方式限流是盯着你的 IP SID 的。真要跑大批量数据建议邮箱登录页里把自动转发、已读回执这些可能触发额外请求的设置关掉减少不必要的负担。我个人经验是列表页每秒 1 个请求以内详情页每 2~3 秒一个请求基本能稳住。再快就有风险。千万别贪快你是在处理自己的邮件数据不是在做压测。4. 兜底方案selenium 驱动真实浏览器绕开加密与验证码4.1 环境准备与登录 iframe 处理当 requests 方案卡在接口抓包分析上或者 163 邮箱的接口协议变化导致你定位不到可靠数据源时selenium 是最实际的兜底手段。它驱动的是一个真实浏览器页面怎么渲染、接口怎么调用、数据怎么显示selenium 全都能等到最终结果。环境准备如下pip install selenium然后根据本机浏览器版本下载对应的 chromedriver/edgedriver或者直接用 Selenium Manager新版 selenium 会自动处理 driver 下载。接下来有一个非常容易踩的坑163 邮箱登录框不在主页面里而是嵌在一个 iframe 里。直接用find_element去找用户名输入框会报找不到元素。正确姿势是先切换到登录框对应的 iframefrom 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 options webdriver.ChromeOptions() # 如果你只想在后台跑可以开启无头模式 # options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) driver.get(https://mail.163.com/) wait WebDriverWait(driver, 20) # 登录框在 iframe 内必须切换进去 iframe wait.until(EC.presence_of_element_located((By.TAG_NAME, iframe))) driver.switch_to.frame(iframe) # 现在可以操作账号密码输入框了 account_input wait.until(EC.presence_of_element_located((By.NAME, email))) account_input.send_keys(你的账号163.com) password_input driver.find_element(By.NAME, password) password_input.send_keys(你的密码) # 点登录按钮 login_btn driver.find_element(By.ID, dologin) login_btn.click()如果账号开启了短信验证或滑块验证程序就卡住了。这时候最省事的做法是切回driver.switch_to.default_content()在弹出的验证码环节手动处理或者干脆用扫码登录登录页面选择扫码登录你用手机 App 扫一下整个会话就通过了。登录成功后建议把登录态保存下来避免每次跑脚本都要重复登录。保存方式就是拿 Cookieimport json cookies driver.get_cookies() with open(netease_cookies.json, w) as f: json.dump(cookies, f)下次运行脚本时直接注入 Cookiedriver.get(https://mail.163.com/) # 先访问一次把域名建立好再加 cookie 才有效 for cookie in json.load(open(netease_cookies.json)): driver.add_cookie(cookie) driver.refresh()4.2 列表渲染后的元素定位与正文提取登录成功后收件箱会刷出一批邮件。这里用selenium找元素要抓住列表里每条邮件在 DOM 里长什么样这个关键问题。163 邮箱的收件箱列表通常会有一些共同特征发件人、主题、时间各自有 class 或 data 属性。你需要在页面上右键点击邮件条目选择检查找到这一条邮件对应的 DOM 节点再向上找到它所在列表项的父节点。假设每条邮件都对应一个li元素那么定位思路是from selenium.webdriver.common.by import By wait WebDriverWait(driver, 20) # 等待邮件列表加载完成选择器以实际 DOM 为准 list_items wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, li[data-id])) ) print(f当前收件箱共 {len(list_items)} 条邮件) for item in list_items[:10]: try: from_elem item.find_element(By.CSS_SELECTOR, .tK) # 发件人 subj_elem item.find_element(By.CSS_SELECTOR, .tJ) # 主题 date_elem item.find_element(By.CSS_SELECTOR, .tA) # 时间 print(from_elem.text, |, subj_elem.text, |, date_elem.text) except Exception as e: print(解析失败:, e)注意上面代码里的.tK、.tJ、.tA这类 class 名来源于网易邮箱早期版本的 DOM 结构不同版本可能不同不要直接照抄。核心思路是找到列表项的共同父节点然后逐条提取子元素文本。只要 DOM 结构不变这个方案就很稳。提取完列表后点击进入邮件详情页拿正文# 点第一条邮件的主题区域进入详情 subj_elem.click() # 等正文区加载出来 body_elem wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, .mail-content)) ) body_text body_elem.text print(body_text[:500]) # 看完之后返回收件箱 driver.back()4.3 非原生下拉框等组合元素定位技巧163 邮箱页面上有不少 UI 控件不是原生 HTML 元素比如邮箱文件夹切换、日期筛选这类下拉框往往是用div包着ul和li做的模拟框。很多人用 selenium 自带的选择器去找select标签结果找不到原因就在这里。定位模拟下拉框的核心思路是先展开、再枚举。首先点击当前显示的下拉框触发器通常是div里的一小段文字等待ul里的li出现然后再枚举每个li的文本匹配目标项后点击import time # 点击触发器展开下拉 trigger driver.find_element(By.CSS_SELECTOR, .dropdown-trigger) trigger.click() time.sleep(0.5) # 枚举所有选项 options driver.find_elements(By.CSS_SELECTOR, .dropdown-menu li) for opt in options: if 已删除 in opt.text: opt.click() break这类模拟下拉框由于没有原生聚焦等事件偶尔会出现点击不生效的情况。如果点了没反应可以尝试用 JavaScript 直接点击driver.execute_script(arguments[0].click();, opt)4.4 selenium 方案的局限与优化经验用 selenium 爬收件箱最大的问题不是跑不通而是慢和脆。慢是客观存在的启动一个浏览器、加载完整页面、等待所有异步请求完成每一步都是秒级耗时拉一百封邮件可能要十几分钟。脆则是页面结构稍有调整你的 CSS 选择器就可能失效。我的建议是这样用 selenium列表页负责拿到每封邮件的 id、发件人、主题、时间信息如果只是导出这批元数据直接处理列表页就够了不用一封一封点进去。真需要正文再针对特定邮件 id 构造 URL 进入详情页同时加WebDriverWait超时控制和异常捕获避免某封邮件渲染失败导致整个脚本崩溃。from selenium.common.exceptions import TimeoutException try: body_elem wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, .mail-content)), timeout10 ) body_text body_elem.text except TimeoutException: body_text [正文加载超时]页面偶尔卡死的问题可以在主循环里定期通过driver.execute_script(return document.readyState)检查页面加载状态长时间卡住就driver.refresh()。5. 邮件正文解析的实战坑base64 正文、发件人编码与附件下载5.1 正文是 base64 编码的不是你想要的纯文本不管用 requests 还是 selenium拿到邮件详情之后都会遇到一个新的解析问题邮件正文经常不是直接在接口里给你一段干净的纯文本而是经过 base64 编码的字符串。尤其是抓包拿到的 JSON 数据里面的正文字段可能长这样5L2g5aW977yM6LZ5piv5LiA5bCB6KeG6aKR遇到这种数据第一反应就是 base64 解码import base64 encoded_text 5L2g5aW977yM6LZ5piv5LiA5bCB6KeG6aKR try: decoded base64.b64decode(encoded_text).decode(utf-8) print(decoded) except Exception as e: print(解码失败:, e)需要注意的是不是所有看起来乱码的文本都是 base64。如果解码结果还是乱码尝试用 GB18030 解码因为 163 邮箱的老邮件有可能是 GBK/GB2312 编码的try: decoded base64.b64decode(encoded_text).decode(gb18030) except Exception: decoded base64.b64decode(encoded_text).decode(utf-8, errorsignore)5.2 发件人和主题的 encoded-word 解码还有一个很隐蔽的坑主题和发件人字段经常是 RFC 2047 编码格式像这样?UTF-8?B?5L2g5aW977yM6LZ5piv5LiA5bCB6KeG6aKR?如果你直接把这段字符串存进数据库导出的 Excel 里就会看到一堆?UTF-8?B?开头的东西。正确做法是用email.header模块解码from email.header import decode_header raw_subject ?UTF-8?B?5L2g5aW977yM6LZ5piv5LiA5bCB6KeG6aKR? # 或者是 ?GBK?B?xxxx? 之类 decoded_parts decode_header(raw_subject) subject for part, charset in decoded_parts: if isinstance(part, bytes): subject part.decode(charset or utf-8) else: subject part print(subject)我见过太多人在这一步直接放弃把编码后的字符串硬塞进 CSV最后分析数据时才发现主题全是乱码。解码逻辑建议封装成工具函数所有字段统一走一遍。5.3 附件下载与 HTML 正文转纯文本如果邮件里有附件情况又复杂一点。通过抓包接口拿到的附件通常是一个下载 URL 或文件 ID需要在请求头里带上 Cookie 才能下载import requests attach_url https://mail.163.com/附件接口地址 res requests.get(attach_url, cookiessession.cookies, streamTrue, timeout30) filename 附件原名.xlsx with open(filename, wb) as f: for chunk in res.iter_content(chunk_size8192): if chunk: f.write(chunk)附件下载这块接收方文件名的中文编码也是一个坑。文件名可能也是 RFC 2047 编码的或是在响应头里以filename*UTF-8...的形式出现。稳妥做法是从详情接口的 JSON 里读附件元数据而不是从响应头猜。占用空间较大的邮件正文网易会给 HTML 格式。HTML 转纯文本有两个选择简单场景用正则剥离标签复杂场景用 BeautifulSoupfrom bs4 import BeautifulSoup html_body divp第一段/pp第二段/p/div soup BeautifulSoup(html_body, html.parser) text soup.get_text(separator\n, stripTrue) print(text)6. 三种方案的选型建议与工程经验6.1 选型对比哪种方案值得长期维护把三种方案放在一起横向看结论会比较清晰维度urllibrequestsselenium开发效率低高中执行速度快快慢稳定性低页面结构一变就挂中依赖接口变化中依赖 DOM 变化登录复杂度高中低可扫码适合阶段原理学习、请求调试主力生产方案接口失效时的备用方案如果你是第一次做这个需求我的建议直接跳过 urllib 深入研究从 requests 抓包开始。当你发现抓不到接口或者接口请求参数特别复杂比如带签名、加密字段再切换到 selenium 不迟。6.2 更省事的替代路径IMAP 协议在收尾之前我必须提一个经常被忽略的正规方案IMAP 协议。如果你的目标只是把 163 收件箱的邮件拉下来存档Python 标准库里的imaplib配合email模块可以比网页爬取稳定一个量级。import imaplib import email from email.header import decode_header # 163 邮箱的 IMAP 服务器和端口 mail imaplib.IMAP4_SSL(imap.163.com, 993) # 密码这里填授权码不是邮箱登录密码需要用手机验证开启 IMAP 后获取 mail.login(你的账号163.com, 你的授权码) mail.select(INBOX) status, data mail.search(None, ALL) msg_ids data[0].split() for msg_id in msg_ids[-10:]: status, msg_data mail.fetch(msg_id, (RFC822)) msg email.message_from_bytes(msg_data[0][1]) subject, charset decode_header(msg[Subject])[0] if isinstance(subject, bytes): subject subject.decode(charset or utf-8) print(主题:, subject, | 发件人:, msg[From])163 邮箱开启 IMAP 服务后系统会发一个授权码这个授权码专门给第三方客户端使用。相比网页爬取IMAP 方案不受前端页面改版影响速度还快唯一的门槛是你需要去设置里开启 IMAP 服务。如果这篇博文能有一个核心结论那就是邮箱自动化优先认准 IMAP 或接口方案实在不行再用浏览器自动化兜底。6.3 反爬规避和个人边界提醒最后聊几句反爬规避的工程经验这部分其实是很多教程不愿意写的。第一HTTP 请求头要完整。UA、Referer、Accept 这些字段尽量跟浏览器保持一致。网易邮箱很多接口会校验 Referer缺少这个头直接拒绝请求。第二请求节奏必须稳定。我踩过最大的坑就是在循环里忘了延时连续快速请求导致触发了账号风控后面整整一个小时内所有接口都返回验证码要求。后来我养成习惯不管是调接口还是跑浏览器每次请求之间至少保持 1~2 秒的随机间隔。宁慢勿快。第三Cookie 会过期。浏览器复制的 Cookie 一般能维持几个小时到一天不等脚本要处理 Cookie 过期后的重新登录逻辑。如果使用授权码接入 IMAP就没有这个问题这也是 IMAP 方案在长期运维上的另一个优势。第四务必只处理自己的邮箱数据。无论何种方案爬取前想清楚合法边界不要碰任何没有权限的第三方邮箱内容也不要对公共邮箱系统做高频压力测试。自动化是为了把重复劳动交给程序不是为了突破系统防线。回到实际需求本身我现在落地的脚本已经稳定跑了好几个月每天定时通过 requests 方案拉取收件箱新邮件命中关键词的正文写入数据库有附件就自动下载归档。整个过程中踩过的坑基本都消化在了上面的各个章节里。最深刻的体会是不管用什么方案先花半小时理清数据流比急着写代码重要得多——搞清楚数据从哪个接口来、以什么格式返回、有哪些编码陷阱后面自然水到渠成。如果你计划动手建议第一版脚本先跑通 10 封邮件的链路再考虑扩展全量数据调试成本会低很多。
返回列表