first commit
هذا الالتزام موجود في:
4
.env
Normal file
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
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
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
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
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
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
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>© 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
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
2495
package-lock.json
مولّد
Normal file
تم حذف اختلاف الملف لأن الملف كبير جداً
تحميل الاختلاف
21
package.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"
|
||||
}
|
||||
}
|
المرجع في مشكلة جديدة
حظر مستخدم