| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725 |
- #!/usr/bin/env node
- /**
- * KS-Chanel Analysis · 快手(Kuaishou)数据采集脚本 V2
- *
- * 针对报告目标优化(L1-6):
- * L1: 蓝领劳工最担心/最看重什么
- * L2: 快手(10个蓝领,九个都用快手)
- * L3: 情绪价值强的关键词
- * L4: 关注的关键字
- * L5: 快聘战略地位(日活几十万简历)
- * L6: 核心痛点深挖
- *
- * 使用 TikHub API (via api.tikhub.io / api.tikhub.dev)
- */
- const fs = require('fs');
- const path = require('path');
- const os = require('os');
- const https = require('https');
- const ROOT = path.resolve(__dirname, '..');
- const RAW_DIR = path.join(ROOT, 'raw');
- const KS_DIR = path.join(RAW_DIR, 'kuaishou');
- const AUDIT_LOG = path.join(RAW_DIR, 'audit.log');
- const API_CONFIG = (() => {
- const p = path.join(os.homedir(), '.openclaw', 'skills', 'xiaohongshu-search-notes', 'api-config.json');
- if (!fs.existsSync(p)) return null;
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
- })();
- const TIKHUB_TOKEN = API_CONFIG?.currentToken
- || API_CONFIG?.endpoint?.headers?.Authorization?.replace(/^Bearer\s+/, '')
- || 'gqsZHfMWgAiMwV+ITbmZy0qALADWBZVS7QnV7kKJe9CwzgWgJG+7bwK+GQ==';
- const API_HOST = process.env.TIKHUB_HOST || API_CONFIG?.endpoint?.host || 'api.tikhub.io';
- [RAW_DIR, KS_DIR].forEach((d) => {
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
- });
- /**
- * V2 假设体系(H1-H12)
- * H1: 快聘是蓝领求职主阵地
- * H2: 薪资透明度是核心关切(日结/月薪/五险一金)
- * H3: 入职便捷性(当天入职/手续简单)
- * H4: 食宿条件影响决策(包吃包住/宿舍)
- * H5: 平台信任与口碑(真实评价/朋友推荐)
- * H6: 被骗焦虑是普遍痛点 ← 重点
- * H7: 黑中介/克扣是核心风险 ← 重点
- * H8: 工资结算方式差异(日结/月结)
- * H9: 职业尊严与社会认同 ← 情绪价值
- * H10: 同事关系与工作氛围 ← 情绪价值
- * H11: 直播带岗降低信息不对称
- * H12: 平台战略地位验证(快聘)
- */
- /**
- * V2 关键词批次 —— 针对L1-6报告目标优化
- */
- const BATCHES = {
- 1: {
- name: 'P1 · 核心担忧:被骗与押金(高情绪价值)',
- hypotheses: ['H6', 'H7'],
- kuaishou: [
- { kw: '招聘骗局', videos: 20, commentPages: 3, hypotheses: ['H6'] },
- { kw: '打工陷阱', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '黑厂', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '克扣工资', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7', 'H8'] },
- { kw: '不退押金', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '被坑', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- ],
- },
- 2: {
- name: 'P2 · 最看重:薪资与权益保障',
- hypotheses: ['H2', 'H3', 'H8'],
- kuaishou: [
- { kw: '五险一金', videos: 20, commentPages: 3, hypotheses: ['H2'] },
- { kw: '加班费', videos: 20, commentPages: 3, hypotheses: ['H2'] },
- { kw: '发工资', videos: 20, commentPages: 3, hypotheses: ['H2', 'H8'] },
- { kw: '不拖欠工资', videos: 20, commentPages: 3, hypotheses: ['H2', 'H8'] },
- { kw: '工资日结', videos: 20, commentPages: 3, hypotheses: ['H2', 'H8'] },
- { kw: '签合同', videos: 20, commentPages: 3, hypotheses: ['H2', 'H6'] },
- ],
- },
- 3: {
- name: 'P3 · 情绪价值:尊严与归属感',
- hypotheses: ['H9', 'H10'],
- kuaishou: [
- { kw: '被骂', videos: 20, commentPages: 3, hypotheses: ['H9'] },
- { kw: '受委屈', videos: 20, commentPages: 3, hypotheses: ['H9', 'H10'] },
- { kw: '同事好', videos: 20, commentPages: 3, hypotheses: ['H10'] },
- { kw: '领导好', videos: 20, commentPages: 3, hypotheses: ['H10'] },
- { kw: '氛围好', videos: 20, commentPages: 3, hypotheses: ['H10'] },
- { kw: '被尊重', videos: 20, commentPages: 3, hypotheses: ['H9'] },
- ],
- },
- 4: {
- name: 'P4 · 平台信任与口碑验证',
- hypotheses: ['H5', 'H11', 'H12'],
- kuaishou: [
- { kw: '靠谱工作', videos: 20, commentPages: 3, hypotheses: ['H5', 'H12'] },
- { kw: '好厂推荐', videos: 20, commentPages: 3, hypotheses: ['H5'] },
- { kw: '真实评价', videos: 20, commentPages: 3, hypotheses: ['H5', 'H11'] },
- { kw: '入职顺利', videos: 20, commentPages: 3, hypotheses: ['H3', 'H5'] },
- { kw: '没被骗', videos: 20, commentPages: 3, hypotheses: ['H5', 'H6'] },
- { kw: '朋友推荐', videos: 20, commentPages: 3, hypotheses: ['H5'] },
- ],
- },
- 5: {
- name: 'P5 · 快聘生态与直播求职',
- hypotheses: ['H1', 'H11', 'H12'],
- kuaishou: [
- { kw: '快聘', videos: 20, commentPages: 3, hypotheses: ['H1', 'H12'] },
- { kw: '直播带岗', videos: 20, commentPages: 3, hypotheses: ['H1', 'H11'] },
- { kw: '直播间找工作', videos: 20, commentPages: 3, hypotheses: ['H1', 'H11'] },
- { kw: '主播靠谱', videos: 20, commentPages: 3, hypotheses: ['H1', 'H5'] },
- { kw: '快手求职', videos: 20, commentPages: 3, hypotheses: ['H1', 'H2'] },
- { kw: '快手工作', videos: 20, commentPages: 3, hypotheses: ['H1'] },
- ],
- },
- 6: {
- name: 'P6 · 风险防范与自我保护',
- hypotheses: ['H6', 'H7'],
- kuaishou: [
- { kw: '防骗指南', videos: 20, commentPages: 3, hypotheses: ['H6'] },
- { kw: '维权', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '劳动仲裁', videos: 20, commentPages: 3, hypotheses: ['H6'] },
- { kw: '黑中介', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '身份证扣押', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- { kw: '不收押金', videos: 20, commentPages: 3, hypotheses: ['H6', 'H7'] },
- ],
- },
- };
- const HYPOTHESIS_KEYWORDS = {
- H1: ['快聘', '直播带岗', '蓝领', '求职', '找工作', '快手求职', '直播间找工作', '快手工作'],
- H2: ['工资', '日结', '月薪', '薪资', '真实薪资', '多少钱', '性价比', '高工资', '五险一金', '加班费', '发工资'],
- H3: ['当天入职', '包吃包住', '入职快', '随时入职', '手续简单', '拎包入住', '入职顺利'],
- H4: ['正规', '靠谱', '放心', '有保障', '安全', '不收押金', '不扣证件'],
- H5: ['朋友推荐', '口碑', '真实评价', '过来人', '亲身经历', '推荐', '好厂推荐', '靠谱工作'],
- H6: ['被骗', '黑中介', '被坑', '押金不退', '虚假招聘', '套路', '陷阱', '投诉', '维权', '克扣工资', '招聘骗局', '打工陷阱', '黑厂', '被骂', '受委屈'],
- H7: ['黑中介', '押金不退', '不退押金', '克扣', '身份证扣押', '中介费', '不退押金'],
- H8: ['工资日结', '日结', '小时工', '月结', '工资结算', '不拖欠工资'],
- H9: ['被骂', '受委屈', '尊严', '不尊重', '骂人', '受气', '被尊重', '尊重'],
- H10: ['同事好', '领导好', '氛围好', '相处好', '工友', '关系好'],
- H11: ['直播间找工作', '直播带岗', '主播靠谱', '真实评价'],
- H12: ['快聘', '快手求职', '靠谱工作'],
- };
- const HYPOTHESIS_COLORS = {
- H1: '#F5A623',
- H2: '#3B82F6',
- H3: '#00DC82',
- H4: '#8B5CF6',
- H5: '#F97316',
- H6: '#FF4D8D',
- H7: '#EF4444',
- H8: '#6B7280',
- H9: '#EC4899',
- H10: '#14B8A6',
- H11: '#22C55E',
- H12: '#06B6D4',
- };
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
- function sanitizeFilename(kw) {
- return kw.replace(/[\/\\:*?"<>|\s]+/g, '-');
- }
- function auditLog(line) {
- const ts = new Date().toISOString();
- try { fs.appendFileSync(AUDIT_LOG, `[${ts}] [ks-v2] ${line}\n`); } catch {}
- }
- function fileExistsNonEmpty(p) {
- if (!fs.existsSync(p)) return false;
- return fs.statSync(p).size > 100;
- }
- function tikhubGet(apiPath, params = {}) {
- const qs = Object.entries(params)
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
- .join('&');
- const url = qs ? `${apiPath}?${qs}` : apiPath;
- return new Promise((resolve) => {
- const opts = {
- hostname: API_HOST,
- path: url,
- method: 'GET',
- headers: {
- Authorization: `Bearer ${TIKHUB_TOKEN}`,
- Accept: 'application/json',
- },
- };
- const req = https.request(opts, (res) => {
- const chunks = [];
- res.on('data', (c) => chunks.push(c));
- res.on('end', () => {
- const body = Buffer.concat(chunks).toString('utf-8');
- try {
- const j = JSON.parse(preserveLargeIntegerFields(body));
- resolve({ data: j, status: res.statusCode });
- } catch (e) {
- resolve({ data: { _parse_error: e.message, _raw: body.slice(0, 500) }, status: res.statusCode });
- }
- });
- });
- req.on('error', (e) => resolve({ data: { _error: e.message }, status: 0 }));
- req.setTimeout(45000, () => { req.destroy(); resolve({ data: { _error: 'timeout' }, status: 0 }); });
- req.end();
- });
- }
- function preserveLargeIntegerFields(text) {
- return text.replace(
- /("(?:[A-Za-z_]*id|[A-Za-z_]*Id|photo_id|photoId|user_id|userId|comment_id|commentId|videoId)"\s*:\s*)(\d{16,})/g,
- '$1"$2"'
- );
- }
- function firstArray(...values) {
- for (const value of values) {
- if (Array.isArray(value)) return value;
- }
- return [];
- }
- function findFirstArray(value, keys = []) {
- if (!value || typeof value !== 'object') return [];
- for (const key of keys) {
- const arr = key.split('.').reduce((cur, part) => cur?.[part], value);
- if (Array.isArray(arr)) return arr;
- }
- const queue = [value];
- const seen = new Set();
- while (queue.length) {
- const cur = queue.shift();
- if (!cur || typeof cur !== 'object' || seen.has(cur)) continue;
- seen.add(cur);
- if (Array.isArray(cur)) {
- if (cur.length === 0) continue;
- if (cur.some((it) => it && typeof it === 'object')) return cur;
- continue;
- }
- for (const next of Object.values(cur)) queue.push(next);
- }
- return [];
- }
- function asCount(value) {
- if (typeof value === 'number') return value;
- if (typeof value === 'string') {
- const text = value.trim().toLowerCase();
- const num = Number(text.replace(/[,+]/g, '').replace(/w|万/g, ''));
- if (!Number.isFinite(num)) return 0;
- return /w|万/.test(text) ? Math.round(num * 10000) : num;
- }
- return 0;
- }
- function pickVideoId(v) {
- return String(
- v?.photo_id
- || v?.photoId
- || v?.id
- || v?.photo?.id
- || v?.photo?.photo_id
- || v?.photo?.photoId
- || v?.work?.id
- || ''
- );
- }
- function pickVideoText(v) {
- return v?.caption
- || v?.title
- || v?.desc
- || v?.description
- || v?.photo?.caption
- || v?.photo?.captionText
- || v?.photo?.title
- || v?.work?.caption
- || '';
- }
- function normalizeVideo(v, rank) {
- const photo = v?.photo || v?.work || v;
- const author = v?.author || v?.user || v?.userInfo || photo?.author || photo?.user || {};
- return {
- photo_id: pickVideoId(v),
- caption: pickVideoText(v),
- like_count: asCount(v?.like_count ?? v?.likeCount ?? v?.liked_count ?? v?.likedCount ?? photo?.like_count ?? photo?.likeCount),
- comment_count: asCount(v?.comment_count ?? v?.commentCount ?? v?.comments_count ?? v?.commentsCount ?? photo?.comment_count ?? photo?.commentCount),
- view_count: asCount(v?.view_count ?? v?.viewCount ?? v?.play_count ?? v?.playCount ?? photo?.view_count ?? photo?.viewCount),
- cover: v?.cover || v?.thumbnail || v?.coverUrl || photo?.cover || photo?.thumbnail || '',
- create_time: v?.create_time || v?.createTime || v?.timestamp || photo?.create_time || photo?.createTime || '',
- rank: v?.rank || v?.index || rank,
- hot_value: asCount(v?.hot_value ?? v?.hotValue ?? v?.score ?? v?.heat ?? v?.hot),
- user: author,
- };
- }
- function normalizeComment(c) {
- const user = c?.user_info || c?.userInfo || c?.author || c?.user || {};
- return {
- id: c?.id || c?.comment_id || c?.commentId || c?.cid || '',
- content: c?.content || c?.text || c?.comment || c?.body || '',
- create_time: c?.create_time || c?.createTime || c?.timestamp || '',
- like_count: asCount(c?.like_count ?? c?.liked_count ?? c?.likeCount ?? c?.likedCount),
- ip_location: c?.ip_location || c?.ipLocation || c?.ip || '',
- user: user ? {
- user_id: user.user_id || user.userId || user.id || '',
- nickname: user.nickname || user.name || user.user_name || user.userName || '',
- } : null,
- sub_comments: firstArray(c?.sub_comments, c?.subComments, c?.replies).slice(0, 3).map((s) => ({
- content: s?.content || s?.text || '',
- like_count: asCount(s?.like_count ?? s?.liked_count ?? s?.likeCount ?? s?.likedCount),
- nickname: s?.user_info?.nickname || s?.userInfo?.nickname || s?.user?.nickname || '',
- })),
- };
- }
- function getCursor(data) {
- return data?.data?.pcursor
- || data?.data?.cursor
- || data?.data?.nextCursor
- || data?.data?.next_cursor
- || data?.pcursor
- || data?.cursor
- || '';
- }
- function hasMoreComments(data, comments, cursor) {
- const raw = data?.data || data || {};
- if (typeof raw.has_more === 'boolean') return raw.has_more;
- if (typeof raw.hasMore === 'boolean') return raw.hasMore;
- return comments.length > 0 && Boolean(cursor);
- }
- async function collectKs(spec, { force = false } = {}) {
- const outPath = path.join(KS_DIR, `${sanitizeFilename(spec.kw)}.json`);
- if (!force && fileExistsNonEmpty(outPath)) {
- console.log(` [ks] ⏭️ ${spec.kw} (已存在, skip)`);
- auditLog(`ks SKIP ${spec.kw} (exists)`);
- return { skipped: true, path: outPath };
- }
- console.log(` [ks] 🔍 搜索视频 "${spec.kw}" · page=1 × videos=${spec.videos}`);
- const startTs = Date.now();
- const allVideos = [];
- const seenIds = new Set();
- for (let page = 1; page <= 3; page++) {
- await sleep(800);
- const searchRes = await tikhubGet('/api/v1/kuaishou/app/search_video_v2', { keyword: spec.kw, page });
- const rawVideos = findFirstArray(searchRes?.data, [
- 'data.visionSearchPhoto',
- 'data.videos',
- 'data.items',
- 'data.feeds',
- 'data.list',
- 'data.result',
- 'data.data',
- 'visionSearchPhoto',
- ]);
- if (rawVideos.length === 0) {
- console.log(` [ks] ⚠️ 第${page}页 API 响应:`, JSON.stringify(searchRes?.data).slice(0, 300));
- break;
- }
- for (const [idx, v] of rawVideos.entries()) {
- const nv = normalizeVideo(v, allVideos.length + idx + 1);
- const photoId = nv.photo_id;
- if (photoId && photoId !== '0' && !seenIds.has(photoId)) {
- seenIds.add(photoId);
- allVideos.push(nv);
- }
- }
- console.log(` [ks] 第${page}页: +${rawVideos.length} 视频,累计 ${allVideos.length}`);
- if (allVideos.length >= spec.videos) break;
- }
- const topVideos = allVideos.slice(0, spec.videos);
- console.log(` [ks] 去重后 ${topVideos.length} 个视频,开始抓评论...`);
- const commentsByVideoId = {};
- let totalComments = 0;
- for (const video of topVideos) {
- if (!video.photo_id) continue;
- commentsByVideoId[video.photo_id] = [];
- let pcursor = '';
- for (let p = 0; p < (spec.commentPages || 2); p++) {
- await sleep(700);
- const commentAttempts = [
- {
- path: '/api/v1/kuaishou/app/fetch_one_video_comment',
- params: { photo_id: video.photo_id, pcursor },
- },
- {
- path: '/api/v1/kuaishou/web/fetch_video_comments',
- params: { photo_id: video.photo_id, pcursor },
- },
- {
- path: '/api/v1/kuaishou/web/fetch_video_comments',
- params: { photo_id: video.photo_id, cursor: pcursor },
- },
- ];
- let cRes = null;
- let cmts = [];
- for (const attempt of commentAttempts) {
- cRes = await tikhubGet(attempt.path, attempt.params);
- cmts = findFirstArray(cRes?.data, [
- 'data.comments',
- 'data.rootComments',
- 'data.commentList',
- 'data.list',
- 'data.items',
- 'data.data',
- 'comments',
- 'rootComments',
- ]);
- if (cmts.length > 0 || cRes.status === 200) break;
- }
- if (cmts.length === 0 && p === 0) {
- console.log(` [ks] ⚠️ 评论响应[photo_id=${video.photo_id}]:`, JSON.stringify(cRes?.data).slice(0, 300));
- }
- for (const c of cmts) {
- const comment = normalizeComment(c);
- if (comment.content) commentsByVideoId[video.photo_id].push(comment);
- }
- totalComments += cmts.length;
- pcursor = getCursor(cRes?.data);
- if (!hasMoreComments(cRes?.data, cmts, pcursor)) break;
- }
- }
- const elapsed = ((Date.now() - startTs) / 1000).toFixed(1);
- console.log(` [ks] ✓ ${spec.kw}: ${topVideos.length} 视频 / ${totalComments} 评论 (${elapsed}s)`);
- auditLog(`ks OK ${spec.kw} videos=${topVideos.length} comments=${totalComments} (${elapsed}s)`);
- const out = {
- platform: 'kuaishou',
- keyword: spec.kw,
- hypotheses: spec.hypotheses,
- collected_at: new Date().toISOString(),
- elapsed_seconds: Number(elapsed),
- total_videos_found: allVideos.length,
- top_videos: topVideos.map((v) => ({
- id: v.photo_id,
- caption: v.caption,
- like_count: v.like_count,
- comment_count: v.comment_count,
- view_count: v.view_count,
- cover: v.cover,
- create_time: v.create_time,
- user: v.user ? {
- user_id: v.user.user_id || v.user.id || '',
- nickname: v.user.nickname || v.user.name || v.user.user_name || '',
- } : null,
- })),
- comments: commentsByVideoId,
- };
- fs.writeFileSync(outPath, JSON.stringify(out, null, 2), 'utf-8');
- return { skipped: false, notes: topVideos.length, comments: totalComments, path: outPath };
- }
- function inferHypotheses(text, keywordHypotheses) {
- const results = new Set(keywordHypotheses || []);
- const t = String(text || '').toLowerCase();
- for (const [h, kws] of Object.entries(HYPOTHESIS_KEYWORDS)) {
- for (const kw of kws) {
- if (t.includes(kw.toLowerCase())) { results.add(h); break; }
- }
- }
- return Array.from(results);
- }
- const TAG_RULES = [
- { tag: '薪资关注', re: /工资|日结|月薪|薪资|钱|待遇|发工资|扣钱|加班费/ },
- { tag: '入职速度', re: /当天入职|入职快|随时入职|明天|今天|马上|入职顺利/ },
- { tag: '包吃包住', re: /包吃包住|包住|宿舍|食堂|吃饭|住宿/ },
- { tag: '防骗意识', re: /被骗|黑中介|押金|套路|陷阱|被坑|虚假|防骗/ },
- { tag: '平台信任', re: /快聘|官方|平台|快手|正规|靠谱/ },
- { tag: '口碑参考', re: /朋友推荐|过来人|亲身|真实评价|口碑|推荐/ },
- { tag: '工作环境', re: /车间|工厂|加班|环境|宿舍|工作条件|氛围/ },
- { tag: '合同保障', re: /合同|签合同|保障|合法|劳动法|权益|五险一金/ },
- { tag: '中介排斥', re: /黑中介|中介|第三方|劳务/ },
- { tag: '求职焦虑', re: /担心|害怕|不放心|纠结|怕|焦虑|犹豫/ },
- { tag: '情绪价值', re: /被骂|受委屈|尊重|尊严|同事好|氛围好|领导好/ },
- { tag: '维权意识', re: /维权|劳动仲裁|投诉|举报/ },
- ];
- function inferTags(text) {
- const tags = [];
- for (const r of TAG_RULES) {
- if (r.re.test(text || '')) tags.push(r.tag);
- }
- return tags;
- }
- const SENTIMENT_POS = /好|推荐|靠谱|放心|真实|满意|不错|工资高|环境好|入职快|顺利|真实评价|良心|没被骗|入职顺利|朋友推荐/;
- const SENTIMENT_NEG = /骗|坑|黑中介|押金不退|虚假|套路|被坑|垃圾|失望|后悔|坑人|恶劣|克扣|拖延|威胁|被骂|受委屈/;
- const SENTIMENT_CONFLICT = /但是|可是|纠结|担心|想又怕|犹豫|说实话又|虽然|不过|可惜/;
- function inferSentiment(text) {
- const t = String(text || '');
- const pos = SENTIMENT_POS.test(t);
- const neg = SENTIMENT_NEG.test(t);
- const conf = SENTIMENT_CONFLICT.test(t);
- if (conf && (pos || neg)) return 'conflicted';
- if (pos && !neg) return 'positive';
- if (neg && !pos) return 'negative';
- return 'neutral';
- }
- function buildItem({ id, platform, product, keyword, type, nickname, ip, content, likes, rating, kwHypos }) {
- if (!content || content.length < 3) return null;
- const hypotheses = inferHypotheses(content, kwHypos);
- const tags = inferTags(content);
- const sentiment = inferSentiment(content);
- return { id, platform, product, keyword, type, nickname, ip, content, likes, rating, hypothesis: hypotheses, tags, sentiment, source: 'v2-collected' };
- }
- function mergeAll() {
- const out = {
- meta: {
- collectedAt: new Date().toISOString(),
- version: '2.0',
- platforms: {},
- products: {},
- hypotheses: {},
- keywords: {},
- stage: 'batch-v2',
- sourceTier: 'v2-collected',
- },
- items: [],
- hotlistItems: [],
- };
- const flat = [];
- if (fs.existsSync(KS_DIR)) {
- for (const f of fs.readdirSync(KS_DIR)) {
- if (!f.endsWith('.json')) continue;
- const raw = JSON.parse(fs.readFileSync(path.join(KS_DIR, f), 'utf8'));
- if (raw._error) continue;
- if (raw.type === 'hotlist' && raw.videos) {
- const catName = raw.name || raw.category;
- const hypos = raw.hypotheses || [];
- for (const v of raw.videos) {
- const caption = v.caption || '';
- if (caption.length < 3) continue;
- const item = buildItem({
- id: `hl_${v.photo_id}`,
- platform: 'kuaishou',
- product: catName,
- keyword: catName,
- type: 'hotlist_video',
- nickname: v.user?.nickname || v.user?.name || '匿名',
- ip: '',
- content: caption.slice(0, 500),
- likes: v.like_count || 0,
- rating: null,
- kwHypos: hypos,
- });
- if (item) {
- out.items.push(item);
- out.hotlistItems.push({ ...item, rank: v.rank, hot_value: v.hot_value });
- flat.push(item);
- }
- }
- out.meta.keywords[catName] = (out.meta.keywords[catName] || 0) + raw.videos.length;
- continue;
- }
- if (!raw.comments) continue;
- const kw = raw.keyword;
- const kwHypos = raw.hypotheses || [];
- for (const v of raw.top_videos || []) {
- const caption = v.caption || '';
- if (!caption || caption.length < 3) continue;
- const item = buildItem({
- id: `ks_video_${v.id}`,
- platform: 'kuaishou',
- product: kw,
- keyword: kw,
- type: 'video_caption',
- nickname: v.user?.nickname || '匿名',
- ip: '',
- content: caption.slice(0, 500),
- likes: v.like_count || 0,
- rating: null,
- kwHypos,
- });
- if (item) { out.items.push(item); flat.push(item); }
- }
- for (const [videoId, cmts] of Object.entries(raw.comments)) {
- for (const c of cmts) {
- if (!c.content) continue;
- const item = buildItem({
- id: `ks_${videoId}_${c.id}`,
- platform: 'kuaishou',
- product: kw,
- keyword: kw,
- type: 'comment',
- nickname: c.user?.nickname || '匿名',
- ip: c.ip_location || '',
- content: c.content.slice(0, 500),
- likes: c.like_count || 0,
- rating: null,
- kwHypos,
- });
- if (item) { out.items.push(item); flat.push(item); }
- }
- }
- out.meta.keywords[kw] = (out.meta.keywords[kw] || 0) + (raw.total_videos_found || 0);
- }
- }
- out.meta.platforms['kuaishou'] = out.items.length;
- out.meta.productsCount = Object.keys(out.meta.keywords).length;
- out.meta.keywordsCount = Object.keys(out.meta.keywords).length;
- out.meta.comments = out.items.length;
- for (const it of out.items) {
- for (const h of (it.hypothesis || [])) {
- out.meta.hypotheses[h] = (out.meta.hypotheses[h] || 0) + 1;
- }
- }
- fs.writeFileSync(path.join(RAW_DIR, '_merged-v2.json'), JSON.stringify(out, null, 2), 'utf8');
- fs.writeFileSync(path.join(RAW_DIR, 'comments-flat-v2.jsonl'), flat.map((it) => JSON.stringify(it)).join('\n'), 'utf8');
- console.log(` ✅ merged: ${out.items.length} items | ${out.meta.productsCount} keywords`);
- return out;
- }
- async function runBatch(batchNum, opts) {
- const batch = BATCHES[batchNum];
- if (!batch) throw new Error(`unknown batch: ${batchNum}`);
- console.log(`\n▶ Batch ${batchNum}: ${batch.name}`);
- let totalNotes = 0;
- let totalComments = 0;
- if (batch.kuaishou) {
- for (const task of batch.kuaishou) {
- try {
- const r = await collectKs(task, opts);
- totalNotes += r.notes || 0;
- totalComments += r.comments || 0;
- } catch (e) {
- console.log(` ✗ ks:${task.kw}: ${e.message}`);
- auditLog(`ks EXC ${task.kw}: ${e.message}`);
- }
- await sleep(500);
- }
- }
- console.log(` 📊 Batch ${batchNum} 合计: ${totalNotes} 视频 / ${totalComments} 评论`);
- }
- async function main() {
- const argv = process.argv.slice(2);
- const opts = { force: argv.includes('--force') };
- const batchArg = argv.find((a) => a.startsWith('--batch='));
- const isMerge = argv.includes('--merge');
- console.log('\n╔═══════════════════════════════════════════════════════════╗');
- console.log('║ KS-Chanel Analysis V2 · 快手(Kuaishou)数据采集 ║');
- console.log('║ 目标: L1-6 最担心/最看重 + 情绪价值关键词 ║');
- console.log('╚═══════════════════════════════════════════════════════════╝');
- console.log(` TikHub Token: ${TIKHUB_TOKEN ? '✓' : '✗'}`);
- console.log(` API Host: ${API_HOST}`);
- if (batchArg) {
- const bn = batchArg.split('=')[1];
- if (bn === 'all') {
- for (const k of Object.keys(BATCHES)) await runBatch(k, opts);
- } else {
- await runBatch(bn, opts);
- }
- }
- if (isMerge || batchArg) {
- console.log('\n▶ Merging V2 data...');
- mergeAll();
- }
- if (!batchArg && !isMerge) {
- console.log('\nUsage:');
- console.log(' --batch=1 P1 · 核心担忧:被骗与押金');
- console.log(' --batch=2 P2 · 最看重:薪资与权益保障');
- console.log(' --batch=3 P3 · 情绪价值:尊严与归属感');
- console.log(' --batch=4 P4 · 平台信任与口碑验证');
- console.log(' --batch=5 P5 · 快聘生态与直播求职');
- console.log(' --batch=6 P6 · 风险防范与自我保护');
- console.log(' --batch=all 执行所有批次');
- console.log(' --merge 合并数据');
- console.log(' --force 强制重抓');
- console.log('\n假设体系: H1-H12');
- console.log(' H1: 快聘是蓝领求职主阵地');
- console.log(' H2: 薪资透明度是核心关切');
- console.log(' H3: 入职便捷性');
- console.log(' H4: 食宿条件影响决策');
- console.log(' H5: 平台信任与口碑');
- console.log(' H6: 被骗焦虑是普遍痛点 ← 重点');
- console.log(' H7: 黑中介/克扣是核心风险 ← 重点');
- console.log(' H8: 工资结算方式差异');
- console.log(' H9: 职业尊严与社会认同 ← 情绪价值');
- console.log(' H10: 同事关系与工作氛围 ← 情绪价值');
- console.log(' H11: 直播带岗降低信息不对称');
- console.log(' H12: 快聘战略地位验证');
- }
- }
- if (require.main === module) {
- main().catch((e) => { console.error('fatal:', e); process.exit(1); });
- }
- module.exports = { BATCHES, HYPOTHESIS_KEYWORDS, HYPOTHESIS_COLORS, mergeAll, collectKs };
|