|
Поговорим о...
|
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .path-input { width: 100%; padding: 12px 15px; border: 1px solid #ced4da; border-radius: 4px; font-size: 16px; margin-bottom: 15px; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; fileContents.forEach(file => { const logs = parseLogFile(file.content, file.name); allLogs = allLogs.concat(logs); }); if (allLogs.length === 0) { throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.'); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; // Регулярное выражение для парсинга строки лога IIS // Формат: дата время s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken // Пример: 2025-10-27 08:19:27 10.64.241.57 POST /22221123/Reportsu.aspx report=Catalog_Point 80 MYPC\IvanovAA 15.34.19.44 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+Yowser/2.5+Safari/537.36 200 0 0 4232 const logRegex = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/; for (const line of lines) { if (!line.trim()) continue; const match = line.match(logRegex); if (match) { const [ fullMatch, date, time, serverIp, method, uriStem, uriQuery, port, username, clientIp, userAgent, statusCode, subStatus, win32Status, timeTaken ] = match; // Фильтруем только успешные запросы и основные страницы // Исключаем статические ресурсы (JS, CSS, изображения и т.д.) if (statusCode === '200' && !uriStem.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && uriStem !== '/favicon.ico' && uriStem !== '/') { logs.push({ date: date, time: time, user: username !== '-' ? username : 'Неизвестно', ip: clientIp, method: method, page: uriStem + (uriQuery && uriQuery !== '-' ? '?' + uriQuery : ''), statusCode: parseInt(statusCode), filename: filename }); } } } return logs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { filteredLogs = allLogs.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { // Сокращаем длинные названия страниц для легенды return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { // Обновляем заголовок календаря const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; // Получаем уникальные даты с данными const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); // Создаем объект для быстрой проверки наличия данных по дате const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); // Очищаем календарь calendar.innerHTML = ''; // Добавляем заголовки дней недели const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Определяем первый и последний день месяца для отображения const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); // Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDayOfMonth.getDay(); // Преобразуем к формату Пн=0, Вт=1, ..., Вс=6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Добавляем пустые ячейки для дней предыдущего месяца for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } // Заполняем календарь днями текущего месяца for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; // Добавляем обработчик клика dayElement.addEventListener('click', function() { // Снимаем выделение с предыдущей выбранной даты if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } // Выделяем новую дату dayElement.classList.add('selected'); selectedDate = dateString; // Устанавливаем фильтр на выбранную дату dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html>
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; let showUnique = false; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve, reject) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.onerror = function() { reject(new Error(`Ошибка чтения файла: ${file.name}`)); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; let totalFiles = 0; let successfulFiles = 0; fileContents.forEach(file => { totalFiles++; try { const logs = parseLogFile(file.content, file.name); if (logs.length > 0) { allLogs = allLogs.concat(logs); successfulFiles++; } } catch (e) { console.warn(`Ошибка парсинга файла ${file.name}:`, e.message); } }); if (allLogs.length === 0) { throw new Error(`Не удалось распознать данные ни в одном файле. Обработано ${successfulFiles}/${totalFiles} файлов`); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами (успешно: ${successfulFiles})`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; if (successfulFiles < totalFiles) { showError(`Успешно обработано ${successfulFiles} из ${totalFiles} файлов. Некоторые файлы содержат ошибки.`); } } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Пропускаем служебные строки (начинающиеся с #) if (trimmedLine.startsWith('#')) continue; try { // Простой парсинг по пробелам const parts = trimmedLine.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; const ip = parts[dateIndex + 2] || ''; const method = parts[dateIndex + 3] || ''; const page = parts[dateIndex + 4] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } catch (e) { // Пропускаем строки с ошибками continue; } } return logs; } // Получение уникальных посещений (пользователь + страница + дата) function getUniqueVisits(logs) { const uniqueMap = new Map(); const uniqueLogs = []; logs.forEach(log => { const key = `${log.date}_${log.user}_${log.page}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); uniqueLogs.push(log); } }); return uniqueLogs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs; filteredLogs = logsToFilter.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } dayElement.classList.add('selected'); selectedDate = dateString; dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html>
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; position: relative; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.in-range { background-color: #ffa07a; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } .calendar-hint { margin-top: 10px; font-size: 14px; color: #6c757d; text-align: center; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> <div class="calendar-hint" id="calendarHint"> Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода. </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const calendarHint = document.getElementById('calendarHint'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDateStart = null; let selectedDateEnd = null; let selectedFiles = []; let showUnique = false; let isShiftPressed = false; // Отслеживаем нажатие Shift document.addEventListener('keydown', function(e) { if (e.key === 'Shift') { isShiftPressed = true; calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.'; } }); document.addEventListener('keyup', function(e) { if (e.key === 'Shift') { isShiftPressed = false; calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.'; } }); // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve, reject) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.onerror = function() { reject(new Error(`Ошибка чтения файла: ${file.name}`)); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; let totalFiles = 0; let successfulFiles = 0; fileContents.forEach(file => { totalFiles++; try { const logs = parseLogFile(file.content, file.name); if (logs.length > 0) { allLogs = allLogs.concat(logs); successfulFiles++; } } catch (e) { console.warn(`Ошибка парсинга файла ${file.name}:`, e.message); } }); if (allLogs.length === 0) { throw new Error(`Не удалось распознать данные ни в одном файле. Обработано ${successfulFiles}/${totalFiles} файлов`); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами (успешно: ${successfulFiles})`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; if (successfulFiles < totalFiles) { showError(`Успешно обработано ${successfulFiles} из ${totalFiles} файлов. Некоторые файлы содержат ошибки.`); } } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов с учетом нескольких заголовков function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; let currentFields = null; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Обрабатываем заголовок Fields if (trimmedLine.startsWith('#Fields:')) { currentFields = trimmedLine.substring(8).trim().split(/\s+/); continue; } // Пропускаем другие служебные строки if (trimmedLine.startsWith('#')) continue; try { const parts = trimmedLine.split(/\s+/); // Если у нас есть информация о полях, используем ее if (currentFields) { const logEntry = {}; let fieldIndex = 0; for (let i = 0; i < parts.length && fieldIndex < currentFields.length; i++) { const field = currentFields[fieldIndex]; logEntry[field] = parts[i]; fieldIndex++; } // Извлекаем данные из структурированной записи const date = logEntry.date; const time = logEntry.time; const method = logEntry['cs-method']; const page = logEntry['cs-uri-stem']; const user = logEntry['cs-username']; const ip = logEntry['c-ip']; const statusCode = logEntry['sc-status']; if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: date, time: time, user: user && user !== '-' ? user : 'Неизвестно', ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } else { // Старый метод парсинга для файлов без заголовка Fields let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; const ip = parts[dateIndex + 2] || ''; const method = parts[dateIndex + 3] || ''; const page = parts[dateIndex + 4] || ''; let statusCode = ''; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } let user = 'Неизвестно'; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } } catch (e) { // Пропускаем строки с ошибками continue; } } return logs; } // Получение уникальных посещений (пользователь + страница + дата) function getUniqueVisits(logs) { const uniqueMap = new Map(); const uniqueLogs = []; logs.forEach(log => { const key = `${log.date}_${log.user}_${log.page}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); uniqueLogs.push(log); } }); return uniqueLogs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs; filteredLogs = logsToFilter.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { handleDateClick(dateString); }); } calendar.appendChild(dayElement); } updateCalendarSelection(); } function handleDateClick(dateString) { if (isShiftPressed && selectedDateStart) { // Выбор периода selectedDateEnd = dateString; // Убедимся, что start раньше end if (new Date(selectedDateStart) > new Date(selectedDateEnd)) { [selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart]; } dateFrom.value = selectedDateStart; dateTo.value = selectedDateEnd; } else { // Выбор одной даты selectedDateStart = dateString; selectedDateEnd = dateString; dateFrom.value = dateString; dateTo.value = dateString; } updateCalendarSelection(); applyFilters(); } function updateCalendarSelection() { // Сбрасываем все выделения document.querySelectorAll('.calendar-day').forEach(day => { day.classList.remove('selected', 'in-range'); }); if (selectedDateStart) { const startDate = new Date(selectedDateStart); const endDate = new Date(selectedDateEnd || selectedDateStart); document.querySelectorAll('.calendar-day').forEach(day => { const dateString = day.dataset.date; if (!dateString) return; const currentDate = new Date(dateString); if (currentDate >= startDate && currentDate <= endDate) { if (currentDate.getTime() === startDate.getTime() || currentDate.getTime() === endDate.getTime()) { day.classList.add('selected'); } else { day.classList.add('in-range'); } } }); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; selectedDateStart = dateRange.min; selectedDateEnd = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html>
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; position: relative; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.in-range { background-color: #ffa07a; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } .calendar-hint { margin-top: 10px; font-size: 14px; color: #6c757d; text-align: center; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearPath" class="btn btn-secondary">Очистить путь</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> <div class="calendar-hint" id="calendarHint"> Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода. </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearPathBtn = document.getElementById('clearPath'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const calendarHint = document.getElementById('calendarHint'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDateStart = null; let selectedDateEnd = null; let selectedFiles = []; let showUnique = false; let isShiftPressed = false; // Отслеживаем нажатие Shift document.addEventListener('keydown', function(e) { if (e.key === 'Shift') { isShiftPressed = true; calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.'; } }); document.addEventListener('keyup', function(e) { if (e.key === 'Shift') { isShiftPressed = false; calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.'; } }); // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Сохранение пути в localStorage function savePathToStorage(path) { try { localStorage.setItem('iisLogsPath', path); } catch (e) { console.warn('Не удалось сохранить путь в localStorage:', e.message); } } // Загрузка пути из localStorage function loadPathFromStorage() { try { return localStorage.getItem('iisLogsPath'); } catch (e) { console.warn('Не удалось загрузить путь из localStorage:', e.message); return null; } } // Очистка пути clearPathBtn.addEventListener('click', function() { try { localStorage.removeItem('iisLogsPath'); fileInfoDiv.textContent = 'Путь очищен'; selectedFiles = []; } catch (e) { console.warn('Не удалось очистить путь:', e.message); } }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`); } }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Папка с ${selectedFiles.length} файлами`); } }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; allLogs = []; // Чтение файлов последовательно для избежания переполнения processFilesSequentially(selectedFiles, 0) .then(() => { if (allLogs.length === 0) { throw new Error('Не удалось распознать данные ни в одном файле'); } dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; fileInfoDiv.textContent += `. Успешно обработано записей: ${allLogs.length}`; }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); }); }); // Последовательная обработка файлов function processFilesSequentially(files, index) { if (index >= files.length) { return Promise.resolve(); } const file = files[index]; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { const logs = parseLogFile(e.target.result, file.name); if (logs.length > 0) { allLogs = allLogs.concat(logs); } resolve(processFilesSequentially(files, index + 1)); } catch (error) { console.warn(`Ошибка парсинга файла ${file.name}:`, error.message); // Продолжаем обработку следующих файлов даже при ошибке resolve(processFilesSequentially(files, index + 1)); } }; reader.onerror = function() { console.warn(`Ошибка чтения файла ${file.name}`); // Продолжаем обработку следующих файлов даже при ошибке resolve(processFilesSequentially(files, index + 1)); }; reader.readAsText(file); }); } // Упрощенный парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Пропускаем служебные строки if (trimmedLine.startsWith('#')) continue; try { // Простой парсинг - ищем дату и основные поля const parts = trimmedLine.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; // Ищем метод (GET/POST) после времени let methodIndex = -1; for (let j = dateIndex + 2; j < parts.length; j++) { if (parts[j] === 'GET' || parts[j] === 'POST') { methodIndex = j; break; } } if (methodIndex === -1) continue; const method = parts[methodIndex]; const page = parts[methodIndex + 1] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } // Ищем IP клиента (формат xxx.xxx.xxx.xxx) let ip = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) { ip = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } catch (e) { // Пропускаем строки с ошибками continue; } } return logs; } // Получение уникальных посещений (пользователь + страница + дата) function getUniqueVisits(logs) { const uniqueMap = new Map(); const uniqueLogs = []; logs.forEach(log => { const key = `${log.date}_${log.user}_${log.page}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); uniqueLogs.push(log); } }); return uniqueLogs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs; filteredLogs = logsToFilter.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { handleDateClick(dateString); }); } calendar.appendChild(dayElement); } updateCalendarSelection(); } function handleDateClick(dateString) { if (isShiftPressed && selectedDateStart) { // Выбор периода selectedDateEnd = dateString; // Убедимся, что start раньше end if (new Date(selectedDateStart) > new Date(selectedDateEnd)) { [selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart]; } dateFrom.value = selectedDateStart; dateTo.value = selectedDateEnd; } else { // Выбор одной даты selectedDateStart = dateString; selectedDateEnd = dateString; dateFrom.value = dateString; dateTo.value = dateString; } updateCalendarSelection(); applyFilters(); } function updateCalendarSelection() { // Сбрасываем все выделения document.querySelectorAll('.calendar-day').forEach(day => { day.classList.remove('selected', 'in-range'); }); if (selectedDateStart) { const startDate = new Date(selectedDateStart); const endDate = new Date(selectedDateEnd || selectedDateStart); document.querySelectorAll('.calendar-day').forEach(day => { const dateString = day.dataset.date; if (!dateString) return; const currentDate = new Date(dateString); if (currentDate >= startDate && currentDate <= endDate) { if (currentDate.getTime() === startDate.getTime() || currentDate.getTime() === endDate.getTime()) { day.classList.add('selected'); } else { day.classList.add('in-range'); } } }); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; selectedDateStart = dateRange.min; selectedDateEnd = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Показываем сохраненный путь при загрузке const savedPath = loadPathFromStorage(); if (savedPath) { fileInfoDiv.textContent = `Последний путь: ${savedPath}`; } }); </script> </body> </html>
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> /* Стили остаются без изменений */ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; position: relative; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.in-range { background-color: #ffa07a; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } .calendar-hint { margin-top: 10px; font-size: 14px; color: #6c757d; text-align: center; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearPath" class="btn btn-secondary">Очистить путь</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> <div class="calendar-hint" id="calendarHint"> Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода. </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearPathBtn = document.getElementById('clearPath'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const calendarHint = document.getElementById('calendarHint'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); // Вместо хранения всех логов в памяти, будем хранить только агрегированные данные let aggregatedData = { dateCounts: {}, pageCounts: {}, userPageDateMap: {}, // Для уникальных посещений allDates: new Set(), minDate: null, maxDate: null }; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDateStart = null; let selectedDateEnd = null; let selectedFiles = []; let showUnique = false; let isShiftPressed = false; let processedFilesCount = 0; // Отслеживаем нажатие Shift document.addEventListener('keydown', function(e) { if (e.key === 'Shift') { isShiftPressed = true; calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.'; } }); document.addEventListener('keyup', function(e) { if (e.key === 'Shift') { isShiftPressed = false; calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.'; } }); // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Сохранение пути в localStorage function savePathToStorage(path) { try { localStorage.setItem('iisLogsPath', path); } catch (e) { console.warn('Не удалось сохранить путь в localStorage:', e.message); } } // Загрузка пути из localStorage function loadPathFromStorage() { try { return localStorage.getItem('iisLogsPath'); } catch (e) { console.warn('Не удалось загрузить путь из localStorage:', e.message); return null; } } // Очистка пути clearPathBtn.addEventListener('click', function() { try { localStorage.removeItem('iisLogsPath'); fileInfoDiv.textContent = 'Путь очищен'; selectedFiles = []; resetAggregatedData(); } catch (e) { console.warn('Не удалось очистить путь:', e.message); } }); // Сброс агрегированных данных function resetAggregatedData() { aggregatedData = { dateCounts: {}, pageCounts: {}, userPageDateMap: {}, allDates: new Set(), minDate: null, maxDate: null }; statsTableBody.innerHTML = ''; updateCharts(); updatePopularPagesList(); updateCalendar(); } // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`); } }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Папка с ${selectedFiles.length} файлами`); } }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов с пошаговой обработкой loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; resetAggregatedData(); processedFilesCount = 0; // Обработка файлов последовательно processFilesSequentially(selectedFiles, 0) .then(() => { if (aggregatedData.allDates.size === 0) { throw new Error('Не удалось распознать данные ни в одном файле'); } dateRange = { min: aggregatedData.minDate, max: aggregatedData.maxDate }; setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; fileInfoDiv.textContent += `. Успешно обработано файлов: ${processedFilesCount}`; }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); }); }); // Последовательная обработка файлов function processFilesSequentially(files, index) { if (index >= files.length) { return Promise.resolve(); } const file = files[index]; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { processLogFile(e.target.result, file.name); processedFilesCount++; // Обновляем прогресс fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}`; resolve(processFilesSequentially(files, index + 1)); } catch (error) { console.warn(`Ошибка парсинга файла ${file.name}:`, error.message); // Продолжаем обработку следующих файлов даже при ошибке resolve(processFilesSequentially(files, index + 1)); } }; reader.onerror = function() { console.warn(`Ошибка чтения файла ${file.name}`); // Продолжаем обработку следующих файлов даже при ошибке resolve(processFilesSequentially(files, index + 1)); }; reader.readAsText(file); }); } // Обработка файла логов с немедленной агрегацией данных function processLogFile(content, filename) { const lines = content.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Пропускаем служебные строки if (trimmedLine.startsWith('#')) continue; try { // Простой парсинг - ищем дату и основные поля const parts = trimmedLine.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; // Ищем метод (GET/POST) после времени let methodIndex = -1; for (let j = dateIndex + 2; j < parts.length; j++) { if (parts[j] === 'GET' || parts[j] === 'POST') { methodIndex = j; break; } } if (methodIndex === -1) continue; const method = parts[methodIndex]; const page = parts[methodIndex + 1] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } // Ищем IP клиента (формат xxx.xxx.xxx.xxx) let ip = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) { ip = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { // Агрегируем данные вместо хранения всех логов aggregateLogData({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } catch (e) { // Пропускаем строки с ошибками continue; } } } // Агрегация данных лога без хранения в памяти function aggregateLogData(log) { // Обновляем диапазон дат aggregatedData.allDates.add(log.date); if (!aggregatedData.minDate || log.date < aggregatedData.minDate) { aggregatedData.minDate = log.date; } if (!aggregatedData.maxDate || log.date > aggregatedData.maxDate) { aggregatedData.maxDate = log.date; } // Считаем посещения по датам aggregatedData.dateCounts[log.date] = (aggregatedData.dateCounts[log.date] || 0) + 1; // Считаем популярные страницы aggregatedData.pageCounts[log.page] = (aggregatedData.pageCounts[log.page] || 0) + 1; // Для уникальных посещений const uniqueKey = `${log.date}_${log.user}_${log.page}`; if (!aggregatedData.userPageDateMap[uniqueKey]) { aggregatedData.userPageDateMap[uniqueKey] = log; } } // Получение уникальных посещений из агрегированных данных function getUniqueVisitsData() { return Object.values(aggregatedData.userPageDateMap); } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; // Для таблицы используем либо все данные, либо уникальные let dataToShow; if (showUnique) { dataToShow = getUniqueVisitsData(); } else { // В реальном приложении здесь нужно будет хранить выборку данных для таблицы // или генерировать их на лету из агрегированных данных dataToShow = Object.values(aggregatedData.userPageDateMap).slice(0, 1000); // Ограничиваем для производительности } // Применяем фильтры к данным для таблицы const filteredData = dataToShow.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); // Ограничиваем количество строк для производительности const displayData = filteredData.slice(0, 1000); displayData.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); if (filteredData.length > 1000) { const infoRow = document.createElement('tr'); const infoCell = document.createElement('td'); infoCell.colSpan = 7; infoCell.textContent = `... и еще ${filteredData.length - 1000} записей`; infoCell.style.textAlign = 'center'; infoCell.style.fontStyle = 'italic'; infoRow.appendChild(infoCell); statsTableBody.appendChild(infoRow); } } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; // Используем агрегированные данные для графиков Object.keys(aggregatedData.dateCounts).forEach(date => { const dateObj = new Date(date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); if (dateObj >= fromDate && dateObj <= toDate) { let periodKey; switch(groupBy.value) { case 'month': periodKey = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${dateObj.getFullYear()}`; break; default: // day periodKey = date; } if (showUnique) { // Для уникальных посещений нужно пересчитать const uniqueCount = Object.keys(aggregatedData.userPageDateMap) .filter(key => key.startsWith(date)) .length; visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + uniqueCount; } else { visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + aggregatedData.dateCounts[date]; } } }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; // Фильтруем страницы по дате и другим критериям Object.values(aggregatedData.userPageDateMap).forEach(log => { const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); if (dateMatch && userMatch && pageMatch) { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; } }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; // Фильтруем страницы по дате и другим критериям Object.values(aggregatedData.userPageDateMap).forEach(log => { const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); if (dateMatch && userMatch && pageMatch) { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; } }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = Array.from(aggregatedData.allDates).sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { handleDateClick(dateString); }); } calendar.appendChild(dayElement); } updateCalendarSelection(); } function handleDateClick(dateString) { if (isShiftPressed && selectedDateStart) { // Выбор периода selectedDateEnd = dateString; // Убедимся, что start раньше end if (new Date(selectedDateStart) > new Date(selectedDateEnd)) { [selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart]; } dateFrom.value = selectedDateStart; dateTo.value = selectedDateEnd; } else { // Выбор одной даты selectedDateStart = dateString; selectedDateEnd = dateString; dateFrom.value = dateString; dateTo.value = dateString; } updateCalendarSelection(); applyFilters(); } function updateCalendarSelection() { // Сбрасываем все выделения document.querySelectorAll('.calendar-day').forEach(day => { day.classList.remove('selected', 'in-range'); }); if (selectedDateStart) { const startDate = new Date(selectedDateStart); const endDate = new Date(selectedDateEnd || selectedDateStart); document.querySelectorAll('.calendar-day').forEach(day => { const dateString = day.dataset.date; if (!dateString) return; const currentDate = new Date(dateString); if (currentDate >= startDate && currentDate <= endDate) { if (currentDate.getTime() === startDate.getTime() || currentDate.getTime() === endDate.getTime()) { day.classList.add('selected'); } else { day.classList.add('in-range'); } } }); } } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; selectedDateStart = dateRange.min; selectedDateEnd = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Показываем сохраненный путь при загрузке const savedPath = loadPathFromStorage(); if (savedPath) { fileInfoDiv.textContent = `Последний путь: ${savedPath}`; } }); </script> </body> </html>
|
|
|
|
|
|
|
|
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> /* Все стили остаются без изменений */ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; position: relative; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.in-range { background-color: #ffa07a; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } .calendar-hint { margin-top: 10px; font-size: 14px; color: #6c757d; text-align: center; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearPath" class="btn btn-secondary">Очистить путь</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> <div class="calendar-hint" id="calendarHint"> Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода. </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearPathBtn = document.getElementById('clearPath'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const calendarHint = document.getElementById('calendarHint'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); // Вместо хранения всех логов в памяти, будем хранить только агрегированные данные let aggregatedData = { dateCounts: {}, pageCounts: {}, userPageDateMap: {}, // Для уникальных посещений allDates: new Set(), minDate: null, maxDate: null, allLogs: [] // Храним ВСЕ логи для отображения в таблице }; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDateStart = null; let selectedDateEnd = null; let selectedFiles = []; let showUnique = false; let isShiftPressed = false; let processedFilesCount = 0; // Отслеживаем нажатие Shift document.addEventListener('keydown', function(e) { if (e.key === 'Shift') { isShiftPressed = true; calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.'; } }); document.addEventListener('keyup', function(e) { if (e.key === 'Shift') { isShiftPressed = false; calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.'; } }); // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Сохранение пути в localStorage function savePathToStorage(path) { try { localStorage.setItem('iisLogsPath', path); } catch (e) { console.warn('Не удалось сохранить путь в localStorage:', e.message); } } // Загрузка пути из localStorage function loadPathFromStorage() { try { return localStorage.getItem('iisLogsPath'); } catch (e) { console.warn('Не удалось загрузить путь из localStorage:', e.message); return null; } } // Очистка пути clearPathBtn.addEventListener('click', function() { try { localStorage.removeItem('iisLogsPath'); fileInfoDiv.textContent = 'Путь очищен'; selectedFiles = []; resetAggregatedData(); } catch (e) { console.warn('Не удалось очистить путь:', e.message); } }); // Сброс агрегированных данных function resetAggregatedData() { aggregatedData = { dateCounts: {}, pageCounts: {}, userPageDateMap: {}, allDates: new Set(), minDate: null, maxDate: null, allLogs: [] }; statsTableBody.innerHTML = ''; updateCharts(); updatePopularPagesList(); updateCalendar(); } // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`); } }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Папка с ${selectedFiles.length} файлами`); } }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов с пошаговой обработкой loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; resetAggregatedData(); processedFilesCount = 0; // Обработка файлов последовательно processFilesSequentially(selectedFiles, 0) .then(() => { if (aggregatedData.allLogs.length === 0) { throw new Error('Не удалось распознать данные ни в одном файле'); } dateRange = getDateRange(aggregatedData.allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; fileInfoDiv.textContent += `. Успешно обработано записей: ${aggregatedData.allLogs.length}`; }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); }); }); // Последовательная обработка файлов function processFilesSequentially(files, index) { if (index >= files.length) { return Promise.resolve(); } const file = files[index]; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { const logs = parseLogFile(e.target.result, file.name); if (logs.length > 0) { // Агрегируем данные logs.forEach(log => aggregateLogData(log)); } processedFilesCount++; fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}`; resolve(processFilesSequentially(files, index + 1)); } catch (error) { console.warn(`Ошибка парсинга файла ${file.name}:`, error.message); resolve(processFilesSequentially(files, index + 1)); } }; reader.onerror = function() { console.warn(`Ошибка чтения файла ${file.name}`); resolve(processFilesSequentially(files, index + 1)); }; reader.readAsText(file); }); } // Оригинальный парсинг файла логов из первого варианта function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Пропускаем служебные строки if (trimmedLine.startsWith('#')) continue; try { // Простой парсинг - ищем дату и основные поля const parts = trimmedLine.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; // Ищем метод (GET/POST) после времени let methodIndex = -1; for (let j = dateIndex + 2; j < parts.length; j++) { if (parts[j] === 'GET' || parts[j] === 'POST') { methodIndex = j; break; } } if (methodIndex === -1) continue; const method = parts[methodIndex]; const page = parts[methodIndex + 1] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } // Ищем IP клиента (формат xxx.xxx.xxx.xxx) let ip = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) { ip = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } catch (e) { // Пропускаем строки с ошибками continue; } } return logs; } // Агрегация данных лога function aggregateLogData(log) { // Сохраняем полный лог для таблицы aggregatedData.allLogs.push(log); // Обновляем диапазон дат aggregatedData.allDates.add(log.date); if (!aggregatedData.minDate || log.date < aggregatedData.minDate) { aggregatedData.minDate = log.date; } if (!aggregatedData.maxDate || log.date > aggregatedData.maxDate) { aggregatedData.maxDate = log.date; } // Считаем посещения по датам aggregatedData.dateCounts[log.date] = (aggregatedData.dateCounts[log.date] || 0) + 1; // Считаем популярные страницы aggregatedData.pageCounts[log.page] = (aggregatedData.pageCounts[log.page] || 0) + 1; // Для уникальных посещений const uniqueKey = `${log.date}_${log.user}_${log.page}`; if (!aggregatedData.userPageDateMap[uniqueKey]) { aggregatedData.userPageDateMap[uniqueKey] = log; } } // Получение уникальных посещений (пользователь + страница + дата) function getUniqueVisits(logs) { const uniqueMap = new Map(); const uniqueLogs = []; logs.forEach(log => { const key = `${log.date}_${log.user}_${log.page}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); uniqueLogs.push(log); } }); return uniqueLogs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { let logsToFilter = showUnique ? getUniqueVisits(aggregatedData.allLogs) : aggregatedData.allLogs; const filteredLogs = logsToFilter.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(filteredLogs); updateCharts(filteredLogs); updatePopularPagesList(filteredLogs); } function updateStatsTable(filteredLogs) { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts(filteredLogs) { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList(filteredLogs) { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = [...new Set(aggregatedData.allLogs.map(log => log.date))].sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { handleDateClick(dateString); }); } calendar.appendChild(dayElement); } updateCalendarSelection(); } function handleDateClick(dateString) { if (isShiftPressed && selectedDateStart) { // Выбор периода selectedDateEnd = dateString; // Убедимся, что start раньше end if (new Date(selectedDateStart) > new Date(selectedDateEnd)) { [selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart]; } dateFrom.value = selectedDateStart; dateTo.value = selectedDateEnd; } else { // Выбор одной даты selectedDateStart = dateString; selectedDateEnd = dateString; dateFrom.value = dateString; dateTo.value = dateString; } updateCalendarSelection(); applyFilters(); } function updateCalendarSelection() { // Сбрасываем все выделения document.querySelectorAll('.calendar-day').forEach(day => { day.classList.remove('selected', 'in-range'); }); if (selectedDateStart) { const startDate = new Date(selectedDateStart); const endDate = new Date(selectedDateEnd || selectedDateStart); document.querySelectorAll('.calendar-day').forEach(day => { const dateString = day.dataset.date; if (!dateString) return; const currentDate = new Date(dateString); if (currentDate >= startDate && currentDate <= endDate) { if (currentDate.getTime() === startDate.getTime() || currentDate.getTime() === endDate.getTime()) { day.classList.add('selected'); } else { day.classList.add('in-range'); } } }); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; selectedDateStart = dateRange.min; selectedDateEnd = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Показываем сохраненный путь при загрузке const savedPath = loadPathFromStorage(); if (savedPath) { fileInfoDiv.textContent = `Последний путь: ${savedPath}`; } }); </script> </body> </html>
|
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; position: relative; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.in-range { background-color: #ffa07a; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .stats-toggle { margin-bottom: 15px; display: flex; gap: 10px; } .toggle-btn { background-color: #e9ecef; border: 1px solid #ced4da; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .toggle-btn.active { background-color: #3498db; color: white; border-color: #3498db; } .calendar-hint { margin-top: 10px; font-size: 14px; color: #6c757d; text-align: center; } .debug-section { margin-top: 10px; padding: 10px; background-color: #fff3cd; border-radius: 4px; font-size: 12px; display: none; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } .stats-toggle { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearPath" class="btn btn-secondary">Очистить путь</button> <button id="debugInfo" class="btn btn-secondary">Отладочная информация</button> <div id="fileInfo" class="file-info"></div> <div id="debugSection" class="debug-section"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <div class="stats-toggle"> <button id="showAllVisits" class="toggle-btn active">Все посещения</button> <button id="showUniqueVisits" class="toggle-btn">Уникальные посещения</button> </div> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> <div class="calendar-hint" id="calendarHint"> Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода. </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearPathBtn = document.getElementById('clearPath'); const debugInfoBtn = document.getElementById('debugInfo'); const showAllVisitsBtn = document.getElementById('showAllVisits'); const showUniqueVisitsBtn = document.getElementById('showUniqueVisits'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const debugSection = document.getElementById('debugSection'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const calendarHint = document.getElementById('calendarHint'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); // Храним все логи для детальной статистики (как в исходнике) let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDateStart = null; let selectedDateEnd = null; let selectedFiles = []; let showUnique = false; let isShiftPressed = false; let processedFilesCount = 0; // Отслеживаем нажатие Shift document.addEventListener('keydown', function(e) { if (e.key === 'Shift') { isShiftPressed = true; calendarHint.textContent = 'Shift нажат. Кликните на вторую дату для выбора периода.'; } }); document.addEventListener('keyup', function(e) { if (e.key === 'Shift') { isShiftPressed = false; calendarHint.textContent = 'Кликните на дату для выбора одного дня. Зажмите Shift и кликните для выбора периода.'; } }); // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение между всеми и уникальными посещениями showAllVisitsBtn.addEventListener('click', function() { showUnique = false; showAllVisitsBtn.classList.add('active'); showUniqueVisitsBtn.classList.remove('active'); applyFilters(); }); showUniqueVisitsBtn.addEventListener('click', function() { showUnique = true; showAllVisitsBtn.classList.remove('active'); showUniqueVisitsBtn.classList.add('active'); applyFilters(); }); // Отладочная информация debugInfoBtn.addEventListener('click', function() { debugSection.style.display = 'block'; debugSection.innerHTML = ` <strong>Отладочная информация:</strong><br> Обработано файлов: ${processedFilesCount}<br> Всего записей: ${allLogs.length}<br> Уникальных дат: ${[...new Set(allLogs.map(log => log.date))].length}<br> Диапазон дат: ${dateRange.min || 'нет'} - ${dateRange.max || 'нет'}<br> Пример данных: ${JSON.stringify(allLogs.slice(0, 2))} `; }); // Сохранение пути в localStorage function savePathToStorage(path) { try { localStorage.setItem('iisLogsPath', path); } catch (e) { console.warn('Не удалось сохранить путь в localStorage:', e.message); } } // Загрузка пути из localStorage function loadPathFromStorage() { try { return localStorage.getItem('iisLogsPath'); } catch (e) { console.warn('Не удалось загрузить путь из localStorage:', e.message); return null; } } // Очистка пути clearPathBtn.addEventListener('click', function() { try { localStorage.removeItem('iisLogsPath'); fileInfoDiv.textContent = 'Путь очищен'; selectedFiles = []; resetData(); } catch (e) { console.warn('Не удалось очистить путь:', e.message); } }); // Сброс данных function resetData() { allLogs = []; filteredLogs = []; processedFilesCount = 0; statsTableBody.innerHTML = ''; updateCharts(); updatePopularPagesList(); updateCalendar(); debugSection.style.display = 'none'; } // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Файлы: ${selectedFiles.map(f => f.name).join(', ')}`); } }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); if (selectedFiles.length > 0) { savePathToStorage(`Папка с ${selectedFiles.length} файлами`); } }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов с пошаговой обработкой loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; debugSection.style.display = 'none'; resetData(); processedFilesCount = 0; // Обработка файлов последовательно processFilesSequentially(selectedFiles, 0) .then(() => { if (allLogs.length === 0) { throw new Error('Не удалось распознать данные ни в одном файле'); } dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; fileInfoDiv.textContent = `Успешно обработано файлов: ${processedFilesCount}/${selectedFiles.length}, записей: ${allLogs.length}`; }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); }); }); // Последовательная обработка файлов function processFilesSequentially(files, index) { if (index >= files.length) { return Promise.resolve(); } const file = files[index]; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { const logs = parseLogFile(e.target.result, file.name); if (logs.length > 0) { allLogs = allLogs.concat(logs); console.log(`Файл ${file.name}: обработано ${logs.length} записей`); } else { console.warn(`Файл ${file.name}: не найдено валидных записей`); } processedFilesCount++; fileInfoDiv.textContent = `Обработано файлов: ${processedFilesCount}/${files.length}, записей: ${allLogs.length}`; resolve(processFilesSequentially(files, index + 1)); } catch (error) { console.warn(`Ошибка парсинга файла ${file.name}:`, error.message); resolve(processFilesSequentially(files, index + 1)); } }; reader.onerror = function() { console.warn(`Ошибка чтения файла ${file.name}`); resolve(processFilesSequentially(files, index + 1)); }; reader.readAsText(file); }); } // Улучшенный парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; // Получаем дату из заголовка файла (#Date:) let fileDate = ''; for (const line of lines) { if (line.startsWith('#Date:')) { const dateMatch = line.match(/#Date:\s*(\d{4}-\d{2}-\d{2})/); if (dateMatch) { fileDate = dateMatch[1]; break; } } } for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Пропускаем служебные строки if (trimmedLine.startsWith('#')) continue; try { // Простой парсинг - ищем дату и основные поля const parts = trimmedLine.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД (из самой строки) let dateIndex = -1; let logDate = fileDate; // Используем дату из заголовка по умолчанию for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; logDate = parts[j]; break; } } if (dateIndex === -1 && !fileDate) continue; const time = parts[dateIndex + 1] || ''; // Ищем метод (GET/POST) после времени let methodIndex = -1; for (let j = dateIndex + 2; j < parts.length; j++) { if (parts[j] === 'GET' || parts[j] === 'POST') { methodIndex = j; break; } } if (methodIndex === -1) continue; const method = parts[methodIndex]; const page = parts[methodIndex + 1] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } // Ищем IP клиента (формат xxx.xxx.xxx.xxx) let ip = ''; for (let j = methodIndex + 2; j < parts.length; j++) { if (parts[j].match(/^\d+\.\d+\.\d+\.\d+$/)) { ip = parts[j]; break; } } if (logDate && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico' && page !== '/') { logs.push({ date: logDate, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); } } catch (e) { // Пропускаем строки с ошибками continue; } } return logs; } // Получение уникальных посещений (пользователь + страница + дата) function getUniqueVisits(logs) { const uniqueMap = new Map(); const uniqueLogs = []; logs.forEach(log => { const key = `${log.date}_${log.user}_${log.page}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, true); uniqueLogs.push(log); } }); return uniqueLogs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { let logsToFilter = showUnique ? getUniqueVisits(allLogs) : allLogs; filteredLogs = logsToFilter.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.data.datasets[0].label = showUnique ? 'Уникальные посещения' : 'Все посещения'; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); calendar.innerHTML = ''; const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); let firstDayOfWeek = firstDayOfMonth.getDay(); firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; dayElement.addEventListener('click', function() { handleDateClick(dateString); }); } calendar.appendChild(dayElement); } updateCalendarSelection(); } function handleDateClick(dateString) { if (isShiftPressed && selectedDateStart) { // Выбор периода selectedDateEnd = dateString; // Убедимся, что start раньше end if (new Date(selectedDateStart) > new Date(selectedDateEnd)) { [selectedDateStart, selectedDateEnd] = [selectedDateEnd, selectedDateStart]; } dateFrom.value = selectedDateStart; dateTo.value = selectedDateEnd; } else { // Выбор одной даты selectedDateStart = dateString; selectedDateEnd = dateString; dateFrom.value = dateString; dateTo.value = dateString; } updateCalendarSelection(); applyFilters(); } function updateCalendarSelection() { // Сбрасываем все выделения document.querySelectorAll('.calendar-day').forEach(day => { day.classList.remove('selected', 'in-range'); }); if (selectedDateStart) { const startDate = new Date(selectedDateStart); const endDate = new Date(selectedDateEnd || selectedDateStart); document.querySelectorAll('.calendar-day').forEach(day => { const dateString = day.dataset.date; if (!dateString) return; const currentDate = new Date(dateString); if (currentDate >= startDate && currentDate <= endDate) { if (currentDate.getTime() === startDate.getTime() || currentDate.getTime() === endDate.getTime()) { day.classList.add('selected'); } else { day.classList.add('in-range'); } } }); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; selectedDateStart = dateRange.min; selectedDateEnd = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Показываем сохраненный путь при загрузке const savedPath = loadPathFromStorage(); if (savedPath) { fileInfoDiv.textContent = `Последний путь: ${savedPath}`; } }); </script> </body> </html>
|
|
|
|
|
|
|
|
|