Centos7 无头浏览器截图 Feb 27, 2026
你的系统是 CentOS 7,glibc 最高 2.17,libstdc++ 也很旧。所以:
- ✅ Node.js 14 - 完全兼容 CentOS 7
- ✅ Chromium - EPEL 源版本兼容 CentOS 7
- ✅ Puppeteer-core 19.x - 兼容 Node.js 14
1.安装Node
官方提供
# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# 代替重启 shell
\. "$HOME/.nvm/nvm.sh"
# 下载并安装 Node.js:
nvm install 14
# 验证 Node.js 版本:
node -v # Should print "v14.21.3".
# 验证 npm 版本:
npm -v # Should print "6.14.18".
如果已经存在其他版本Node
# 1. 完全卸载当前 Node.js
nvm deactivate
# 你自己的版本
nvm uninstall v16.20.2
# 2. 清理 NVM 缓存
nvm cache clear
# 3. 安装与 CentOS 7 完全兼容的 Node.js 14
nvm install 14.21.3
nvm use 14.21.3
nvm alias default 14.21.3
# 4. 验证
node --version
npm --version
如果 NVM 安装失败,直接下载二进制包
# 下载预编译的 Node.js 14(CentOS 7 兼容版)
cd /tmp
wget https://nodejs.org/dist/v14.21.3/node-v14.21.3-linux-x64.tar.xz
tar -xf node-v14.21.3-linux-x64.tar.xz
sudo cp -r node-v14.21.3-linux-x64/* /usr/local/
# 验证
/usr/local/bin/node --version
2. 下载浏览器
# 放弃 Chrome,使用 Chromium 旧版本
# 1. 安装 EPEL 源(如果还没装)
yum install -y epel-release
# 2. 安装 Chromium(EPEL 源提供的是与 CentOS 7 兼容的旧版本)
yum install -y chromium
# 3. 确认安装路径
which chromium-browser
# 通常是 /usr/bin/chromium-browser
3. 安装模块
# 1. 确保 Node.js 14 可用
node --version # 应该是 v14.21.3
# 2. 清理项目,填写自己的项目路径
cd /browser_screenshot
rm -rf node_modules package-lock.json
# 3. 重新创建 package.json,根据自己项目修改
cat > package.json << 'EOF'
{
"name": "browser_screenshot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "node test.js"
},
"dependencies": {
"puppeteer-core": "19.11.1"
}
}
EOF
# 4. 设置跳过下载
export PUPPETEER_SKIP_DOWNLOAD=true
export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# 5. 安装
npm install
字体乱码
最快的临时解决方案
# 安装文泉驿字体(最简单)
sudo yum install -y wqy-microhei-fonts wqy-zenhei-fonts
sudo fc-cache -fv
# 然后重新运行你的脚本
######################### 或者
# 1. 先修缓存(免费)
sudo fc-cache -fv
# 2. 如果还乱码,装文鼎(30秒)
sudo yum install -y arphic-ukai-fonts arphic-uming-fonts
sudo fc-cache -fv
# 3. 再不行,直接上思源黑体
sudo yum install -y google-noto-sans-cjk-ttc-fonts
sudo fc-cache -fv
✅ 方案一:文鼎 PL 系列(已明确2022年重申免费商用)
这是最稳妥的开源备选,文鼎官方2022年明确公告:四款1999版PL字体永久免费商用,且授权宽松度与文泉驿相同 。
# CentOS 7 直接安装(EPEL源已包含)
sudo yum install -y arphic-ukai-fonts arphic-uming-fonts
# 或者手动下载(官方提供TTF)
cd /tmp
wget http://ftp.gnu.org/non-gnu/chinese-fonts-truetype/arphic/ckai.ttf # 文鼎PL中楷
wget http://ftp.gnu.org/non-gnu/chinese-fonts-truetype/arphic/cbsong.ttf # 文鼎PL简报宋
sudo cp *.ttf /usr/share/fonts/chinese/
✅ 方案二:思源系列(SIL开源,最全字符集)
# 安装思源黑体(Google Noto Sans CJK)
sudo yum install -y google-noto-sans-cjk-ttc-fonts
# 安装思源宋体(如果源里有)
sudo yum install -y google-noto-serif-cjk-ttc-fonts
如果你的yum源没有,手动下载也很简单(字体文件约50MB):
cd /tmp
# 思源黑体 Normal 2.004(官方GitHub)
wget https://github.com/adobe-fonts/source-han-sans/releases/download/2.004R/SourceHanSansHWSC.zip
unzip SourceHanSansHWSC.zip
sudo cp SubsetTTF/*.ttf /usr/share/fonts/chinese/
✅ 方案三:Ubuntu 字体(自由字体,可用)
Ubuntu 官方字体包含完整中文字形,同样开源。
sudo yum install -y ubuntu-font-family
# 或手动下载
wget https://assets.ubuntu.com/v1/0cef8205-ubuntu-font-family-0.83.zip
✅ 方案四:Noto Sans SC(Google 官方中文变体)
如果你想要简体字形优先,Noto Sans SC 是最干净的选择
sudo yum install -y google-noto-sans-sc-fonts
脚本
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ====================== 动态参数生成器 ======================
const DynamicGenerator = {
// 生成随机字符串
randomString(length = 8) {
return crypto.randomBytes(length).toString('hex').slice(0, length);
},
// 生成随机数字字符串
randomNumberString(length = 10) {
return Math.floor(Math.random() * Math.pow(10, length)).toString().padStart(length, '0');
},
// 生成随机时间戳
randomTimestamp() {
return Math.floor(Date.now() / 1000) + Math.floor(Math.random() * 1000);
},
// 生成随机IP
randomIP() {
return `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`;
},
// 生成随机User-Agent
randomUserAgent() {
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
];
return userAgents[Math.floor(Math.random() * userAgents.length)];
},
// 生成随机Accept-Language
randomAcceptLanguage() {
const languages = [
'zh-CN,zh;q=0.9,en;q=0.8',
'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7',
'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
'zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7',
];
return languages[Math.floor(Math.random() * languages.length)];
},
// 生成随机Accept-Encoding
randomAcceptEncoding() {
const encodings = [
'gzip, deflate, br',
'gzip, deflate',
'gzip, deflate, br, zstd',
'gzip, deflate, br, compress',
];
return encodings[Math.floor(Math.random() * encodings.length)];
},
// 生成随机Connection
randomConnection() {
return Math.random() > 0.5 ? 'keep-alive' : 'close';
},
// 生成随机Sec-Ch-Ua
randomSecChUa() {
const versions = [
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'"Not_A Brand";v="8", "Chromium";v="119", "Google Chrome";v="119"',
'"Not_A Brand";v="8", "Chromium";v="118", "Microsoft Edge";v="118"',
'"Not_A Brand";v="8", "Chromium";v="120", "Safari";v="17.1"',
];
return versions[Math.floor(Math.random() * versions.length)];
},
// 生成随机Sec-Ch-Ua-Platform
randomSecChUaPlatform() {
const platforms = ['"Windows"', '"macOS"', '"Linux"'];
return platforms[Math.floor(Math.random() * platforms.length)];
},
// 生成随机Sec-Ch-Ua-Mobile
randomSecChUaMobile() {
return Math.random() > 0.9 ? '?1' : '?0';
},
// 生成百度Cookie
generateBaiduCookies() {
const timestamp = Date.now();
const randomId = this.randomString(16);
const pstm = this.randomTimestamp();
const bduid = `${this.randomString(32).toUpperCase()}:FG=1`;
return {
'BAIDUID': bduid,
'BIDUPSID': this.randomString(32).toUpperCase(),
'PSTM': pstm,
'BDUSS': this.randomString(192),
'__yjs_duid': `${this.randomNumberString(17)}${this.randomNumberString(5)}`,
'BDSVRTM': Math.floor(Math.random() * 10).toString(),
'H_PS_PSSID': `${this.randomNumberString(4)}_${this.randomNumberString(4)}_${this.randomNumberString(3)}`,
'delPer': Math.random() > 0.5 ? '0' : '1',
'BD_CK_SAM': Math.random() > 0.5 ? '1' : '0',
'BD_HOME': Math.random() > 0.5 ? '1' : '0',
'BDRCVFR': `${this.randomString(8).toUpperCase()}`,
};
},
// 生成Cookie字符串
generateCookieString() {
const cookies = this.generateBaiduCookies();
return Object.entries(cookies)
.map(([key, value]) => `${key}=${value}`)
.join('; ');
},
// 生成随机X-Request-Id
generateRequestId() {
return `${this.randomString(8)}-${this.randomString(4)}-${this.randomString(4)}-${this.randomString(4)}-${this.randomString(12)}`;
},
// 生成随机Viewport尺寸
randomViewport() {
const widths = [1366, 1440, 1536, 1600, 1680, 1920, 2560];
const heights = [768, 800, 864, 900, 1050, 1200, 1440];
return {
width: widths[Math.floor(Math.random() * widths.length)],
height: heights[Math.floor(Math.random() * heights.length)],
};
},
// 生成随机滚动行为
randomScrollBehavior() {
return Math.random() > 0.7 ? 'smooth' : 'auto';
},
// 生成随机等待时间
randomWaitTime(base = 1000) {
return base + Math.floor(Math.random() * 2000);
},
// 生成随机屏幕缩放
randomDeviceScaleFactor() {
const scales = [1, 1.25, 1.5, 1.75, 2];
return scales[Math.floor(Math.random() * scales.length)];
},
// 生成随机Referer
randomReferer() {
const referers = [
'https://www.baidu.com/',
'https://www.baidu.com/s?wd=',
'https://www.baidu.com/index.php',
'https://m.baidu.com/',
'https://www.baidu.com/?tn=',
];
return referers[Math.floor(Math.random() * referers.length)];
},
// 生成随机屏幕颜色深度
randomColorDepth() {
return Math.random() > 0.5 ? 24 : 30;
},
// 生成随机时区
randomTimezone() {
const timezones = [
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Taipei',
'Asia/Tokyo',
'Asia/Singapore',
];
return timezones[Math.floor(Math.random() * timezones.length)];
}
};
// ====================== 可配置参数 ======================
const CONFIG = {
search: {
url: process.argv[2] ? `https://www.baidu.com/s?wd=${encodeURIComponent(process.argv[2])}` : 'https://www.baidu.com/s?wd=联动优势',
targetText: process.argv[3] || '文心智能体·AI生成',
},
screenshot: {
width: 1920,
height: 800,
type: 'jpeg',
quality: 85,
},
elementPosition: {
topMargin: 120,
mode: 'center',
customPositionPercent: 10,
},
browser: {
headless: 'new',
windowWidth: 1920,
windowHeight: 800,
userAgent: DynamicGenerator.randomUserAgent(),
},
output: {
directory: process.argv[4] || 'screenshots',
filenamePrefix: process.argv[2] ? crypto.createHash('md5').update(process.argv[2] + Date.now()).digest('hex') : `截图_${Date.now()}`,
saveLogFile: false,
saveFullPageScreenshot: false,
},
behavior: {
waitAfterLoad: DynamicGenerator.randomWaitTime(2000),
waitAfterScroll: DynamicGenerator.randomWaitTime(500),
highlightElement: false,
highlightColor: 'red',
addMarkerLabel: false,
timeout: 30000,
scrollBehavior: DynamicGenerator.randomScrollBehavior(),
},
};
// ====================== 主程序 ======================
(async () => {
let browser = null;
try {
// 每次请求重新生成动态参数
const dynamicUA = DynamicGenerator.randomUserAgent();
const dynamicViewport = DynamicGenerator.randomViewport();
const dynamicCookies = DynamicGenerator.generateCookieString();
const dynamicReferer = DynamicGenerator.randomReferer();
const dynamicRequestId = DynamicGenerator.generateRequestId();
const dynamicAcceptLang = DynamicGenerator.randomAcceptLanguage();
const dynamicAcceptEncoding = DynamicGenerator.randomAcceptEncoding();
const dynamicConnection = DynamicGenerator.randomConnection();
const dynamicSecChUa = DynamicGenerator.randomSecChUa();
const dynamicSecChUaPlatform = DynamicGenerator.randomSecChUaPlatform();
const dynamicSecChUaMobile = DynamicGenerator.randomSecChUaMobile();
const dynamicColorDepth = DynamicGenerator.randomColorDepth();
const dynamicTimezone = DynamicGenerator.randomTimezone();
const dynamicDeviceScaleFactor = DynamicGenerator.randomDeviceScaleFactor();
// 更新配置
CONFIG.browser.userAgent = dynamicUA;
CONFIG.behavior.waitAfterLoad = DynamicGenerator.randomWaitTime(2000);
CONFIG.behavior.waitAfterScroll = DynamicGenerator.randomWaitTime(500);
CONFIG.behavior.scrollBehavior = DynamicGenerator.randomScrollBehavior();
CONFIG.output.filenamePrefix = process.argv[2]
? crypto.createHash('md5').update(process.argv[2] + Date.now() + Math.random()).digest('hex')
: `截图_${Date.now()}_${DynamicGenerator.randomString(4)}`;
fs.mkdirSync(CONFIG.output.directory, { recursive: true });
browser = await puppeteer.launch({
headless: CONFIG.browser.headless,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
'--disable-gpu',
'--no-first-run',
'--disable-dev-shm-usage',
`--window-size=${dynamicViewport.width},${dynamicViewport.height}`,
`--window-position=${Math.floor(Math.random() * 100)},${Math.floor(Math.random() * 100)}`,
`--lang=${dynamicAcceptLang.split(',')[0]}`,
'--disable-features=IsolateOrigins,site-per-process',
'--disable-web-security',
'--disable-features=BlockInsecurePrivateNetworkRequests',
],
defaultViewport: {
width: dynamicViewport.width,
height: dynamicViewport.height,
deviceScaleFactor: dynamicDeviceScaleFactor,
isMobile: dynamicSecChUaMobile === '?1',
hasTouch: dynamicSecChUaMobile === '?1',
},
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
// 设置动态User-Agent
await page.setUserAgent(dynamicUA);
// 设置动态Viewport
await page.setViewport({
width: dynamicViewport.width,
height: dynamicViewport.height,
deviceScaleFactor: dynamicDeviceScaleFactor,
});
// 设置动态Cookie
await page.setCookie(...dynamicCookies.split('; ').map(cookie => {
const [name, value] = cookie.split('=');
return {
name,
value,
domain: '.baidu.com',
path: '/',
secure: Math.random() > 0.5,
httpOnly: Math.random() > 0.5,
sameSite: ['Strict', 'Lax', 'None'][Math.floor(Math.random() * 3)],
};
}));
// 设置动态Headers
await page.setExtraHTTPHeaders({
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': dynamicAcceptLang,
'Accept-Encoding': dynamicAcceptEncoding,
'Connection': dynamicConnection,
'Upgrade-Insecure-Requests': Math.random() > 0.5 ? '1' : '0',
'Sec-Ch-Ua': dynamicSecChUa,
'Sec-Ch-Ua-Platform': dynamicSecChUaPlatform,
'Sec-Ch-Ua-Mobile': dynamicSecChUaMobile,
'Sec-Fetch-Site': ['none', 'cross-site', 'same-origin', 'same-site'][Math.floor(Math.random() * 4)],
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'X-Request-Id': dynamicRequestId,
'X-Forwarded-For': DynamicGenerator.randomIP(),
'X-Real-IP': DynamicGenerator.randomIP(),
'Referer': dynamicReferer,
'Origin': 'https://www.baidu.com',
'Cache-Control': ['max-age=0', 'no-cache', 'no-store'][Math.floor(Math.random() * 3)],
'DNT': Math.random() > 0.5 ? '1' : '0',
});
// 动态浏览器特征
await page.evaluateOnNewDocument((dynamicTimezone, dynamicColorDepth) => {
// 修改webdriver属性
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
// 修改插件
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5].map(() => ({ name: 'Chrome PDF Plugin' }))
});
// 修改语言
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
// 修改时区
Object.defineProperty(Intl.DateTimeFormat.prototype, 'resolvedOptions', {
value: function() {
const result = originalResolvedOptions.call(this);
result.timeZone = dynamicTimezone;
return result;
}
});
// 修改屏幕属性
Object.defineProperty(screen, 'colorDepth', { get: () => dynamicColorDepth });
Object.defineProperty(screen, 'pixelDepth', { get: () => dynamicColorDepth });
// 修改硬件并发数
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => [4, 6, 8, 12][Math.floor(Math.random() * 4)] });
// 修改设备内存
Object.defineProperty(navigator, 'deviceMemory', { get: () => [4, 8, 16][Math.floor(Math.random() * 3)] });
// 添加随机Canvas指纹
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type) {
const result = originalToDataURL.apply(this, arguments);
if (type === 'image/png' || !type) {
// 添加微小扰动
return result.replace(/[a-f0-9]{10,}/g, (match) => {
return match + Math.random().toString(16).substring(2, 4);
});
}
return result;
};
}, dynamicTimezone, dynamicColorDepth);
// 动态等待时间
await new Promise(resolve => setTimeout(resolve, CONFIG.behavior.waitAfterLoad));
// 访问页面
await page.goto(CONFIG.search.url, {
waitUntil: 'networkidle2',
timeout: CONFIG.behavior.timeout
});
await page.waitForFunction(() => {
return document.readyState === 'complete';
}, { timeout: 10000 });
// 动态滚动
await page.evaluate((y, behavior) => {
window.scrollTo({ top: y, behavior: behavior });
}, Math.floor(Math.random() * 300), CONFIG.behavior.scrollBehavior);
await new Promise(resolve => setTimeout(resolve, CONFIG.behavior.waitAfterLoad));
// ====================== 优化的元素查找逻辑 ======================
const elementInfo = await page.evaluate((searchText, screenshotHeight, mode, topMargin, customPercent) => {
const getFullText = (element) => {
if (!element) return '';
let text = element.textContent || element.innerText || '';
if (text.length < 5) {
text = element.getAttribute('title') || element.getAttribute('alt') || text;
}
return text.trim();
};
const cleanTextForMatch = (text) => {
if (!text) return '';
return text.replace(/\s+/g, '').toLowerCase();
};
const findElementByOptimizedStrategy = () => {
// 策略1: 精确匹配指定标签结构
try {
const exactSpans = document.querySelectorAll('span.cosc-source-text.cos-line-clamp-1');
for (const span of exactSpans) {
const text = getFullText(span);
if (text === searchText || text.includes(searchText)) {
const rect = span.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return span;
}
}
}
} catch (e) {}
// 策略2: 模糊匹配指定标签结构
try {
const sourceSpans = document.querySelectorAll('span.cosc-source-text');
for (const span of sourceSpans) {
const text = getFullText(span);
if (text.includes(searchText) || searchText.includes(text)) {
const rect = span.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return span;
}
}
}
} catch (e) {}
// 策略3: 其他CSS选择器匹配
const selectors = [
'.cosc-source-text',
'.common_17ggi',
'.cosc-title-slot',
'.common-content_6I4X7',
'.cosc-card-content',
'.agent-wrapper_Ycxpf',
'.content_48ora',
'[srcid="60601"]',
'[tpl="ai_agent_distribute"]',
'.result-op.c-container.new-pmd',
];
for (const selector of selectors) {
try {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const text = getFullText(element);
if (text.includes(searchText) || searchText.includes(text)) {
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return element;
}
}
}
} catch (e) {}
}
// 策略4: 文本内容匹配
const searchTextLower = searchText.toLowerCase();
const searchTextClean = cleanTextForMatch(searchText);
const allElements = document.querySelectorAll('*');
let bestMatch = null;
let bestScore = 0;
for (const element of allElements) {
const text = getFullText(element);
if (!text || text.length < 3) continue;
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0 || rect.top < 0) continue;
let score = 0;
const textLower = text.toLowerCase();
const textClean = cleanTextForMatch(text);
if (textLower === searchTextLower) {
score = 100;
} else if (textLower.includes(searchTextLower)) {
score = 80;
} else if (textClean.includes(searchTextClean)) {
score = 70;
} else if (searchTextLower.includes(textLower) && text.length >= 5) {
score = 60;
}
const className = (element.className || '').toString();
if (className.includes('cosc-') ||
className.includes('common_') ||
className.includes('agent') ||
className.includes('AI') ||
className.includes('智能')) {
score += 30;
}
if (score > bestScore) {
bestScore = score;
bestMatch = element;
}
}
if (bestMatch && bestScore >= 60) {
return bestMatch;
}
return null;
};
let element = findElementByOptimizedStrategy();
// 最终保底
if (!element) {
const keywords = ['文心智能体', 'AI生成', '智能分身', '联动优势', searchText];
const allElements = document.querySelectorAll('*');
for (const el of allElements) {
const text = getFullText(el);
for (const keyword of keywords) {
if (text && text.includes(keyword)) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
element = el;
break;
}
}
}
if (element) break;
}
}
if (!element) {
return { found: false };
}
const rect = element.getBoundingClientRect();
const scrollY = window.scrollY;
let scrollToY;
switch (mode) {
case 'top':
scrollToY = Math.max(0, (rect.top + scrollY) - topMargin);
break;
case 'center':
const centerOffset = (screenshotHeight / 2) - (rect.height / 2);
scrollToY = Math.max(0, (rect.top + scrollY) - centerOffset);
break;
case 'custom':
const customPixelPosition = (screenshotHeight * customPercent) / 100;
scrollToY = Math.max(0, (rect.top + scrollY) - customPixelPosition + topMargin);
break;
default:
scrollToY = Math.max(0, (rect.top + scrollY) - topMargin);
}
const isExactMatch = element.tagName === 'SPAN' &&
(element.className || '').includes('cosc-source-text');
return {
found: true,
elementTop: rect.top + scrollY,
top: rect.top + scrollY,
left: rect.left,
width: rect.width,
height: rect.height,
scrollToY: scrollToY,
elementText: getFullText(element).substring(0, 100),
tagName: element.tagName,
className: element.className || '',
id: element.id || '',
isExactMatch: isExactMatch,
matchStrategy: isExactMatch ? 'exact_span_match' : 'fuzzy_match'
};
}, CONFIG.search.targetText, CONFIG.screenshot.height, CONFIG.elementPosition.mode,
CONFIG.elementPosition.topMargin, CONFIG.elementPosition.customPositionPercent);
if (!elementInfo.found) {
const result = { code: "2", msg: "未找到匹配内容" };
console.log(JSON.stringify(result));
await browser.close();
process.exit(0);
return;
}
// 动态滚动到位置
await page.evaluate((y, behavior) => {
window.scrollTo({ top: y, behavior: behavior });
}, elementInfo.scrollToY, CONFIG.behavior.scrollBehavior);
await new Promise(resolve => setTimeout(resolve, CONFIG.behavior.waitAfterScroll));
const currentScrollY = await page.evaluate(() => window.scrollY);
// 动态文件名
const screenshotPath = path.join(
CONFIG.output.directory,
`${CONFIG.output.filenamePrefix}.${CONFIG.screenshot.type}`
);
// 截图
await page.screenshot({
path: screenshotPath,
clip: {
x: Math.floor(Math.random() * 5), // 微小偏移,更自然
y: currentScrollY + Math.floor(Math.random() * 3) - 1,
width: CONFIG.screenshot.width,
height: CONFIG.screenshot.height
},
type: CONFIG.screenshot.type,
quality: CONFIG.screenshot.quality
});
let fileSizeKB = 0;
try {
const stats = fs.statSync(screenshotPath);
fileSizeKB = Math.round(stats.size / 1024);
} catch (e) {}
const result = {
code: "1",
msg: "成功",
data: path.resolve(screenshotPath),
fileSize: fileSizeKB,
elementInfo: {
text: elementInfo.elementText,
position: Math.round(elementInfo.elementTop),
tag: elementInfo.tagName,
className: elementInfo.className,
exactMatch: elementInfo.isExactMatch || false
},
requestId: dynamicRequestId, // 返回请求ID便于追踪
timestamp: Date.now()
};
console.log(JSON.stringify(result));
await browser.close();
process.exit(0);
} catch (error) {
const result = {
code: "0",
msg: `错误: ${error.message}`
};
console.log(JSON.stringify(result));
if (browser) {
try {
await browser.close();
} catch (e) {}
}
process.exit(1);
}
})();