Outlook便笺里的数据全导出来

终于把Outlook便笺里的数据全导出来了!微软不给做功能,我自己用Gemini搓了个脚本(附代码)

前言:心态崩了

真的无语。

我平时特喜欢用Win10自带那个“便笺” (Sticky Notes)**,随时随地记点东西,手机上装个OneNote或者Outlook也能同步,挺方便的。这几年零零散散记了大概有80多条笔记,什么账号密码、灵感、备忘录都有。

结果前两天我想把这些数据迁到Notion里去,才发现一个惊天大坑:微软居然压根没给“批量导出”的功能!!

我去Outlook网页版看,好家伙,只能一条一条复制粘贴。我有80多条啊,这得弄到猴年马月?网上搜了一圈,也没找到特别好用的现成工具,大部分教程都过时了。

第一次尝试:翻车了

一开始我想着,既然网页版能看到,写个简单的JS脚本把列表里的字抓下来不就行了?
结果运行了一下,发现两个大问题:

  1. 数量不对:我有80条,脚本只导出来30条。后来才发现Outlook网页版用的是“虚拟滚动”(好像是React搞的),你不往下滑,下面的DOM元素根本就不存在。
  2. 内容皆断:这个最坑爹!左边列表里显示的只是预览,字数多一点的便签,后面全是省略号... 必须得鼠标点一下,右边才会加载出完整的全文。

解决方案:暴力美学

既然它不肯一次性给数据,那就别怪我“暴力”了。

我连夜改了个油猴(Tampermonkey)脚本,逻辑非常简单粗暴,就是模拟真人操作:

  1. 脚本自动去左侧列表里的第一条便签。
  2. 等个几百毫秒(防止网速慢右边没加载出来)。
  3. 从右边的编辑器里把完整内容抠出来,存到内存里。
  4. 自动控制滚动条往下滑一点,去找下一个没点过的便签。
  5. ...循环直到全部点完。

虽然这个方发比较笨,跑起来的时候页面会像放PPT一样自动切换,速度有点慢(80条大概要跑个一分多钟),但是数据绝对完整!不管是颜色、时间还是长篇大论的正文,都能原样导出来。

怎么用?

小白也能用,很简单:

  1. 给你的浏览器(Chrome/Edge都行)装个 Tampermonkey 插件。
  2. 点插件图标 -> 添加新脚本。
  3. 把下面的代码全选复制,覆盖进去,保存。
  4. 打开 Outlook网页版的便笺页面(在左边侧边栏找那个黄色便签图标)。
  5. 刷新一下,左下角会出现一个蓝色的按钮,叫“📥 深度导出便笺”。
  6. 重点来了: 点了按钮之后,手别乱动鼠标! 让脚本自己在那点点点,你去喝口水,等它弹窗说“导出完成”再回来。

脚本代码

// ==UserScript==
// @name Outlook Sticky Notes Full Content Exporter (Click & Scrape)
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 全自动逐个点击便笺,抓取右侧详情页的完整内容并导出 CSV
// @author Gemini 3 pro
// @match https://outlook.live.com/mail/*
// @match https://outlook.office.com/mail/*
// @match https://outlook.office365.com/mail/*
// @grant none
// ==/UserScript==

