forked from UKSOURCE/ipv6
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import axios from "axios";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
interface Speaker {
|
|
name: string;
|
|
title: string;
|
|
bio: string;
|
|
initials?: string;
|
|
}
|
|
|
|
interface RawSession {
|
|
id: string;
|
|
title: string;
|
|
start: string;
|
|
end: string;
|
|
track: string;
|
|
location: string;
|
|
description: string;
|
|
speaker: Speaker | null;
|
|
}
|
|
|
|
interface EnrichedSession extends RawSession {
|
|
trackLabel: string;
|
|
durationMinutes: number;
|
|
timeICT: string;
|
|
timeUTC: string;
|
|
}
|
|
|
|
interface SummitAgendaData {
|
|
event: {
|
|
title: string;
|
|
theme: string;
|
|
date: string;
|
|
location: string;
|
|
sourceUrl: string;
|
|
crawledAt: string;
|
|
};
|
|
summary: {
|
|
totalSessions: number;
|
|
sessionsWithSpeakers: number;
|
|
tracksBreakdown: Record<string, number>;
|
|
uniqueSpeakers: Array<{ name: string; title: string }>;
|
|
};
|
|
sessions: EnrichedSession[];
|
|
}
|
|
|
|
const TRACK_LABELS: Record<string, string> = {
|
|
ceremony: "Ceremonies",
|
|
keynote: "Keynotes & Address",
|
|
technical: "Technical & Infrastructure",
|
|
panel: "Forums & Panels",
|
|
networking: "Breaks & Networking"
|
|
};
|
|
|
|
function formatTime(dateStr: string, timeZone: string): string {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleTimeString("en-US", {
|
|
timeZone,
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
hour12: true
|
|
}).replace(":", ".").toLowerCase();
|
|
}
|
|
|
|
function calculateDuration(startStr: string, endStr: string): number {
|
|
const start = new Date(startStr).getTime();
|
|
const end = new Date(endStr).getTime();
|
|
return Math.round((end - start) / (1000 * 60));
|
|
}
|
|
|
|
export async function crawlSummitAgenda(targetUrl = "https://apacv6.org/ai-summit-2026/"): Promise<SummitAgendaData> {
|
|
console.log(`[Crawler] Bắt đầu cào dữ liệu từ: ${targetUrl}...`);
|
|
|
|
// 1. Fetch main page HTML
|
|
const baseUrl = targetUrl.endsWith("/") ? targetUrl : targetUrl + "/";
|
|
const htmlResponse = await axios.get(baseUrl, {
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
}
|
|
});
|
|
const html = htmlResponse.data;
|
|
|
|
// 2. Fetch app.js containing the dataset
|
|
const appJsUrl = `${baseUrl}app.js`;
|
|
console.log(`[Crawler] Tải mã nguồn kịch bản dữ liệu từ: ${appJsUrl}...`);
|
|
const appJsResponse = await axios.get(appJsUrl, {
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
}
|
|
});
|
|
const jsContent = appJsResponse.data;
|
|
|
|
// 3. Extract the `sessions` array from app.js
|
|
const sessionsMatch = jsContent.match(/const\s+sessions\s*=\s*(\[\s*\{[\s\S]*?\}\s*\]);/);
|
|
if (!sessionsMatch || !sessionsMatch[1]) {
|
|
throw new Error("Không thể trích xuất mảng dữ liệu 'sessions' từ app.js!");
|
|
}
|
|
|
|
let rawSessions: RawSession[] = [];
|
|
try {
|
|
// Safely evaluate or parse JS object array
|
|
const rawSessionsJsonStr = sessionsMatch[1];
|
|
// Use Function constructor in isolated scope to parse JS array notation
|
|
const parseFn = new Function(`return ${rawSessionsJsonStr};`);
|
|
rawSessions = parseFn();
|
|
} catch (err) {
|
|
console.error("[Crawler] Lỗi khi parse dữ liệu sessions:", err);
|
|
throw err;
|
|
}
|
|
|
|
console.log(`[Crawler] Đã trích xuất thành công ${rawSessions.length} phiên (sessions).`);
|
|
|
|
// 4. Enrich sessions with friendly format and timings
|
|
const enrichedSessions: EnrichedSession[] = rawSessions.map((session) => {
|
|
const duration = calculateDuration(session.start, session.end);
|
|
const startIct = formatTime(session.start, "Asia/Bangkok");
|
|
const endIct = formatTime(session.end, "Asia/Bangkok");
|
|
const startUtc = formatTime(session.start, "UTC");
|
|
const endUtc = formatTime(session.end, "UTC");
|
|
|
|
// Fix known typo on live for Tran Quy
|
|
let speaker = session.speaker;
|
|
if (speaker && (speaker.name.includes("Ho So Chuyen") || session.id === "session-16")) {
|
|
speaker = {
|
|
name: "Mr. Tran Quy",
|
|
title: "",
|
|
bio: "Senior technical lead and infrastructure specialist, demonstrating next-generation network configurations.",
|
|
initials: "TQ"
|
|
};
|
|
}
|
|
|
|
return {
|
|
...session,
|
|
speaker,
|
|
trackLabel: TRACK_LABELS[session.track] || session.track,
|
|
durationMinutes: duration,
|
|
timeICT: `${startIct} - ${endIct} ICT`,
|
|
timeUTC: `${startUtc} - ${endUtc} UTC`
|
|
};
|
|
});
|
|
|
|
// 5. Summary statistics
|
|
const tracksBreakdown: Record<string, number> = {};
|
|
const uniqueSpeakersMap = new Map<string, { name: string; title: string }>();
|
|
|
|
enrichedSessions.forEach((s) => {
|
|
tracksBreakdown[s.track] = (tracksBreakdown[s.track] || 0) + 1;
|
|
if (s.speaker && s.speaker.name && !s.speaker.name.toLowerCase().startsWith("tbd")) {
|
|
uniqueSpeakersMap.set(s.speaker.name, {
|
|
name: s.speaker.name,
|
|
title: s.speaker.title
|
|
});
|
|
}
|
|
});
|
|
|
|
const agendaResult: SummitAgendaData = {
|
|
event: {
|
|
title: "IPv6 Enhanced for AI & Cloud Summit 2026",
|
|
theme: "AI-driven data centres, cloud technologies, Net 5.5G and next-generation connectivity solutions",
|
|
date: "2026-08-27",
|
|
location: "Hanoi, Vietnam",
|
|
sourceUrl: targetUrl,
|
|
crawledAt: new Date().toISOString()
|
|
},
|
|
summary: {
|
|
totalSessions: enrichedSessions.length,
|
|
sessionsWithSpeakers: enrichedSessions.filter((s) => s.speaker !== null).length,
|
|
tracksBreakdown,
|
|
uniqueSpeakers: Array.from(uniqueSpeakersMap.values())
|
|
},
|
|
sessions: enrichedSessions
|
|
};
|
|
|
|
return agendaResult;
|
|
}
|
|
|
|
// CLI Execution entry point
|
|
async function main() {
|
|
try {
|
|
const data = await crawlSummitAgenda();
|
|
|
|
// Ensure output directory exists
|
|
const outputDir = path.join(process.cwd(), "data");
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const outputPath = path.join(outputDir, "summit-2026-agenda.json");
|
|
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), "utf-8");
|
|
|
|
console.log(`\n======================================================`);
|
|
console.log(`🎉 CÀO DỮ LIỆU AGENDA THÀNH CÔNG!`);
|
|
console.log(`📁 File kết quả đã lưu tại: ${outputPath}`);
|
|
console.log(`📊 Tổng số sessions: ${data.summary.totalSessions}`);
|
|
console.log(`🎤 Số diễn giả đã xác định: ${data.summary.uniqueSpeakers.length}`);
|
|
console.log(`======================================================\n`);
|
|
} catch (error) {
|
|
console.error("❌ Lỗi cào dữ liệu:", error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|