هذا الالتزام موجود في:
Abdelsabour
2025-09-03 03:03:42 +03:00
التزام 905ad8f612
10 ملفات معدلة مع 3201 إضافات و0 حذوفات

4
.env Normal file
عرض الملف

@@ -0,0 +1,4 @@
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=YourAccessKey
AWS_SECRET_ACCESS_KEY=YourSecretKey
S3_BUCKET_NAME=your-bucket-name

17
.ghaymah.json Normal file
عرض الملف

@@ -0,0 +1,17 @@
{
"id": "890b1d1c-7f1a-4088-98ac-0f568a4f7b85",
"name": "crad-google-maps",
"projectId": "7d2d8d28-edc4-4ff0-b467-979d7fa23451",
"ports": [
{
"expose": true,
"number": 3000
}
],
"publicAccess": {
"enabled": true,
"domain": "auto"
},
"resourceTier": "t3",
"dockerFileName": "Dockerfile"
}

41
Dockerfile Normal file
عرض الملف

@@ -0,0 +1,41 @@
# المرحلة الأولى: اختيار صورة أساسية خفيفة تحتوي على Node.js
FROM node:18-slim
# تحديث وتثبيت المكتبات اللازمة لتشغيل متصفح Chromium الذي يعتمد عليه Puppeteer
# هذه الخطوة ضرورية ليعمل Puppeteer داخل Docker
RUN apt-get update \
&& apt-get install -y \
wget \
gnupg \
ca-certificates \
procps \
libxss1 \
--no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y \
google-chrome-stable \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# تحديد مجلد العمل داخل الحاوية
WORKDIR /usr/src/app
# نسخ ملفات package.json و package-lock.json أولاً للاستفادة من التخزين المؤقت (caching) في Docker
COPY package*.json ./
# تثبيت جميع الاعتماديات المذكورة في package.json
RUN npm install
# نسخ باقي ملفات المشروع إلى مجلد العمل
COPY . .
# puppeteer يحتاج إلى هذا المتغير ليجد المتصفح الذي قمنا بتثبيته
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
# فتح المنفذ (Port) الذي يعمل عليه السيرفر للسماح بالاتصال الخارجي
EXPOSE 3000
# الأمر الافتراضي الذي سيتم تشغيله عند بدء تشغيل الحاوية
CMD [ "node", "backend/index.js" ]

217
backend/index.js Normal file
عرض الملف

