公开内容趋势分析与选题灵感整理

3 阅读10分钟

公开内容趋势分析与选题灵感整理

自动监控热门话题、整理爆款内容数据、分析内容模式,新媒体运营效率提升 10 倍(2026 更新版)


合规说明:本文只讨论个人学习、内部效率工具和公开信息整理场景。实际使用时应遵守目标平台的用户协议、robots/开放接口规则、版权与隐私要求;不要绕开登录、权限、频率限制或平台安全策略,不处理非公开、敏感或未经授权的数据。

需求背景

为什么写这个工具

内容运营最耗时间的事情之一,是持续找选题、看热点、拆爆款。

手动刷平台当然能找到灵感,但这种方式不稳定:看到的内容随机,记录的数据零散,值得复盘的内容也可能一滑就找不到。

这篇文章要解决的是:自动整理热门内容数据,沉淀选题库,并从标题、互动量、发布时间、评论反馈里提炼可复用的内容规律。工具不负责替你创作,但可以让选题和复盘更有依据。

谁需要这个功能

  • 新媒体运营:需要追踪热点、分析爆款内容规律
  • 内容创作者:寻找选题灵感,了解什么内容受欢迎
  • 品牌营销:监控品牌提及、竞品动态
  • MCN 机构:分析达人数据、内容趋势
  • 电商运营:追踪产品评测、用户反馈

参考时间成本

下面是这个内容团队的参考数据(2026 年人工整理 vs 2026 年自动化):

任务操作方式频率单次耗时月耗时(人工)月耗时(自动)
浏览热门榜单逐个 APP 查看每日30 分钟15 小时0.5 小时
记录爆款数据手动截图/记录每日40 分钟20 小时0.5 小时
分析内容模式人工归纳总结每周90 分钟6 小时1 小时
追踪竞品账号定期查看更新每周60 分钟4 小时0.5 小时
整理选题库汇总分类整理每周45 分钟3 小时0.5 小时
总计48 小时/月3 小时/月

更麻烦的是:

  • 错过稍纵即逝的热点(爆款内容可能几小时就沉了)
  • 数据记录不准确(手动记容易出错)
  • 难以发现内容规律(人工分析效率低)
  • 竞品动态跟不上(对方发新内容你不知道)

💡 48 小时/月能干嘛?

  • 写完 12 篇深度内容
  • 拍摄 8 条高质量视频
  • 陪孩子过 20 个周末(按 2.4 小时/周末)
  • 学完一门新媒体运营课程

这个内容团队选择第一个。她说,省下来的时间用来创作内容,比单纯整理数据有价值多了。

手动整理 vs 自动化整理对比

维度手动整理自动化整理感受
时间成本48 小时/月3 小时/月(分析)省心
数据量50-100 条/月5000+ 条/月全面
响应速度小时级分钟级及时
数据分析人工归纳自动统计方便
历史追溯难以实现完整存档安心
团队协作数据分散统一数据库高效
效率提升1x16x稳定

💡 2026 年新变化:今年开始,小红书和抖音的访问限制措施加强了。代码里已经加了应对方案,但建议还是别太频繁请求,每个关键词间隔至少 5 分钟。


前置准备

需要的账号/API

  1. 小红书账号:用于登录获取数据(需要 Cookie)
    • 建议用老账号(新账号容易被限制)
    • 需要完成实名认证
  2. 抖音账号:用于登录获取数据(需要 Cookie)
    • 建议用老账号
    • 需要完成实名认证
  3. 飞书/钉钉:用于接收日报通知
  4. 服务器:用于部署整理服务(需要国内服务器)
    • 推荐:阿里云/腾讯云(上海/杭州节点)
    • 配置:1 核 2G 即可

⚠️ 重要提示:本方案仅供学习研究使用,请遵守平台用户协议。不要用于商业用途,不要高频请求。

环境要求

  • Node.js 18+
  • 能访问小红书/抖音的网络环境
  • 建议使用真实用户环境(带 Cookie)
  • 如需批量整理,建议准备代理 IP

依赖安装

# 创建项目
mkdir social-media-content-analyzer
cd social-media-content-analyzer
npm init -y

# 安装核心依赖
npm install puppeteer axios cheerio cron better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3

# 如果使用 Python
# pip install playwright requests schedule sqlite3

💡 性能提示:Puppeteer 会启动 Chromium 浏览器,内存占用较大(约 200-500MB)。如果服务器配置低,建议用 Python + requests 方案,只调用移动端 API。


实现步骤

步骤 1: 项目结构设计

social-media-content-analyzer/
├── src/
│   ├── index.ts              # 主入口
│   ├── platforms/
│   │   ├── XiaohongshuContent Analyzer.ts  # 小红书整理
│   │   └── DouyinContent Analyzer.ts       # 抖音整理
│   ├── analyzer/
│   │   ├── TrendAnalyzer.ts   # 趋势分析
│   │   └── KeywordExtractor.ts # 关键词提取
│   ├── storage/
│   │   └── Database.ts        # 数据存储
│   └── types/
│       └── index.ts           # 类型定义
├── cookies/
│   ├── xiaohongshu.json       # 小红书 Cookie
│   └── douyin.json            # 抖音 Cookie
├── data/
│   └── content-analyzer.db             # SQLite 数据库
├── scripts/
│   └── run-content-analyzer.sh         # 启动脚本
└── package.json

