Greasy Fork is available in English.
文章采集
当前为
// ==UserScript==
// @name 夸克资源采集
// @namespace http://tampermonkey.net/
// @version V1.0.0
// @description 文章采集
// @author PYY
// @match https://kuakezy.cc/thread-*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const platform = "Kuake";
const serverUrl = "https://zys.52huahua.cn/api/biz/collection/save"; // ✅ 你的服务器接口
const checkUrl = "https://zys.52huahua.cn/api/biz/collection/isExist"; // ✅ 检查文章是否存在的接口
// ========== 工具函数 ==========
// 格式化日期为 YYYY-MM-DD HH:mm:ss
const formatDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const collection = {
// 平台
collectionPlatform: "kkwpzys",
// 资源链接
resourceLink: null,
// 标题
title: null,
// 用户
username: null,
// UID
uid: null,
// 内容
content: null,
// 节点
node: null,
// 标签
tags: null,
// 夸克链接
quarkLink: null,
// 状态
status: "1",
// 创建时间
createTime: formatDateTime(new Date()),
// 创建用户
createUser: "1543837863788879871",
// 删除标志
deleteFlag: "NOT_DELETE",
// 绑定用户
bindCookieId: "1543837863788879871",
};
// XPath 辅助函数 - 获取单个元素
const getElementByXPath = (xpath) => {
try {
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue;
} catch (e) {
console.error("XPath 错误:", e);
return null;
}
};
// XPath 辅助函数 - 获取所有匹配的元素
const getElementsByXPath = (xpath) => {
try {
const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
const elements = [];
for (let i = 0; i < result.snapshotLength; i++) {
elements.push(result.snapshotItem(i));
}
return elements;
} catch (e) {
console.error("XPath 错误:", e);
return [];
}
};
const logBox = document.createElement("div");
logBox.style = `
position: fixed; bottom: 20px; right: 20px;
width: 350px; background: #fff;
border: 1px solid #ccc; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-size: 13px; padding: 10px; z-index: 999999;
max-height: 60vh; overflow: auto;
`;
// 添加状态灯
const statusLight = document.createElement("div");
statusLight.id = "status-light";
statusLight.style = `
width: 12px; height: 12px; border-radius: 50%;
background: #ccc; display: inline-block;
margin-left: 10px; vertical-align: middle;
transition: background 0.3s ease;
`;
const statusText = document.createElement("span");
statusText.id = "status-text";
statusText.textContent = "未检查";
statusText.style = "margin-left: 5px; vertical-align: middle; font-size: 12px; color: #666;";
const statusContainer = document.createElement("div");
statusContainer.style = "margin-bottom:10px;padding:8px;background:#f9f9f9;border-radius:4px;";
statusContainer.innerHTML = "<strong>文章状态:</strong> ";
statusContainer.appendChild(statusLight);
statusContainer.appendChild(statusText);
logBox.appendChild(statusContainer);
// 添加账号选择下拉框
const accountSelectContainer = document.createElement("div");
accountSelectContainer.style = "margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #ddd;";
const accountLabel = document.createElement("label");
accountLabel.textContent = "选择绑定账号: ";
accountLabel.style = "display:inline-block;margin-right:5px;";
const accountSelect = document.createElement("select");
accountSelect.id = "account-selector";
accountSelect.style = `
display:inline-block;padding:5px 10px;
border:1px solid #aaa;border-radius:4px;
background:#fff;cursor:pointer;
min-width:150px;
`;
// 账号选项(可根据需要修改)
const accounts = [
{ label: "我想我是海", value: "1896186752012374017" },
{ label: "书生", value: "1900922270486798338" },
{ label: "海海游戏社", value: "1900354501367640065" },
];
accounts.forEach(({ label, value }) => {
const option = document.createElement("option");
option.textContent = label;
option.value = value;
accountSelect.appendChild(option);
});
// 从缓存读取上次选择的账号
const STORAGE_KEY = 'quark_tool_bindCookieId';
const savedBindCookieId = localStorage.getItem(STORAGE_KEY);
// 判断保存的值是否在账号列表中
const isValidAccount = accounts.some(acc => acc.value === savedBindCookieId);
const defaultBindCookieId = isValidAccount ? savedBindCookieId : accounts[0].value;
// 设置选中的账号并初始化 bindCookieId
accountSelect.value = defaultBindCookieId;
collection.bindCookieId = defaultBindCookieId;
// 监听选择变化
accountSelect.addEventListener("change", (e) => {
const selectedValue = e.target.value;
collection.bindCookieId = selectedValue;
// 保存到缓存
localStorage.setItem(STORAGE_KEY, selectedValue);
addLog("已切换到账号: " + e.target.options[e.target.selectedIndex].text);
});
accountSelectContainer.appendChild(accountLabel);
accountSelectContainer.appendChild(accountSelect);
logBox.appendChild(accountSelectContainer);
const logArea = document.createElement("div");
logArea.style = "max-height:200px;overflow:auto;border-top:1px solid #ddd;margin-top:5px;padding-top:5px;";
const addLog = (msg) => {
console.log(`${platform}>>> ${msg}`);
const p = document.createElement("div");
p.textContent = msg;
logArea.appendChild(p);
logArea.scrollTop = logArea.scrollHeight;
};
// ========== 控制按钮 ==========
const btns = [
{ text: "提取所有内容", fn: async () => await extractAll() },
{ text: "上传服务器", fn: uploadServer },
{ text: "查看数据", fn: showData },
];
btns.forEach(({ text, fn }) => {
const b = document.createElement("button");
b.textContent = text;
b.style = `
display:inline-block;margin:3px;padding:5px 10px;
border:1px solid #aaa;border-radius:4px;
background:#f5f5f5;cursor:pointer;
`;
b.addEventListener("click", fn);
logBox.appendChild(b);
});
logBox.appendChild(logArea);
document.body.appendChild(logBox);
addLog("✅ 脚本已启动,正在自动检测文章状态...");
// ========== 各步骤逻辑 ==========
// 检查文章是否已存在
async function checkArticleExists() {
if (!collection.title) {
updateStatusLight('gray', '未检查');
return false;
}
updateStatusLight('#FFA500', '检查中...');
try {
const response = await fetch(checkUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: collection.title
});
const data = await response.json();
// 解析响应格式:{"code":200,"msg":"操作成功","data":false}
// data 字段为 true 表示已存在,false 表示不存在
const exists = data.data === true || data.data === 'true' || data.data === 1 || data.data === '1';
if (exists) {
updateStatusLight('#f44336', '文章已存在');
addLog("⚠️ 该文章已在数据库中");
return true;
} else {
updateStatusLight('#4CAF50', '文章不存在');
addLog("✅ 该文章为新内容");
return false;
}
} catch (err) {
updateStatusLight('#FF9800', '检查失败');
addLog("❌ 检查接口失败: " + err.message);
return false;
}
}
// 更新状态灯
function updateStatusLight(color, text) {
const light = document.getElementById('status-light');
const textSpan = document.getElementById('status-text');
if (light) light.style.background = color;
if (textSpan) textSpan.textContent = text;
}
// 统一提取所有内容的函数
async function extractAll() {
addLog("开始提取所有内容...");
// 第一步:先提取夸克链接
addLog("1. 检查夸克链接...");
const alertDiv = document.querySelector("div.alert.alert-success[role='alert']");
if (alertDiv) {
const allText = alertDiv.textContent || alertDiv.innerText || '';
const quarkPattern = /https?:\/\/pan\.quark\.(cn|com)\/s\/[a-zA-Z0-9]+/g;
const matches = allText.match(quarkPattern);
if (matches && matches.length > 0) {
collection.quarkLink = matches[0];
addLog("✓ 夸克链接提取成功: " + collection.quarkLink);
} else {
addLog("❌ 未找到夸克链接。请确认已回帖。");
return; // 中断,不继续提取
}
} else {
addLog("❌ 未找到回帖提示框。请先回帖查看链接。");
return; // 中断,不继续提取
}
// 第二步:提取基本信息和节点
addLog("2. 提取标题、作者、节点和资源链接...");
await extractMeta();
// 第三步:提取标签
addLog("3. 提取标签...");
extractTags();
// 第四步:提取正文
addLog("4. 提取正文...");
extractContent();
addLog("✅ 所有内容提取完成!");
addLog("可以点击【查看数据】查看完整数据,然后点击【上传服务器】");
}
async function extractMeta() {
// 提取当前页面链接
const currentUrl = window.location.href;
try {
const urlObj = new URL(currentUrl);
// 提取域名(仅用于日志显示验证)
const domain = urlObj.hostname;
// 提取 resourceLink(URL 的最后一段)
const pathParts = urlObj.pathname.split('/').filter(part => part);
if (pathParts.length > 0) {
collection.resourceLink = pathParts[pathParts.length - 1];
}
addLog("页面链接解析成功");
addLog("完整URL: " + currentUrl);
addLog("域名: " + domain);
addLog("资源链接: " + collection.resourceLink);
} catch (e) {
addLog("URL 解析失败: " + e.message);
}
// 提取标题
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
collection.title = titleEl.textContent.trim().replace(/\s+/g, " ");
addLog("标题提取成功: " + collection.title);
} else {
addLog("未找到标题。");
}
// 提取作者
const userEl = document.querySelector("span.username.font-weight-bold.small a");
if (userEl) {
collection.username = userEl.textContent.trim();
addLog("作者提取成功: " + collection.username);
} else {
addLog("未找到作者。");
}
// 提取节点(分类)
const nodeEl = getElementByXPath("//*[@id='body']/div/div/div[2]/ol/li[2]/a");
if (nodeEl) {
collection.node = nodeEl.textContent.trim();
addLog("节点提取成功: " + collection.node);
} else {
addLog("未找到节点信息。");
}
}
function extractTags() {
const tagsXPath = "/html/body/main/div/div/div[2]/div[1]/div[2]/div[2]//a";
const tagElements = getElementsByXPath(tagsXPath);
if (tagElements && tagElements.length > 0) {
const tagTexts = tagElements.map(tag => tag.textContent.trim()).filter(text => text);
collection.tags = tagTexts.join(",");
addLog("标签提取成功,共 " + tagTexts.length + " 个");
addLog("标签内容: " + collection.tags);
} else {
addLog("未找到标签信息。");
}
}
function extractQuark() {
// 定位到指定的div
const alertDiv = document.querySelector("div.alert.alert-success[role='alert']");
if (alertDiv) {
// 获取整个div节点的所有文字内容
const allText = alertDiv.textContent || alertDiv.innerText || '';
// 使用正则表达式匹配夸克链接形式
// 匹配: https://pan.quark.cn/s/xxx 或 https://pan.quark.com/s/xxx
const quarkPattern = /https?:\/\/pan\.quark\.(cn|com)\/s\/[a-zA-Z0-9]+/g;
const matches = allText.match(quarkPattern);
if (matches && matches.length > 0) {
collection.quarkLink = matches[0]; // 取第一个匹配的链接
addLog("夸克链接提取成功: " + collection.quarkLink);
} else {
addLog("未找到夸克链接。");
}
} else {
addLog("未找到指定的提示框。");
}
}
async function extractContent() {
const contentXPath = "/html/body/main/div/div/div[2]/div[1]/div[2]";
const contentEl = getElementByXPath(contentXPath);
if (!contentEl) {
addLog("未找到正文区域。");
return;
}
// 克隆节点
const clonedContent = contentEl.cloneNode(true);
// 删除多余的提示框元素
try {
let deleteCount = 0;
const removeList = ['.tt-license', '.alert.alert-success', '.mt-3'];
removeList.forEach(sel => {
const el = clonedContent.querySelector(sel);
if (el && el.parentNode) {
el.parentNode.removeChild(el);
deleteCount++;
}
});
addLog(`正文提取成功,已删除 ${deleteCount} 个指定元素。`);
} catch (e) {
addLog("删除元素时出错: " + e.message);
}
// ✅ 处理图片:转成 Base64 并替换
const imgEls = clonedContent.querySelectorAll("img");
let converted = 0;
const convertToBase64 = async (url) => {
try {
const response = await fetch(url);
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (err) {
console.error("图片转Base64失败:", err);
return url; // 保留原始地址
}
};
// 异步转换所有图片
const tasks = Array.from(imgEls).map(async (img) => {
const src = img.getAttribute("src");
if (!src) return;
try {
// 相对路径转绝对路径
const absoluteUrl = new URL(src, window.location.href).href;
const base64 = await convertToBase64(absoluteUrl);
img.setAttribute("src", base64);
converted++;
} catch (e) {
console.warn("处理图片失败:", src, e);
}
});
await Promise.all(tasks);
addLog(`共处理图片 ${imgEls.length} 张,成功转为Base64:${converted} 张。`);
// 保存完整 HTML
collection.content = clonedContent.outerHTML;
addLog("✅ 正文提取完成并包含Base64图片数据。");
}
function uploadServer() {
if (!serverUrl.startsWith("http")) {
addLog("❌ 请先设置服务器地址!");
return;
}
addLog("开始上传到服务器...");
fetch(serverUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(collection)
})
.then(res => res.json())
.then(data => addLog("✅ 上传成功: " + JSON.stringify(data)))
.catch(err => addLog("❌ 上传失败: " + err));
}
function showData() {
addLog("当前收集数据:");
addLog(JSON.stringify(collection, null, 2));
}
function fixCommentInput() {
const el = document.querySelector('#comment_input');
if (!el) return false;
// 设置固定在底部的样式
el.style.position = 'fixed';
el.style.bottom = '0';
el.style.width = '50%';
el.style.background = '#fff';
el.style.borderTop = '1px solid #ccc';
el.style.padding = '10px';
el.style.display = 'flex';
el.style.gap = '8px';
el.style.boxShadow = '0 -2px 5px rgba(0,0,0,0.1)';
el.style.zIndex = '999999';
return true;
}
// 页面加载后尝试执行一次
if (!fixCommentInput()) {
// 若页面是异步加载的元素,使用定时器轮询直到找到目标
const observer = new MutationObserver(() => {
if (fixCommentInput()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 页面加载完成后自动检查文章状态
function autoCheckOnLoad() {
// 立即提取标题并检查
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
const title = titleEl.textContent.trim().replace(/\s+/g, " ");
collection.title = title;
// 自动检查文章是否已存在
checkArticleExists();
} else {
// 如果立即没找到,使用观察者等待元素出现
const checkObserver = new MutationObserver(() => {
const titleEl = document.querySelector("h4.break-all.font-weight-bold");
if (titleEl) {
const title = titleEl.textContent.trim().replace(/\s+/g, " ");
collection.title = title;
checkArticleExists();
checkObserver.disconnect();
}
});
checkObserver.observe(document.body, { childList: true, subtree: true });
}
}
// 立即执行一次(DOM加载完成后)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoCheckOnLoad);
} else {
autoCheckOnLoad();
}
})();