first commit
هذا الالتزام موجود في:
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}`));
|
المرجع في مشكلة جديدة
حظر مستخدم