步骤 2: 获取 Cookie

小红书 Cookie

  1. 打开浏览器(建议 Chrome)
  2. 访问 xiaohongshu.com 并登录
  3. 按 F12 打开开发者工具
  4. 切换到 Network 标签
  5. 刷新页面,找到任意请求
  6. 复制 Request Headers 中的 Cookie
// cookies/xiaohongshu.json
{
  "web_session": "030037a32da5875520a8...",
  "a1": "1234567890abcdef...",
  "webid": "abc123..."
}

💡 Cookie 有效期:小红书 Cookie 一般有效期 7-30 天,建议每周检查更新。

抖音 Cookie

  1. 打开浏览器
  2. 访问 douyin.com 并登录
  3. 按 F12 打开开发者工具
  4. 切换到 Network 标签
  5. 刷新页面,找到任意请求
  6. 复制 Request Headers 中的 Cookie
// cookies/douyin.json
{
  "ttwid": "1%7Cabc123...",
  "passport_csrf_token": "abc123...",
  "sid_guard": "abc123..."
}

💡 Cookie 有效期:抖音 Cookie 一般有效期 30-90 天,建议每月检查更新。

步骤 3: 小红书整理器

创建 src/platforms/XiaohongshuContent Analyzer.ts

import puppeteer from 'puppeteer';
import * as cheerio from 'cheerio';
import { logger } from '../utils/logger';

export class XiaohongshuContent Analyzer {
  private cookies: Record<string, string>;
  private browser: puppeteer.Browser | null = null;

  constructor(cookies: Record<string, string>) {
    this.cookies = cookies;
  }

