@hanivanrizky/nestjs-html-parser
Version:
A powerful NestJS HTML parsing service with XPath and CSS selector support, proxy configuration, random user agents, and rich response metadata including headers and status codes
314 lines ⢠15.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.demonstrateOtakudesuParser = demonstrateOtakudesuParser;
const html_parser_service_1 = require("../html-parser.service");
async function demonstrateOtakudesuParser(verbose = false) {
const parser = new html_parser_service_1.HtmlParserService();
console.log('š Otakudesu.cloud - Indonesian Anime Parser Demo');
console.log('='.repeat(60));
try {
// Fetch the main page with random user agent for better success rate
console.log('š” Fetching Otakudesu homepage...');
const response = await parser.fetchHtml('https://otakudesu.cloud/', {
useRandomUserAgent: true,
timeout: 15000,
verbose,
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'id-ID,id;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
},
});
console.log(`ā
Successfully fetched page (${response.status} ${response.statusText})`);
console.log(`š Page size: ${response.data.length} characters`);
console.log(`š Content-Type: ${response.headers['content-type']}`);
console.log();
// Extract ongoing anime information using the exact structure from the website
console.log('š Extracting ongoing anime information...');
// Schema for extracting individual anime entries from li elements
const ongoingAnimeSchema = {
title: {
selector: './/h2[@class="jdlflm"]/text()',
type: 'xpath',
transform: (title) => title.trim(),
},
link: {
selector: './/a[@href]',
type: 'xpath',
attribute: 'href',
},
episode: {
selector: './/div[@class="epz"]/text()',
type: 'xpath',
transform: (text) => {
const match = text.match(/Episode\s+(\d+)/);
return match ? parseInt(match[1]) : 0;
},
},
releaseDay: {
selector: './/div[@class="epztipe"]/text()',
type: 'xpath',
transform: (text) => text.trim(),
},
releaseDate: {
selector: './/div[@class="newnime"]/text()',
type: 'xpath',
transform: (text) => text.trim(),
},
imageUrl: {
selector: './/img',
type: 'xpath',
attribute: 'src',
},
imageAlt: {
selector: './/img',
type: 'xpath',
attribute: 'alt',
},
};
// Extract ongoing anime using the specific container structure
const ongoingAnimeItems = parser.extractStructuredList(response.data, '//div[@class="venz"]//ul//li', ongoingAnimeSchema, 'xpath', { verbose });
console.log(`šÆ Found ${ongoingAnimeItems.length} anime items in the ongoing section`);
// Process and clean the extracted data
const processedAnime = ongoingAnimeItems
.filter((item) => item.title && item.link && item.episode)
.map((item) => {
const cleanTitle = item.title.replace(/\s*Sub Indo\s*$/i, '').trim();
const hasSubIndo = item.title.toLowerCase().includes('sub indo') ||
(item.imageAlt && item.imageAlt.toLowerCase().includes('sub indo'));
return {
title: item.title,
cleanTitle: cleanTitle,
episode: item.episode || 0,
releaseDay: item.releaseDay || 'Unknown',
releaseDate: item.releaseDate || 'Unknown',
link: item.link,
imageUrl: item.imageUrl || '',
hasSubIndo: hasSubIndo,
status: 'ongoing',
};
});
// Display results
console.log('š ONGOING ANIME EXTRACTION RESULTS');
console.log('='.repeat(50));
if (processedAnime.length > 0) {
console.log(`ā
Successfully extracted ${processedAnime.length} ongoing anime`);
console.log();
// Display first 10 anime with details
console.log('šÆ Latest Ongoing Anime (Top 10):');
console.log('-'.repeat(40));
processedAnime.slice(0, 10).forEach((anime, index) => {
console.log(`${index + 1}. ${anime.cleanTitle}`);
console.log(` šŗ Episode: ${anime.episode}`);
console.log(` š
Release: ${anime.releaseDay}, ${anime.releaseDate}`);
console.log(` š Link: ${anime.link}`);
console.log(` š¼ļø Image: ${anime.imageUrl}`);
console.log(` š Sub Indonesia: ${anime.hasSubIndo ? 'ā
' : 'ā'}`);
console.log();
});
// Generate statistics
const stats = {
totalOngoingAnime: processedAnime.length,
latestEpisodes: processedAnime.slice(0, 5),
releaseSchedule: {},
popularTitles: processedAnime
.filter((anime) => anime.episode > 5)
.map((anime) => anime.cleanTitle)
.slice(0, 5),
averageEpisode: Math.round(processedAnime.reduce((sum, anime) => sum + anime.episode, 0) /
processedAnime.length),
};
// Count release schedule by day
processedAnime.forEach((anime) => {
const day = anime.releaseDay;
if (day && day !== 'Unknown') {
stats.releaseSchedule[day] = (stats.releaseSchedule[day] || 0) + 1;
}
});
console.log('š OTAKUDESU STATISTICS');
console.log('='.repeat(40));
console.log(`š Total Ongoing Anime: ${stats.totalOngoingAnime}`);
console.log(`š Average Episode: ${stats.averageEpisode}`);
console.log();
console.log('š
Release Schedule:');
Object.entries(stats.releaseSchedule)
.sort(([, a], [, b]) => b - a)
.forEach(([day, count]) => {
console.log(` ${day}: ${count} anime`);
});
console.log();
console.log('š„ Popular Ongoing Titles (Episode > 5):');
stats.popularTitles.forEach((title, index) => {
console.log(` ${index + 1}. ${title}`);
});
console.log();
// Show anime by release day distribution
console.log('š Anime Distribution by Release Day:');
const dayOrder = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu',
];
dayOrder.forEach((day) => {
const count = stats.releaseSchedule[day] || 0;
if (count > 0) {
const animeList = processedAnime
.filter((anime) => anime.releaseDay === day)
.map((anime) => anime.cleanTitle)
.slice(0, 3);
console.log(` ${day}: ${count} anime - ${animeList.join(', ')}${animeList.length < count ? '...' : ''}`);
}
});
// Show highest episode numbers
console.log('\nš Anime with Highest Episodes:');
const sortedByEpisode = processedAnime
.sort((a, b) => b.episode - a.episode)
.slice(0, 5);
sortedByEpisode.forEach((anime, index) => {
console.log(` ${index + 1}. ${anime.cleanTitle} - Episode ${anime.episode}`);
});
}
else {
console.log('ā ļø No ongoing anime found. Possible reasons:');
console.log(' - Website structure has changed');
console.log(' - Content is loaded dynamically');
console.log(' - Network or access issues');
console.log();
// Debug information - check for the container elements
const venzContainer = parser.exists(response.data, '//div[@class="venz"]', 'xpath', { verbose });
const ulElements = parser.count(response.data, '//div[@class="venz"]//ul', 'xpath', { verbose });
const liElements = parser.count(response.data, '//div[@class="venz"]//ul//li', 'xpath', { verbose });
const detpostElements = parser.count(response.data, '//div[@class="detpost"]', 'xpath', { verbose });
console.log('š§ DEBUG INFORMATION');
console.log(` Venz container found: ${venzContainer ? 'ā
' : 'ā'}`);
console.log(` UL elements found: ${ulElements}`);
console.log(` LI elements found: ${liElements}`);
console.log(` Detpost elements found: ${detpostElements}`);
// Try to find any anime-related content
const anyAnimeLinks = parser.extractMultiple(response.data, '//a[contains(@href, "anime")]', 'xpath', 'href', { verbose });
const anyAnimeTitles = parser.extractMultiple(response.data, '//h2[@class="jdlflm"]/text()', 'xpath', undefined, { verbose });
console.log(` Any anime links found: ${anyAnimeLinks.length}`);
console.log(` Any anime titles found: ${anyAnimeTitles.length}`);
if (anyAnimeTitles.length > 0) {
console.log('\n Sample titles found:');
anyAnimeTitles.slice(0, 3).forEach((title, i) => {
console.log(` ${i + 1}. ${title}`);
});
}
}
// === Advanced Transform Example ===
// --- Example class definitions ---
class UppercasePipe {
transform(value) {
return value.toUpperCase();
}
}
class SuffixPipe {
suffix;
constructor(suffix) {
this.suffix = suffix;
}
transform(value) {
return value + this.suffix;
}
}
// --- Example usage: with and without 'new' keyword ---
// Type assertion is used here to allow class constructors in the transform array for demonstration.
const advancedSchema = {
title: {
selector: './/h2[@class="jdlflm"]/text()',
type: 'xpath',
transform: [
(title) => title.trim(),
UppercasePipe, // <-- class, will be instantiated automatically
new SuffixPipe(' [ADVANCED]'), // <-- instance, used as is
],
},
episode: {
selector: './/div[@class="epz"]',
type: 'xpath',
transform: [
(text) => {
if (typeof text !== 'string')
return 0;
let match = text.match(/Episode\s+(\d+)/i);
if (!match)
match = text.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
},
new SuffixPipe(' (ep)'), // <-- instance, used as is
],
},
};
const advancedAnime = parser.extractStructuredList(response.data, '//div[@class="venz"]//ul//li', advancedSchema, 'xpath', { verbose });
if (advancedAnime.length > 0) {
console.log('\n=== ADVANCED TRANSFORM EXAMPLE ===');
advancedAnime.slice(0, 3).forEach((anime, i) => {
console.log(`${i + 1}. ${anime.title} | Episode: ${anime.episode}`);
});
}
// Demonstrate advanced parsing techniques
console.log('\nš¬ ADVANCED PARSING TECHNIQUES');
console.log('='.repeat(50));
// Check for specific anime genres or types in titles
const actionAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('action') ||
anime.cleanTitle.toLowerCase().includes('battle') ||
anime.cleanTitle.toLowerCase().includes('hero')).length;
const schoolAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('school') ||
anime.cleanTitle.toLowerCase().includes('academia')).length;
const fantasyAnime = processedAnime.filter((anime) => anime.cleanTitle.toLowerCase().includes('magic') ||
anime.cleanTitle.toLowerCase().includes('witch') ||
anime.cleanTitle.toLowerCase().includes('fantasy')).length;
console.log('š Anime Categories Detection:');
console.log(` Action/Hero: ${actionAnime} anime`);
console.log(` School/Academia: ${schoolAnime} anime`);
console.log(` Magic/Fantasy: ${fantasyAnime} anime`);
// Extract page metadata
const pageTitle = parser.extractSingle(response.data, '//title/text()', 'xpath', undefined, { verbose });
const metaDescription = parser.extractSingle(response.data, '//meta[@name="description"]', 'xpath', 'content', { verbose });
console.log('\nš Page Metadata:');
console.log(` Title: ${pageTitle || 'Not found'}`);
console.log(` Description: ${metaDescription || 'Not found'}`);
// Check for responsive design elements
const hasViewport = parser.exists(response.data, '//meta[@name="viewport"]', 'xpath', { verbose });
const hasBootstrap = parser.exists(response.data, '//*[contains(@href, "bootstrap") or contains(@src, "bootstrap")]', 'xpath', { verbose });
console.log('\nš± Technical Features:');
console.log(` Responsive Design: ${hasViewport ? 'ā
' : 'ā'}`);
console.log(` Bootstrap Framework: ${hasBootstrap ? 'ā
' : 'ā'}`);
// Additional technical analysis
const hasJQuery = parser.exists(response.data, '//*[contains(@src, "jquery")]', 'xpath', { verbose });
const totalImages = parser.count(response.data, '//img', 'xpath', {
verbose,
});
const lazyLoadImages = parser.count(response.data, '//img[@loading="lazy"]', 'xpath', { verbose });
console.log(` jQuery: ${hasJQuery ? 'ā
' : 'ā'}`);
console.log(` Total Images: ${totalImages}`);
console.log(` Lazy Loading Images: ${lazyLoadImages}`);
}
catch (error) {
console.error('ā Error parsing Otakudesu:');
console.error(` ${error instanceof Error ? error.message : 'Unknown error'}`);
console.log('\nš” Troubleshooting tips:');
console.log(' - Check if website is accessible');
console.log(' - Try using different user agents');
console.log(' - Website might have anti-scraping measures');
console.log(' - Consider using proxy if blocked');
console.log(' - Check if the HTML structure has changed');
console.log(' - Use verbose mode for detailed error output: demonstrateOtakudesuParser(true)');
}
}
// Run the demonstration if this file is executed directly
if (require.main === module) {
// Check for verbose flag in command line arguments
const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
demonstrateOtakudesuParser(verbose)
.then(() => console.log('\nš Otakudesu parsing demo completed!'))
.catch(console.error);
}
//# sourceMappingURL=otakudesu.cloud.js.map