UNPKG

@hanivanrizky/nestjs-html-parser

Version:

⚠️ ARCHIVED - This package is no longer maintained. Please migrate to @hanivanrizky/nestjs-xpath-parser (https://github.com/Hanivan/nestjs-xpath-parser)

805 lines (804 loc) 30.8 kB
import { LogLevel } from '@nestjs/common'; import { ExtractionOptions, ExtractionSchema, HtmlFetchResponse, HtmlParserOptions, PaginationPage, ProxyConfig } from './types'; /** * HTML Parser Service for NestJS * * A powerful service for parsing HTML content with support for: * - XPath and CSS selector extraction * - Proxy configuration with authentication * - Random user agent rotation * - Retry logic with configurable delays * - Verbose logging for debugging * - Rich response metadata including headers and status codes * * @example * ```typescript * const parser = new HtmlParserService(); * * // Fetch HTML with options * const response = await parser.fetchHtml('https://example.com', { * timeout: 10000, * useRandomUserAgent: true, * verbose: true * }); * * // Extract data using XPath * const title = parser.extractSingle(response.data, '//title/text()'); * * // Extract structured data * const articles = parser.extractStructuredList(response.data, '//article', { * title: { selector: './/h2/text()', type: 'xpath' }, * link: { selector: './/a', type: 'xpath', attribute: 'href' } * }); * ``` */ export declare class HtmlParserService { private readonly logger; private readonly loggerLevel; /** * Default configuration options for HTML parsing operations */ private defaultOptions; /** * Initialize the HTML Parser Service */ constructor(loggerLevel?: LogLevel | Array<LogLevel>); /** * Helper to check if a log level should be logged * Fixed to properly handle both single and array logger configurations */ private shouldLog; /** * Helper method to log with proper level checking */ private logWithLevel; /** * Suppress console output when verbose is false */ private suppressConsole; /** * Restore console output */ private restoreConsole; /** * Fetch HTML content from a URL with comprehensive configuration options * * Supports proxy configuration, custom headers, user agent rotation, * retry logic, SSL error handling, and rich response metadata. Automatically handles * different proxy types (HTTP, HTTPS, SOCKS4, SOCKS5) and provides * detailed error information on failures. * * @param url - The URL to fetch HTML content from * @param options - Configuration options for the request * @param options.timeout - Request timeout in milliseconds (default: 10000) * @param options.headers - Custom headers to send with the request * @param options.userAgent - Custom user agent string * @param options.useRandomUserAgent - Use a random user agent instead of specified one * @param options.proxy - Proxy configuration for the request * @param options.retries - Number of retry attempts on failure (default: 3) * @param options.retryDelay - Delay between retries in milliseconds (default: 1000) * @param options.verbose - Enable verbose logging for debugging * @param options.rejectUnauthorized - Reject unauthorized SSL certificates (default: true) * @param options.ignoreSSLErrors - Skip SSL certificate verification entirely * @param options.disableServerIdentityCheck - Disable server name indication (SNI) validation * @param options.maxRedirects - Maximum number of redirects to follow (default: 5) * @param options.retryOnErrors - Configure retry behavior for specific error types * * @returns Promise resolving to HtmlFetchResponse with HTML content, headers, and status * * @throws Error when all retry attempts fail * * @example * ```typescript * // Basic usage * const response = await parser.fetchHtml('https://example.com'); * * // Handle SSL errors for sites with invalid certificates * const response = await parser.fetchHtml('https://self-signed-site.com', { * rejectUnauthorized: false, * retryOnErrors: { ssl: true } * }); * * // Ignore SSL completely (use with caution) * const response = await parser.fetchHtml('https://expired-cert-site.com', { * ignoreSSLErrors: true * }); * * // Disable only server identity validation (for hostname mismatches) * const response = await parser.fetchHtml('https://hostname-mismatch-site.com', { * disableServerIdentityCheck: true * }); * * // Robust configuration for unreliable sites * const response = await parser.fetchHtml('https://unreliable-site.com', { * retries: 5, * retryDelay: 2000, * timeout: 15000, * retryOnErrors: { * ssl: true, * timeout: true, * dns: true, * connectionRefused: true * } * }); * ``` */ fetchHtml(url: string, options?: HtmlParserOptions): Promise<HtmlFetchResponse>; /** * Create a proxy agent based on proxy configuration */ private createProxyAgent; /** * Detect proxy type from URL */ private detectProxyType; /** * Delay function for retries */ private delay; /** * Generate a random user agent string * * Returns a realistic user agent string selected randomly from a pool * of current browser user agents. Useful for avoiding detection when * scraping websites that block requests with default user agents. * Falls back to a default user agent if the random generation fails. * * @returns Promise resolving to a random user agent string * * @example * ```typescript * // Get a random user agent * const userAgent = await parser.getRandomUserAgent(); * console.log(userAgent); * // Result: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..." * * // Use with fetchHtml for better stealth * const response = await parser.fetchHtml('https://example.com', { * useRandomUserAgent: true // This uses getRandomUserAgent() internally * }); * * // Or manually * const customUserAgent = await parser.getRandomUserAgent(); * const response = await parser.fetchHtml('https://example.com', { * userAgent: customUserAgent * }); * ``` */ getRandomUserAgent(): Promise<string>; /** * Test proxy connection and authentication * * Validates that a proxy configuration is working by attempting * to fetch a test URL through the proxy. Useful for verifying * proxy credentials and connectivity before using it for actual * HTML parsing operations. * * @param proxy - Proxy configuration to test * @param testUrl - URL to use for testing proxy connection (default: 'https://httpbin.org/ip') * * @returns Promise resolving to true if proxy works, false otherwise * * @example * ```typescript * const proxyConfig = { * url: 'http://proxy.example.com:8080', * username: 'user', * password: 'pass' * }; * * const isWorking = await parser.testProxy(proxyConfig); * if (isWorking) { * console.log('Proxy is working!'); * } else { * console.log('Proxy failed or authentication invalid'); * } * * // Test with custom URL * const isWorking = await parser.testProxy(proxyConfig, 'https://example.com'); * ``` */ testProxy(proxy: ProxyConfig, testUrl?: string): Promise<boolean>; /** * Extract a single value from HTML using XPath or CSS selectors * * Extracts the first matching element's text content or attribute value. * Supports both XPath expressions (recommended) and CSS selectors. * Can extract specific attributes from elements or their text content. * * @param html - HTML content to parse * @param selector - XPath expression or CSS selector to locate the element * @param type - Type of selector: 'xpath' (default) or 'css' * @param attribute - HTML attribute to extract (optional, extracts text content if not specified) * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns The extracted text/attribute value, or null if no match found * * @example * ```typescript * const html = '<div><h1 id="title">Welcome</h1><a href="/home">Home</a></div>'; * * // Extract text content using XPath * const title = parser.extractSingle(html, '//h1[@id="title"]/text()'); * // Result: "Welcome" * * // Extract attribute using XPath * const link = parser.extractSingle(html, '//a', 'xpath', 'href'); * // Result: "/home" * * // Extract using CSS selector * const titleCSS = parser.extractSingle(html, 'h1#title', 'css'); * // Result: "Welcome" * * // With type safety and transformation * const id = parser.extractSingle<number>(html, '//div/@data-id', 'xpath', undefined, { * transform: (value: string) => parseInt(value) * }); * // Result: number | null * * // With verbose logging * const result = parser.extractSingle(html, '//h1/text()', 'xpath', undefined, { verbose: true }); * ``` */ extractSingle<T = string>(html: string, selector: string, type?: 'xpath' | 'css', attribute?: string, options?: ExtractionOptions): T | null; /** * Extract multiple values from HTML using XPath or CSS selectors * * Extracts text content or attribute values from all matching elements. * Returns an array of strings containing all found values. Supports * both XPath expressions and CSS selectors with attribute extraction. * * @param html - HTML content to parse * @param selector - XPath expression or CSS selector to locate elements * @param type - Type of selector: 'xpath' (default) or 'css' * @param attribute - HTML attribute to extract (optional, extracts text content if not specified) * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns Array of extracted text/attribute values (empty array if no matches) * * @example * ```typescript * const html = ` * <ul> * <li><a href="/page1">Page 1</a></li> * <li><a href="/page2">Page 2</a></li> * <li><a href="/page3">Page 3</a></li> * </ul> * `; * * // Extract all link texts using XPath * const linkTexts = parser.extractMultiple(html, '//a/text()'); * // Result: ["Page 1", "Page 2", "Page 3"] * * // Extract all href attributes using XPath * const hrefs = parser.extractMultiple(html, '//a', 'xpath', 'href'); * // Result: ["/page1", "/page2", "/page3"] * * // Extract using CSS selector * const linksCSS = parser.extractMultiple(html, 'li a', 'css'); * // Result: ["Page 1", "Page 2", "Page 3"] * * // With type safety and transformation * const ids = parser.extractMultiple<number>(html, '//li/@data-id', 'xpath', undefined, { * transform: (value: string) => parseInt(value) * }); * // Result: number[] * * // With verbose logging * const results = parser.extractMultiple(html, '//li', 'xpath', undefined, { verbose: true }); * ``` */ extractMultiple<T = string>(html: string, selector: string, type?: 'xpath' | 'css', attribute?: string, options?: ExtractionOptions): T[]; /** * Extract text content from HTML elements * * Convenience method specifically for extracting text content from elements. * This is equivalent to calling extractSingle without an attribute parameter. * Useful when you only need the text content and want clearer intent. * * @param html - HTML content to parse * @param selector - XPath expression or CSS selector to locate the element * @param type - Type of selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns The extracted text content, or null if no match found * * @example * ```typescript * const html = '<div><h1>Main Title</h1><p>Description text</p></div>'; * * // Extract heading text * const title = parser.extractText(html, '//h1'); * // Result: "Main Title" * * // Extract paragraph text using CSS * const description = parser.extractText(html, 'p', 'css'); * // Result: "Description text" * * // With type safety and transformation * const wordCount = parser.extractText<number>(html, '//p', 'xpath', { * transform: (text: string) => text.split(' ').length * }); * // Result: number | null * * // With verbose logging * const text = parser.extractText(html, '//p/text()', 'xpath', { verbose: true }); * ``` */ extractText<T = string>(html: string, selector: string, type?: 'xpath' | 'css', options?: ExtractionOptions): T | null; /** * Extract attribute values from multiple HTML elements * * Convenience method for extracting a specific attribute from all matching elements. * This is equivalent to calling extractMultiple with an attribute parameter. * Returns all attribute values from elements that match the selector. * * @param html - HTML content to parse * @param selector - XPath expression or CSS selector to locate elements * @param attribute - HTML attribute name to extract * @param type - Type of selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns Array of attribute values (empty array if no matches or no attribute) * * @example * ```typescript * const html = ` * <nav> * <a href="/home" title="Home Page">Home</a> * <a href="/about" title="About Us">About</a> * <a href="/contact" title="Contact Form">Contact</a> * </nav> * `; * * // Extract all href attributes * const links = parser.extractAttributes(html, '//a', 'href'); * // Result: ["/home", "/about", "/contact"] * * // Extract all title attributes * const titles = parser.extractAttributes(html, '//a', 'title', 'xpath'); * // Result: ["Home Page", "About Us", "Contact Form"] * * // Using CSS selector * const hrefs = parser.extractAttributes(html, 'nav a', 'href', 'css'); * // Result: ["/home", "/about", "/contact"] * * // With type safety and transformation * const ids = parser.extractAttributes<number>(html, '//img', 'data-id', 'xpath', { * transform: (value: string) => parseInt(value) * }); * // Result: number[] * ``` */ extractAttributes<T = string>(html: string, selector: string, attribute: string, type?: 'xpath' | 'css', options?: ExtractionOptions): T[]; /** * Check if elements exist in HTML content * * Tests whether the specified selector matches any elements in the HTML. * Useful for conditional logic based on element presence or for validating * HTML structure before attempting extractions. * * @param html - HTML content to search * @param selector - XPath expression or CSS selector to test * @param type - Type of selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns true if at least one element matches, false otherwise * * @example * ```typescript * const html = '<div><h1>Title</h1><p class="content">Text</p></div>'; * * // Check if title exists * const hasTitle = parser.exists(html, '//h1'); * // Result: true * * // Check if specific class exists * const hasContent = parser.exists(html, '//p[@class="content"]'); * // Result: true * * // Check for non-existent element * const hasFooter = parser.exists(html, '//footer'); * // Result: false * * // Using CSS selector * const hasContentCSS = parser.exists(html, 'p.content', 'css'); * // Result: true * * // Conditional extraction based on existence * if (parser.exists(html, '//nav')) { * const navigation = parser.extractStructured(html, navSchema); * } * ``` */ exists(html: string, selector: string, type?: 'xpath' | 'css', options?: { verbose?: boolean; }): boolean; /** * Count the number of matching elements in HTML content * * Returns the total number of elements that match the specified selector. * Useful for pagination, validation, or determining the size of data sets * before processing them. * * @param html - HTML content to search * @param selector - XPath expression or CSS selector to count * @param type - Type of selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns Number of matching elements (0 if no matches) * * @example * ```typescript * const html = ` * <ul> * <li>Item 1</li> * <li>Item 2</li> * <li>Item 3</li> * </ul> * <div class="highlight">Special</div> * `; * * // Count list items * const itemCount = parser.count(html, '//li'); * // Result: 3 * * // Count elements with specific class * const highlightCount = parser.count(html, '//div[@class="highlight"]'); * // Result: 1 * * // Count using CSS selector * const listItemsCSS = parser.count(html, 'ul li', 'css'); * // Result: 3 * * // Use count for conditional processing * const articleCount = parser.count(html, '//article'); * if (articleCount > 10) { * console.log('Large dataset detected, processing in batches'); * } * ``` */ count(html: string, selector: string, type?: 'xpath' | 'css', options?: { verbose?: boolean; }): number; /** * Extract structured data from HTML using a schema definition * * Applies a schema object to extract multiple related fields from HTML content. * Each field in the schema defines its own selector, type, optional attribute, * transformation function, and now supports 'multiple' and 'raw' flags for array and raw HTML extraction. * * If a schema field includes `multiple: true`, the extracted value for that field * will be an array of results (with transform applied to each item if provided). * If a schema field includes `raw: true`, the extracted value will be the raw HTML of the matched element(s). * Otherwise, a single value is returned as before. * * @param html - HTML content to parse * @param schema - Schema object defining fields to extract (see ExtractionSchema) * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns Object with extracted data matching the schema structure. Fields with * `multiple: true` will be arrays, `raw: true` will be raw HTML, others will be single values. * * @example * ```typescript * const html = ` * <article> * <h1>Product Name</h1> * <span class="price">$29.99</span> * <img src="/image.jpg" alt="Product"> * <div class="rating" data-stars="4">★★★★☆</div> * <div class="tags"> * <span class="tag">electronics</span> * <span class="tag">gadget</span> * </div> * </article> * `; * * // Define typed interface * interface Product { * title: string; * price: number; * image: string; * rating: number; * tags: string[]; // <-- array field * titleHtml: string; // <-- raw HTML field * } * * const productSchema: ExtractionSchema<Product> = { * title: { * selector: '//h1/text()', * type: 'xpath' * }, * price: { * selector: '//span[@class="price"]/text()', * type: 'xpath', * transform: (price) => parseFloat(price.replace('$', '')) * }, * image: { * selector: '//img', * type: 'xpath', * attribute: 'src' * }, * rating: { * selector: '//div[@class="rating"]', * type: 'xpath', * attribute: 'data-stars', * transform: (stars) => parseInt(stars) * }, * tags: { * selector: '//span[@class="tag"]/text()', * type: 'xpath', * multiple: true // <-- NEW: array extraction * }, * titleHtml: { * selector: '//h1', * type: 'xpath', * raw: true // <-- NEW: raw HTML extraction * } * }; * * const product = parser.extractStructured<Product>(html, productSchema); * // Result: Product type with full type safety * // { * // title: "Product Name", * // price: 29.99, * // image: "/image.jpg", * // rating: 4, * // tags: ["electronics", "gadget"], // <-- array result * // titleHtml: "<h1>Product Name</h1>" // <-- raw HTML result * // } * ``` */ extractStructured<T = Record<string, any>>(html: string, schema: ExtractionSchema<T>, options?: { verbose?: boolean; baseUrl?: string; }): T; /** * Extract structured data directly from a container node (preserves XPath context) * * This method works directly with DOM nodes to preserve the XPath evaluation context, * allowing relative XPath expressions like './td[2]//a/@href' to work correctly. * * @param containerNode - The DOM node to extract data from * @param schema - Schema object defining fields to extract * @param options - Parsing options */ private extractStructuredFromNode; /** * Extract array of structured data from repeating HTML elements * * For each container, applies the schema as in extractStructured. If a schema field * includes `multiple: true`, the extracted value for that field will be an array of results. * If a schema field includes `raw: true`, the extracted value will be the raw HTML of the matched element(s). * * @param html - HTML content to parse * @param containerSelector - XPath or CSS selector to find container elements * @param schema - Schema object defining fields to extract from each container * @param containerType - Type of container selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * * @returns Array of objects with extracted data matching the schema structure. Fields with * `multiple: true` will be arrays, `raw: true` will be raw HTML, others will be single values. * * @example * ```typescript * const html = ` * <div class="products"> * <div class="product"> * <h3>Product A</h3> * <span class="price">$19.99</span> * <span class="tag">electronics</span> * <span class="tag">gadget</span> * </div> * <div class="product"> * <h3>Product B</h3> * <span class="price">$29.99</span> * <span class="tag">accessory</span> * </div> * </div> * `; * * // Define typed interface * interface Product { * name: string; * price: number; * tags: string[]; // <-- array field * nameHtml: string; // <-- raw HTML field * } * * const productSchema: ExtractionSchema<Product> = { * name: { * selector: './/h3/text()', * type: 'xpath' * }, * price: { * selector: './/span[@class="price"]/text()', * type: 'xpath', * transform: (value) => parseFloat(value.replace('$', '')) * }, * tags: { * selector: './/span[@class="tag"]/text()', * type: 'xpath', * multiple: true // <-- NEW: array extraction * }, * nameHtml: { * selector: './/h3', * type: 'xpath', * raw: true // <-- NEW: raw HTML extraction * } * }; * * const products = parser.extractStructuredList<Product>( * html, * '//div[@class="product"]', * productSchema * ); * // Result: Product[] with tags as array and nameHtml as raw HTML for each product * // [ * // { name: "Product A", price: 19.99, tags: ["electronics", "gadget"], nameHtml: "<h3>Product A</h3>" }, * // { name: "Product B", price: 29.99, tags: ["accessory"], nameHtml: "<h3>Product B</h3>" } * // ] * ``` */ extractStructuredList<T = Record<string, any>>(html: string, containerSelector: string, schema: ExtractionSchema<T>, containerType?: 'xpath' | 'css', options?: { verbose?: boolean; baseUrl?: string; }): T[]; /** * Evaluate XPath expression and return matching nodes */ private evaluateXPath; /** * Extract single value from a container node using XPath (preserves context) */ private extractSingleXPathFromNode; /** * Extract multiple values from a container node using XPath (preserves context) */ private extractMultipleXPathFromNode; private getElementHTML; private extractSingleXPath; private extractMultipleXPath; private extractSingleCSS; private extractMultipleCSS; private normalizeHeaders; /** * Categorize error types for better handling and retry logic * * @param error - The error to categorize * @returns Object with error type and description */ private categorizeError; /** * Determine if an error should trigger a retry based on configuration * * @param errorInfo - Categorized error information * @param config - Parser configuration options * @returns Whether to retry the request */ private shouldRetryOnError; /** * Extract pagination data from HTML using a predefined or custom schema * * This method provides a convenient way to extract pagination information. It can use * either a default pagination schema or a custom schema provided by the user. When no * custom schema is provided, it extracts all links from the container selector. * * @param html - HTML content to parse * @param containerSelector - XPath or CSS selector to find the container element(s) * @param containerType - Type of container selector: 'xpath' (default) or 'css' * @param options - Parsing options * @param options.verbose - Enable verbose logging for debugging * @param options.baseUrl - Base URL for relative link resolution * @param options.schema - Optional custom schema for extraction. If not provided, extracts all links from container * @param options.linkSelector - Optional selector to find links within container (default: './/a' for xpath, 'a' for css) * @param options.transform - Optional transformation function or pipe to apply to results (only used when no custom schema) * * @returns Array of pagination objects with href and text properties (or custom schema structure) * * @example * ```typescript * const html = ` * <div class="pageNav"> * <a href="/page-4" class="pageNav-jump">Prev</a> * <ul class="pageNav-main"> * <li><a href="/page-1">1</a></li> * <li><a href="/page-2">2</a></li> * </ul> * <a href="/page-6" class="pageNav-jump">Next</a> * </div> * `; * * // Extract all links from container * const allLinks = parser.extractPagination(html, '//div[@class="pageNav"]'); * // Result: [ * // { href: "/page-4", text: "Prev" }, * // { href: "/page-1", text: "1" }, * // { href: "/page-2", text: "2" }, * // { href: "/page-6", text: "Next" } * // ] * * // With transform pipe * const transformedPages = parser.extractPagination( * html, * '//div[@class="pageNav"]', * 'xpath', * { * baseUrl: 'https://example.com', * transform: { class: PathAppendPipe, payload: { pathSegment: '/view' } } * } * ); * // Result: URLs will have '/view' appended to the path * * // Extract specific pagination items only * const pages = parser.extractPagination( * html, * "//li", * 'xpath' * ); * // Result: [ * // { href: "/page-1", text: "1" }, * // { href: "/page-2", text: "2" } * // ] * * // With custom schema * interface CustomPage { * url: string; * label: string; * isActive: boolean; * } * * const customPages = parser.extractPagination<CustomPage>( * html, * "//li", * 'xpath', * { * schema: { * url: { selector: './/a', type: 'xpath', attribute: 'href' }, * label: { selector: './/a/text()', type: 'xpath' }, * isActive: { * selector: '.', * type: 'xpath', * attribute: 'class', * transform: (cls: string) => cls?.includes('current') || false * } * } * } * ); * ``` */ extractPagination<T = PaginationPage>(html: string, containerSelector: string, containerType?: 'xpath' | 'css', options?: { verbose?: boolean; baseUrl?: string; schema?: ExtractionSchema<T>; linkSelector?: string; transform?: any; }): T[]; /** * Extract the origin (protocol + hostname + port) from a URL * * @param url - Full URL string * @returns Origin string (e.g., "https://example.com") * * @example * ```typescript * const origin = parser.getOrigin('https://www.bmw-sg.com/forums/forums/introduction-greetings.24/page-5'); * // Result: "https://www.bmw-sg.com" * ``` */ getOrigin(url: string): string; /** * Apply a transform (object with class and payload, or array of these) to a value or array of values. * Supports pipe classes with transform method that need baseUrl context and properly chains transform outputs as inputs to next transform. */ private applyTransform; }