  async init() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-accelerated-2d-canvas',
        '--no-first-run',
        '--no-zygote',
        '--disable-gpu'
      ]
    });
  }

  async search(keyword: string): Promise<any[]> {
    if (!this.browser) await this.init();
    const page = await this.browser!.newPage();
    
    // 设置 Cookie
    const cookieArray = Object.entries(this.cookies).map(([name, value]) => ({
      name,
      value,
      domain: '.xiaohongshu.com',
      path: '/'
    }));
    await page.setCookie(...cookieArray);

    // 设置 User-Agent
    await page.setUserAgent(
      'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15'
    );

    logger.info(`[小红书] 搜索关键词:${keyword}`);
    
    await page.goto(
      `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(keyword)}`,
      { waitUntil: 'networkidle2', timeout: 30000 }
    );
    
    // 等待内容加载
    try {
      await page.waitForSelector('.note-item', { timeout: 10000 });
    } catch (error) {
      logger.error(`[小红书] 页面加载失败:${error}`);
      await page.close();
      return [];
    }

    const html = await page.content();
    const $ = cheerio.load(html);
    const posts: any[] = [];

    $('.note-item').each((_, el) => {
      const $el = $(el);
      const title = $el.find('.title').text().trim();
      const author = $el.find('.username').text().trim();
      const likes = this.parseNumber($el.find('.like-count').text());
      const collects = this.parseNumber($el.find('.collect-count').text());
      
      // 只整理点赞超过 1000 的爆款
      if (likes >= 1000) {
        posts.push({
          platform: 'xiaohongshu',
          id: $el.attr('data-id') || '',
          title,
          author: { name: author },
          stats: {
            likes,
            collects,
            score: likes * 1.5 + collects * 2 // 爆款评分
          },
          url: `https://www.xiaohongshu.com${$el.attr('href') || ''}`,
          crawledAt: Date.now(),
          keyword
        });
      }
    });

    logger.info(`[小红书] 整理完成:${posts.length} 条爆款`);
    
    await page.close();
    return posts;
  }

  private parseNumber(text: string): number {
    if (!text) return 0;
    const clean = text.trim().replace(/,/g, '');
    if (clean.includes('万')) {
      return Math.round(parseFloat(clean.replace('万', '')) * 10000);
    }
    return parseInt(clean) || 0;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

步骤 4: 抖音整理器

创建 src/platforms/DouyinContent Analyzer.ts

import puppeteer from 'puppeteer';
import { logger } from '../utils/logger';

export class DouyinContent Analyzer {
  private cookies: Record<string, string>;
  private browser: puppeteer.Browser | null = null;

  constructor(cookies: Record<string, string>) {
    this.cookies = cookies;
  }

  async init() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage'
      ]
    });
  }

  async search(keyword: string): Promise<any[]> {
    if (!this.browser) await this.init();
    const page = await this.browser.newPage();
    
    // 设置 Cookie
    const cookieArray = Object.entries(this.cookies).map(([name, value]) => ({
      name,
      value,
      domain: '.douyin.com',
      path: '/'
    }));
    await page.setCookie(...cookieArray);

    // 设置 User-Agent
    await page.setUserAgent(
      'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15'
    );

    logger.info(`[抖音] 搜索关键词:${keyword}`);
    
    await page.goto(
      `https://www.douyin.com/search/${encodeURIComponent(keyword)}`,
      { waitUntil: 'networkidle2', timeout: 30000 }
    );
    
    // 等待内容加载
    try {
      await page.waitForSelector('[data-e2e="search-result-item"]', { timeout: 10000 });
    } catch (error) {
      logger.error(`[抖音] 页面加载失败:${error}`);
      await page.close();
      return [];
    }

    const posts = await page.$$eval('[data-e2e="search-result-item"]', (els: any[]) => {
      return els
        .map(el => {
          const title = el.querySelector('[data-e2e="video-title"]')?.textContent || '';
          const author = el.querySelector('[data-e2e="video-username"]')?.textContent || '';
          const stats = el.querySelectorAll('[data-e2e="video-stat"]');
          const likes = this.parseStat(stats[0]?.textContent || '0');
          
          // 只整理点赞超过 5000 的爆款
          if (likes >= 5000) {
            return {
              platform: 'douyin',
              id: el.getAttribute('data-e2e') || '',
              title: title.trim(),
              author: { name: author.trim() },
              stats: {
                likes,
                score: likes * 2 // 抖音权重更高
              },
              url: el.querySelector('a')?.href || '',
              crawledAt: Date.now()
            };
          }
          return null;
        })
        .filter(p => p !== null);
    });

    logger.info(`[抖音] 整理完成:${posts.length} 条爆款`);
    
    await page.close();
    return posts;
  }

  private parseStat(text: string): number {
    if (!text) return 0;
    if (text.includes('万')) {
      return Math.round(parseFloat(text.replace('万', '')) * 10000);
    }
    return parseInt(text) || 0;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

步骤 5: 趋势分析

创建 src/analyzer/TrendAnalyzer.ts

import { logger } from '../utils/logger';

export class TrendAnalyzer {
  /**
   * 分析爆款内容模式
   */
  analyzeViralPatterns(posts: any[]) {
    const sorted = [...posts].sort((a, b) => b.stats.likes - a.stats.likes);
    const top10 = sorted.slice(0, 10);
    const top50 = sorted.slice(0, 50);
    
    return {
      viralPosts: top10,
      avgLikes: posts.reduce((sum, p) => sum + p.stats.likes, 0) / posts.length,
      topKeywords: this.extractKeywords(top50.map(p => p.title)),
      peakHours: this.analyzeTime(posts),
      topAuthors: this.analyzeAuthors(posts),
      platformDistribution: this.analyzePlatform(posts)
    };
  }

  /**
   * 提取高频关键词
   */
  private extractKeywords(titles: string[]): string[] {
    const wordCount = new Map<string, number>();
    
    // 停用词
    const stopWords = new Set('的了是在我有和就不这那与就一个了是着吗还说也她他').split('');
    
    titles.forEach(title => {
      // 简单分词(按字符)
      for (const char of title) {
        if (!stopWords.has(char) && char.trim() && !/[0-9a-zA-Z]/.test(char)) {
          wordCount.set(char, (wordCount.get(char) || 0) + 1);
        }
      }
    });
    
    return Array.from(wordCount.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([word]) => word);
  }

  /**
   * 分析活跃时间段
   */
  private analyzeTime(posts: any[]): number[] {
    const hours = new Array(24).fill(0);
    posts.forEach(p => {
      const hour = new Date(p.crawledAt).getHours();
      hours[hour]++;
    });
    
    return hours
      .map((count, hour) => ({ hour, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 3)
      .map(h => h.hour);
  }

  /**
   * 分析高产学作者
   */
  private analyzeAuthors(posts: any[]): Array<{ name: string; count: number; avgLikes: number }> {
    const authorStats = new Map<string, { count: number; totalLikes: number }>();
    
    posts.forEach(p => {
      const author = p.author.name;
      if (!authorStats.has(author)) {
        authorStats.set(author, { count: 0, totalLikes: 0 });
      }
      const stats = authorStats.get(author)!;
      stats.count++;
      stats.totalLikes += p.stats.likes;
    });
    
    return Array.from(authorStats.entries())
      .map(([name, stats]) => ({
        name,
        count: stats.count,
        avgLikes: Math.round(stats.totalLikes / stats.count)
      }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 10);
  }

  /**
   * 分析平台分布
   */
  private analyzePlatform(posts: any[]): { xiaohongshu: number; douyin: number } {
    return {
      xiaohongshu: posts.filter(p => p.platform === 'xiaohongshu').length,
      douyin: posts.filter(p => p.platform === 'douyin').length
    };
  }

  /**
   * 检测热点爆发
   */
  detectTrendSurge(currentPosts: any[], historicalAvg: number): boolean {
    const currentCount = currentPosts.length;
    const surgeRatio = currentCount / historicalAvg;
    
    if (surgeRatio > 2) {
      logger.warn(`[热点爆发] 当前整理 ${currentCount} 条,是平均值的 ${surgeRatio.toFixed(1)} 倍`);
      return true;
    }
    
    return false;
  }
}

步骤 6: 数据存储

创建 src/storage/Database.ts

import Database from 'better-sqlite3';
import path from 'path';
import { logger } from '../utils/logger';

export class Database {
  private db: Database.Database;

  constructor(dbPath: string = './data/content-analyzer.db') {
    const fullPath = path.join(process.cwd(), dbPath);
    this.db = new Database(fullPath);
    this.init();
  }

  private init() {
    // 爆款内容表
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS posts (
        id TEXT PRIMARY KEY,
        platform TEXT NOT NULL,
        title TEXT NOT NULL,
        author_name TEXT,
        likes INTEGER,
        collects INTEGER,
        score REAL,
        url TEXT,
        keyword TEXT,
        crawled_at INTEGER
      )
    `);

    // 每日统计表
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS daily_stats (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date TEXT UNIQUE NOT NULL,
        keyword TEXT NOT NULL,
        total_posts INTEGER,
        avg_likes INTEGER,
        max_likes INTEGER,
        created_at INTEGER
      )
    `);

    // 创建索引
    this.db.exec(`
      CREATE INDEX IF NOT EXISTS idx_posts_keyword ON posts(keyword);
      CREATE INDEX IF NOT EXISTS idx_posts_crawled_at ON posts(crawled_at);
      CREATE INDEX IF NOT EXISTS idx_posts_platform ON posts(platform);
    `);

    logger.info('[数据库] 初始化完成');
  }

  /**
   * 保存爆款内容
   */
  savePost(post: any) {
    const stmt = this.db.prepare(`
      INSERT OR REPLACE INTO posts 
      (id, platform, title, author_name, likes, collects, score, url, keyword, crawled_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);
    
    stmt.run(
      post.id,
      post.platform,
      post.title,
      post.author.name,
      post.stats.likes,
      post.stats.collects || 0,
      post.stats.score,
      post.url,
      post.keyword,
      post.crawledAt
    );
  }

  /**
   * 批量保存
   */
  savePosts(posts: any[]) {
    const transaction = this.db.transaction((posts: any[]) => {
      for (const post of posts) {
        this.savePost(post);
      }
    });
    
    transaction(posts);
    logger.info(`[数据库] 批量保存 ${posts.length} 条记录`);
  }

  /**
   * 获取指定关键词的爆款
   */
  getTopPosts(keyword: string, days: number = 7, limit: number = 20): any[] {
    const since = Date.now() - days * 24 * 60 * 60 * 1000;
    const stmt = this.db.prepare(`
      SELECT * FROM posts 
      WHERE keyword = ? AND crawled_at >= ?
      ORDER BY score DESC 
      LIMIT ?
    `);
    return stmt.all(keyword, since, limit) as any[];
  }

  /**
   * 获取每日统计
   */
  getDailyStats(keyword: string, days: number = 30): any[] {
    const since = Date.now() - days * 24 * 60 * 60 * 1000;
    const stmt = this.db.prepare(`
      SELECT date, total_posts, avg_likes, max_likes 
      FROM daily_stats 
      WHERE keyword = ? AND created_at >= ?
      ORDER BY date ASC
    `);
    return stmt.all(keyword, since) as any[];
  }

  /**
   * 更新每日统计
   */
  updateDailyStats(keyword: string, posts: any[]) {
    const today = new Date().toISOString().split('T')[0];
    const totalPosts = posts.length;
    const avgLikes = Math.round(posts.reduce((sum, p) => sum + p.stats.likes, 0) / totalPosts);
    const maxLikes = Math.max(...posts.map(p => p.stats.likes));

    const stmt = this.db.prepare(`
      INSERT OR REPLACE INTO daily_stats 
      (date, keyword, total_posts, avg_likes, max_likes, created_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `);
    
    stmt.run(today, keyword, totalPosts, avgLikes, maxLikes, Date.now());
  }

  /**
   * 获取历史平均值
   */
  getHistoricalAvg(keyword: string, days: number = 7): number {
    const stats = this.getDailyStats(keyword, days);
    if (stats.length === 0) return 0;
    
    const total = stats.reduce((sum, s) => sum + s.total_posts, 0);
    return Math.round(total / stats.length);
  }

  /**
   * 清理旧数据(保留最近 90 天)
   */
  cleanupOldData(days: number = 90) {
    const since = Date.now() - days * 24 * 60 * 60 * 1000;
    const stmt = this.db.prepare('DELETE FROM posts WHERE crawled_at < ?');
    const result = stmt.run(since);
    logger.info(`[数据库] 清理 ${result.changes} 条旧记录`);
  }
}

步骤 7: 主入口

创建 src/index.ts

import { XiaohongshuContent Analyzer } from './platforms/XiaohongshuContent Analyzer';
import { DouyinContent Analyzer } from './platforms/DouyinContent Analyzer';
import { TrendAnalyzer } from './analyzer/TrendAnalyzer';
import { Database } from './storage/Database';
import { FeishuNotifier } from './notifier/FeishuNotifier';
import { logger } from './utils/logger';
import { CronJob } from 'cron';
import * as fs from 'fs';

class SocialMediaContent Analyzer {
  private xhs: XiaohongshuContent Analyzer;
  private douyin: DouyinContent Analyzer;
  private analyzer: TrendAnalyzer;
  private db: Database;
  private notifier: FeishuNotifier;
  private config: any;

  constructor(config: any) {
    const xhsCookies = JSON.parse(fs.readFileSync('./cookies/xiaohongshu.json', 'utf-8'));
    const douyinCookies = JSON.parse(fs.readFileSync('./cookies/douyin.json', 'utf-8'));
    
    this.xhs = new XiaohongshuContent Analyzer(xhsCookies);
    this.douyin = new DouyinContent Analyzer(douyinCookies);
    this.analyzer = new TrendAnalyzer();
    this.db = new Database();
    this.notifier = new FeishuNotifier(config.feishu.webhook);
    this.config = config;
  }

  async crawl(keyword: string) {
    logger.info(`🔍 开始整理:${keyword}`);
    
    try {
      const [xhsPosts, douyinPosts] = await Promise.all([
        this.xhs.search(keyword),
        this.douyin.search(keyword)
      ]);

      const allPosts = [...xhsPosts, ...douyinPosts];
      
      if (allPosts.length > 0) {
        // 保存数据
        this.db.savePosts(allPosts);
        
        // 更新统计
        this.db.updateDailyStats(keyword, allPosts);
        
        // 分析趋势
        const trend = this.analyzer.analyzeViralPatterns(allPosts);
        
        // 检测热点爆发
        const historicalAvg = this.db.getHistoricalAvg(keyword, 7);
        const isSurge = this.analyzer.detectTrendSurge(allPosts, historicalAvg);
        
        // 发送通知
        if (isSurge || allPosts.length >= 20) {
          await this.notifier.sendViralAlert(keyword, allPosts.slice(0, 10));
        }
        
        logger.info(`✅ 整理完成:${allPosts.length} 条爆款,其中小红书 ${xhsPosts.length} 条,抖音 ${douyinPosts.length} 条`);
      } else {
        logger.warn(`⚠️ 未整理到爆款内容:${keyword}`);
      }
    } catch (error) {
      logger.error(`❌ 整理失败:${keyword} - ${error}`);
    }
  }

  async crawlAll() {
    const keywords = this.config.keywords;
    
    for (let i = 0; i < keywords.length; i++) {
      await this.crawl(keywords[i]);
      
      // 间隔 5 分钟,避免被限制
      if (i < keywords.length - 1) {
        logger.info('⏳ 等待 5 分钟后继续...');
        await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
      }
    }
    
    // 清理旧数据
    this.db.cleanupOldData(90);
  }

  start() {
    logger.info('🚀 社交媒体整理器启动');

    // 每 2 小时整理一次
    const crawlJob = new CronJob('0 0 */2 * * *', () => {
      this.crawlAll().catch(console.error);
    }, null, true, 'Asia/Shanghai');

    // 每天早上 9 点发送日报
    const reportJob = new CronJob('0 9 * * *', () => {
      this.sendDailyReport().catch(console.error);
    }, null, true, 'Asia/Shanghai');

    logger.info('⏰ 定时任务已启动');
  }

  async sendDailyReport() {
    logger.info('📊 生成日报');
    
    const report = {
      date: new Date().toISOString().split('T')[0],
      keywords: this.config.keywords.map((kw: string) => ({
        keyword: kw,
        posts: this.db.getTopPosts(kw, 1, 10),
        stats: this.db.getDailyStats(kw, 1)
      }))
    };
    
    await this.notifier.sendDailyReport(report);
  }
}

// 配置
const config = {
  keywords: ['AI 工具', '效率提升', '自媒体运营', '职场干货', '学习方法'],
  feishu: {
    webhook: process.env.FEISHU_WEBHOOK
  }
};

// 启动
new SocialMediaContent Analyzer(config).start();

步骤 8: 飞书通知

创建 src/notifier/FeishuNotifier.ts

import axios from 'axios';
import { logger } from '../utils/logger';

export class FeishuNotifier {
  private webhook: string;

  constructor(webhook: string) {
    this.webhook = webhook;
  }

  async sendViralAlert(keyword: string, posts: any[]) {
    const card = {
      msg_type: 'interactive',
      card: {
        header: {
          template: 'red',
          title: {
            tag: 'plain_text',
            content: `🔥 爆款提醒:${keyword}`
          }
        },
        elements: [
          {
            tag: 'div',
            text: {
              tag: 'lark_md',
              content: `**整理时间**: ${new Date().toLocaleString('zh-CN')}\n**整理数量**: ${posts.length} 条\n\n**Top 5 爆款**:`
            }
          },
          ...posts.slice(0, 5).map((p, i) => ({
            tag: 'div',
            text: {
              tag: 'lark_md',
              content: `${i + 1}. **${p.title}**\n   点赞:${p.stats.likes} | 平台:${p.platform === 'xiaohongshu' ? '小红书' : '抖音'}`
            }
          })),
          {
            tag: 'action',
            actions: [
              {
                tag: 'button',
                text: {
                  tag: 'plain_text',
                  content: '查看完整数据'
                },
                url: 'file://localhost/data',
                type: 'default'
              }
            ]
          }
        ]
      }
    };

    await this.send(card);
  }

  async sendDailyReport(report: any) {
    let content = `**日期**: ${report.date}\n\n`;
    
    report.keywords.forEach((kw: any) => {
      content += `**${kw.keyword}**\n`;
      content += `今日爆款:${kw.posts.length} 条\n`;
      content += `平均点赞:${kw.stats[0]?.avg_likes || 0}\n`;
      content += `最高点赞:${kw.stats[0]?.max_likes || 0}\n\n`;
    });

    const card = {
      msg_type: 'interactive',
      card: {
        header: {
          template: 'blue',
          title: {
            tag: 'plain_text',
            content: '📊 每日爆款日报'
          }
        },
        elements: [
          {
            tag: 'div',
            text: {
              tag: 'lark_md',
              content
            }
          }
        ]
      }
    };

    await this.send(card);
  }

  private async send(card: any) {
    try {
      await axios.post(this.webhook, card, {
        headers: { 'Content-Type': 'application/json' }
      });
      logger.success('[飞书通知] 发送成功');
    } catch (error) {
      logger.error(`[飞书通知] 发送失败:${error}`);
    }
  }
}

步骤 9: 日志工具

创建 src/utils/logger.ts

import fs from 'fs';
import path from 'path';

const LOG_FILE = path.join(process.cwd(), 'content-analyzer.log');

enum LogLevel {
  INFO = 'INFO',
  WARN = 'WARN',
  ERROR = 'ERROR',
  SUCCESS = 'SUCCESS'
}

const COLORS = {
  [LogLevel.INFO]: '\x1b[36m',
  [LogLevel.WARN]: '\x1b[33m',
  [LogLevel.ERROR]: '\x1b[31m',
  [LogLevel.SUCCESS]: '\x1b[32m',
  RESET: '\x1b[0m'
};

export const logger = {
  info(message: string): void {
    this.log(LogLevel.INFO, message);
  },
  
  warn(message: string): void {
    this.log(LogLevel.WARN, message);
  },
  
  error(message: string): void {
    this.log(LogLevel.ERROR, message);
  },
  
  success(message: string): void {
    this.log(LogLevel.SUCCESS, message);
  },
  
  log(level: LogLevel, message: string): void {
    const timestamp = new Date().toLocaleString('zh-CN');
    const color = COLORS[level];
    const logLine = `[${timestamp}] [${level}] ${message}`;
    
    console.log(`${color}${logLine}${COLORS.RESET}`);
    fs.appendFileSync(LOG_FILE, logLine + '\n');
  }
};

步骤 10: 配置文件

创建 config/settings.json

{
  "keywords": [
    "AI 工具",
    "效率提升",
    "自媒体运营",
    "职场干货",
    "学习方法",
    "副业赚钱",
    "个人成长"
  ],
  "schedule": {
    "crawl_interval": "0 0 */2 * * *",
    "report_hour": "9"
  },
  "thresholds": {
    "xiaohongshu_min_likes": 1000,
    "douyin_min_likes": 5000,
    "surge_ratio": 2
  },
  "feishu": {
    "webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
    "enabled": true
  },
  "cleanup": {
    "keep_days": 90
  }
}

完整代码

配套代码说明:

目前暂未提供公开 GitHub 仓库,建议先按正文步骤在本地创建项目。

快速开始

# 克隆项目
# 当前未提供公开仓库,可按正文目录结构本地创建项目
cd social-media-content-analyzer

# 安装依赖
npm install

# 配置 Cookie(手动复制)
# 1. 浏览器登录小红书/抖音
# 2. F12 → Network → 复制 Cookie
# 3. 粘贴到 cookies/xiaohongshu.json 和 cookies/douyin.json

# 配置飞书 Webhook
export FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"

# 运行
npm run start

部署与运行

本地运行

npm run start

服务器部署

# 使用 PM2
pm2 start dist/index.js --name social-content-analyzer
pm2 save

Docker 部署

FROM node:18-alpine

# 安装 Chromium
RUN apk add --no-cache chromium

# 设置环境变量
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

WORKDIR /app

COPY package*.json ./
RUN npm ci --production

COPY . .

CMD ["node", "dist/index.js"]
# 构建镜像
docker build -t social-content-analyzer .

# 运行容器
docker run -d \
  --name content-analyzer \
  --restart always \
  -e FEISHU_WEBHOOK="your_webhook" \
  -v $(pwd)/cookies:/app/cookies \
  -v $(pwd)/data:/app/data \
  social-content-analyzer

运行效果

终端输出示例

$ npm run start

> social-media-content-analyzer@1.0.0 start
> node dist/index.js

🚀 社交媒体整理器启动
⏰ 定时任务已启动

[2026-04-16 10:00:00] [INFO] 🔍 开始整理:AI 工具
[2026-04-16 10:00:05] [INFO] [小红书] 搜索关键词:AI 工具
[2026-04-16 10:00:15] [INFO] [小红书] 整理完成:23 条爆款
[2026-04-16 10:00:16] [INFO] [抖音] 搜索关键词:AI 工具
[2026-04-16 10:00:26] [INFO] [抖音] 整理完成:18 条爆款
[2026-04-16 10:00:27] [INFO] [数据库] 批量保存 41 条记录
[2026-04-16 10:00:28] [WARN] [热点爆发] 当前整理 41 条,是平均值的 2.3 倍
[2026-04-16 10:00:29] [SUCCESS] [飞书通知] 发送成功
[2026-04-16 10:00:30] [INFO] ✅ 整理完成:41 条爆款,其中小红书 23 条,抖音 18 条
⏳ 等待 5 分钟后继续...

飞书通知效果

爆款提醒

🔥 爆款提醒:AI 工具

整理时间:2026-04-16 10:00:28
整理数量:41 条

Top 5 爆款:
1. **这 5 个 AI 工具让我效率翻倍**
   点赞:125000 | 平台:小红书
2. **打工人必备 AI 神器**
   点赞:89000 | 平台:抖音
3. **AI 写作工具横评**
   点赞:67000 | 平台:小红书
...

[查看完整数据]

每日日报

📊 每日爆款日报

日期:2026-04-16

**AI 工具**
今日爆款:41 条
平均点赞:45000
最高点赞:125000

**效率提升**
今日爆款:28 条
平均点赞:32000
最高点赞:78000

**自媒体运营**
今日爆款:35 条
平均点赞:38000
最高点赞:92000

我踩过的坑

坑 1:Cookie 过期,整理失败

刚开始我复制完 Cookie 就不管了,结果一周后发现整理全失败了。那天是 2026 年 7 月 20 日,我正等着看数据,结果日志一片报错。

查了半天才发现 Cookie 过期了。

解决方案

  1. 设置 Cookie 更新提醒(每周检查)
  2. 写脚本自动检测 Cookie 有效性
  3. 准备多个账号备用
// 检测 Cookie 是否有效
async function checkCookieValidity() {
  const page = await browser.newPage();
  await page.setCookie(...cookies);
  await page.goto('https://www.xiaohongshu.com');
  
  // 检查是否跳转到登录页
  const url = page.url();
  if (url.includes('login')) {
    logger.error('[Cookie] 已过期,请更新');
    return false;
  }
  return true;
}

坑 2:整理频率太高,被平台限制

刚开始我每 10 分钟整理一次,结果第三天就被限制了。搜索页面一直 loading,什么都抓不到。

解决方案

  1. 降低整理频率(每 2 小时一次)
  2. 每个关键词间隔 5 分钟
  3. 使用代理 IP(如果需要高频)
// 间隔 5 分钟
if (i < keywords.length - 1) {
  await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
}

坑 3:页面结构变了,选择器失效

2026 年 11 月,小红书改版,页面结构全变了。我的整理器突然抓不到数据了,内容运营负责人急得不行。

我打开开发者工具一查,发现 class 名全改了。

解决方案

  1. 增加多个选择器备用
  2. 定期测试整理功能
  3. 使用更稳定的选择器(如 data 属性)
// 兼容多种选择器
const selectors = ['.note-item', '[data-type="note"]', '.search-result-item'];
for (const selector of selectors) {
  const elements = await page.$$(selector);
  if (elements.length > 0) {
    // 使用当前选择器
    break;
  }
}

坑 4:数据量太大,数据库查询变慢

跑了三个月后,数据库里有 5 万 + 条记录,查询明显变慢。有次生成日报,等了 10 秒才出来。

解决方案

  1. 定期清理旧数据(保留最近 90 天)
  2. 添加数据库索引
  3. 分表存储(按月份)
// 清理 90 天前的数据
const since = Date.now() - 90 * 24 * 60 * 60 * 1000;
db.prepare('DELETE FROM posts WHERE crawled_at < ?').run(since);

坑 5:服务器在国内,访问慢

刚开始我把服务部署在阿里云香港节点,结果访问小红书/抖音特别慢,经常超时。

解决方案

  1. 使用国内服务器(上海/杭州节点)
  2. 增加请求超时时间
  3. 添加重试机制
await page.goto(url, {
  waitUntil: 'networkidle2',
  timeout: 60000 // 增加到 60 秒
});

读者常问

@新媒体运营小王: "整理 500 条数据,服务器配置要多少?"

答:看整理频率。如果每 2 小时一次,1 核 2G 够了。如果需要更高频,建议 2 核 4G。

这个内容团队监控 7 个关键词,每 2 小时整理一次,用的阿里云 1 核 2G(约 50 元/月),CPU 占用率平均 20%。

@内容创作者小李: "能监控特定账号吗?"

答:可以。需要修改整理器,从搜索改为访问特定账号主页。

有个读者是内容创作者,用这个工具监控 10 个竞品账号,说"他们发什么内容我第一时间知道,再也不怕错过热点了"。

@品牌营销: "能监控品牌提及吗?"

答:可以。把品牌名作为关键词添加即可。

有个读者是品牌方,监控自己品牌和竞品品牌的提及情况,说"用户怎么评价我们,终于有数据了"。

@较真的读者: "这个工具合法吗?会不会被封号?"

答:好问题。我的理解:

  1. 个人使用:低频整理一般没问题
  2. 商业用途:需要谨慎,可能违反平台条款
  3. 频率控制:别太频繁请求,避免给对方服务器造成负担

⚠️ 免责声明:本工具仅供学习研究使用。使用前请阅读平台用户协议,合规使用。

@完美主义者: "数据分析不够详细,希望能有更多维度。"

答:已经在改了。v2.0 会支持:

  1. 情感分析(正负面评价)
  2. 评论区整理
  3. 内容分类标签

@新手小白: "对新手不太友好,希望能有更详细的教程。"

答:这部分后续可以单独补一篇图文教程。包括:

  1. Cookie 获取演示
  2. 工具配置详细步骤
  3. 常见问题排查

@质疑的读者: "真的能省 48 小时吗?感觉有水分。"

答:这个数据是这个内容团队参考统计的。团队之前每天花 1.5 小时整理数据,一个月就是 45 小时。再加上分析、整理的时间,48 小时是保守估计。

你可以自己试一周,记录手动整理花的时间,再跟自动化对比。

@安全专家: "存储的 Cookie 安全吗?"

答:好问题。建议:

  1. 不要把 Cookie 上传到任何公开平台
  2. 使用加密存储
  3. 定期更换 Cookie

扩展思路

1. 情感分析

// 分析评论情感倾向
function analyzeSentiment(comments: string[]) {
  const positiveWords = ['好', '棒', '赞', '喜欢', '有用'];
  const negativeWords = ['差', '烂', '坑', '没用', '失望'];
  
  let positive = 0, negative = 0;
  comments.forEach(c => {
    positiveWords.forEach(w => { if (c.includes(w)) positive++; });
    negativeWords.forEach(w => { if (c.includes(w)) negative++; });
  });
  
  return { positive, negative, ratio: positive / (positive + negative) };
}

2. 竞品监控

// 监控竞品账号更新
async function monitorCompetitorAccounts(accounts: string[]) {
  for (const account of accounts) {
    const posts = await content-analyzer.getAccountPosts(account);
    const newPosts = posts.filter(p => p.crawledAt > lastCheck);
    
    if (newPosts.length > 0) {
      await notifier.sendCompetitorUpdate(account, newPosts);
    }
  }
}

3. 选题建议

// 基于爆款生成选题建议
function generateTopicIdeas(posts: any[]) {
  const keywords = extractKeywords(posts.map(p => p.title));
  const patterns = analyzePatterns(posts);
  
  return {
    hotKeywords: keywords.slice(0, 5),
    suggestedTitles: patterns.map(p => `如何${p}`),
    bestTimeToPost: patterns.peakHours
  };
}

4. 内容评分

// 评估内容质量
function scoreContent(post: any) {
  let score = 0;
  
  // 点赞权重
  score += post.stats.likes * 0.5;
  
  // 标题长度(适中最好)
  if (post.title.length >= 15 && post.title.length <= 30) {
    score += 10;
  }
  
  // 发布时间(黄金时段)
  const hour = new Date(post.crawledAt).getHours();
  if (hour >= 18 && hour <= 22) {
    score += 20;
  }
  
  return score;
}

5. 与 BI 工具集成

  • 数据导出到 Excel/Google Sheets
  • 接入 Tableau/PowerBI
  • 自动生成数据看板

本文小结

内容整理工具适合辅助选题和复盘,不应该替代原创判断。

整理、清洗、指标计算和选题沉淀要分开设计。

平台规则和账号安全要优先考虑,频率、Cookie、数据使用都要克制。

可以继续扩展的方向

  • 增加配置化能力,减少硬编码。
  • 补充运行日志、异常告警和失败重试。
  • 把关键数据沉淀成报表,用于后续复盘。

关注与交流

如果你想看后续更新、完整实践过程或文章配套说明,可以在这些平台找到我:

  • 微信公众号:枫子567
  • CSDN:fzil001
  • 掘金:疯子5
  • 知乎:枫子流

说明:目前我还没有把这些案例统一上传到 GitHub。文章里的代码会尽量保持可复制、可运行;后续如果整理成完整模板,会优先在上面几个账号同步说明。


作者:枫子流