(function() {
'use strict';

const BUTTON_ID = 'olk-note-export-btn-v3';

// 核心选择器 (这是根据最新的Outlook页面结构分析出来的)
const SCROLL_CONTAINER_SELECTOR = '.n-noteListContainer';
const LIST_ITEM_WRAPPER = '.n-noteListPreviewWrapper';
const LIST_ITEM_CLICK_TARGET = '.n-notePreview';
const DETAIL_EDITOR_SELECTOR = '.public-DraftEditor-content';
const PREVIEW_DATE_SELECTOR = '.n-noteTimestamp';

// 配置参数
const WAIT_FOR_RENDER_MS = 800; // 点击后等多久,网速慢的可以改大点,比如1500
const SCROLL_STEP_DELAY = 1000;

// 初始化按钮
function initButton() {
if (document.getElementById(BUTTON_ID)) return;
if (!document.querySelector(SCROLL_CONTAINER_SELECTOR)) return;

const btn = document.createElement('button');
btn.id = BUTTON_ID;
btn.innerHTML = '📥 深度导出便笺<br/><span style="font-size:10px">(需逐个点击,请耐心等待)</span>';
btn.style.cssText = `
position: fixed;
bottom: 30px;
left: 20px;
z-index: 9999;
padding: 10px 20px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: 'Segoe UI', sans-serif;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
text-align: center;
transition: all 0.3s;
`;

btn.onclick = startDeepScrape;
document.body.appendChild(btn);
}

// CSV 转义处理,防止逗号换行符搞乱格式
function escapeCsv(text) {
if (!text) return "";
let result = String(text).replace(/"/g, '""');
if (result.search(/("|,|\n)/g) >= 0) {
result = '"' + result + '"';
}
return result;
}

// 主逻辑:边滚边点
async function startDeepScrape() {
const btn = document.getElementById(BUTTON_ID);
const container = document.querySelector(SCROLL_CONTAINER_SELECTOR);

if (!container) { alert('未找到列表,请刷新页面。'); return; }

btn.disabled = true;
btn.style.backgroundColor = '#d13438'; // 变红表示正在干活

const uniqueNotes = new Map();
const processedIds = new Set();

// 先滚回顶部
container.scrollTop = 0;
await new Promise(r => setTimeout(r, 1000));

let noNewItemsCounter = 0;
let lastScrollTop = -1;

while (true) {
// 获取当前能看见的所有便签
const visibleWrappers = Array.from(document.querySelectorAll(LIST_ITEM_WRAPPER));
let hasProcessedNewInThisLoop = false;

for (const wrapper of visibleWrappers) {
const noteId = wrapper.getAttribute('data-noteid');

// 处理过的就不点了
if (!noteId || processedIds.has(noteId)) continue;

hasProcessedNewInThisLoop = true;
processedIds.add(noteId);

// 1. 获取时间
const dateEl = wrapper.querySelector(PREVIEW_DATE_SELECTOR);
const dateStr = dateEl ? dateEl.innerText.trim() : '';

// 获取颜色
let color = 'Unknown';
const noteEl = wrapper.querySelector(LIST_ITEM_CLICK_TARGET);
if (noteEl) {
noteEl.classList.forEach(cls => {
if (cls.startsWith('n-') && !['n-notePreview', 'n-notePreviewActive', 'n-noteCanvas', 'n-shadowWrapper'].includes(cls)) {
color = cls.replace('n-', '');
}
});

// 2. 关键一步:点击它!
noteEl.click();
noteEl.scrollIntoView({block: "center", behavior: "auto"});
}

// 更新按钮上的字,让你知道它没死机
btn.innerHTML = `正在读取第 ${processedIds.size} 条...<br/><span style="font-size:10px">ID: ${noteId.substring(0,6)}...</span>`;

// 4. 等待加载
await new Promise(r => setTimeout(r, WAIT_FOR_RENDER_MS));

// 5. 抓取右侧内容
const editorEl = document.querySelector(DETAIL_EDITOR_SELECTOR);
let fullContent = "";

if (editorEl) {
const textBlocks = editorEl.querySelectorAll('[data-block="true"]');
if (textBlocks.length > 0) {
fullContent = Array.from(textBlocks)
.map(block => block.innerText.replace(/\n$/, ''))
.join('\n');
} else {
fullContent = editorEl.innerText;
}
} else {
fullContent = "[读取失败]";
}

// 6. 存起来
uniqueNotes.set(noteId, {
id: noteId,
date: dateStr,
color: color,
content: fullContent
});
}

// 没人新的了?那就往下滚
if (!hasProcessedNewInThisLoop) {
noNewItemsCounter++;
} else {
noNewItemsCounter = 0;
}

const currentScrollTop = container.scrollTop;
const maxScroll = container.scrollHeight - container.clientHeight;

// 到底了或者滚不动了
if (Math.abs(currentScrollTop - maxScroll) < 5 || (currentScrollTop === lastScrollTop && noNewItemsCounter > 2)) {
if (noNewItemsCounter > 2) break;
}

lastScrollTop = currentScrollTop;

// 向下滚80%屏幕
container.scrollBy({ top: container.clientHeight * 0.8, behavior: 'smooth' });

// 等待滚动后的加载
await new Promise(r => setTimeout(r, SCROLL_STEP_DELAY));
}

// 导出
downloadCSV(uniqueNotes);

btn.innerHTML = `✅ 搞定!<br/>共 ${uniqueNotes.size} 条`;
btn.style.backgroundColor = '#107c10';
btn.disabled = false;

setTimeout(() => {
btn.innerHTML = '📥 深度导出便笺';
btn.style.backgroundColor = '#0078d4';
}, 8000);
}

function downloadCSV(mapData) {
let csvContent = "\uFEFF"; // 加个BOM头,不然Excel打开是乱码
csvContent += "ID,修改时间,颜色,完整内容\n";

mapData.forEach(note => {
csvContent += `${escapeCsv(note.id)},${escapeCsv(note.date)},${escapeCsv(note.color)},${escapeCsv(note.content)}\n`;
});

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().slice(0,19).replace(/T|:/g, '-');

link.setAttribute("href", url);
link.setAttribute("download", `Outlook_Full_Notes_${timestamp}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

// 监控页面变化,有时候切换页面按钮会消失
const observer = new MutationObserver(() => {
if (document.querySelector(SCROLL_CONTAINER_SELECTOR)) {
initButton();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(initButton, 2000);

})();

碎碎念

导出来的CSV文件用Excel打开如果看到是乱码,记得检查一下是不是Excel版本太老了,我在代码里加了BOM头一般没问题。

如果脚本跑到一半停了,多半是网速卡了,刷新页面重来一次就好。

希望能帮到同样被微软这个“只管生不管养”的产品策略折磨的老铁们。数据拽在自己手里才是最安稳的!

posted @ 2026-01-12 16:07  HNsnow  阅读(1)  评论(0)    收藏  举报