// استدعاء المكتبات الأساسية والمكتبات الجديدة للتخفي 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}`));