RED(小紅書)トレンド投稿データの収集と分析
プロジェクト概要
このチュートリアルでは、REDのデータ収集・分析ツールの開発方法を説明します。このツールには以下の主要な機能が含まれています:
- トレンド投稿データの自動収集
- トレンドコンテンツの特徴分析
- データのエクスポートと可視化
- 分析レポートの自動生成
技術スタック
効率的で安定したデータ収集と分析を実現するため、以下の技術スタックを採用します:
バックエンド技術:
- Node.js 実行環境
- TypeScript 型システム
- Puppeteer 自動化
- Express APIサービス
- MongoDB データストレージ
フロントエンド技術:
- React コンポーネント開発
- Chart.js データ可視化
開発フロー
1. 環境構築
まず、プロジェクトを作成し、必要な依存関係をインストールします:
bash
mkdir redbook-scraper
cd redbook-scraper
npm init -y
npm install typescript ts-node @types/node puppeteer express mongodb
npm install -D nodemon @types/express
2. ディレクトリ構造
モジュール化されたディレクトリ構造でコードを整理します:
redbook-scraper/
├── src/
│ ├── config/ # 設定ファイル
│ ├── models/ # データモデル
│ ├── services/ # コアサービス
│ ├── utils/ # ユーティリティ関数
│ └── index.ts # エントリーポイント
├── data/ # データストレージ
├── logs/ # ログファイル
└── tsconfig.json # TS設定
3. システム設定
typescript
// src/config/config.ts
export const config = {
mongodb: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017/redbook',
dbName: 'redbook'
},
scraper: {
headless: true,
timeout: 30000,
userAgent: 'Mozilla/5.0 ...'
},
api: {
port: process.env.PORT || 3000
}
}
4. データモデリング
データモデルは、投稿、ユーザー、統計など複数のモデルを含むモジュール化された設計を採用します:
typescript
// src/models/Note.ts
import { ObjectId } from 'mongodb'
import { z } from 'zod' // ランタイム型検証にZodを使用
// 投稿モデル検証スキーマ
const NoteSchema = z.object({
_id: z.instanceof(ObjectId).optional(),
noteId: z.string().min(1),
title: z.string().min(1).max(100),
content: z.string().min(1),
images: z.array(z.string().url()),
likes: z.number().int().min(0),
comments: z.number().int().min(0),
shares: z.number().int().min(0),
author: z.object({
id: z.string(),
name: z.string(),
followers: z.number().int().min(0)
}),
tags: z.array(z.string()),
createdAt: z.date(),
scrapedAt: z.date(),
// 追加フィールド
location: z.string().optional(),
device: z.string().optional(),
topics: z.array(z.string()).optional(),
mentions: z.array(z.string()).optional(),
products: z.array(z.object({
id: z.string(),
name: z.string(),
price: z.number().optional()
})).optional()
})
// 型のエクスポート
export type Note = z.infer<typeof NoteSchema>
// データベースインデックス設計
export const NoteIndexes = [
{ key: { noteId: 1 }, unique: true },
{ key: { createdAt: -1 } },
{ key: { 'author.id': 1 } },
{ key: { tags: 1 } },
{ key: { likes: -1 } }
] as const
// ユーザーモデル
export interface User {
_id?: ObjectId
userId: string
nickname: string
avatar: string
followers: number
following: number
notes: number
verified: boolean
verifiedType?: string
description?: string
location?: string
createdAt: Date
updatedAt: Date
}
// 統計モデル
export interface Stats {
_id?: ObjectId
date: Date
totalNotes: number
totalUsers: number
avgLikes: number
avgComments: number
avgShares: number
topTags: Array<{
tag: string
count: number
}>
topAuthors: Array<{
userId: string
noteCount: number
totalEngagement: number
}>
updatedAt: Date
}
5. データ収集サービス
収集サービスには複数の高度な機能が実装されています:
typescript
import type { Note } from '../../models/Note'
// src/services/scraper/index.ts
import puppeteer from 'puppeteer'
import { config } from '../../config/config'
import { Logger } from '../../utils/logger'
import { retry } from '../../utils/retry'
import { sanitize } from '../../utils/sanitizer'
import { validate } from '../../utils/validator'
import { ProxyManager } from '../proxy'
import { RateLimiter } from '../ratelimit'
import { UserAgentManager } from '../useragent'
export class Scraper {
private browser: puppeteer.Browser | null = null
private proxyManager: ProxyManager
private uaManager: UserAgentManager
private rateLimiter: RateLimiter
private logger: Logger
constructor() {
this.proxyManager = new ProxyManager()
this.uaManager = new UserAgentManager()
this.rateLimiter = new RateLimiter({
maxRequests: 100,
perMinute: 1
})
this.logger = new Logger('scraper')
}
async initialize() {
const proxy = await this.proxyManager.getProxy()
this.browser = await puppeteer.launch({
headless: config.scraper.headless,
args: [
'--no-sandbox',
`--proxy-server=${proxy.host}:${proxy.port}`,
'--disable-dev-shm-usage',
'--disable-gpu'
],
defaultViewport: {
width: 1920,
height: 1080
}
})
}
async scrapeNote(url: string): Promise<Note> {
// レート制限
await this.rateLimiter.acquire()
if (!this.browser)
throw new Error('Browser not initialized')
const page = await this.browser.newPage()
try {
// リクエストインターセプトの設定
await page.setRequestInterception(true)
page.on('request', (request) => {
if (request.resourceType() === 'image') {
request.abort()
}
else {
request.continue()
}
})
// User-Agentの設定
await page.setUserAgent(this.uaManager.getRandomUA())
// タイムアウトの設定
await page.setDefaultNavigationTimeout(config.scraper.timeout)
// ページアクセス
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: config.scraper.timeout
})
// コンテンツの読み込み待機
await page.waitForSelector('.note-content', {
timeout: config.scraper.timeout
})
// データの抽出
const noteData = await page.evaluate(() => {
const title = document.querySelector('.title')?.textContent
const content = document.querySelector('.content')?.textContent
const likes = document.querySelector('.likes')?.textContent
const comments = document.querySelector('.comments')?.textContent
const shares = document.querySelector('.shares')?.textContent
const author = {
id: document.querySelector('.author-id')?.getAttribute('data-id'),
name: document.querySelector('.author-name')?.textContent,
followers: document.querySelector('.followers')?.textContent
}
const tags = Array.from(
document.querySelectorAll('.tag')
).map(tag => tag.textContent)
const images = Array.from(
document.querySelectorAll('.note-image')
).map(img => img.getAttribute('src'))
return {
title,
content,
likes: Number.parseInt(likes || '0'),
comments: Number.parseInt(comments || '0'),
shares: Number.parseInt(shares || '0'),
author: {
id: author.id || '',
name: author.name || '',
followers: Number.parseInt(author.followers || '0')
},
tags: tags.filter(Boolean) as string[],
images: images.filter(Boolean) as string[]
}
})
// データのクリーニング
const cleanData = sanitize(noteData)
// データの検証
const validData = validate(cleanData)
return {
noteId: url.split('/').pop() || '',
...validData,
createdAt: new Date(),
scrapedAt: new Date()
}
}
catch (error) {
this.logger.error('Scrape failed:', error)
throw error
}
finally {
await page.close()
}
}
// バッチ収集
async scrapeNotes(urls: string[]): Promise<Note[]> {
const concurrency = config.scraper.maxConcurrency
const results: Note[] = []
// 並行制御
for (let i = 0; i < urls.length; i += concurrency) {
const batch = urls.slice(i, i + concurrency)
const promises = batch.map(url =>
retry(() => this.scrapeNote(url), {
retries: 3,
minTimeout: 1000,
maxTimeout: 5000
})
)
const batchResults = await Promise.allSettled(promises)
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.push(result.value)
}
else {
this.logger.error(
`Failed to scrape ${batch[index]}:`,
result.reason
)
}
})
// バッチ間のディレイ
await new Promise(resolve =>
setTimeout(resolve, Math.random() * 2000 + 1000)
)
}
return results
}
async close() {
if (this.browser) {
await this.browser.close()
this.browser = null
}
}
}
6. データ分析エンジン
データ分析エンジンは多次元のコンテンツ分析機能を提供します:
typescript
// src/services/analyzer/index.ts
import { Note } from '../../models/Note'
import { Logger } from '../../utils/logger'
import { ImageAnalyzer } from '../image'
import { NLPService } from '../nlp'
import { StatisticsService } from '../statistics'
export class Analyzer {
private nlp: NLPService
private imageAnalyzer: ImageAnalyzer
private statsService: StatisticsService
private logger: Logger
constructor() {
this.nlp = new NLPService()
this.imageAnalyzer = new ImageAnalyzer()
this.statsService = new StatisticsService()
this.logger = new Logger('analyzer')
}
// エンゲージメント分析
async analyzeEngagement(note: Note) {
try {
const totalEngagement = note.likes + note.comments + note.shares
const engagement = {
total: totalEngagement,
likeRatio: note.likes / totalEngagement,
commentRatio: note.comments / totalEngagement,
shareRatio: note.shares / totalEngagement,
// エンゲージメント率の計算
engagementRate: totalEngagement / note.author.followers,
// 時間軸
hourOfDay: new Date(note.createdAt).getHours(),
dayOfWeek: new Date(note.createdAt).getDay(),
// 分類
isViral: totalEngagement > 10000,
hasHighEngagement: totalEngagement > 5000,
// エンゲージメント品質
qualityScore: this.calculateQualityScore(note)
}
// 履歴データとの比較
const historical = await this.statsService.getHistoricalStats(note.author.id)
return {
...engagement,
// 相対パフォーマンス
performanceVsAvg: totalEngagement / historical.avgEngagement,
trend: this.analyzeTrend(historical.engagements)
}
}
catch (error) {
this.logger.error('Engagement analysis failed:', error)
throw error
}
}
// コンテンツ分析
async analyzeContent(note: Note) {
try {
// テキスト分析
const textAnalysis = await this.nlp.analyze(note.content)
// 画像分析
const imageAnalysis = await Promise.all(
note.images.map(img => this.imageAnalyzer.analyze(img))
)
return {
text: {
length: note.content.length,
sentiment: textAnalysis.sentiment,
keywords: textAnalysis.keywords,
topics: textAnalysis.topics,
readability: textAnalysis.readability,
emoticons: textAnalysis.emoticons,
language: textAnalysis.language
},
images: {
count: note.images.length,
types: imageAnalysis.map(img => img.type),
colors: imageAnalysis.map(img => img.dominantColors),
objects: imageAnalysis.map(img => img.detectedObjects),
quality: imageAnalysis.map(img => img.qualityScore)
},
tags: {
count: note.tags.length,
categories: this.categorizeTags(note.tags),
popularity: await this.statsService.getTagsPopularity(note.tags)
},
products: note.products?.map(product => ({
...product,
category: this.categorizeProduct(product),
priceRange: this.getPriceRange(product.price)
}))
}
}
catch (error) {
this.logger.error('Content analysis failed:', error)
throw error
}
}
// ユーザープロファイル分析
async analyzeAuthor(note: Note) {
try {
const authorStats = await this.statsService.getAuthorStats(note.author.id)
return {
profile: {
followers: note.author.followers,
followersGrowth: authorStats.followersGrowth,
engagementRate: authorStats.avgEngagement / note.author.followers,
postFrequency: authorStats.postFrequency,
activeTime: authorStats.activeTimeDistribution
},
content: {
topCategories: authorStats.topCategories,
commonTags: authorStats.commonTags,
writingStyle: await this.nlp.analyzeStyle(authorStats.recentPosts),
imageStyle: await this.imageAnalyzer.analyzeStyle(authorStats.recentImages)
},
influence: {
score: this.calculateInfluenceScore(authorStats),
reach: this.calculateReach(authorStats),
niche: this.identifyNiche(authorStats)
}
}
}
catch (error) {
this.logger.error('Author analysis failed:', error)
throw error
}
}
private calculateQualityScore(note: Note): number {
// 品質スコア計算ロジックの実装
return 0
}
private analyzeTrend(engagements: number[]): string {
// トレンド分析ロジックの実装
return 'up'
}
private categorizeTags(tags: string[]): Record<string, string[]> {
// タグ分類ロジックの実装
return {}
}
private categorizeProduct(product: any): string {
// 商品分類ロジックの実装
return ''
}
private getPriceRange(price?: number): string {
// 価格帯分類ロジックの実装
return ''
}
private calculateInfluenceScore(stats: any): number {
// 影響力スコア計算ロジックの実装
return 0
}
private calculateReach(stats: any): number {
// リーチ計算ロジックの実装
return 0
}
private identifyNiche(stats: any): string {
// ニッチ特定ロジックの実装
return ''
}
}
7. モニタリングシステム
システム全体のモニタリングを実装:
typescript
// src/services/monitor/index.ts
import { EventEmitter } from 'node:events'
import { Logger } from '../../utils/logger'
import { AlertManager } from './alert'
import { Metrics } from './metrics'
export class MonitoringSystem extends EventEmitter {
private metrics: Metrics
private alertManager: AlertManager
private logger: Logger
constructor() {
super()
this.metrics = new Metrics()
this.alertManager = new AlertManager()
this.logger = new Logger('monitor')
this.setupEventHandlers()
}
private setupEventHandlers() {
// パフォーマンスモニタリング
this.on('request', this.handleRequest.bind(this))
this.on('error', this.handleError.bind(this))
this.on('scrapeComplete', this.handleScrapeComplete.bind(this))
// リソースモニタリング
this.on('cpuUsage', this.handleCPUUsage.bind(this))
this.on('memoryUsage', this.handleMemoryUsage.bind(this))
this.on('diskUsage', this.handleDiskUsage.bind(this))
// ビジネスモニタリング
this.on('dataQuality', this.handleDataQuality.bind(this))
this.on('scrapeFail', this.handleScrapeFail.bind(this))
this.on('proxyFail', this.handleProxyFail.bind(this))
}
// リクエストイベントの処理
private async handleRequest(data: any) {
try {
await this.metrics.recordRequest(data)
if (data.duration > 5000) {
await this.alertManager.sendAlert({
level: 'warning',
title: 'リクエスト応答時間が長すぎます',
message: `リクエスト ${data.url} の応答時間は ${data.duration}ms です`
})
}
}
catch (error) {
this.logger.error('Failed to handle request event:', error)
}
}
// エラーイベントの処理
private async handleError(error: any) {
try {
await this.metrics.recordError(error)
await this.alertManager.sendAlert({
level: 'error',
title: 'システムエラー',
message: error.message,
stack: error.stack
})
}
catch (err) {
this.logger.error('Failed to handle error event:', err)
}
}
// スクレイピング完了イベントの処理
private async handleScrapeComplete(data: any) {
try {
await this.metrics.recordScrape(data)
if (data.failureRate > 0.1) {
await this.alertManager.sendAlert({
level: 'warning',
title: 'スクレイピング失敗率が高すぎます',
message: `現在の失敗率は ${data.failureRate} です`
})
}
}
catch (error) {
this.logger.error('Failed to handle scrape complete event:', error)
}
}
// ... その他のイベントハンドラー
}
使用方法
1. サービスの起動
bash
npm run dev
2. APIの呼び出し
bash
curl -X POST http://localhost:3000/api/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://www.xiaohongshu.com/note/..."}'
システム最適化
1. アンチスクレイピング対策
データ収集の安定性を確保するため、以下の最適化を実装:
- 動的遅延制御
- User Agent ローテーション
- IPプロキシプール
- キャプチャの自動処理
typescript
// インテリジェント遅延制御
async function randomDelay() {
const delay = Math.floor(Math.random() * 2000) + 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
2. データバックアップ
自動データバックアップメカニズムの実装:
typescript
import { readFileSync, writeFileSync } from 'node:fs'
// src/utils/backup.ts
import { MongoClient } from 'mongodb'
export async function backupData() {
const client = await MongoClient.connect(config.mongodb.url)
const db = client.db(config.mongodb.dbName)
const notes = await db.collection('notes').find().toArray()
writeFileSync(
`./data/backup-${Date.now()}.json`,
JSON.stringify(notes, null, 2)
)
await client.close()
}
3. エラー処理メカニズム
インテリジェントなリトライ戦略の実装:
typescript
async function withRetry<T>(
fn: () => Promise<T>,
retries = 3
): Promise<T> {
try {
return await fn()
}
catch (error) {
if (retries > 0) {
await randomDelay()
return withRetry(fn, retries - 1)
}
throw error
}
}
高度な機能
1. データ可視化
ReactとChart.jsを使用した分析ダッシュボードの構築:
typescript
// frontend/src/components/Dashboard.tsx
import { Line } from 'react-chartjs-2'
export function Dashboard({ data }) {
const chartData = {
labels: data.map(d => d.date),
datasets: [{
label: 'エンゲージメントトレンド',
data: data.map(d => d.engagement.total)
}]
}
return (
<div className="dashboard">
<Line data={chartData} />
</div>
)
}
2. 定期タスク
自動収集タスクの統合:
typescript
import cron from 'node-cron'
// 毎日午前2時に実行
cron.schedule('0 2 * * *', async () => {
try {
await scraper.initialize()
// 収集タスクの実行
await scraper.close()
}
catch (error) {
console.error('Cron job failed:', error)
}
})
よくある問題と解決方法
1. 収集の異常
- ネットワーク接続の確認
- URL形式の検証
- アンチスクレイピング対策
2. データの完全性
- 読み込みタイムアウトの最適化
- セレクターのメンテナンス
- ページ構造の検証
3. パフォーマンスチューニング
- データベース接続プール
- キャッシュ戦略
- クエリ最適化
ベストプラクティス
- プラットフォームのルールを遵守
- 適切な頻度制御
- 定期的なメンテナンス
重要な注意事項
- データセキュリティの保護
- コンプライアンス要件
- リスク管理戦略