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"
|
||||||
|
}
|
||||||
|
}
|
المرجع في مشكلة جديدة
حظر مستخدم