Files
projects/Dockers/puppeteer-api/app/services/browser.py
T
Bram e2cfcfad34
Build and Push Docker Images / build-and-push (push) Successful in 5m25s
lightweight meta tags
2025-06-05 14:05:38 +02:00

569 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from app.utils.browser_utils import safe_browser_operation
from app.utils.http_utils import fetch_url
import asyncio
from bs4 import BeautifulSoup
async def visit_url_service(decoded_url):
"""Service function to visit a URL and get its content"""
print(f"Visiting URL: {decoded_url}")
# Define the operation to perform with the browser
async def visit_operation(page):
try:
response = await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000)
if not response:
print(f"Warning: No response object returned for {decoded_url}")
# Get page content
content = await page.content()
return {"status": "success", "content": content}
except Exception as e:
print(f"Error during page navigation: {e}")
# Try to get content anyway
try:
content = await page.content()
return {"status": "partial", "content": content, "error": str(e)}
except:
raise Exception(f"Failed to get page content: {str(e)}")
# Perform the operation
return await safe_browser_operation(decoded_url, visit_operation)
async def extract_seo_service(decoded_url):
"""Service function to extract SEO information from a website"""
print(f"Extracting SEO from: {decoded_url}")
# Define the operation to perform with the browser
async def seo_operation(page):
try:
response = await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000)
# Extract SEO information
seo_data = await page.evaluate('''() => {
const data = {
title: document.title || '',
description: '',
canonical: '',
h1: [],
h2: [],
images: 0,
links: 0
};
// Get meta description
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
data.description = metaDescription.getAttribute('content') || '';
}
// Get canonical link
const canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
data.canonical = canonicalLink.getAttribute('href') || '';
}
// Get h1 tags
document.querySelectorAll('h1').forEach(h1 => {
const text = h1.innerText.trim();
if (text) data.h1.push(text);
});
// Get h2 tags
document.querySelectorAll('h2').forEach(h2 => {
const text = h2.innerText.trim();
if (text) data.h2.push(text);
});
// Count images
data.images = document.querySelectorAll('img').length;
// Count links
data.links = document.querySelectorAll('a').length;
return data;
}''')
result = {
"status": "success",
"url": decoded_url,
"seo": seo_data
}
return result
except Exception as e:
print(f"Error during SEO extraction: {e}")
return {"status": "error", "url": decoded_url, "error": str(e)}
# Perform the operation
return await safe_browser_operation(decoded_url, seo_operation)
async def extract_meta_tags_service(decoded_url):
"""Service function to extract meta tags from a website"""
print(f"Extracting meta tags from: {decoded_url}")
try:
# Fetch the page content using plain HTTP request
content, status = await fetch_url(decoded_url)
# Parse the HTML
soup = BeautifulSoup(content, 'html.parser')
# Extract all meta tags
meta_tags = []
for meta in soup.find_all('meta'):
attributes = {}
for attr in meta.attrs:
attributes[attr] = meta[attr]
meta_tags.append(attributes)
# Extract Open Graph tags
og_tags = {}
for meta in soup.find_all('meta', property=lambda x: x and x.startswith('og:')):
og_tags[meta.get('property')] = meta.get('content')
# Extract Twitter card tags
twitter_tags = {}
for meta in soup.find_all('meta', attrs={'name': lambda x: x and x.startswith('twitter:')}):
twitter_tags[meta.get('name')] = meta.get('content')
# Get page title
title = soup.title.string if soup.title else ''
result = {
"status": "success",
"url": decoded_url,
"meta_tags": meta_tags,
"open_graph": og_tags,
"twitter_card": twitter_tags,
"title": title
}
return result
except Exception as e:
print(f"Error during meta tag extraction: {e}")
return {"status": "error", "url": decoded_url, "error": str(e)}
async def detect_pagination_service(decoded_url):
"""Service function to detect pagination on a website"""
print(f"Detecting pagination on: {decoded_url}")
# Define the operation to perform with the browser
async def pagination_operation(page):
try:
# Navigate to the URL
await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000)
original_url = page.url
# Check for common pagination indicators
pagination_data = await page.evaluate('''() => {
const data = {
hasPagination: false,
paginationType: null,
paginationElements: [],
detectedParameter: null,
lastPageNumber: null
};
// Look for numbered pagination links (1, 2, 3...)
const numberedLinks = Array.from(document.querySelectorAll('a, button, span'))
.filter(el => {
const text = el.innerText.trim();
// check for data-page attribute
const dataPage = el.getAttribute('data-page');
if (dataPage) {
return /^[0-9]+$/.test(dataPage);
}
return /^[0-9]+$/.test(text) &&
(el.tagName === 'A' || el.onclick ||
el.closest('button, [role="button"]'));
});
console.log(numberedLinks);
// Look for next/prev buttons
const nextButtons = Array.from(document.querySelectorAll('a, button, [role="button"]'))
.filter(el => {
const text = el.innerText.trim().toLowerCase();
const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || '';
const hasNextIcon = el.querySelector('i.fa-chevron-right, i.fa-arrow-right, svg[class*="arrow"], svg[class*="next"]');
return text.includes('next') ||
text.includes('') ||
text.includes('»') ||
text.includes('→') ||
ariaLabel.includes('next') ||
hasNextIcon;
});
// Check for pagination containers
const paginationContainers = Array.from(document.querySelectorAll(
'.pagination, [class*="pagination"], [class*="pager"], nav[aria-label*="pagination"], [role="navigation"]'
));
// Collect all potential pagination elements
if (numberedLinks.length > 0) {
data.hasPagination = true;
data.paginationType = 'numbered';
// Get href attributes or other identifiers from numbered links
data.paginationElements = numberedLinks.slice(0, 5).map(el => {
return {
text: el.innerText.trim(),
dataPage: el.getAttribute('data-page'),
href: el.tagName === 'A' ? el.href : null,
classes: el.className,
id: el.id
};
});
// Try to find the last page number
const pageNumbers = numberedLinks
.map(el => parseInt(el.innerText.replace(/[^\d]/g, '')))
.filter(num => !isNaN(num));
if (pageNumbers.length > 0) {
data.lastPageNumber = Math.max(...pageNumbers);
}
// Also look for a "last page" element that might have text like "Last" or "»"
const lastPageElement = Array.from(document.querySelectorAll('a, button'))
.find(el => {
const text = el.innerText.trim().toLowerCase();
const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || '';
return text.includes('last') ||
text === '»' ||
ariaLabel.includes('last page');
});
if (lastPageElement && lastPageElement.href) {
// Try to extract page number from the URL
try {
const url = new URL(lastPageElement.href);
// Check common pagination parameters
['page', 'p', 'pg'].forEach(param => {
if (url.searchParams.has(param)) {
const value = parseInt(url.searchParams.get(param));
if (!isNaN(value) && (data.lastPageNumber === null || value > data.lastPageNumber)) {
data.lastPageNumber = value;
}
}
});
// Check for path-based pagination (like /page/10)
const pathMatch = url.pathname.match(/\/(?:page|p)\/(\d+)/i);
if (pathMatch && pathMatch[1]) {
const value = parseInt(pathMatch[1]);
if (!isNaN(value) && (data.lastPageNumber === null || value > data.lastPageNumber)) {
data.lastPageNumber = value;
}
}
} catch (e) {
console.error("Error parsing last page URL:", e);
}
}
} else if (nextButtons.length > 0) {
data.hasPagination = true;
data.paginationType = 'next-prev';
// Get information about next buttons
data.paginationElements = nextButtons.slice(0, 3).map(el => {
return {
text: el.innerText.trim(),
href: el.tagName === 'A' ? el.href : null,
classes: el.className,
id: el.id
};
});
}
// Check for URL parameters that might indicate pagination
const currentUrl = window.location.href;
const urlParams = new URL(currentUrl).searchParams;
// Common pagination parameters
const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit'];
for (const param of paginationParams) {
if (urlParams.has(param)) {
data.detectedParameter = {
name: param,
value: urlParams.get(param)
};
break;
}
}
return data;
}''')
# If pagination is detected, try to navigate to the next page by clicking
next_page_url = None
pagination_parameter = None
url_template = None
step_size = None
if pagination_data['hasPagination']:
print("Pagination detected, attempting to click on a pagination element")
# Always try to click on a pagination element, regardless of type
clicked = False
# First try to click on a numbered link (preferably "2" if we're on page 1)
try:
clicked = await page.evaluate('''() => {
// First try to find and click on a "2" link or button
const page2Elements = Array.from(document.querySelectorAll('a[href], button, [role="button"]'))
.filter(el => {
// Check for text content "2"
if (el.innerText.trim() === '2') {
return true;
}
// Check for href with page=2 or similar (for anchor elements)
if (el.tagName === 'A' && el.href) {
try {
const url = new URL(el.href, window.location.origin);
const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit',
'currentpage', 'pagenum', 'pageNumber', 'paged'];
for (const param of paginationParams) {
if (url.searchParams.has(param) && url.searchParams.get(param) === '2') {
return true;
}
}
// Check for path-based pagination like /page/2/
const pathMatch = url.pathname.match(/\/(page|p)\/2\/?$/i);
if (pathMatch) {
return true;
}
} catch (e) {}
}
// Check for data attributes that might indicate pagination
if (el.getAttribute('data-page') === '2' ||
el.getAttribute('data-pagenumber') === '2' ||
el.getAttribute('data-page-number') === '2') {
return true;
}
return false;
});
if (page2Elements.length > 0) {
console.log("Clicking on page 2 element");
page2Elements[0].click();
return true;
}
// If no "2" link found, try any numbered link or button
const numberedElements = Array.from(document.querySelectorAll('a[href], button, [role="button"]'))
.filter(el => /^\d+$/.test(el.innerText.trim()));
if (numberedElements.length > 0) {
// Sort by number and get the second one (likely page 2)
const sorted = numberedElements.sort((a, b) => {
return parseInt(a.innerText.trim()) - parseInt(b.innerText.trim());
});
// Get the second element if available (page 2), otherwise the first one
const elementToClick = sorted.length > 1 ? sorted[1] : sorted[0];
console.log("Clicking on numbered element: " + elementToClick.innerText);
elementToClick.click();
return true;
}
// If no numbered links, try next button
const nextTexts = ['next', '', '»', '→'];
const nextElements = Array.from(document.querySelectorAll('a, button, [role="button"]'))
.filter(el => {
const text = el.textContent.trim().toLowerCase();
const ariaLabel = el.getAttribute('aria-label')?.toLowerCase() || '';
return nextTexts.some(t => text.includes(t)) ||
ariaLabel.includes('next') ||
el.querySelector('i.fa-chevron-right, i.fa-arrow-right, svg[class*="arrow"], svg[class*="next"]');
});
if (nextElements.length > 0) {
console.log("Clicking on next button");
nextElements[0].click();
return true;
}
return false;
}''')
if clicked:
print("Successfully clicked on pagination element")
# Wait for navigation to complete
await asyncio.sleep(1)
next_page_url = page.url
else:
print("No clickable pagination element found")
except Exception as e:
print(f"Error clicking on pagination element: {e}")
# If we successfully navigated to the next page, analyze the URL difference
if next_page_url:
print('Searching for pagination parameter')
# Parse both URLs
original_parsed = await page.evaluate(f'''(originalUrl) => {{
const original = new URL(originalUrl);
const current = new URL(window.location.href);
// Check for differences in query parameters
let paramDiff = null;
// Common pagination parameters to check
const paginationParams = ['page', 'p', 'pg', 'offset', 'o', 'from', 'start', 'limit', 'currentPage', 'current_page', 'currentpage', 'pagenum', 'pageNumber', 'paged'];
for (const param of paginationParams) {{
const originalValue = original.searchParams.get(param);
const currentValue = current.searchParams.get(param);
if (originalValue !== currentValue && currentValue !== null) {{
paramDiff = {{
name: param,
originalValue: originalValue,
currentValue: currentValue
}};
break;
}}
}}
// Check for path differences (like /page/1 vs /page/2)
const originalPath = original.pathname;
const currentPath = current.pathname;
let pathDiff = null;
if (originalPath !== currentPath) {{
const originalSegments = originalPath.split('/').filter(s => s);
const currentSegments = currentPath.split('/').filter(s => s);
// Find the segment that changed
if (originalSegments.length === currentSegments.length) {{
for (let i = 0; i < originalSegments.length; i++) {{
if (originalSegments[i] !== currentSegments[i]) {{
// Check if the difference is numeric
if (!isNaN(originalSegments[i]) && !isNaN(currentSegments[i])) {{
pathDiff = {{
index: i,
originalValue: originalSegments[i],
currentValue: currentSegments[i]
}};
}}
}}
}}
}}
}}
return {{
paramDiff,
pathDiff,
originalUrl: originalUrl,
currentUrl: window.location.href
}};
}}''', original_url)
# Determine the pagination parameter and create URL template
print(original_parsed)
if original_parsed['paramDiff']:
param_name = original_parsed['paramDiff']['name']
pagination_parameter = {
'type': 'query',
'name': param_name,
'value': original_parsed['paramDiff']['currentValue']
}
# --- STEP SIZE DETECTION FOR QUERY PARAM ---
orig_val = original_parsed['paramDiff']['originalValue'] if original_parsed['paramDiff']['originalValue'] is not None else 0
curr_val = original_parsed['paramDiff']['currentValue']
try:
if orig_val is not None and curr_val is not None:
orig_num = int(orig_val)
curr_num = int(curr_val)
step_size = abs(curr_num - orig_num)
except Exception:
step_size = None
# Create URL template for query parameter
url_obj = await page.evaluate(f'''(url, paramName) => {{
const urlObj = new URL(url);
urlObj.searchParams.set(paramName, "{{PAGE_NUMBER}}");
return urlObj.toString();
}}''', original_url, param_name)
url_template = url_obj
elif original_parsed['pathDiff']:
path_index = original_parsed['pathDiff']['index']
pagination_parameter = {
'type': 'path',
'index': path_index,
'value': original_parsed['pathDiff']['currentValue']
}
# --- STEP SIZE DETECTION FOR PATH PARAM ---
orig_val = original_parsed['pathDiff']['originalValue']
curr_val = original_parsed['pathDiff']['currentValue']
try:
if orig_val is not None and curr_val is not None:
orig_num = int(orig_val)
curr_num = int(curr_val)
step_size = abs(curr_num - orig_num)
except Exception:
step_size = None
# Create URL template for path parameter
url_template = await page.evaluate(f'''(url, pathIndex) => {{
const urlObj = new URL(url);
const pathSegments = urlObj.pathname.split('/').filter(s => s);
pathSegments[pathIndex] = "{{PAGE_NUMBER}}";
urlObj.pathname = '/' + pathSegments.join('/');
return urlObj.toString();
}}''', original_url, path_index)
if url_template:
# Decode URL-encoded characters in the template
import urllib.parse
url_template = urllib.parse.unquote(url_template)
# If we couldn't determine the URL template from navigation, try to infer it
if not url_template and pagination_data['detectedParameter']:
param_name = pagination_data['detectedParameter']['name']
url_template = await page.evaluate(f'''(url, paramName) => {{
const urlObj = new URL(url);
urlObj.searchParams.set(paramName, "{{PAGE_NUMBER}}");
return urlObj.toString();
}}''', original_url, param_name)
# Return the pagination detection results with a simplified structure
result = {
"status": "success",
"hasPagination": pagination_data['hasPagination'],
"urlTemplate": url_template,
"lastPage": pagination_data['lastPageNumber'],
"stepSize": step_size if step_size is not None and step_size >= 5 else 1
}
return result
except Exception as e:
print(f"Error during pagination detection: {e}")
return {
"status": "error",
"hasPagination": False,
"urlTemplate": None,
"lastPage": None,
"stepSize": None,
"error": str(e)
}
# Perform the operation
return await safe_browser_operation(decoded_url, pagination_operation)