@@ -0,0 +1,217 @@
// استدعاء المكتبات الأساسية والمكتبات الجديدة للتخفي
const express = require('express');
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const cors = require('cors');
const path = require('path');
const fs = require('fs').promises;
// تفعيل مكون التخفي لتجنب الحظر
puppeteer.use(StealthPlugin());
const app = express();
app.use(cors());
app.use(express.json());
// خدمة الملفات الثابتة من مجلد frontend
app.use(express.static(path.join(__dirname, '../frontend')));
// route للصفحة الرئيسية
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/index.html'));
});
// دالة مساعدة للتأخير
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
];
const cache = new Map();
const CACHE_DURATION = 30 * 60 * 1000; // 30 دقيقة
// --- route استخراج البيانات (النسخة الاحترافية) ---
app.post('/scrape', async (req, res) => {
const { url } = req.body;
// ==================================================================
// ## بداية التعديل: جعل الكود يقبل جميع أنواع روابط خرائط جوجل ##
// ==================================================================
// الشرط الجديد يتحقق إذا كان الرابط يحتوي على "google.com/maps" أو "maps.app.goo.gl"
if (!url.includes('google.com/maps') && !url.includes('maps.app.goo.gl')) {
return res.status(400).json({ error: 'الرابط يجب أن يكون من خرائط جوجل (مثل google.com/maps أو maps.app.goo.gl)' });
}
// ==================================================================
// ## نهاية التعديل ##
// ==================================================================
const cachedData = cache.get(url);
if (cachedData && (Date.now() - cachedData.timestamp) < CACHE_DURATION) {
console.log('إرجاع البيانات من الذاكرة المؤقتة:', url);
return res.json(cachedData.data);
}
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--lang=ar-EG,ar'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1366, height: 768 });
await page.setUserAgent(userAgents[Math.floor(Math.random() * userAgents.length)]);
console.log(`\nبدء المحاكاة البشرية الكاملة للرابط: ${url}`);
await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
try {
const consentButtonSelector = 'button[aria-label*="Accept all"], button[aria-label*="قبول الكل"]';
await page.waitForSelector(consentButtonSelector, { timeout: 7000 });
await page.click(consentButtonSelector);
console.log('تم قبول ملفات تعريف الارتباط.');
await delay(1500);
} catch (e) {
console.log('نافذة قبول الكوكيز لم تظهر.');
}
const mainPanelSelector = 'div[role="main"]';
await page.waitForSelector(mainPanelSelector, { timeout: 15000 });
let images = [];
try {
const directImages = await page.evaluate(() =>
Array.from(document.querySelectorAll('img[src*="googleusercontent.com"]'))
.map(img => img.src.replace(/=w\d+-h\d+/, '=s1024'))
);
if (directImages.length > 0) {
images.push(...directImages);
console.log(`تم استخراج ${directImages.length} صورة مباشرة من الـ HTML.`);
}
const mainImageButtonSelector = 'button[jsaction*="hero-header-photo"]';
await page.waitForSelector(mainImageButtonSelector, { timeout: 8000 });
await page.click(mainImageButtonSelector);
console.log('تم النقر على الصورة الرئيسية لفتح المعرض.');
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 });
await delay(2000);
const galleryImages = await page.evaluate(() =>
Array.from(document.querySelectorAll('div[role="img"] > img'))
.map(img => img.src.replace(/=w\d+-h\d+/, '=s1024'))
);
if (galleryImages.length > 0) {
images.push(...galleryImages);
console.log(`تم استخراج ${galleryImages.length} صورة إضافية من المعرض.`);
}
await page.goBack({ waitUntil: 'networkidle2' });
await delay(1500);
} catch (e) {
console.warn('لم يتم العثور على معرض الصور أو حدث خطأ، سيتم الاعتماد على الصور المستخرجة مباشرة فقط.');
}
images = [...new Set(images)].slice(0, 10);
console.log(`إجمالي عدد الصور الفريدة التي تم العثور عليها: ${images.length}`);
const finalUrl = page.url();
const coordMatch = finalUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
const coordinates = coordMatch ? { latitude: parseFloat(coordMatch[1]), longitude: parseFloat(coordMatch[2]) } : null;
const fullData = await page.evaluate(() => {
const get = (selector, attribute = 'textContent') => {
const element = document.querySelector(selector);
if (!element) return null;
return attribute === 'textContent' ? element.textContent.trim() : element.getAttribute(attribute);
};
const baseData = {
name: get('h1'),
category: get('button[jsaction*="category"]'),
address: get('button[data-item-id="address"]', 'aria-label'),
hours: get('div[data-item-id*="hours"]', 'aria-label'),
phone: get('button[data-item-id*="phone"]', 'aria-label'),
website: get('a[data-item-id="authority"]', 'href'),
};
const reviewButton = document.querySelector('button[jsaction*="reviewchart.click"]');
if (reviewButton) {
const reviewText = reviewButton.getAttribute('aria-label') || '';
const ratingMatch = reviewText.match(/(\d[\.,]\d|\d+)\s+نجوم/);
const reviewsMatch = reviewText.match(/([\d,]+)\s+(تعليقات|مراجعات)/);
baseData.rating = ratingMatch ? ratingMatch[1] : null;
baseData.reviewsCount = reviewsMatch ? reviewsMatch[1] : null;
}
const aboutSections = [];
document.querySelectorAll('div.iP2t7d').forEach(section => {
const title = section.querySelector('h2.b9tNq')?.textContent.trim();
const items = Array.from(section.querySelectorAll('li.hpLkke')).map(li => li.textContent.trim());
if (title && items.length > 0) {
aboutSections.push({ title, items });
}
});
baseData.about = aboutSections;
const reviewsList = [];
document.querySelectorAll('div.jftiEf').forEach(reviewElement => {
const author = reviewElement.querySelector('.d4r55')?.textContent.trim();
const avatar = reviewElement.querySelector('.NBa7we')?.getAttribute('src');
const text = reviewElement.querySelector('.wiI7pd')?.textContent.trim();
const ratingMatch = reviewElement.querySelector('.kvMYJc')?.getAttribute('aria-label')?.match(/(\d+)/);
if (author && text) {
reviewsList.push({
author: author,
avatar: avatar || null,
text: text,
rating: ratingMatch ? ratingMatch[0] : null,
});
}
});
baseData.reviews = reviewsList.slice(0, 5);
return baseData;
});
const finalData = { ...fullData, images, coordinates };
if (!finalData.name) {
throw new Error("فشل استخراج البيانات الأساسية (الاسم).");
}
cache.set(url, { data: finalData, timestamp: Date.now() });
console.log('تم استخراج البيانات الشاملة بنجاح لـ:', finalData.name);
try {
const dataDir = path.join(__dirname, 'scraped_data');
await fs.mkdir(dataDir, { recursive: true });
const safeName = finalData.name.replace(/[^a-z0-9\u0600-\u06FF]/gi, '_').toLowerCase();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `${safeName}_${timestamp}.json`;
const filePath = path.join(dataDir, fileName);
await fs.writeFile(filePath, JSON.stringify(finalData, null, 2), 'utf-8');
console.log(`تم حفظ البيانات بنجاح في ملف: ${fileName}`);
} catch (fileError) {
console.error('حدث خطأ أثناء حفظ الملف:', fileError);
}
res.json(finalData);
} catch (error) {
console.error('حدث خطأ فادح أثناء عملية الاستخراج:', error.message.split('\n')[0]);
res.status(500).json({ error: 'فشل استخراج البيانات. قد يكون الرابط غير صحيح أو أن جوجل تمنع الوصول حاليًا.' });
} finally {
if (browser) {
await browser.close();
}
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

19
backend/proxies.js Normal file
عرض الملف

@@ -0,0 +1,19 @@
const { getRandomProxy } = require('./proxies');
// قائمة proxies مجانية (يجب تحديثها بانتظام)
const freeProxies = [
'103.149.162.194:80',
'45.7.64.106:999',
'167.71.5.83:3128',
'138.68.60.8:3128',
'167.99.131.11:80',
// إضافة المزيد من الـ proxies هنا
];
// دالة للحصول على proxy عشوائي
function getRandomProxy() {
return freeProxies[Math.floor(Math.random() * freeProxies.length)];
}
module.exports = { getRandomProxy };

13
docker-compose.yml Normal file
عرض الملف

@@ -0,0 +1,13 @@
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev

283
frontend/index.html Normal file
عرض الملف

@@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مستخرج بيانات Google Maps الاحترافي</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Cairo', sans-serif;
background-color: #f8fafc;
}
.icon-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
color: #4b5563;
}
.icon-wrapper svg {
width: 1.25rem;
height: 1.25rem;
color: #4f46e5;
flex-shrink: 0;
}
.badge {
background-color: #eef2ff;
color: #4338ca;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
}
</style>
</head>
<body class="antialiased">
<div class="container mx-auto p-4 md:p-8 max-w-5xl">
<header class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white p-8 rounded-xl shadow-2xl mb-10 text-center">
<h1 class="text-3xl md:text-4xl font-bold">📍 مستخرج بيانات Google Maps الاحترافي</h1>
<p class="mt-3 text-lg opacity-90">استخراج شامل لكل التفاصيل: الخدمات، المراجعات، الصور والمزيد</p>
</header>
<main>
<div class="bg-white p-6 rounded-xl shadow-lg mb-8 border border-gray-200">
<form id="scrape-form" class="flex flex-col sm:flex-row gap-4 items-center">
<input type="url" id="url-input" placeholder="الصق رابط خرائط جوجل هنا..." class="flex-grow w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none transition" required>
<button type="submit" class="bg-indigo-600 text-white font-bold py-4 px-8 rounded-lg hover:bg-indigo-700 transition-colors duration-300 shadow-md flex items-center justify-center w-full sm:w-auto">
<svg id="search-icon" class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<svg id="loading-spinner" class="animate-spin h-5 w-5 text-white hidden ml-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="button-text">استخراج</span>
</button>
</form>
</div>
<div id="results-container">
<div class="text-center p-10 bg-white rounded-xl shadow-md border border-gray-200">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">لا توجد بيانات بعد</h3>
<p class="mt-1 text-sm text-gray-500">ابدأ بلصق رابط في الحقل أعلاه.</p>
</div>
</div>
</main>
<footer class="text-center text-gray-500 mt-12">
<p>&copy; 2025 مستخرج بيانات Google Maps. Web Scraping باستخدام Node.js</p>
</footer>
</div>
<script>
const form = document.getElementById('scrape-form');
const urlInput = document.getElementById('url-input');
const resultsContainer = document.getElementById('results-container');
const searchIcon = document.getElementById('search-icon');
const loadingSpinner = document.getElementById('loading-spinner');
const submitButton = form.querySelector('button');
const buttonText = document.getElementById('button-text');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value;
resultsContainer.innerHTML = '';
searchIcon.classList.add('hidden');
loadingSpinner.classList.remove('hidden');
submitButton.disabled = true;
buttonText.textContent = 'جاري العمل...';
try {
const response = await fetch('/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayCard(data);
} catch (error) {
displayError(error.message);
} finally {
searchIcon.classList.remove('hidden');
loadingSpinner.classList.add('hidden');
submitButton.disabled = false;
buttonText.textContent = 'استخراج';
}
});
function displayCard(data) {
const mainImage = Array.isArray(data.images) && data.images.length > 0 ? data.images[0] : 'https://placehold.co/800x400/e2e8f0/4a5568?text=No+Image';
let galleryHTML = '';
if (Array.isArray(data.images) && data.images.length > 1) {
galleryHTML = `
<div class="mt-6">
<h4 class="font-semibold text-gray-700 mb-3">معرض الصور:</h4>
<div class="grid grid-cols-3 sm:grid-cols-5 gap-3">
${data.images.slice(0, 10).map(img => `
<img src="${img}" alt="صورة مصغرة" class="w-full h-24 object-cover rounded-lg cursor-pointer hover:opacity-80 transition-opacity shadow" onclick="document.getElementById('main-image').src='${img}'">
`).join('')}
</div>
</div>
`;
}
let aboutHTML = '';
if (Array.isArray(data.about) && data.about.length > 0) {
aboutHTML = data.about.map(section => `
<div class="mb-4">
<h4 class="font-semibold text-gray-800 mb-2">${section.title}</h4>
<div class="flex flex-wrap gap-2">
${section.items.map(item => `<span class="badge">${item}</span>`).join('')}
</div>
</div>
`).join('');
}
let webResultsHTML = '';
if(Array.isArray(data.webResults) && data.webResults.length > 0) {
webResultsHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">نتائج الويب</h3>
<div class="space-y-4">
${data.webResults.map(result => generateWebsiteRow(result.link, result.title)).join('')}
</div>
</div>
`;
}
let reviewSummaryHTML = '';
if(Array.isArray(data.reviewSummary) && data.reviewSummary.length > 0) {
reviewSummaryHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">ملخص التقييمات</h3>
<div class="space-y-2">
${data.reviewSummary.map(summary => `
<div class="flex items-center">
<span class="text-sm text-gray-600 w-12">${summary.stars} نجوم</span>
<div class="w-full bg-gray-200 rounded-full h-2.5 mx-2">
<div class="bg-yellow-400 h-2.5 rounded-full" style="width: ${summary.percentage || '0%'}"></div>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
let reviewsHTML = '';
if (Array.isArray(data.reviews) && data.reviews.length > 0) {
reviewsHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">المراجعات</h3>
<div class="space-y-4">
${data.reviews.map(review => `
<div class="flex items-start space-x-4 space-x-reverse">
<img class="h-10 w-10 rounded-full" src="${review.avatar || 'https://placehold.co/40x40'}" alt="صورة المراجع">
<div class="flex-1">
<div class="flex items-center justify-between">
<p class="text-sm font-semibold text-gray-900">${review.author || 'مستخدم جوجل'}</p>
${review.rating ? `
<div class="flex items-center text-yellow-500">
<span class="text-sm font-bold ml-1">${review.rating}</span>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
</div>` : ''}
</div>
<p class="mt-1 text-gray-600">${review.text || ''}</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
const cardHTML = `
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200 animate-fade-in">
<style>.animate-fade-in { animation: fadeIn 0.5s ease-in-out; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } }</style>
<img id="main-image" src="${mainImage}" alt="صورة ${data.name || 'المكان'}" class="w-full h-72 object-cover">
<div class="p-6 md:p-8">
<p class="text-indigo-600 font-semibold mb-2">${data.category || 'فئة غير متوفرة'}</p>
<h2 class="text-3xl font-bold text-gray-900 mb-4">${data.name || 'غير متوفر'}</h2>
${data.rating ? `
<div class="flex items-center mb-6">
<div class="flex items-center text-yellow-500">
<span class="text-xl font-bold ml-2">${data.rating}</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
</div>
<span class="text-gray-500 text-sm mr-3">(${data.reviewsCount || '0'} مراجعة)</span>
</div>` : ''}
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 border-t border-b py-6 my-6">
${generateDetailRow('M15 10.5a3 3 0 11-6 0 3 3 0 016 0z M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z', data.address)}
${generateDetailRow('M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z', data.phone)}
${generateWebsiteRow(data.website, 'زيارة الموقع الرسمي')}
${generateCoordinatesRow(data.coordinates)}
${generateDetailRow('M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z', data.hours)}
</div>
${aboutHTML}
${webResultsHTML}
${reviewSummaryHTML}
${reviewsHTML}
${galleryHTML}
</div>
</div>
`;
resultsContainer.innerHTML = cardHTML;
}
function generateDetailRow(svgPath, text) {
if (!text) return '';
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<span>${text}</span>
</div>`;
}
function generateWebsiteRow(website, title) {
if (!website) return '';
const svgPath = "M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A11.953 11.953 0 0112 16.5c-2.998 0-5.74-1.1-7.843-2.918m15.686-5.834A8.959 8.959 0 0021 12c0 .778-.099 1.533-.284 2.253m0 0a11.953 11.953 0 00-2.284 2.253m-13.402 0a11.953 11.953 0 00-2.284-2.253";
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<a href="${website}" target="_blank" class="text-indigo-600 hover:underline">${title || website}</a>
</div>`;
}
function generateCoordinatesRow(coords) {
if (!coords || !coords.latitude) return '';
const text = `${coords.latitude}, ${coords.longitude}`;
const gmapsUrl = `https://www.google.com/maps?q=${coords.latitude},${coords.longitude}`;
const svgPath = "M15 10.5a3 3 0 11-6 0 3 3 0 016 0z M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z";
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<span>${text} <a href="${gmapsUrl}" target="_blank" class="text-indigo-600 hover:underline mr-1">(فتح في الخريطة)</a></span>
</div>`;
}
function displayError(message) {
const errorHTML = `
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg" role="alert">
<p class="font-bold">حدث خطأ!</p>
<p>${message}</p>
</div>
`;
resultsContainer.innerHTML = errorHTML;
}
</script>
</body>
</html>

91
frontend/settings.html Normal file
عرض الملف

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>إعدادات مستخرج البيانات</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.setting-group {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
button {
padding: 10px 15px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>إعدادات مستخرج بيانات Google Maps</h1>
<div class="setting-group">
<h3>إعدادات تجنب الحظر</h3>
<label>
<input type="checkbox" id="useProxy" checked>
استخدام Proxy لتجنب الحظر
</label>
<br>
<label>
<input type="checkbox" id="randomDelay" checked>
إضافة تأخير عشوائي بين الطلبات
</label>
<br>
<label>
عدد الطلبات في الدقيقة:
<input type="number" id="requestsPerMinute" value="5" min="1" max="10">
</label>
</div>
<div class="setting-group">
<h3>إعدادات البيانات</h3>
<label>
<input type="checkbox" id="extractImages" checked>
استخراج الصور
</label>
<br>
<label>
<input type="checkbox" id="extractReviews" checked>
استخراج التقييمات
</label>
</div>
<button onclick="saveSettings()">حفظ الإعدادات</button>
<script>
function saveSettings() {
const settings = {
useProxy: document.getElementById('useProxy').checked,
randomDelay: document.getElementById('randomDelay').checked,
requestsPerMinute: document.getElementById('requestsPerMinute').value,
extractImages: document.getElementById('extractImages').checked,
extractReviews: document.getElementById('extractReviews').checked
};
localStorage.setItem('scraperSettings', JSON.stringify(settings));
alert('تم حفظ الإعدادات بنجاح!');
}
// تحميل الإعدادات المحفوظة
const savedSettings = localStorage.getItem('scraperSettings');
if (savedSettings) {
const settings = JSON.parse(savedSettings);
document.getElementById('useProxy').checked = settings.useProxy;
document.getElementById('randomDelay').checked = settings.randomDelay;
document.getElementById('requestsPerMinute').value = settings.requestsPerMinute;
document.getElementById('extractImages').checked = settings.extractImages;
document.getElementById('extractReviews').checked = settings.extractReviews;
}
</script>
</body>
</html>

2495
package-lock.json مولّد Normal file

تم حذف اختلاف الملف لأن الملف كبير جداً تحميل الاختلاف

21
package.json Normal file
عرض الملف

@@ -0,0 +1,21 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.11.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"puppeteer": "^24.17.0",
"puppeteer-cluster": "^0.24.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}