USER
// ==UserScript==
// @name 종합 웹 크롤러 8.6 초격차 개선 버전
// @namespace http://tampermonkey.net/
// @version 8.6.2
// @description 고급 기능을 갖춘 웹 크롤러로 더 빠르고 많은 검색 결과를 실시간으로 수집하고 자세한 분석을 제공합니다 (초격차 개선 버전)
// @author YourName
// @match *://www.google.com/search*
// @match *://www.bing.com/search*
// @match *://search.daum.net/search*
// @match *://search.naver.com/search*
// @match *://search.yahoo.com/search*
// @match *://duckduckgo.com/*
// @match *://www.startpage.com/do/search*
// @match *://www.ecosia.org/search*
// @match *://www.qwant.com/*
// @match *://ahmia.fi/*
// @match *://torchsearch.de/*
// @match *://notracker.org/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @require https://cdn.jsdelivr.net/npm/chart.js
// ==/UserScript==
(function() {
'use strict';
// *******************************
// API 키 설정 (필요 시 본인의 API 키로 변경)
// *******************************
const WHOIS_API_KEY = 'YOUR_JSONWHOIS_API_KEY'; // 여기에 본인의 JSONWHOIS API 키를 입력하세요.
GM_addStyle(`
/* UI 전반적인 스타일 개선 */
#crawler-container {
position: fixed;
top: 20px;
right: 20px;
background-color: #ffffff;
border: 3px solid #2196F3;
padding: 15px;
z-index: 10000;
width: 700px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
border-radius: 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
resize: both;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
#crawler-container header {
cursor: move;
background-color: #2196F3;
color: white;
padding: 10px 15px;
border-radius: 8px 8px 0 0;
text-align: center;
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
#crawler-container header .title {
flex-grow: 1;
}
#crawler-container header .close-btn {
cursor: pointer;
font-size: 24px;
color: #FF5252;
margin-left: 15px;
}
#crawler-container .tabs {
display: flex;
border-bottom: 3px solid #2196F3;
margin-top: 10px;
}
#crawler-container .tab {
flex: 1;
padding: 10px 0;
text-align: center;
cursor: pointer;
background-color: #E3F2FD;
border-right: 1px solid #BBDEFB;
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
font-size: 14px;
}
#crawler-container .tab:last-child {
border-right: none;
}
#crawler-container .tab.active {
background-color: #FFFFFF;
border-bottom: 4px solid #2196F3;
color: #2196F3;
}
#crawler-container .tab-content {
display: none;
padding: 15px;
overflow-y: auto;
flex-grow: 1;
}
#crawler-container .tab-content.active {
display: block;
}
#crawler-container button {
padding: 8px 14px;
margin: 5px 3px;
border: none;
border-radius: 5px;
background-color: #2196F3;
color: white;
cursor: pointer;
transition: background-color 0.3s;
font-size: 14px;
}
#crawler-container button:hover {
background-color: #1976D2;
}
#crawler-container button.export-btn {
background-color: #4CAF50;
}
#crawler-container button.export-btn:hover {
background-color: #388E3C;
}
#crawler-container button.pause-btn {
background-color: #FFC107;
}
#crawler-container button.pause-btn:hover {
background-color: #FFA000;
}
#crawler-container .options,
#crawler-container .filters,
#crawler-container .exclude-domains,
#crawler-container .advanced-options,
#crawler-container .controls,
#crawler-container .search-engines {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
#crawler-container .options label,
#crawler-container .advanced-options label,
#crawler-container .controls label,
#crawler-container .search-engines label {
margin-right: 15px;
display: inline-flex;
align-items: center;
font-weight: normal;
font-size: 14px;
}
#crawler-container .options input[type="checkbox"],
#crawler-container .advanced-options input[type="checkbox"],
#crawler-container .controls input[type="number"],
#crawler-container .search-engines select {
margin-right: 8px;
}
#crawler-container input[type="text"],
#crawler-container textarea,
#crawler-container select {
width: 100%;
padding: 8px;
margin: 6px 0 10px 0;
border: 1px solid #B0BEC5;
border-radius: 5px;
font-size: 14px;
}
#crawler-container table {
border-collapse: collapse;
width: 100%;
margin-top: 10px;
font-size: 14px;
}
#crawler-container th, #crawler-container td {
padding: 10px;
border: 1px solid #E0E0E0;
text-align: left;
vertical-align: top;
}
#crawler-container th {
background-color: #2196F3;
color: white;
position: sticky;
top: 0;
z-index: 1;
cursor: pointer;
}
#crawler-container .log {
max-height: 180px;
overflow-y: auto;
background-color: #E3F2FD;
padding: 10px;
border: 1px solid #81D4FA;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
}
#crawler-container progress {
width: 80%;
height: 20px;
margin-right: 10px;
border-radius: 5px;
}
#crawler-container .status {
margin-top: 10px;
font-weight: bold;
font-size: 14px;
}
#crawler-container .chart-container {
position: relative;
height: 250px;
width: 100%;
margin-top: 20px;
}
#crawler-container .notification-area {
flex-direction: row;
align-items: center;
width: 100%;
}
@media (max-width: 800px) {
#crawler-container {
width: 95%;
right: 2.5%;
}
#crawler-container .controls,
#crawler-container .options,
#crawler-container .advanced-options,
#crawler-container .notification-area,
#crawler-container .search-engines {
flex-direction: column;
align-items: flex-start;
}
#crawler-container .search-engines select {
width: 100%;
}
#crawler-container progress {
width: 100%;
}
}
/* WHOIS Modal 스타일 */
#whois-modal {
display: none;
position: fixed;
z-index: 10001;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 900px;
height: 80vh;
background-color: #ffffff;
border: 3px solid #2196F3;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
overflow: hidden;
flex-direction: column;
display: flex;
}
#whois-modal header {
background-color: #2196F3;
color: white;
padding: 12px 20px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
#whois-modal header .close-modal-btn {
cursor: pointer;
font-size: 24px;
color: #FF5252;
}
#whois-modal .modal-content {
padding: 20px;
overflow-y: auto;
flex-grow: 1;
background-color: #E3F2FD;
}
/* Tooltip 스타일 */
.preview-tooltip {
position: absolute;
background-color: #ffffff;
border: 1px solid #90CAF9;
border-radius: 5px;
padding: 8px;
z-index: 10002;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: none;
width: 300px;
}
.preview-tooltip iframe {
width: 100%;
height: 200px;
border: none;
border-radius: 5px;
}
`);
const container = document.createElement('div');
container.id = 'crawler-container';
container.innerHTML = `
<header>
<span class="title">🔥 웹 크롤러 8.6</span>
<span class="close-btn" title="닫기">×</span>
</header>
<div class="tabs">
<div class="tab active" data-tab="settings">설정</div>
<div class="tab" data-tab="results">결과</div>
<div class="tab" data-tab="logs">로그</div>
<div class="tab" data-tab="analytics">분석</div>
<div class="tab" data-tab="about">정보</div>
</div>
<div class="tab-content active" id="settings">
<div class="notification-area">
<button id="startBtn" title="크롤링 시작">▶️ 시작</button>
<button id="pauseBtn" class="pause-btn" title="크롤링 일시정지/재개">⏸️ 일시정지</button>
<button id="stopBtn" title="크롤링 중지">⏹️ 중지</button>
<button id="resetBtn" title="크롤링 초기화">🔄 초기화</button>
<button id="exportBtn" class="export-btn" title="결과 내보내기">💾 내보내기</button>
<button id="clearLogsBtn" title="로그 초기화">🧹 로그 초기화</button>
</div>
<div class="search-engines">
<label for="searchEngineSelect">검색 엔진 선택:</label>
<select id="searchEngineSelect">
<option value="google" selected>Google</option>
<option value="bing">Bing</option>
<option value="daum">Daum</option>
<option value="naver">Naver</option>
<option value="yahoo">Yahoo</option>
<option value="duckduckgo">DuckDuckGo</option>
<option value="startpage">StartPage</option>
<option value="ecosia">Ecosia</option>
<option value="qwant">Qwant</option>
<option value="ahmia">Ahmia (Tor)</option>
<option value="torch">Torch (Tor)</option>
<option value="notracker">NOTRACKER (Tor)</option>
</select>
</div>
<div class="options">
<label><input type="checkbox" id="toggleHTML" checked> HTML 가져오기</label>
<label><input type="checkbox" id="toggleImages" checked> 이미지 가져오기</label>
<label><input type="checkbox" id="toggleAds" checked> 광고 제외</label>
<label><input type="checkbox" id="toggleTechStack" checked> 기술 스택 분석</label>
<label><input type="checkbox" id="toggleOpenGraph" checked> OpenGraph 정보 가져오기</label>
<label><input type="checkbox" id="toggleSecurityHeaders" checked> 보안 헤더 분석</label>
<label><input type="checkbox" id="toggleDomainAge" checked> 도메인 연령 분석</label>
</div>
<div class="filters">
<label for="filterInput">필터 단어 (정규식 또는 단어, 쉼표로 구분):</label>
<input type="text" id="filterInput" placeholder="예: 기부, 광고, /promo/i">
</div>
<div class="exclude-domains">
<label for="excludeDomainsInput">제외할 도메인 (쉼표로 구분):</label>
<input type="text" id="excludeDomainsInput" placeholder="예: facebook.com, youtube.com">
</div>
<div class="advanced-options">
<label for="duplicateThreshold">중복 허용 횟수:</label>
<input type="number" id="duplicateThreshold" value="2" min="1" style="width: 70px;">
<label for="retryAttempts">재시도 횟수:</label>
<input type="number" id="retryAttempts" value="3" min="1" style="width: 70px;">
</div>
<div style="margin-top:15px;">
<label for="progressBar">진행률:</label>
<progress id="progressBar" value="0" max="100"></progress>
<span id="progressText">0%</span>
</div>
<div class="status">
현재 페이지: <span id="currentPage">0</span> /
총 페이지: <span id="totalPages">0</span>
<br>
방문된 사이트: <span id="visitedCount">0</span>
<br>
총 요청 수: <span id="totalRequests">0</span>
<br>
성공 요청 수: <span id="successfulRequests">0</span>
<br>
실패 요청 수: <span id="failedRequests">0</span>
<br>
경과 시간: <span id="elapsedTime">00:00:00</span>
<br>
예상 남은 시간: <span id="remainingTime">00:00:00</span>
</div>
</div>
<div class="tab-content" id="results">
<div style="margin-top:10px; max-height: 350px; overflow: auto;">
<table id="resultsTable">
<thead>
<tr>
<th data-sort="0">사이트 이름 ▲▼</th>
<th data-sort="1">주소 ▲▼</th>
<th data-sort="2">설명 ▲▼</th>
<th>이미지</th>
<th>HTML 링크</th>
<th>WHOIS 정보</th>
<th data-sort="6">페이지 로딩 시간 (ms) ▲▼</th>
<th>언어</th>
<th>메타 키워드</th>
<th>메타 robots</th>
<th>OpenGraph</th>
<th>보안 헤더</th>
<th>도메인 연령</th>
<th>기술 스택</th>
</tr>
</thead>
<tbody id="resultsBody">
</tbody>
</table>
</div>
<div style="margin-top:15px;">
<input type="text" id="searchResultsInput" placeholder="결과 검색...">
</div>
</div>
<div class="tab-content" id="logs">
<div class="log" id="log">
<strong>로그:</strong>
</div>
</div>
<div class="tab-content" id="analytics">
<div class="chart-container">
<canvas id="crawlChart"></canvas>
</div>
<div class="chart-container">
<canvas id="loadTimeChart"></canvas>
</div>
<div class="chart-container">
<canvas id="domainDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="errorRateChart"></canvas>
</div>
<div class="chart-container">
<canvas id="cumulativeVisitsChart"></canvas>
</div>
<div class="chart-container">
<canvas id="keywordDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="languageDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="techStackChart"></canvas>
</div>
<div class="chart-container">
<canvas id="domainAgeChart"></canvas>
</div>
<div id="whois-modal">
<header>
<span>WHOIS 정보</span>
<span class="close-modal-btn" title="닫기">×</span>
</header>
<div class="modal-content" id="whoisContent">
<!-- WHOIS 정보가 여기에 표시됩니다 -->
</div>
</div>
</div>
<div class="tab-content" id="about">
<h2>웹 크롤러 8.6 정보</h2>
<p>종합 웹 크롤러는 다양한 검색 엔진에서 빠르게 검색 결과를 수집하고 심층 분석할 수 있는 도구입니다. 사용자의 편의성과 데이터 가독성을 극대화하여 초격차로 개선된 버전입니다.</p>
<ul>
<li>지원하는 검색 엔진: Google, Bing, Daum, Naver, Yahoo, DuckDuckGo, StartPage, Ecosia, Qwant, Ahmia (Tor), Torch (Tor), NOTRACKER (Tor)</li>
<li>실시간 검색어 감지 및 고속 크롤링</li>
<li>고급 필터링 및 제외 옵션 제공</li>
<li>결과를 XLSX, CSV, JSON 형식으로 내보내기</li>
<li>시각화된 데이터 차트 및 그래프 제공</li>
<li>사용자 친화적인 인터페이스 및 반응형 디자인</li>
<li>결과 테이블 정렬 및 실시간 검색 기능 추가</li>
<li>상세한 로그 제공</li>
<li>페이지 로딩 시간 및 언어 분석</li>
<li>WHOIS 정보 연동 및 즉시 보기 기능</li>
<li>도메인 연령 분석 기능 추가</li>
<li>추가적인 보안 헤더 분석 기능</li>
<li>기술 스택 분석 기능 강화</li>
<li>다양한 알림 방식 지원</li>
<li>자동 스크롤 및 실시간 업데이트</li>
<li>고급 에러 핸들링 및 재시도 로직 강화</li>
<li>로그 초기화 및 결과 검색 기능 추가</li>
<li>추가 분석 탭에서 다양한 그래프 제공</li>
<li>도메인 분포 및 오류 발생률 분석</li>
<li>누적 방문 횟수 추적</li>
<li>HTML 링크 클릭 시 UI 내에서 사이트 미리보기 제공</li>
<li>마우스 호버 시 사이트 미리보기 표시</li>
<li>WHOIS 정보 바로보기 및 상세 분석 기능 추가</li>
<li>추가 메타 정보 수집 및 분석 기능 강화</li>
<li>더 빠르고 많은 사이트 정보 수집 기능 추가</li>
<li>사이트 기술 스택 심층 분석 기능 추가</li>
</ul>
<p><strong>버전:</strong> 8.6.2</p>
<p><strong>제작자:</strong> YourName</p>
<p><strong>문의:</strong> your.email@example.com</p>
</div>
`;
document.body.appendChild(container);
// 드래그 기능
(function makeDraggable(elmnt) {
const header = elmnt.querySelector('header');
const closeBtn = elmnt.querySelector('.close-btn');
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
header.onmousedown = dragMouseDown;
closeBtn.onclick = () => {
elmnt.style.display = 'none';
log('🗑 크롤러 창을 닫았습니다.');
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
})(container);
// 탭 설정
(function setupTabs() {
const tabs = container.querySelectorAll('.tab');
const tabContents = container.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = tab.getAttribute('data-tab');
tabContents.forEach(content => {
if(content.id === target){
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
});
});
})();
// 변수 초기화
let isRunning = false;
let isPaused = false;
let currentPage = 1;
let totalPages = 0;
let results = [];
let searchQuery = '';
let searchEngine = 'google';
let visitedLinks = new Map();
let logMessages = [];
let filterPatterns = [];
let excludeDomains = [];
let duplicateThreshold = 2;
let retryAttempts = 3;
let retryInterval = 100; // 고정적으로 100ms 설정
let startTime = null;
let elapsedInterval = null;
let crawlChart = null;
let loadTimeChart = null;
let domainDistributionChart = null;
let errorRateChart = null;
let cumulativeVisitsChart = null;
let keywordDistributionChart = null;
let languageDistributionChart = null;
let techStackChart = null;
let domainAgeChart = null;
let crawlData = {
labels: [],
datasets: [{
label: '방문 횟수',
data: [],
backgroundColor: 'rgba(33, 150, 243, 0.6)',
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
},
{
label: '평균 페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(76, 175, 80, 0.6)',
borderColor: 'rgba(76, 175, 80, 1)',
borderWidth: 1
}]
};
let loadTimeData = {
labels: [],
datasets: [{
label: '페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(255, 193, 7, 0.6)',
borderColor: 'rgba(255,193,7,1)',
borderWidth: 1
}]
};
let domainDistributionData = {
labels: [],
datasets: [{
label: '도메인 분포',
data: [],
backgroundColor: [
'rgba(33, 150, 243, 0.6)',
'rgba(255, 87, 34, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(158, 158, 158, 0.6)'
],
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
}]
};
let errorRateData = {
labels: [],
datasets: [{
label: '오류 발생 수',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132,1)',
borderWidth: 1
}]
};
let cumulativeVisitsData = {
labels: [],
datasets: [{
label: '누적 방문 횟수',
data: [],
backgroundColor: 'rgba(3, 169, 244, 0.6)',
borderColor: 'rgba(3, 169, 244,1)',
borderWidth: 1
}]
};
let keywordDistributionData = {
labels: [],
datasets: [{
label: '키워드 분포',
data: [],
backgroundColor: [
'rgba(63, 81, 181, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(33, 150, 243, 0.6)',
'rgba(153, 102, 255, 0.6)'
],
borderColor: 'rgba(63, 81, 181,1)',
borderWidth: 1
}]
};
let languageDistributionData = {
labels: [],
datasets: [{
label: '언어 분포',
data: [],
backgroundColor: [
'rgba(38, 166, 154, 0.6)',
'rgba(211, 47, 47, 0.6)',
'rgba(3, 169, 244, 0.6)',
'rgba(255, 193, 7, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(0, 188, 212, 0.6)'
],
borderColor: 'rgba(38, 166, 154,1)',
borderWidth: 1
}]
};
let techStackData = {
labels: [],
datasets: [{
label: '기술 스택 분포',
data: [],
backgroundColor: [
'rgba(103, 58, 183, 0.6)',
'rgba(255, 202, 40, 0.6)',
'rgba(0, 150, 136, 0.6)',
'rgba(233, 30, 99, 0.6)',
'rgba(245, 124, 0, 0.6)',
'rgba(255, 235, 59, 0.6)'
],
borderColor: 'rgba(103, 58, 183,1)',
borderWidth: 1
}]
};
let domainAgeData = {
labels: [],
datasets: [{
label: '도메인 연령 분포 (년)',
data: [],
backgroundColor: [
'rgba(255, 159, 64, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(153, 102, 255, 0.6)',
'rgba(255, 99, 132, 0.6)'
],
borderColor: 'rgba(255, 159, 64,1)',
borderWidth: 1
}]
};
// 요소 참조
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const exportBtn = document.getElementById('exportBtn');
const clearLogsBtn = document.getElementById('clearLogsBtn');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultsBody = document.getElementById('resultsBody');
const logContainer = document.getElementById('log');
const currentPageElem = document.getElementById('currentPage');
const totalPagesElem = document.getElementById('totalPages');
const toggleHTML = document.getElementById('toggleHTML');
const toggleImages = document.getElementById('toggleImages');
const toggleAds = document.getElementById('toggleAds');
const toggleTechStack = document.getElementById('toggleTechStack');
const toggleOpenGraph = document.getElementById('toggleOpenGraph');
const toggleSecurityHeaders = document.getElementById('toggleSecurityHeaders');
const toggleDomainAge = document.getElementById('toggleDomainAge');
const filterInput = document.getElementById('filterInput');
const excludeDomainsInput = document.getElementById('excludeDomainsInput');
const duplicateThresholdInput = document.getElementById('duplicateThreshold');
const retryAttemptsInput = document.getElementById('retryAttempts');
const visitedCountElem = document.getElementById('visitedCount');
const elapsedTimeElem = document.getElementById('elapsedTime');
const remainingTimeElem = document.getElementById('remainingTime');
const crawlChartCtx = document.getElementById('crawlChart').getContext('2d');
const loadTimeChartCtx = document.getElementById('loadTimeChart').getContext('2d');
const domainDistributionChartCtx = document.getElementById('domainDistributionChart').getContext('2d');
const errorRateChartCtx = document.getElementById('errorRateChart').getContext('2d');
const cumulativeVisitsChartCtx = document.getElementById('cumulativeVisitsChart').getContext('2d');
const keywordDistributionChartCtx = document.getElementById('keywordDistributionChart').getContext('2d');
const languageDistributionChartCtx = document.getElementById('languageDistributionChart').getContext('2d');
const techStackChartCtx = document.getElementById('techStackChart').getContext('2d');
const domainAgeChartCtx = document.getElementById('domainAgeChart').getContext('2d');
const searchEngineSelect = document.getElementById('searchEngineSelect');
const searchResultsInput = document.getElementById('searchResultsInput');
const whoisModal = document.getElementById('whois-modal');
const whoisContent = document.getElementById('whoisContent');
const closeModalBtn = whoisModal.querySelector('.close-modal-btn');
// 미리보기 툴팁 생성
const tooltip = document.createElement('div');
tooltip.className = 'preview-tooltip';
document.body.appendChild(tooltip);
// 이벤트 리스너 등록
startBtn.addEventListener('click', startCrawling);
pauseBtn.addEventListener('click', togglePause);
stopBtn.addEventListener('click', stopCrawling);
resetBtn.addEventListener('click', resetCrawling);
exportBtn.addEventListener('click', exportResults);
clearLogsBtn.addEventListener('click', clearLogs);
filterInput.addEventListener('input', filterResults);
excludeDomainsInput.addEventListener('input', updateExcludeDomains);
duplicateThresholdInput.addEventListener('change', updateDuplicateThreshold);
retryAttemptsInput.addEventListener('change', updateRetryAttempts);
searchEngineSelect.addEventListener('change', updateSearchEngine);
searchResultsInput.addEventListener('input', filterResults);
closeModalBtn.addEventListener('click', () => {
whoisModal.style.display = 'none';
whoisContent.innerHTML = '';
});
window.addEventListener('beforeunload', () => {
resetCrawling();
});
// 로그 및 크롤 데이터 로드
loadLogs();
loadCrawlData();
loadSettings();
// 차트 초기화 함수
function initializeChart() {
crawlChart = new Chart(crawlChartCtx, {
type: 'bar',
data: crawlData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
loadTimeChart = new Chart(loadTimeChartCtx, {
type: 'line',
data: loadTimeData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
domainDistributionChart = new Chart(domainDistributionChartCtx, {
type: 'pie',
data: domainDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
errorRateChart = new Chart(errorRateChartCtx, {
type: 'bar',
data: errorRateData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
cumulativeVisitsChart = new Chart(cumulativeVisitsChartCtx, {
type: 'line',
data: cumulativeVisitsData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
keywordDistributionChart = new Chart(keywordDistributionChartCtx, {
type: 'doughnut',
data: keywordDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
languageDistributionChart = new Chart(languageDistributionChartCtx, {
type: 'pie',
data: languageDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
techStackChart = new Chart(techStackChartCtx, {
type: 'bar',
data: techStackData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
domainAgeChart = new Chart(domainAgeChartCtx, {
type: 'bar',
data: domainAgeData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '연령 (년)'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
}
initializeChart();
// 설정 로드
function loadSettings() {
const storedSearchEngine = GM_getValue('searchEngine', 'google');
searchEngine = storedSearchEngine;
searchEngineSelect.value = searchEngine;
const storedToggleHTML = GM_getValue('toggleHTML', true);
toggleHTML.checked = storedToggleHTML;
const storedToggleImages = GM_getValue('toggleImages', true);
toggleImages.checked = storedToggleImages;
const storedToggleAds = GM_getValue('toggleAds', true);
toggleAds.checked = storedToggleAds;
const storedToggleTechStack = GM_getValue('toggleTechStack', true);
toggleTechStack.checked = storedToggleTechStack;
const storedToggleOpenGraph = GM_getValue('toggleOpenGraph', true);
toggleOpenGraph.checked = storedToggleOpenGraph;
const storedToggleSecurityHeaders = GM_getValue('toggleSecurityHeaders', true);
toggleSecurityHeaders.checked = storedToggleSecurityHeaders;
const storedToggleDomainAge = GM_getValue('toggleDomainAge', true);
toggleDomainAge.checked = storedToggleDomainAge;
const storedDuplicateThreshold = GM_getValue('duplicateThreshold', 2);
duplicateThreshold = storedDuplicateThreshold;
duplicateThresholdInput.value = duplicateThreshold;
const storedRetryAttempts = GM_getValue('retryAttempts', 3);
retryAttempts = storedRetryAttempts;
retryAttemptsInput.value = retryAttempts;
const storedFilterInput = GM_getValue('filterInput', '');
filterInput.value = storedFilterInput;
updateFilterPatterns();
const storedExcludeDomainsInput = GM_getValue('excludeDomainsInput', '');
excludeDomainsInput.value = storedExcludeDomainsInput;
updateExcludeDomains();
}
// 설정 저장 함수
function saveSettings() {
GM_setValue('searchEngine', searchEngine);
GM_setValue('toggleHTML', toggleHTML.checked);
GM_setValue('toggleImages', toggleImages.checked);
GM_setValue('toggleAds', toggleAds.checked);
GM_setValue('toggleTechStack', toggleTechStack.checked);
GM_setValue('toggleOpenGraph', toggleOpenGraph.checked);
GM_setValue('toggleSecurityHeaders', toggleSecurityHeaders.checked);
GM_setValue('toggleDomainAge', toggleDomainAge.checked);
GM_setValue('duplicateThreshold', duplicateThreshold);
GM_setValue('retryAttempts', retryAttempts);
GM_setValue('filterInput', filterInput.value.trim());
GM_setValue('excludeDomainsInput', excludeDomainsInput.value.trim());
}
// 검색 엔진 변경 시 처리
function updateSearchEngine() {
searchEngine = searchEngineSelect.value;
GM_setValue('searchEngine', searchEngine);
log(`🔄 검색 엔진 변경: ${capitalizeFirstLetter(searchEngine)}`);
}
// 다양한 설정 변경 시 설정 저장
toggleHTML.addEventListener('change', saveSettings);
toggleImages.addEventListener('change', saveSettings);
toggleAds.addEventListener('change', saveSettings);
toggleTechStack.addEventListener('change', saveSettings);
toggleOpenGraph.addEventListener('change', saveSettings);
toggleSecurityHeaders.addEventListener('change', saveSettings);
toggleDomainAge.addEventListener('change', saveSettings);
filterInput.addEventListener('change', saveSettings);
excludeDomainsInput.addEventListener('change', saveSettings);
// 로그 로드
function loadLogs() {
const storedLogs = GM_getValue('crawlerLogs', []);
if (storedLogs.length > 0) {
logMessages = JSON.parse(storedLogs);
logContainer.innerHTML = `<strong>로그:</strong><br>` + logMessages.join('<br>');
logContainer.scrollTop = logContainer.scrollHeight;
}
}
// 필터 패턴 업데이트
function updateFilterPatterns() {
const input = filterInput.value.trim();
filterPatterns = input ? input.split(',').map(word => {
word = word.trim();
if (word.startsWith('/') && word.endsWith('/')) {
try {
return new RegExp(word.slice(1, -1), 'i');
} catch (e) {
log(`⚠️ 유효하지 않은 정규식: ${word}`);
return null;
}
} else {
return new RegExp(word, 'i');
}
}).filter(regex => regex !== null) : [];
log(`🛠 필터 패턴 업데이트: [${filterPatterns.map(r => r.toString()).join(', ')}]`);
saveSettings();
}
// 제외 도메인 업데이트
function updateExcludeDomains() {
const input = excludeDomainsInput.value.trim();
excludeDomains = input ? input.split(',').map(domain => domain.trim().toLowerCase()) : [];
log(`🚫 제외 도메인 업데이트: [${excludeDomains.join(', ')}]`);
saveSettings();
}
// 중복 허용 횟수 업데이트
function updateDuplicateThreshold() {
const value = parseInt(duplicateThresholdInput.value);
duplicateThreshold = isNaN(value) || value < 1 ? 2 : value;
log(`🔄 중복 허용 횟수 설정: ${duplicateThreshold}`);
saveSettings();
}
// 재시도 횟수 업데이트
function updateRetryAttempts() {
const value = parseInt(retryAttemptsInput.value);
retryAttempts = isNaN(value) || value < 1 ? 3 : value;
log(`🔄 재시도 횟수 설정: ${retryAttempts}`);
saveSettings();
}
// 크롤링 시작 함수
async function startCrawling() {
if (isRunning) {
alert('이미 크롤링이 진행 중입니다.');
return;
}
getSearchQueryAndEngine();
if (!searchQuery) {
alert('현재 페이지에서 검색어를 찾을 수 없습니다.');
log('⚠️ 검색어 미입력 또는 인식 실패.');
return;
}
isRunning = true;
isPaused = false;
currentPage = 1;
totalPages = 0;
results = [];
visitedLinks.clear();
resultsBody.innerHTML = '';
visitedCountElem.innerText = '0';
document.getElementById('totalRequests').innerText = '0';
document.getElementById('successfulRequests').innerText = '0';
document.getElementById('failedRequests').innerText = '0';
elapsedTimeElem.innerText = '00:00:00';
remainingTimeElem.innerText = '00:00:00';
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
log('✅ 크롤링을 시작합니다.');
updateDuplicateThreshold();
updateRetryAttempts();
await crawlPage();
startTime = Date.now();
elapsedInterval = setInterval(updateElapsedTime, 1000);
}
// 일시정지/재개 함수
function togglePause() {
if (!isRunning) {
alert('일시정지/재개할 크롤링이 없습니다.');
return;
}
isPaused = !isPaused;
if (isPaused) {
log('⏸️ 크롤링을 일시정지하였습니다.');
pauseBtn.innerText = '▶️ 재개';
pauseBtn.title = '크롤링 재개';
} else {
log('▶️ 크롤링을 재개하였습니다.');
pauseBtn.innerText = '⏸️ 일시정지';
pauseBtn.title = '크롤링 일시정지';
crawlPage();
}
}
// 크롤링 중지 함수
function stopCrawling() {
if (!isRunning) {
alert('중지할 크롤링이 없습니다.');
return;
}
isRunning = false;
isPaused = false;
pauseBtn.innerText = '⏸️ 일시정지';
pauseBtn.title = '크롤링 일시정지';
clearInterval(elapsedInterval);
updateProgress();
log('🚫 크롤링을 중지하였습니다.');
GM_notification({
text: '크롤링이 중지되었습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
}
// 크롤링 초기화 함수
function resetCrawling() {
stopCrawling();
currentPage = 1;
totalPages = 0;
results = [];
visitedLinks.clear();
resultsBody.innerHTML = '';
visitedCountElem.innerText = '0';
document.getElementById('totalRequests').innerText = '0';
document.getElementById('successfulRequests').innerText = '0';
document.getElementById('failedRequests').innerText = '0';
progressBar.value = 0;
progressText.innerText = '0%';
elapsedTimeElem.innerText = '00:00:00';
remainingTimeElem.innerText = '00:00:00';
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
crawlData = {
labels: [],
datasets: [{
label: '방문 횟수',
data: [],
backgroundColor: 'rgba(33, 150, 243, 0.6)',
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
},
{
label: '평균 페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(76, 175, 80, 0.6)',
borderColor: 'rgba(76, 175, 80, 1)',
borderWidth: 1
}]
};
loadTimeData = {
labels: [],
datasets: [{
label: '페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(255, 193, 7, 0.6)',
borderColor: 'rgba(255,193,7,1)',
borderWidth: 1
}]
};
domainDistributionData = {
labels: [],
datasets: [{
label: '도메인 분포',
data: [],
backgroundColor: [
'rgba(33, 150, 243, 0.6)',
'rgba(255, 87, 34, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(158, 158, 158, 0.6)'
],
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
}]
};
errorRateData = {
labels: [],
datasets: [{
label: '오류 발생 수',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132,1)',
borderWidth: 1
}]
};
cumulativeVisitsData = {
labels: [],
datasets: [{
label: '누적 방문 횟수',
data: [],
backgroundColor: 'rgba(3, 169, 244, 0.6)',
borderColor: 'rgba(3, 169, 244,1)',
borderWidth: 1
}]
};
keywordDistributionData = {
labels: [],
datasets: [{
label: '키워드 분포',
data: [],
backgroundColor: [
'rgba(63, 81, 181, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(33, 150, 243, 0.6)',
'rgba(153, 102, 255, 0.6)'
],
borderColor: 'rgba(63, 81, 181,1)',
borderWidth: 1
}]
};
languageDistributionData = {
labels: [],
datasets: [{
label: '언어 분포',
data: [],
backgroundColor: [
'rgba(38, 166, 154, 0.6)',
'rgba(211, 47, 47, 0.6)',
'rgba(3, 169, 244, 0.6)',
'rgba(255, 193, 7, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(0, 188, 212, 0.6)'
],
borderColor: 'rgba(38, 166, 154,1)',
borderWidth: 1
}]
};
techStackData = {
labels: [],
datasets: [{
label: '기술 스택 분포',
data: [],
backgroundColor: [
'rgba(103, 58, 183, 0.6)',
'rgba(255, 202, 40, 0.6)',
'rgba(0, 150, 136, 0.6)',
'rgba(233, 30, 99, 0.6)',
'rgba(245, 124, 0, 0.6)',
'rgba(255, 235, 59, 0.6)'
],
borderColor: 'rgba(103, 58, 183,1)',
borderWidth: 1
}]
};
domainAgeData = {
labels: [],
datasets: [{
label: '도메인 연령 분포 (년)',
data: [],
backgroundColor: [
'rgba(255, 159, 64, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(153, 102, 255, 0.6)',
'rgba(255, 99, 132, 0.6)'
],
borderColor: 'rgba(255, 159, 64,1)',
borderWidth: 1
}]
};
if (crawlChart) {
crawlChart.destroy();
}
if (loadTimeChart) {
loadTimeChart.destroy();
}
if (domainDistributionChart) {
domainDistributionChart.destroy();
}
if (errorRateChart) {
errorRateChart.destroy();
}
if (cumulativeVisitsChart) {
cumulativeVisitsChart.destroy();
}
if (keywordDistributionChart) {
keywordDistributionChart.destroy();
}
if (languageDistributionChart) {
languageDistributionChart.destroy();
}
if (techStackChart) {
techStackChart.destroy();
}
if (domainAgeChart) {
domainAgeChart.destroy();
}
initializeChart();
log('🔄 크롤링을 초기화하였습니다.');
}
// 크롤링 페이지 함수
async function crawlPage() {
if (!isRunning || isPaused) {
return;
}
let url = constructURL(currentPage);
if (!url) {
log('❌ 지원하지 않는 검색 엔진입니다.');
alert('지원하지 않는 검색 엔진입니다.');
isRunning = false;
return;
}
log(`📄 페이지 ${currentPage} 크롤링 중: ${url}`);
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"User-Agent": navigator.userAgent,
"Accept": "text/html"
},
onload: function(response) {
if (response.status !== 200) {
log(`❌ 페이지 ${currentPage} 로드 실패: 상태 ${response.status}`);
handleCrawlError(currentPage, retryAttempts);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
return;
}
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
let items = extractItems(doc);
if (currentPage === 1) {
totalPages = determineTotalPages(doc);
totalPagesElem.innerText = totalPages;
}
log(`📊 페이지 ${currentPage}에서 ${items.length}개 항목 발견.`);
document.getElementById('totalRequests').innerText = parseInt(document.getElementById('totalRequests').innerText) + items.length;
items.forEach(item => {
processItem(item);
});
updateChart();
updateLoadTimeChart();
updateDomainDistributionChart();
updateErrorRateChart();
updateCumulativeVisitsChart();
updateKeywordDistributionChart();
updateLanguageDistributionChart();
updateTechStackChart();
updateDomainAgeChart();
updateProgress();
currentPage++;
currentPageElem.innerText = currentPage;
if (currentPage <= totalPages && isRunning && !isPaused) {
setTimeout(crawlPage, 100); // 고정된 100ms 크롤링 속도
} else {
finalizeCrawling();
}
},
onerror: function() {
log(`❌ 페이지 ${currentPage} 요청 중 오류 발생.`);
handleCrawlError(currentPage, retryAttempts);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
});
}
// 크롤링 완료 시 처리 함수
function finalizeCrawling() {
isRunning = false;
clearInterval(elapsedInterval);
log('✅ 크롤링이 완료되었습니다.');
GM_notification({
text: '크롤링이 완료되었습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
}
// 크롤링 오류 처리 함수
function handleCrawlError(page, retriesLeft) {
if (retriesLeft > 0 && isRunning) {
log(`🔄 페이지 ${page} 재시도 (${retriesLeft}회 남음).`);
setTimeout(() => {
const url = constructURL(page);
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"User-Agent": navigator.userAgent,
"Accept": "text/html"
},
onload: function(response) {
if (response.status === 200) {
log(`✅ 페이지 ${page} 재시도 성공.`);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
crawlPage();
} else {
handleCrawlError(page, retriesLeft - 1);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
},
onerror: function() {
handleCrawlError(page, retriesLeft - 1);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
});
}, retryInterval);
} else {
log(`❌ 페이지 ${page} 로드 실패: 최대 재시도 횟수 초과.`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
currentPage++;
currentPageElem.innerText = currentPage;
if (currentPage <= totalPages && isRunning && !isPaused) {
setTimeout(crawlPage, 100);
} else {
finalizeCrawling();
}
}
}
// URL 구성 함수
function constructURL(page) {
switch(searchEngine) {
case 'google':
return `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}&start=${(page-1)*10}`;
case 'bing':
return `https://www.bing.com/search?q=${encodeURIComponent(searchQuery)}&first=${(page-1)*10 +1}`;
case 'daum':
return `https://search.daum.net/search?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'naver':
return `https://search.naver.com/search.naver?query=${encodeURIComponent(searchQuery)}&start=${(page-1)*10 +1}`;
case 'yahoo':
return `https://search.yahoo.com/search?p=${encodeURIComponent(searchQuery)}&b=${(page-1)*10 +1}`;
case 'duckduckgo':
return `https://duckduckgo.com/html/?q=${encodeURIComponent(searchQuery)}&s=${(page-1)*30}`;
case 'startpage':
return `https://www.startpage.com/do/search?query=${encodeURIComponent(searchQuery)}&b=${(page-1)*10 +1}`;
case 'ecosia':
return `https://www.ecosia.org/search?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'qwant':
return `https://www.qwant.com/?q=${encodeURIComponent(searchQuery)}&t=web&s=${page}`;
case 'ahmia':
return `http://ahmia.fi/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'torch':
return `http://torchsearch.de/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'notracker':
return `http://notracker.org/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
default:
return '';
}
}
// 항목 추출 함수
function extractItems(doc) {
let items = [];
switch(searchEngine) {
case 'google':
items = doc.querySelectorAll('div.g');
break;
case 'bing':
items = doc.querySelectorAll('li.b_algo');
break;
case 'daum':
items = doc.querySelectorAll('div.wrap_cont');
break;
case 'naver':
items = doc.querySelectorAll('div.basic1');
break;
case 'yahoo':
items = doc.querySelectorAll('div.compTitle');
break;
case 'duckduckgo':
items = doc.querySelectorAll('div.result');
break;
case 'startpage':
items = doc.querySelectorAll('div.w-gl__result');
break;
case 'ecosia':
items = doc.querySelectorAll('div.result');
break;
case 'qwant':
items = doc.querySelectorAll('div.results div.result');
break;
case 'ahmia':
items = doc.querySelectorAll('div#main div.result');
break;
case 'torch':
items = doc.querySelectorAll('div.result');
break;
case 'notracker':
items = doc.querySelectorAll('div.result');
break;
default:
break;
}
return items;
}
// 총 페이지 수 결정 함수
function determineTotalPages(doc) {
let pages = 1;
switch(searchEngine) {
case 'google':
const googlePager = doc.querySelectorAll('a[href*="start="]');
pages = Math.min(googlePager.length, 10) || 1;
break;
case 'bing':
const bingPager = doc.querySelectorAll('a.sb_pagS');
pages = bingPager.length || 1;
break;
case 'daum':
const daumPager = doc.querySelectorAll('a.pagination__link');
pages = daumPager.length || 1;
break;
case 'naver':
const naverPager = doc.querySelectorAll('a.nav');
pages = naverPager.length || 1;
break;
case 'yahoo':
const yahooPager = doc.querySelectorAll('a.fz-l.tco-pagination__anchor');
pages = yahooPager.length || 1;
break;
case 'duckduckgo':
const ddgPager = doc.querySelectorAll('a.result--more__btn');
pages = ddgPager.length || 1;
break;
case 'startpage':
const spPager = doc.querySelectorAll('a.paginator__link');
pages = spPager.length || 1;
break;
case 'ecosia':
const ecosiaPager = doc.querySelectorAll('a.pagination__link');
pages = ecosiaPager.length || 1;
break;
case 'qwant':
const qwantPager = doc.querySelectorAll('a.pagination__link');
pages = qwantPager.length || 1;
break;
case 'ahmia':
const ahmiaPager = doc.querySelectorAll('a.pagination__link');
pages = ahmiaPager.length || 1;
break;
case 'torch':
const torchPager = doc.querySelectorAll('a.pagination__link');
pages = torchPager.length || 1;
break;
case 'notracker':
const ntPager = doc.querySelectorAll('a.pagination__link');
pages = ntPager.length || 1;
break;
default:
pages = 1;
break;
}
return pages;
}
// 항목 처리 함수
function processItem(item) {
let title = 'N/A';
let link = 'N/A';
let description = 'N/A';
let image = 'N/A';
let htmlLink = 'N/A';
let whoisInfo = 'N/A';
let pageLoadTime = 'N/A';
let language = 'N/A';
let metaKeywords = 'N/A';
let metaRobots = 'N/A';
let openGraph = 'N/A';
let securityHeaders = 'N/A';
let domainAge = 'N/A';
let techStack = 'N/A'; // 추가된 기술 스택 정보
switch(searchEngine) {
case 'google':
const gTitleElem = item.querySelector('h3');
const gLinkElem = item.querySelector('a');
const gDescElem = item.querySelector('.IsZvec') || item.querySelector('.VwiC3b');
if (gTitleElem) title = gTitleElem.innerText;
if (gLinkElem) link = gLinkElem.href;
if (gDescElem) description = gDescElem.innerText;
const gImgElem = item.querySelector('img');
if (gImgElem && gImgElem.src) {
image = `<img src="${gImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'bing':
const bTitleElem = item.querySelector('h2 a');
const bDescElem = item.querySelector('p');
if (bTitleElem) {
title = bTitleElem.innerText;
link = bTitleElem.href;
}
if (bDescElem) description = bDescElem.innerText;
const bImgElem = item.querySelector('img');
if (bImgElem && bImgElem.src) {
image = `<img src="${bImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'daum':
const dTitleElem = item.querySelector('a.link_fax');
const dDescElem = item.querySelector('div.cont_inner');
if (dTitleElem) {
title = dTitleElem.innerText.trim();
link = dTitleElem.href;
}
if (dDescElem) description = dDescElem.innerText.trim();
const dImgElem = item.querySelector('img');
if (dImgElem && dImgElem.src) {
image = `<img src="${dImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'naver':
const nTitleElem = item.querySelector('a.tit');
const nDescElem = item.querySelector('div.desc');
if (nTitleElem) {
title = nTitleElem.innerText;
link = nTitleElem.href;
}
if (nDescElem) description = nDescElem.innerText;
const nImgElem = item.querySelector('img.thumb');
if (nImgElem && nImgElem.src) {
image = `<img src="${nImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'yahoo':
const yTitleElem = item.querySelector('h3');
const yLinkElem = item.querySelector('a');
const yDescElem = item.querySelector('div.compText');
if (yTitleElem) title = yTitleElem.innerText;
if (yLinkElem) link = yLinkElem.href;
if (yDescElem) description = yDescElem.innerText;
const yImgElem = item.querySelector('img');
if (yImgElem && yImgElem.src) {
image = `<img src="${yImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'duckduckgo':
const ddgTitleElem = item.querySelector('a.result__a');
const ddgDescElem = item.querySelector('a.result__snippet');
if (ddgTitleElem) {
title = ddgTitleElem.innerText;
link = ddgTitleElem.href;
}
if (ddgDescElem) description = ddgDescElem.innerText;
const ddgImgElem = item.querySelector('img.result__image');
if (ddgImgElem && ddgImgElem.src) {
image = `<img src="${ddgImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'startpage':
const spTitleElem = item.querySelector('a.w-gl__result-title');
const spDescElem = item.querySelector('p.w-gl__result-description');
if (spTitleElem) {
title = spTitleElem.innerText;
link = spTitleElem.href;
}
if (spDescElem) description = spDescElem.innerText;
const spImgElem = item.querySelector('img.w-gl__result-image');
if (spImgElem && spImgElem.src) {
image = `<img src="${spImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'ecosia':
const ecosiaTitleElem = item.querySelector('h2.result-title a');
const ecosiaDescElem = item.querySelector('p.result-description');
if (ecosiaTitleElem) {
title = ecosiaTitleElem.innerText;
link = ecosiaTitleElem.href;
}
if (ecosiaDescElem) description = ecosiaDescElem.innerText;
const ecosiaImgElem = item.querySelector('img.result-image');
if (ecosiaImgElem && ecosiaImgElem.src) {
image = `<img src="${ecosiaImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'qwant':
const qwantTitleElem = item.querySelector('a.result__title');
const qwantDescElem = item.querySelector('p.result__description');
if (qwantTitleElem) {
title = qwantTitleElem.innerText;
link = qwantTitleElem.href;
}
if (qwantDescElem) description = qwantDescElem.innerText;
const qwantImgElem = item.querySelector('img.result__image');
if (qwantImgElem && qwantImgElem.src) {
image = `<img src="${qwantImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'ahmia':
const ahmiaTitleElem = item.querySelector('h2.result-title a');
const ahmiaDescElem = item.querySelector('p.result-description');
if (ahmiaTitleElem) {
title = ahmiaTitleElem.innerText;
link = ahmiaTitleElem.href;
}
if (ahmiaDescElem) description = ahmiaDescElem.innerText;
const ahmiaImgElem = item.querySelector('img.result-image');
if (ahmiaImgElem && ahmiaImgElem.src) {
image = `<img src="${ahmiaImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'torch':
const torchTitleElem = item.querySelector('h3.result-title a');
const torchDescElem = item.querySelector('p.result-description');
if (torchTitleElem) {
title = torchTitleElem.innerText;
link = torchTitleElem.href;
}
if (torchDescElem) description = torchDescElem.innerText;
const torchImgElem = item.querySelector('img.result-image');
if (torchImgElem && torchImgElem.src) {
image = `<img src="${torchImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'notracker':
const ntTitleElem = item.querySelector('h3.result-title a');
const ntDescElem = item.querySelector('p.result-description');
if (ntTitleElem) {
title = ntTitleElem.innerText;
link = ntTitleElem.href;
}
if (ntDescElem) description = ntDescElem.innerText;
const ntImgElem = item.querySelector('img.result-image');
if (ntImgElem && ntImgElem.src) {
image = `<img src="${ntImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
default:
break;
}
// 추가 메타 정보 수집
try {
const metaKeywordElem = item.querySelector('meta[name="keywords"]');
if (metaKeywordElem) {
metaKeywords = metaKeywordElem.getAttribute('content');
if (metaKeywords) {
metaKeywords = metaKeywords.split(',').map(k => k.trim()).join(', ');
} else {
metaKeywords = 'N/A';
}
}
const metaRobotsElem = item.querySelector('meta[name="robots"]');
if (metaRobotsElem) {
metaRobots = metaRobotsElem.getAttribute('content') || 'N/A';
}
const ogTitle = item.querySelector('meta[property="og:title"]')?.getAttribute('content') || 'N/A';
const ogDescription = item.querySelector('meta[property="og:description"]')?.getAttribute('content') || 'N/A';
const ogImage = item.querySelector('meta[property="og:image"]')?.getAttribute('content') || 'N/A';
openGraph = `Title: ${ogTitle}, Description: ${ogDescription}, Image: ${ogImage}`;
// 보안 헤더 추출 (간단한 예시)
const securityHeaders = [];
const hsts = item.querySelector('meta[http-equiv="Strict-Transport-Security"]');
if (hsts) securityHeaders.push('HSTS');
const contentSecurityPolicy = item.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (contentSecurityPolicy) securityHeaders.push('CSP');
securityHeaders = securityHeaders.length > 0 ? securityHeaders.join(', ') : 'N/A';
const domainInfo = getDomainInfo(link);
domainAge = domainInfo ? domainInfo.age : 'N/A';
const htmlLang = item.querySelector('html');
if (htmlLang && htmlLang.lang) {
language = htmlLang.lang;
}
// 기술 스택 정보 수집 (예시: 간단한 스택 감지)
const techs = [];
if (item.querySelector('.wordpress')) techs.push('WordPress');
if (item.querySelector('.shopify')) techs.push('Shopify');
if (item.querySelector('.react')) techs.push('React');
if (item.querySelector('.angular')) techs.push('Angular');
if (item.querySelector('.vue')) techs.push('Vue.js');
techStack = techs.length > 0 ? techs.join(', ') : 'N/A';
} catch (e) {
log(`⚠️ 메타 정보 수집 중 오류 발생: ${e.message}`);
}
// 광고 제외 필터링
if (toggleAds.checked) {
const combinedText = `${title} ${description}`.toLowerCase();
const isAd = combinedText.includes('advertisement') || combinedText.includes('광고') || combinedText.includes('ad ');
if (isAd) {
log(`🚫 광고 항목 제외: "${title}"`);
return;
}
}
// 필터 패턴 적용
if (filterPatterns.length > 0) {
const combinedText = `${title} ${description}`;
const isFiltered = filterPatterns.some(regex => regex.test(combinedText));
if (isFiltered) {
log(`❌ 필터 단어 포함 항목 제외: "${title}"`);
return;
}
}
// 도메인 추출 및 제외 도메인 적용
const domain = extractDomain(link);
if (!domain) return;
if (excludeDomains.includes(domain.toLowerCase())) {
log(`🚫 제외 도메인 항목 제외: "${link}"`);
return;
}
// 중복 방문 제한
let visitCount = visitedLinks.get(domain) || 0;
if (visitCount >= duplicateThreshold) {
log(`🔄 중복 방문 제한: "${domain}"`);
return;
}
visitedLinks.set(domain, visitCount + 1);
visitedCountElem.innerText = visitedLinks.size.toString();
// 페이지 로딩 시간 측정
const startLoad = performance.now();
fetch(link, { mode: 'no-cors' })
.then(response => {
const endLoad = performance.now();
pageLoadTime = Math.round(endLoad - startLoad);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
updateRow();
})
.catch(err => {
pageLoadTime = '계산 불가';
log(`❌ 페이지 로딩 오류: "${link}"`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
updateRow();
});
// 언어, 키워드, 로봇 메타 정보 저장
const itemLanguage = language;
const itemKeywords = metaKeywords;
const itemRobots = metaRobots;
// WHOIS 정보 버튼 이벤트 추가 (이미 처리됨)
// 이미지 옵션 적용
if (!toggleImages.checked) {
image = '비활성화';
}
// OpenGraph 옵션 적용
if (!toggleOpenGraph.checked) {
openGraph = '비활성화';
}
// 보안 헤더 옵션 적용
if (!toggleSecurityHeaders.checked) {
securityHeaders = '비활성화';
}
// 도메인 연령 옵션 적용
if (!toggleDomainAge.checked) {
domainAge = '비활성화';
}
// 결과 배열에 추가
results.push({ title, link, description, image, htmlLink, whoisInfo, pageLoadTime, language, metaKeywords, metaRobots, openGraph, securityHeaders, domainAge, techStack });
// 결과 테이블에 행 추가
const row = document.createElement('tr');
row.innerHTML = `
<td>${escapeHTML(title)}</td>
<td><a href="${escapeHTML(link)}" target="_blank">${escapeHTML(link)}</a></td>
<td>${escapeHTML(description)}</td>
<td>${image}</td>
<td>${htmlLink}</td>
<td>${whoisInfo}</td>
<td>${pageLoadTime}</td>
<td>${escapeHTML(itemLanguage)}</td>
<td>${escapeHTML(itemKeywords)}</td>
<td>${escapeHTML(itemRobots)}</td>
<td>${escapeHTML(openGraph)}</td>
<td>${escapeHTML(securityHeaders)}</td>
<td>${escapeHTML(domainAge)}</td>
<td>${escapeHTML(techStack)}</td>
`;
resultsBody.appendChild(row);
// 미리보기 이벤트 추가
addPreviewEvents(row, link);
// WHOIS 버튼 이벤트 추가
const whoisBtn = row.querySelector('.whois-view-btn');
if (whoisBtn) {
whoisBtn.addEventListener('click', () => {
const encodedDomain = whoisBtn.getAttribute('data-domain');
const decodedDomain = decodeURIComponent(encodedDomain);
fetchWhoisData(decodedDomain);
});
}
function updateRow() {
// 업데이트된 로딩 시간을 테이블에 반영
row.cells[6].innerText = pageLoadTime;
row.cells[10].innerText = openGraph;
row.cells[11].innerText = securityHeaders;
row.cells[12].innerText = domainAge;
updateChart();
updateLoadTimeChart();
updateDomainDistributionChart();
updateErrorRateChart();
updateCumulativeVisitsChart();
updateKeywordDistributionChart();
updateLanguageDistributionChart();
updateTechStackChart();
updateDomainAgeChart();
updateProgress();
}
}
// HTML 이스케프 함수
function escapeHTML(str) {
if (!str) return '';
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 도메인 추출 함수
function extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace('www.', '');
} catch (e) {
return null;
}
}
// 도메인 연령 정보 가져오기 함수
function getDomainInfo(url) {
try {
const urlObj = new URL(url);
const now = new Date();
const creationDate = new Date(urlObj.host.creationDate); // 예시: 실제로는 WHOIS 데이터를 통해 가져와야 함
if (isNaN(creationDate)) return null;
const age = now.getFullYear() - creationDate.getFullYear();
return { age };
} catch (e) {
return null;
}
}
// 경과 시간 업데이트 함수
function updateElapsedTime() {
if (!startTime) return;
const now = Date.now();
const elapsed = Math.floor((now - startTime) / 1000);
elapsedTimeElem.innerText = formatTime(elapsed);
const estimatedTotal = totalPages * ((elapsed > 0) ? (elapsed / (currentPage - 1)) : 0.5);
const remaining = Math.max(Math.round(estimatedTotal - elapsed), 0);
remainingTimeElem.innerText = formatTime(remaining);
}
// 시간 형식화 함수
function formatTime(seconds) {
const hrs = String(Math.floor(seconds / 3600)).padStart(2, '0');
const mins = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
const secs = String(seconds % 60).padStart(2, '0');
return `${hrs}:${mins}:${secs}`;
}
// 차트 업데이트 함수
function updateChart() {
crawlData.labels = Array.from(visitedLinks.keys());
crawlData.datasets[0].data = Array.from(visitedLinks.values());
// 평균 페이지 로딩 시간 계산
let loadTimes = results.filter(r => typeof r.pageLoadTime === 'number').map(r => r.pageLoadTime);
let averageLoadTime = loadTimes.length > 0 ? Math.round(loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length) : 0;
crawlData.datasets[1].data = Array.from(visitedLinks.keys()).map(domain => {
const times = results.filter(r => extractDomain(r.link) === domain && typeof r.pageLoadTime === 'number').map(r => r.pageLoadTime);
return times.length > 0 ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : 0;
});
if (crawlChart) {
crawlChart.update();
}
GM_setValue('crawlerData', JSON.stringify(crawlData));
}
// 추가 차트 업데이트 함수
function updateLoadTimeChart() {
loadTimeData.labels = results.map(r => r.title).slice(-20); // 최근 20개
loadTimeData.datasets[0].data = results.map(r => r.pageLoadTime).slice(-20);
if (loadTimeChart) {
loadTimeChart.update();
}
}
function updateDomainDistributionChart() {
domainDistributionData.labels = Array.from(visitedLinks.keys());
domainDistributionData.datasets[0].data = Array.from(visitedLinks.values());
if (domainDistributionChart) {
domainDistributionChart.update();
}
}
function updateErrorRateChart() {
// 오류 발생 수를 도메인별로 집계
const errorCounts = {};
results.forEach(r => {
if (r.pageLoadTime === '계산 불가') {
const domain = extractDomain(r.link);
if (domain) {
errorCounts[domain] = (errorCounts[domain] || 0) + 1;
}
}
});
errorRateData.labels = Object.keys(errorCounts);
errorRateData.datasets[0].data = Object.values(errorCounts);
if (errorRateChart) {
errorRateChart.update();
}
}
function updateCumulativeVisitsChart() {
let cumulative = 0;
cumulativeVisitsData.labels = results.map((_, index) => index + 1);
cumulativeVisitsData.datasets[0].data = results.map(() => { cumulative += 1; return cumulative; });
if (cumulativeVisitsChart) {
cumulativeVisitsChart.update();
}
}
function updateKeywordDistributionChart() {
const keywordCounts = {};
results.forEach(r => {
if (r.metaKeywords && r.metaKeywords !== 'N/A') {
const keywords = r.metaKeywords.split(',').map(k => k.trim());
keywords.forEach(k => {
if (k) {
keywordCounts[k] = (keywordCounts[k] || 0) + 1;
}
});
}
});
keywordDistributionData.labels = Object.keys(keywordCounts);
keywordDistributionData.datasets[0].data = Object.values(keywordCounts);
if (keywordDistributionChart) {
keywordDistributionChart.update();
}
}
function updateLanguageDistributionChart() {
const languageCounts = {};
results.forEach(r => {
if (r.language && r.language !== 'N/A') {
languageCounts[r.language] = (languageCounts[r.language] || 0) + 1;
}
});
languageDistributionData.labels = Object.keys(languageCounts);
languageDistributionData.datasets[0].data = Object.values(languageCounts);
if (languageDistributionChart) {
languageDistributionChart.update();
}
}
function updateTechStackChart() {
const techCounts = {};
results.forEach(r => {
if (r.techStack && r.techStack !== 'N/A') {
const techs = r.techStack.split(',').map(t => t.trim());
techs.forEach(t => {
if (t) {
techCounts[t] = (techCounts[t] || 0) + 1;
}
});
}
});
techStackData.labels = Object.keys(techCounts);
techStackData.datasets[0].data = Object.values(techCounts);
if (techStackChart) {
techStackChart.update();
}
}
function updateDomainAgeChart() {
const ageCounts = {};
results.forEach(r => {
if (r.domainAge && r.domainAge !== 'N/A') {
const age = r.domainAge;
if (age >= 0 && age <= 1) {
ageCounts['0-1'] = (ageCounts['0-1'] || 0) + 1;
} else if (age <= 3) {
ageCounts['2-3'] = (ageCounts['2-3'] || 0) + 1;
} else if (age <= 5) {
ageCounts['4-5'] = (ageCounts['4-5'] || 0) + 1;
} else if (age <= 10) {
ageCounts['6-10'] = (ageCounts['6-10'] || 0) + 1;
} else {
ageCounts['10+'] = (ageCounts['10+'] || 0) + 1;
}
}
});
domainAgeData.labels = Object.keys(ageCounts);
domainAgeData.datasets[0].data = Object.values(ageCounts);
if (domainAgeChart) {
domainAgeChart.update();
}
}
// 크롤 데이터 로드 함수
function loadCrawlData() {
const storedData = GM_getValue('crawlerData', null);
if (storedData) {
const parsedData = JSON.parse(storedData);
crawlData = parsedData;
if (crawlChart) {
crawlChart.data = crawlData;
crawlChart.update();
}
}
}
// 크롤 데이터 저장 함수
function saveCrawlData() {
GM_setValue('crawlerData', JSON.stringify(crawlData));
}
// 주기적으로 크롤 데이터 저장
setInterval(saveCrawlData, 5000);
// 검색어 및 검색 엔진 감지 함수
function getSearchQueryAndEngine() {
const url = window.location.href;
if (url.includes('google.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'google';
} else if (url.includes('bing.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'bing';
} else if (url.includes('daum.net/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'daum';
} else if (url.includes('search.naver.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('query') || '';
searchEngine = 'naver';
} else if (url.includes('search.yahoo.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('p') || '';
searchEngine = 'yahoo';
} else if (url.includes('duckduckgo.com/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'duckduckgo';
} else if (url.includes('startpage.com/do/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('query') || '';
searchEngine = 'startpage';
} else if (url.includes('ecosia.org/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'ecosia';
} else if (url.includes('qwant.com/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'qwant';
} else if (url.includes('ahmia.fi/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'ahmia';
} else if (url.includes('torchsearch.de/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'torch';
} else if (url.includes('notracker.org/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'notracker';
} else {
searchQuery = '';
searchEngine = '';
}
log(`🔍 검색 엔진: ${capitalizeFirstLetter(searchEngine)}, 검색어: "${searchQuery}"`);
}
// 로그 기록 함수
function log(message) {
const time = new Date().toLocaleTimeString();
const entry = `[${time}] ${message}`;
logMessages.push(entry);
logContainer.innerHTML += `${entry}<br>`;
logContainer.scrollTop = logContainer.scrollHeight;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
}
// 결과 내보내기 함수 (중복 제거 및 정렬 추가)
function exportResults() {
if (results.length === 0) {
alert('내보낼 결과가 없습니다.');
return;
}
const format = prompt('내보낼 형식을 선택하세요 (xlsx, csv, json):', 'xlsx');
if (!format) return;
if (format.toLowerCase() === 'xlsx') {
const ws = XLSX.utils.json_to_sheet(results);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "결과");
XLSX.writeFile(wb, "크롤링_결과.xlsx");
log('💾 결과를 "크롤링_결과.xlsx" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.xlsx" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else if (format.toLowerCase() === 'csv') {
const csv = results.map(row => `"${row.title.replace(/"/g, '""')}","${row.link.replace(/"/g, '""')}","${row.description.replace(/"/g, '""')}","${row.image.replace(/"/g, '""')}","${row.htmlLink}","${row.whoisInfo}","${row.pageLoadTime}","${row.language}","${row.metaKeywords}","${row.metaRobots}","${row.openGraph}","${row.securityHeaders}","${row.domainAge}","${row.techStack}"`).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '크롤링_결과.csv';
a.click();
log('💾 결과를 "크롤링_결과.csv" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.csv" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else if (format.toLowerCase() === 'json') {
const json = JSON.stringify(results, null, 2);
const blob = new Blob([json], { type: 'application/json;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '크롤링_결과.json';
a.click();
log('💾 결과를 "크롤링_결과.json" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.json" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else {
alert('지원하지 않는 형식입니다.');
}
}
// 결과 테이블 정렬 함수
function sortTable(n) {
const table = document.getElementById("resultsTable");
let switching = true;
let dir = "asc";
let switchcount = 0;
while (switching) {
switching = false;
const rows = table.rows;
for (let i = 1; i < (rows.length - 1); i++) {
let shouldSwitch = false;
const x = rows[i].getElementsByTagName("TD")[n];
const y = rows[i + 1].getElementsByTagName("TD")[n];
let xContent = x.textContent || x.innerText;
let yContent = y.textContent || y.innerText;
// 숫자 비교
if (n === 6 || n === 12) { // 페이지 로딩 시간과 도메인 연령
xContent = parseInt(xContent);
yContent = parseInt(yContent);
}
if (dir === "asc") {
if (xContent > yContent) {
shouldSwitch = true;
break;
}
} else if (dir === "desc") {
if (xContent < yContent) {
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount === 0 && dir === "asc") {
dir = "desc";
switching = true;
}
}
}
}
// 사용자 경험 향상을 위한 함수 추가
function capitalizeFirstLetter(string) {
if (!string) return '';
return string.charAt(0).toUpperCase() + string.slice(1);
}
// 항목 검색 기능
function filterResults() {
const filter = searchResultsInput.value.toLowerCase();
const rows = resultsBody.getElementsByTagName('tr');
for (let i = 0; i < rows.length; i++) {
const cells = rows[i].getElementsByTagName('td');
let match = false;
for (let j = 0; j < cells.length - 9; j++) { // 제외 항목 인덱스 조정
if (cells[j].innerText.toLowerCase().includes(filter)) {
match = true;
break;
}
}
rows[i].style.display = match ? '' : 'none';
}
}
// 로그 초기화 함수
function clearLogs() {
if(confirm('로그를 초기화하시겠습니까?')) {
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
log('🧹 로그를 초기화하였습니다.');
}
}
// 진행률 업데이트 함수
function updateProgress() {
if (totalPages === 0) {
progressBar.value = 0;
progressText.innerText = '0%';
return;
}
const progress = Math.min((currentPage - 1) / totalPages * 100, 100);
progressBar.value = progress;
progressText.innerText = `${Math.round(progress)}%`;
}
// 크롤링 오류가 발생한 행에 대한 미리보기 추가
function addPreviewEvents(row, link) {
const htmlBtn = row.querySelector('.html-view-btn');
if (htmlBtn) {
htmlBtn.addEventListener('click', () => {
const decodedLink = decodeURIComponent(htmlBtn.getAttribute('data-link'));
openPreview(decodedLink);
});
// HTML 버튼에 마우스 호버 시 미리보기 표시
htmlBtn.addEventListener('mouseover', (e) => {
const decodedLink = decodeURIComponent(htmlBtn.getAttribute('data-link'));
showPreview(e, decodedLink);
});
htmlBtn.addEventListener('mouseout', hidePreview);
}
}
// 미리보기 표시 함수
function showPreview(event, link) {
tooltip.style.left = event.pageX + 'px';
tooltip.style.top = event.pageY + 'px';
tooltip.style.display = 'block';
tooltip.innerHTML = `<iframe src="${link}" title="미리보기"></iframe>`;
}
function hidePreview() {
tooltip.style.display = 'none';
tooltip.innerHTML = '';
}
// 사이트 미리보기 열기 함수
function openPreview(link) {
const analyticsTab = container.querySelector('#analytics');
const previewContainerId = 'site-preview-container';
let previewContainer = container.querySelector(`#${previewContainerId}`);
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.id = previewContainerId;
previewContainer.className = 'chart-container';
previewContainer.style.height = '400px';
analyticsTab.appendChild(previewContainer);
}
previewContainer.innerHTML = `
<h3>사이트 미리보기: <a href="${link}" target="_blank">${link}</a></h3>
<iframe src="${link}" style="width: 100%; height: 350px; border: none; border-radius: 5px;"></iframe>
`;
// 분석 탭으로 이동
const analyticsTabBtn = container.querySelector('.tab[data-tab="analytics"]');
analyticsTabBtn.click();
}
// WHOIS 정보 가져오기 및 표시 함수
function fetchWhoisData(domain) {
log(`📡 WHOIS 정보를 가져오는 중: ${domain}`);
const apiUrl = `https://jsonwhois.com/api/v1/whois?domain=${encodeURIComponent(domain)}`;
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Content-Type": "application/json",
"Authorization": `Token token=${WHOIS_API_KEY}`
},
onload: function(response) {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
displayWhoisData(data, domain);
log(`✅ WHOIS 정보를 성공적으로 가져왔습니다: ${domain}`);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
} else {
log(`❌ WHOIS 정보 가져오기 실패: ${domain}, 상태 코드: ${response.status}`);
whoisContent.innerHTML = `<p>WHOIS 정보를 가져오는 데 실패했습니다.</p>`;
whoisModal.style.display = 'block';
}
},
onerror: function() {
log(`❌ WHOIS 정보 요청 중 오류 발생: ${domain}`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
whoisContent.innerHTML = `<p>WHOIS 정보를 가져오는 데 오류가 발생했습니다.</p>`;
whoisModal.style.display = 'block';
}
});
}
// WHOIS 데이터 표시 함수
function displayWhoisData(data, domain) {
if (!data) {
whoisContent.innerHTML = `<p>No WHOIS data available.</p>`;
whoisModal.style.display = 'block';
return;
}
const whoisHtml = `
<h3>${domain}의 WHOIS 정보</h3>
<pre>${JSON.stringify(data, null, 2)}</pre>
`;
whoisContent.innerHTML = whoisHtml;
whoisModal.style.display = 'block';
}
// *******************************
// 시작
// *******************************
})();WHOIS 정보에서 버튼을 누르면 바로 보이게 하라고.
사이트 정보를 더 빨리 많이 얻을 수 있는 기능들을 추가해.
사이트 정보를 분석할 수 있는 기능들을 추가해
분석을 더 자세하게 하기 위한 다양한 가능들을 추가해! 그리고 기능들을 대폭 대폭 개선해! + 모듈화나 코드 최적화 코드 삭제 이런 거는 절대 하지 마셈
스크립트 시스템 대폭 대폭 개선
스크립트 기능들도 초 격차로 대폭 대폭 개선
기능들을 사용자의 편의성과 가독성을 위해 대폭 개선
(크롤링 속도 조절 기능은 그냥 아예 없애샘 만약 있으면 없애셈
설정 ㅈㄴ ㅈㄴ ㅈㄴ ㅈㄴ 다양하게 하셈 코드 상으로만 구현되어 있는 기능도 포함
코드 가독성 향상 (주석 다는 거) 이거 진짜 하지 마셈
기능 최적화: 불필요한 코드 제거 및 로직 최적화를 통해 성능을 향상시켰습니다. (이겨 진짜 하지 마셈 )
한 단계 올릴 수준으로 기능들을 대폭 개선하셈 스크립트의 수준을 한 단계 올릴 만큼
그리고 내가 지금 말하는 거에 집중해 전에 말한 거 에 집중하지 말고 알겠지? 전에 한 거는 신경쓰지 마셈
그리고 "기존에 있던 기능들" 성능들 편의성이나 가도성 대폭 대폭 대폭 대폭 초 대폭 초초초초초초초초초초초초 대폭 개선하셈
ui 대폭 대폭 편의성과 가독성을 전부다 가져온 ui 이여야 함
시작 자체가 안 되면 안 됨. 시작 자체는 되야 함 + 이 스크립트의 크롤링의 속도를 제한 시킬 수 있는 게 있다면 없애! 지금 당장! + 무조건 속도는 100ms으로 통일 한다. + ui 대폭 개선+ 창 좀 작게 해주셈
❌ 페이지 로딩 오류: "https://www.tiktok.com/@qazws.4" 이거 안 나게 하셈ASSISTANT
```javascript
// ==UserScript==
// @name 종합 웹 크롤러 8.6 초격차 개선 버전
// @namespace http://tampermonkey.net/
// @version 8.6.3
// @description 고급 기능을 갖춘 웹 크롤러로 더 빠르고 많은 검색 결과를 실시간으로 수집하고 자세한 분석을 제공합니다 (초격차 개선 버전)
// @author YourName
// @match *://www.google.com/search*
// @match *://www.bing.com/search*
// @match *://search.daum.net/search*
// @match *://search.naver.com/search*
// @match *://search.yahoo.com/search*
// @match *://duckduckgo.com/*
// @match *://www.startpage.com/do/search*
// @match *://www.ecosia.org/search*
// @match *://www.qwant.com/*
// @match *://ahmia.fi/*
// @match *://torchsearch.de/*
// @match *://notracker.org/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @require https://cdn.jsdelivr.net/npm/chart.js
// ==/UserScript==
(function() {
'use strict';
// *******************************
// API 키 설정 (필요 시 본인의 API 키로 변경)
// *******************************
const WHOIS_API_KEY = 'YOUR_JSONWHOIS_API_KEY'; // 여기에 본인의 JSONWHOIS API 키를 입력하세요.
GM_addStyle(`
/* UI 전반적인 스타일 개선 */
#crawler-container {
position: fixed;
top: 20px;
right: 20px;
background-color: #ffffff;
border: 3px solid #2196F3;
padding: 10px;
z-index: 10000;
width: 500px;
max-height: 75vh;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
border-radius: 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
resize: both;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
#crawler-container header {
cursor: move;
background-color: #2196F3;
color: white;
padding: 8px 12px;
border-radius: 8px 8px 0 0;
text-align: center;
font-size: 18px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
#crawler-container header .title {
flex-grow: 1;
}
#crawler-container header .close-btn {
cursor: pointer;
font-size: 20px;
color: #FF5252;
margin-left: 10px;
}
#crawler-container .tabs {
display: flex;
border-bottom: 2px solid #2196F3;
margin-top: 5px;
}
#crawler-container .tab {
flex: 1;
padding: 8px 0;
text-align: center;
cursor: pointer;
background-color: #E3F2FD;
border-right: 1px solid #BBDEFB;
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
font-size: 13px;
}
#crawler-container .tab:last-child {
border-right: none;
}
#crawler-container .tab.active {
background-color: #FFFFFF;
border-bottom: 3px solid #2196F3;
color: #2196F3;
}
#crawler-container .tab-content {
display: none;
padding: 10px;
overflow-y: auto;
flex-grow: 1;
}
#crawler-container .tab-content.active {
display: block;
}
#crawler-container button {
padding: 6px 10px;
margin: 4px 2px;
border: none;
border-radius: 4px;
background-color: #2196F3;
color: white;
cursor: pointer;
transition: background-color 0.3s;
font-size: 13px;
}
#crawler-container button:hover {
background-color: #1976D2;
}
#crawler-container button.export-btn {
background-color: #4CAF50;
}
#crawler-container button.export-btn:hover {
background-color: #388E3C;
}
#crawler-container button.pause-btn {
background-color: #FFC107;
}
#crawler-container button.pause-btn:hover {
background-color: #FFA000;
}
#crawler-container .options,
#crawler-container .filters,
#crawler-container .exclude-domains,
#crawler-container .advanced-options,
#crawler-container .controls,
#crawler-container .search-engines {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
#crawler-container .options label,
#crawler-container .advanced-options label,
#crawler-container .controls label,
#crawler-container .search-engines label {
margin-right: 10px;
display: inline-flex;
align-items: center;
font-weight: normal;
font-size: 13px;
}
#crawler-container .options input[type="checkbox"],
#crawler-container .advanced-options input[type="checkbox"],
#crawler-container .controls input[type="number"],
#crawler-container .search-engines select {
margin-right: 6px;
}
#crawler-container input[type="text"],
#crawler-container textarea,
#crawler-container select {
width: 100%;
padding: 6px;
margin: 4px 0 8px 0;
border: 1px solid #B0BEC5;
border-radius: 4px;
font-size: 13px;
}
#crawler-container table {
border-collapse: collapse;
width: 100%;
margin-top: 8px;
font-size: 13px;
}
#crawler-container th, #crawler-container td {
padding: 8px;
border: 1px solid #E0E0E0;
text-align: left;
vertical-align: top;
}
#crawler-container th {
background-color: #2196F3;
color: white;
position: sticky;
top: 0;
z-index: 1;
cursor: pointer;
}
#crawler-container .log {
max-height: 150px;
overflow-y: auto;
background-color: #E3F2FD;
padding: 8px;
border: 1px solid #81D4FA;
border-radius: 4px;
font-size: 13px;
white-space: pre-wrap;
}
#crawler-container progress {
width: 75%;
height: 18px;
margin-right: 8px;
border-radius: 4px;
}
#crawler-container .status {
margin-top: 8px;
font-weight: bold;
font-size: 13px;
}
#crawler-container .chart-container {
position: relative;
height: 200px;
width: 100%;
margin-top: 15px;
}
#crawler-container .notification-area {
flex-direction: row;
align-items: center;
width: 100%;
}
@media (max-width: 600px) {
#crawler-container {
width: 95%;
right: 2.5%;
}
#crawler-container .controls,
#crawler-container .options,
#crawler-container .advanced-options,
#crawler-container .notification-area,
#crawler-container .search-engines {
flex-direction: column;
align-items: flex-start;
}
#crawler-container .search-engines select {
width: 100%;
}
#crawler-container progress {
width: 100%;
}
}
/* WHOIS Modal 스타일 */
#whois-modal {
display: none;
position: fixed;
z-index: 10001;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
height: 70vh;
background-color: #ffffff;
border: 3px solid #2196F3;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
overflow: hidden;
flex-direction: column;
display: flex;
}
#whois-modal header {
background-color: #2196F3;
color: white;
padding: 10px 16px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
#whois-modal header .close-modal-btn {
cursor: pointer;
font-size: 20px;
color: #FF5252;
}
#whois-modal .modal-content {
padding: 16px;
overflow-y: auto;
flex-grow: 1;
background-color: #E3F2FD;
}
/* Tooltip 스타일 */
.preview-tooltip {
position: absolute;
background-color: #ffffff;
border: 1px solid #90CAF9;
border-radius: 5px;
padding: 6px;
z-index: 10002;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: none;
width: 250px;
}
.preview-tooltip iframe {
width: 100%;
height: 150px;
border: none;
border-radius: 5px;
}
`);
const container = document.createElement('div');
container.id = 'crawler-container';
container.innerHTML = `
<header>
<span class="title">🔥 웹 크롤러 8.6</span>
<span class="close-btn" title="닫기">×</span>
</header>
<div class="tabs">
<div class="tab active" data-tab="settings">설정</div>
<div class="tab" data-tab="results">결과</div>
<div class="tab" data-tab="logs">로그</div>
<div class="tab" data-tab="analytics">분석</div>
<div class="tab" data-tab="about">정보</div>
</div>
<div class="tab-content active" id="settings">
<div class="notification-area">
<button id="startBtn" title="크롤링 시작">▶️ 시작</button>
<button id="pauseBtn" class="pause-btn" title="크롤링 일시정지/재개">⏸️ 일시정지</button>
<button id="stopBtn" title="크롤링 중지">⏹️ 중지</button>
<button id="resetBtn" title="크롤링 초기화">🔄 초기화</button>
<button id="exportBtn" class="export-btn" title="결과 내보내기">💾 내보내기</button>
<button id="clearLogsBtn" title="로그 초기화">🧹 로그 초기화</button>
</div>
<div class="search-engines">
<label for="searchEngineSelect">검색 엔진 선택:</label>
<select id="searchEngineSelect">
<option value="google" selected>Google</option>
<option value="bing">Bing</option>
<option value="daum">Daum</option>
<option value="naver">Naver</option>
<option value="yahoo">Yahoo</option>
<option value="duckduckgo">DuckDuckGo</option>
<option value="startpage">StartPage</option>
<option value="ecosia">Ecosia</option>
<option value="qwant">Qwant</option>
<option value="ahmia">Ahmia (Tor)</option>
<option value="torch">Torch (Tor)</option>
<option value="notracker">NOTRACKER (Tor)</option>
</select>
</div>
<div class="options">
<label><input type="checkbox" id="toggleHTML" checked> HTML 가져오기</label>
<label><input type="checkbox" id="toggleImages" checked> 이미지 가져오기</label>
<label><input type="checkbox" id="toggleAds" checked> 광고 제외</label>
<label><input type="checkbox" id="toggleTechStack" checked> 기술 스택 분석</label>
<label><input type="checkbox" id="toggleOpenGraph" checked> OpenGraph 정보 가져오기</label>
<label><input type="checkbox" id="toggleSecurityHeaders" checked> 보안 헤더 분석</label>
<label><input type="checkbox" id="toggleDomainAge" checked> 도메인 연령 분석</label>
</div>
<div class="filters">
<label for="filterInput">필터 단어 (정규식 또는 단어, 쉼표로 구분):</label>
<input type="text" id="filterInput" placeholder="예: 기부, 광고, /promo/i">
</div>
<div class="exclude-domains">
<label for="excludeDomainsInput">제외할 도메인 (쉼표로 구분):</label>
<input type="text" id="excludeDomainsInput" placeholder="예: facebook.com, youtube.com">
</div>
<div class="advanced-options">
<label for="duplicateThreshold">중복 허용 횟수:</label>
<input type="number" id="duplicateThreshold" value="2" min="1" style="width: 60px;">
<label for="retryAttempts">재시도 횟수:</label>
<input type="number" id="retryAttempts" value="3" min="1" style="width: 60px;">
</div>
<div style="margin-top:10px;">
<label for="progressBar">진행률:</label>
<progress id="progressBar" value="0" max="100"></progress>
<span id="progressText">0%</span>
</div>
<div class="status">
현재 페이지: <span id="currentPage">0</span> /
총 페이지: <span id="totalPages">0</span>
<br>
방문된 사이트: <span id="visitedCount">0</span>
<br>
총 요청 수: <span id="totalRequests">0</span>
<br>
성공 요청 수: <span id="successfulRequests">0</span>
<br>
실패 요청 수: <span id="failedRequests">0</span>
<br>
경과 시간: <span id="elapsedTime">00:00:00</span>
<br>
예상 남은 시간: <span id="remainingTime">00:00:00</span>
</div>
</div>
<div class="tab-content" id="results">
<div style="margin-top:8px; max-height: 300px; overflow: auto;">
<table id="resultsTable">
<thead>
<tr>
<th data-sort="0">사이트 이름 ▲▼</th>
<th data-sort="1">주소 ▲▼</th>
<th data-sort="2">설명 ▲▼</th>
<th>이미지</th>
<th>HTML 링크</th>
<th>WHOIS 정보</th>
<th data-sort="6">페이지 로딩 시간 (ms) ▲▼</th>
<th>언어</th>
<th>메타 키워드</th>
<th>메타 robots</th>
<th>OpenGraph</th>
<th>보안 헤더</th>
<th>도메인 연령</th>
<th>기술 스택</th>
</tr>
</thead>
<tbody id="resultsBody">
</tbody>
</table>
</div>
<div style="margin-top:10px;">
<input type="text" id="searchResultsInput" placeholder="결과 검색...">
</div>
</div>
<div class="tab-content" id="logs">
<div class="log" id="log">
<strong>로그:</strong>
</div>
</div>
<div class="tab-content" id="analytics">
<div class="chart-container">
<canvas id="crawlChart"></canvas>
</div>
<div class="chart-container">
<canvas id="loadTimeChart"></canvas>
</div>
<div class="chart-container">
<canvas id="domainDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="errorRateChart"></canvas>
</div>
<div class="chart-container">
<canvas id="cumulativeVisitsChart"></canvas>
</div>
<div class="chart-container">
<canvas id="keywordDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="languageDistributionChart"></canvas>
</div>
<div class="chart-container">
<canvas id="techStackChart"></canvas>
</div>
<div class="chart-container">
<canvas id="domainAgeChart"></canvas>
</div>
<div id="whois-modal">
<header>
<span>WHOIS 정보</span>
<span class="close-modal-btn" title="닫기">×</span>
</header>
<div class="modal-content" id="whoisContent">
<!-- WHOIS 정보가 여기에 표시됩니다 -->
</div>
</div>
</div>
<div class="tab-content" id="about">
<h2>웹 크롤러 8.6 정보</h2>
<p>종합 웹 크롤러는 다양한 검색 엔진에서 빠르게 검색 결과를 수집하고 심층 분석할 수 있는 도구입니다. 사용자의 편의성과 데이터 가독성을 극대화하여 초격차로 개선된 버전입니다.</p>
<ul>
<li>지원하는 검색 엔진: Google, Bing, Daum, Naver, Yahoo, DuckDuckGo, StartPage, Ecosia, Qwant, Ahmia (Tor), Torch (Tor), NOTRACKER (Tor)</li>
<li>실시간 검색어 감지 및 고속 크롤링</li>
<li>고급 필터링 및 제외 옵션 제공</li>
<li>결과를 XLSX, CSV, JSON 형식으로 내보내기</li>
<li>시각화된 데이터 차트 및 그래프 제공</li>
<li>사용자 친화적인 인터페이스 및 반응형 디자인</li>
<li>결과 테이블 정렬 및 실시간 검색 기능 추가</li>
<li>상세한 로그 제공</li>
<li>페이지 로딩 시간 및 언어 분석</li>
<li>WHOIS 정보 연동 및 즉시 보기 기능</li>
<li>도메인 연령 분석 기능 추가</li>
<li>추가적인 보안 헤더 분석 기능</li>
<li>기술 스택 분석 기능 강화</li>
<li>다양한 알림 방식 지원</li>
<li>자동 스크롤 및 실시간 업데이트</li>
<li>고급 에러 핸들링 및 재시도 로직 강화</li>
<li>로그 초기화 및 결과 검색 기능 추가</li>
<li>추가 분석 탭에서 다양한 그래프 제공</li>
<li>도메인 분포 및 오류 발생률 분석</li>
<li>누적 방문 횟수 추적</li>
<li>HTML 링크 클릭 시 UI 내에서 사이트 미리보기 제공</li>
<li>마우스 호버 시 사이트 미리보기 표시</li>
<li>WHOIS 정보 바로보기 및 상세 분석 기능 추가</li>
<li>추가 메타 정보 수집 및 분석 기능 강화</li>
<li>더 빠르고 많은 사이트 정보 수집 기능 추가</li>
<li>사이트 기술 스택 심층 분석 기능 추가</li>
</ul>
<p><strong>버전:</strong> 8.6.3</p>
<p><strong>제작자:</strong> YourName</p>
<p><strong>문의:</strong> your.email@example.com</p>
</div>
`;
document.body.appendChild(container);
// 드래그 기능
(function makeDraggable(elmnt) {
const header = elmnt.querySelector('header');
const closeBtn = elmnt.querySelector('.close-btn');
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
header.onmousedown = dragMouseDown;
closeBtn.onclick = () => {
elmnt.style.display = 'none';
log('🗑 크롤러 창을 닫았습니다.');
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
})(container);
// 탭 설정
(function setupTabs() {
const tabs = container.querySelectorAll('.tab');
const tabContents = container.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = tab.getAttribute('data-tab');
tabContents.forEach(content => {
if(content.id === target){
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
});
});
})();
// 변수 초기화
let isRunning = false;
let isPaused = false;
let currentPage = 1;
let totalPages = 0;
let results = [];
let searchQuery = '';
let searchEngine = 'google';
let visitedLinks = new Map();
let logMessages = [];
let filterPatterns = [];
let excludeDomains = [];
let duplicateThreshold = 2;
let retryAttempts = 3;
let retryInterval = 100; // 고정적으로 100ms 설정
let startTime = null;
let elapsedInterval = null;
let crawlChart = null;
let loadTimeChart = null;
let domainDistributionChart = null;
let errorRateChart = null;
let cumulativeVisitsChart = null;
let keywordDistributionChart = null;
let languageDistributionChart = null;
let techStackChart = null;
let domainAgeChart = null;
let crawlData = {
labels: [],
datasets: [{
label: '방문 횟수',
data: [],
backgroundColor: 'rgba(33, 150, 243, 0.6)',
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
},
{
label: '평균 페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(76, 175, 80, 0.6)',
borderColor: 'rgba(76, 175, 80, 1)',
borderWidth: 1
}]
};
let loadTimeData = {
labels: [],
datasets: [{
label: '페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(255, 193, 7, 0.6)',
borderColor: 'rgba(255,193,7,1)',
borderWidth: 1
}]
};
let domainDistributionData = {
labels: [],
datasets: [{
label: '도메인 분포',
data: [],
backgroundColor: [
'rgba(33, 150, 243, 0.6)',
'rgba(255, 87, 34, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(158, 158, 158, 0.6)'
],
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
}]
};
let errorRateData = {
labels: [],
datasets: [{
label: '오류 발생 수',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132,1)',
borderWidth: 1
}]
};
let cumulativeVisitsData = {
labels: [],
datasets: [{
label: '누적 방문 횟수',
data: [],
backgroundColor: 'rgba(3, 169, 244, 0.6)',
borderColor: 'rgba(3, 169, 244,1)',
borderWidth: 1
}]
};
let keywordDistributionData = {
labels: [],
datasets: [{
label: '키워드 분포',
data: [],
backgroundColor: [
'rgba(63, 81, 181, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(33, 150, 243, 0.6)',
'rgba(153, 102, 255, 0.6)'
],
borderColor: 'rgba(63, 81, 181,1)',
borderWidth: 1
}]
};
let languageDistributionData = {
labels: [],
datasets: [{
label: '언어 분포',
data: [],
backgroundColor: [
'rgba(38, 166, 154, 0.6)',
'rgba(211, 47, 47, 0.6)',
'rgba(3, 169, 244, 0.6)',
'rgba(255, 193, 7, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(0, 188, 212, 0.6)'
],
borderColor: 'rgba(38, 166, 154,1)',
borderWidth: 1
}]
};
let techStackData = {
labels: [],
datasets: [{
label: '기술 스택 분포',
data: [],
backgroundColor: [
'rgba(103, 58, 183, 0.6)',
'rgba(255, 202, 40, 0.6)',
'rgba(0, 150, 136, 0.6)',
'rgba(233, 30, 99, 0.6)',
'rgba(245, 124, 0, 0.6)',
'rgba(255, 235, 59, 0.6)'
],
borderColor: 'rgba(103, 58, 183,1)',
borderWidth: 1
}]
};
let domainAgeData = {
labels: [],
datasets: [{
label: '도메인 연령 분포 (년)',
data: [],
backgroundColor: [
'rgba(255, 159, 64, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(153, 102, 255, 0.6)',
'rgba(255, 99, 132, 0.6)'
],
borderColor: 'rgba(255, 159, 64,1)',
borderWidth: 1
}]
};
// 요소 참조
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const exportBtn = document.getElementById('exportBtn');
const clearLogsBtn = document.getElementById('clearLogsBtn');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultsBody = document.getElementById('resultsBody');
const logContainer = document.getElementById('log');
const currentPageElem = document.getElementById('currentPage');
const totalPagesElem = document.getElementById('totalPages');
const toggleHTML = document.getElementById('toggleHTML');
const toggleImages = document.getElementById('toggleImages');
const toggleAds = document.getElementById('toggleAds');
const toggleTechStack = document.getElementById('toggleTechStack');
const toggleOpenGraph = document.getElementById('toggleOpenGraph');
const toggleSecurityHeaders = document.getElementById('toggleSecurityHeaders');
const toggleDomainAge = document.getElementById('toggleDomainAge');
const filterInput = document.getElementById('filterInput');
const excludeDomainsInput = document.getElementById('excludeDomainsInput');
const duplicateThresholdInput = document.getElementById('duplicateThreshold');
const retryAttemptsInput = document.getElementById('retryAttempts');
const visitedCountElem = document.getElementById('visitedCount');
const elapsedTimeElem = document.getElementById('elapsedTime');
const remainingTimeElem = document.getElementById('remainingTime');
const crawlChartCtx = document.getElementById('crawlChart').getContext('2d');
const loadTimeChartCtx = document.getElementById('loadTimeChart').getContext('2d');
const domainDistributionChartCtx = document.getElementById('domainDistributionChart').getContext('2d');
const errorRateChartCtx = document.getElementById('errorRateChart').getContext('2d');
const cumulativeVisitsChartCtx = document.getElementById('cumulativeVisitsChart').getContext('2d');
const keywordDistributionChartCtx = document.getElementById('keywordDistributionChart').getContext('2d');
const languageDistributionChartCtx = document.getElementById('languageDistributionChart').getContext('2d');
const techStackChartCtx = document.getElementById('techStackChart').getContext('2d');
const domainAgeChartCtx = document.getElementById('domainAgeChart').getContext('2d');
const searchEngineSelect = document.getElementById('searchEngineSelect');
const searchResultsInput = document.getElementById('searchResultsInput');
const whoisModal = document.getElementById('whois-modal');
const whoisContent = document.getElementById('whoisContent');
const closeModalBtn = whoisModal.querySelector('.close-modal-btn');
// 미리보기 툴팁 생성
const tooltip = document.createElement('div');
tooltip.className = 'preview-tooltip';
document.body.appendChild(tooltip);
// 이벤트 리스너 등록
startBtn.addEventListener('click', startCrawling);
pauseBtn.addEventListener('click', togglePause);
stopBtn.addEventListener('click', stopCrawling);
resetBtn.addEventListener('click', resetCrawling);
exportBtn.addEventListener('click', exportResults);
clearLogsBtn.addEventListener('click', clearLogs);
filterInput.addEventListener('input', filterResults);
excludeDomainsInput.addEventListener('input', updateExcludeDomains);
duplicateThresholdInput.addEventListener('change', updateDuplicateThreshold);
retryAttemptsInput.addEventListener('change', updateRetryAttempts);
searchEngineSelect.addEventListener('change', updateSearchEngine);
searchResultsInput.addEventListener('input', filterResults);
closeModalBtn.addEventListener('click', () => {
whoisModal.style.display = 'none';
whoisContent.innerHTML = '';
});
window.addEventListener('beforeunload', () => {
resetCrawling();
});
// 로그 및 크롤 데이터 로드
loadLogs();
loadCrawlData();
loadSettings();
// 차트 초기화 함수
function initializeChart() {
crawlChart = new Chart(crawlChartCtx, {
type: 'bar',
data: crawlData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
loadTimeChart = new Chart(loadTimeChartCtx, {
type: 'line',
data: loadTimeData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
domainDistributionChart = new Chart(domainDistributionChartCtx, {
type: 'pie',
data: domainDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
errorRateChart = new Chart(errorRateChartCtx, {
type: 'bar',
data: errorRateData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
cumulativeVisitsChart = new Chart(cumulativeVisitsChartCtx, {
type: 'line',
data: cumulativeVisitsData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true
}
}
}
});
keywordDistributionChart = new Chart(keywordDistributionChartCtx, {
type: 'doughnut',
data: keywordDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
languageDistributionChart = new Chart(languageDistributionChartCtx, {
type: 'pie',
data: languageDistributionData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right'
},
tooltip: {
enabled: true
}
}
}
});
techStackChart = new Chart(techStackChartCtx, {
type: 'bar',
data: techStackData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision:0
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
domainAgeChart = new Chart(domainAgeChartCtx, {
type: 'bar',
data: domainAgeData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '연령 (년)'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: true
}
}
}
});
}
initializeChart();
// 설정 로드
function loadSettings() {
const storedSearchEngine = GM_getValue('searchEngine', 'google');
searchEngine = storedSearchEngine;
searchEngineSelect.value = searchEngine;
const storedToggleHTML = GM_getValue('toggleHTML', true);
toggleHTML.checked = storedToggleHTML;
const storedToggleImages = GM_getValue('toggleImages', true);
toggleImages.checked = storedToggleImages;
const storedToggleAds = GM_getValue('toggleAds', true);
toggleAds.checked = storedToggleAds;
const storedToggleTechStack = GM_getValue('toggleTechStack', true);
toggleTechStack.checked = storedToggleTechStack;
const storedToggleOpenGraph = GM_getValue('toggleOpenGraph', true);
toggleOpenGraph.checked = storedToggleOpenGraph;
const storedToggleSecurityHeaders = GM_getValue('toggleSecurityHeaders', true);
toggleSecurityHeaders.checked = storedToggleSecurityHeaders;
const storedToggleDomainAge = GM_getValue('toggleDomainAge', true);
toggleDomainAge.checked = storedToggleDomainAge;
const storedDuplicateThreshold = GM_getValue('duplicateThreshold', 2);
duplicateThreshold = storedDuplicateThreshold;
duplicateThresholdInput.value = duplicateThreshold;
const storedRetryAttempts = GM_getValue('retryAttempts', 3);
retryAttempts = storedRetryAttempts;
retryAttemptsInput.value = retryAttempts;
const storedFilterInput = GM_getValue('filterInput', '');
filterInput.value = storedFilterInput;
updateFilterPatterns();
const storedExcludeDomainsInput = GM_getValue('excludeDomainsInput', '');
excludeDomainsInput.value = storedExcludeDomainsInput;
updateExcludeDomains();
}
// 설정 저장 함수
function saveSettings() {
GM_setValue('searchEngine', searchEngine);
GM_setValue('toggleHTML', toggleHTML.checked);
GM_setValue('toggleImages', toggleImages.checked);
GM_setValue('toggleAds', toggleAds.checked);
GM_setValue('toggleTechStack', toggleTechStack.checked);
GM_setValue('toggleOpenGraph', toggleOpenGraph.checked);
GM_setValue('toggleSecurityHeaders', toggleSecurityHeaders.checked);
GM_setValue('toggleDomainAge', toggleDomainAge.checked);
GM_setValue('duplicateThreshold', duplicateThreshold);
GM_setValue('retryAttempts', retryAttempts);
GM_setValue('filterInput', filterInput.value.trim());
GM_setValue('excludeDomainsInput', excludeDomainsInput.value.trim());
}
// 검색 엔진 변경 시 처리
function updateSearchEngine() {
searchEngine = searchEngineSelect.value;
GM_setValue('searchEngine', searchEngine);
log(`🔄 검색 엔진 변경: ${capitalizeFirstLetter(searchEngine)}`);
}
// 다양한 설정 변경 시 설정 저장
toggleHTML.addEventListener('change', saveSettings);
toggleImages.addEventListener('change', saveSettings);
toggleAds.addEventListener('change', saveSettings);
toggleTechStack.addEventListener('change', saveSettings);
toggleOpenGraph.addEventListener('change', saveSettings);
toggleSecurityHeaders.addEventListener('change', saveSettings);
toggleDomainAge.addEventListener('change', saveSettings);
filterInput.addEventListener('change', saveSettings);
excludeDomainsInput.addEventListener('change', saveSettings);
// 로그 로드
function loadLogs() {
const storedLogs = GM_getValue('crawlerLogs', []);
if (storedLogs.length > 0) {
logMessages = JSON.parse(storedLogs);
logContainer.innerHTML = `<strong>로그:</strong><br>` + logMessages.join('<br>');
logContainer.scrollTop = logContainer.scrollHeight;
}
}
// 필터 패턴 업데이트
function updateFilterPatterns() {
const input = filterInput.value.trim();
filterPatterns = input ? input.split(',').map(word => {
word = word.trim();
if (word.startsWith('/') && word.endsWith('/')) {
try {
return new RegExp(word.slice(1, -1), 'i');
} catch (e) {
log(`⚠️ 유효하지 않은 정규식: ${word}`);
return null;
}
} else {
return new RegExp(word, 'i');
}
}).filter(regex => regex !== null) : [];
log(`🛠 필터 패턴 업데이트: [${filterPatterns.map(r => r.toString()).join(', ')}]`);
saveSettings();
}
// 제외 도메인 업데이트
function updateExcludeDomains() {
const input = excludeDomainsInput.value.trim();
excludeDomains = input ? input.split(',').map(domain => domain.trim().toLowerCase()) : [];
log(`🚫 제외 도메인 업데이트: [${excludeDomains.join(', ')}]`);
saveSettings();
}
// 중복 허용 횟수 업데이트
function updateDuplicateThreshold() {
const value = parseInt(duplicateThresholdInput.value);
duplicateThreshold = isNaN(value) || value < 1 ? 2 : value;
log(`🔄 중복 허용 횟수 설정: ${duplicateThreshold}`);
saveSettings();
}
// 재시도 횟수 업데이트
function updateRetryAttempts() {
const value = parseInt(retryAttemptsInput.value);
retryAttempts = isNaN(value) || value < 1 ? 3 : value;
log(`🔄 재시도 횟수 설정: ${retryAttempts}`);
saveSettings();
}
// 크롤링 시작 함수
async function startCrawling() {
if (isRunning) {
alert('이미 크롤링이 진행 중입니다.');
return;
}
getSearchQueryAndEngine();
if (!searchQuery) {
alert('현재 페이지에서 검색어를 찾을 수 없습니다.');
log('⚠️ 검색어 미입력 또는 인식 실패.');
return;
}
isRunning = true;
isPaused = false;
currentPage = 1;
totalPages = 0;
results = [];
visitedLinks.clear();
resultsBody.innerHTML = '';
visitedCountElem.innerText = '0';
document.getElementById('totalRequests').innerText = '0';
document.getElementById('successfulRequests').innerText = '0';
document.getElementById('failedRequests').innerText = '0';
elapsedTimeElem.innerText = '00:00:00';
remainingTimeElem.innerText = '00:00:00';
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
log('✅ 크롤링을 시작합니다.');
updateDuplicateThreshold();
updateRetryAttempts();
await crawlPage();
startTime = Date.now();
elapsedInterval = setInterval(updateElapsedTime, 1000);
}
// 일시정지/재개 함수
function togglePause() {
if (!isRunning) {
alert('일시정지/재개할 크롤링이 없습니다.');
return;
}
isPaused = !isPaused;
if (isPaused) {
log('⏸️ 크롤링을 일시정지하였습니다.');
pauseBtn.innerText = '▶️ 재개';
pauseBtn.title = '크롤링 재개';
} else {
log('▶️ 크롤링을 재개하였습니다.');
pauseBtn.innerText = '⏸️ 일시정지';
pauseBtn.title = '크롤링 일시정지';
crawlPage();
}
}
// 크롤링 중지 함수
function stopCrawling() {
if (!isRunning) {
alert('중지할 크롤링이 없습니다.');
return;
}
isRunning = false;
isPaused = false;
pauseBtn.innerText = '⏸️ 일시정지';
pauseBtn.title = '크롤링 일시정지';
clearInterval(elapsedInterval);
updateProgress();
log('🚫 크롤링을 중지하였습니다.');
GM_notification({
text: '크롤링이 중지되었습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
}
// 크롤링 초기화 함수
function resetCrawling() {
stopCrawling();
currentPage = 1;
totalPages = 0;
results = [];
visitedLinks.clear();
resultsBody.innerHTML = '';
visitedCountElem.innerText = '0';
document.getElementById('totalRequests').innerText = '0';
document.getElementById('successfulRequests').innerText = '0';
document.getElementById('failedRequests').innerText = '0';
progressBar.value = 0;
progressText.innerText = '0%';
elapsedTimeElem.innerText = '00:00:00';
remainingTimeElem.innerText = '00:00:00';
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
crawlData = {
labels: [],
datasets: [{
label: '방문 횟수',
data: [],
backgroundColor: 'rgba(33, 150, 243, 0.6)',
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
},
{
label: '평균 페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(76, 175, 80, 0.6)',
borderColor: 'rgba(76, 175, 80, 1)',
borderWidth: 1
}]
};
loadTimeData = {
labels: [],
datasets: [{
label: '페이지 로딩 시간 (ms)',
data: [],
backgroundColor: 'rgba(255, 193, 7, 0.6)',
borderColor: 'rgba(255,193,7,1)',
borderWidth: 1
}]
};
domainDistributionData = {
labels: [],
datasets: [{
label: '도메인 분포',
data: [],
backgroundColor: [
'rgba(33, 150, 243, 0.6)',
'rgba(255, 87, 34, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(158, 158, 158, 0.6)'
],
borderColor: 'rgba(33, 150, 243, 1)',
borderWidth: 1
}]
};
errorRateData = {
labels: [],
datasets: [{
label: '오류 발생 수',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132,1)',
borderWidth: 1
}]
};
cumulativeVisitsData = {
labels: [],
datasets: [{
label: '누적 방문 횟수',
data: [],
backgroundColor: 'rgba(3, 169, 244, 0.6)',
borderColor: 'rgba(3, 169, 244,1)',
borderWidth: 1
}]
};
keywordDistributionData = {
labels: [],
datasets: [{
label: '키워드 분포',
data: [],
backgroundColor: [
'rgba(63, 81, 181, 0.6)',
'rgba(255, 152, 0, 0.6)',
'rgba(76, 175, 80, 0.6)',
'rgba(244, 67, 54, 0.6)',
'rgba(33, 150, 243, 0.6)',
'rgba(153, 102, 255, 0.6)'
],
borderColor: 'rgba(63, 81, 181,1)',
borderWidth: 1
}]
};
languageDistributionData = {
labels: [],
datasets: [{
label: '언어 분포',
data: [],
backgroundColor: [
'rgba(38, 166, 154, 0.6)',
'rgba(211, 47, 47, 0.6)',
'rgba(3, 169, 244, 0.6)',
'rgba(255, 193, 7, 0.6)',
'rgba(156, 39, 176, 0.6)',
'rgba(0, 188, 212, 0.6)'
],
borderColor: 'rgba(38, 166, 154,1)',
borderWidth: 1
}]
};
techStackData = {
labels: [],
datasets: [{
label: '기술 스택 분포',
data: [],
backgroundColor: [
'rgba(103, 58, 183, 0.6)',
'rgba(255, 202, 40, 0.6)',
'rgba(0, 150, 136, 0.6)',
'rgba(233, 30, 99, 0.6)',
'rgba(245, 124, 0, 0.6)',
'rgba(255, 235, 59, 0.6)'
],
borderColor: 'rgba(103, 58, 183,1)',
borderWidth: 1
}]
};
domainAgeData = {
labels: [],
datasets: [{
label: '도메인 연령 분포 (년)',
data: [],
backgroundColor: [
'rgba(255, 159, 64, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(153, 102, 255, 0.6)',
'rgba(255, 99, 132, 0.6)'
],
borderColor: 'rgba(255, 159, 64,1)',
borderWidth: 1
}]
};
if (crawlChart) {
crawlChart.destroy();
}
if (loadTimeChart) {
loadTimeChart.destroy();
}
if (domainDistributionChart) {
domainDistributionChart.destroy();
}
if (errorRateChart) {
errorRateChart.destroy();
}
if (cumulativeVisitsChart) {
cumulativeVisitsChart.destroy();
}
if (keywordDistributionChart) {
keywordDistributionChart.destroy();
}
if (languageDistributionChart) {
languageDistributionChart.destroy();
}
if (techStackChart) {
techStackChart.destroy();
}
if (domainAgeChart) {
domainAgeChart.destroy();
}
initializeChart();
log('🔄 크롤링을 초기화하였습니다.');
}
// 크롤링 페이지 함수
async function crawlPage() {
if (!isRunning || isPaused) {
return;
}
let url = constructURL(currentPage);
if (!url) {
log('❌ 지원하지 않는 검색 엔진입니다.');
alert('지원하지 않는 검색 엔진입니다.');
isRunning = false;
return;
}
log(`📄 페이지 ${currentPage} 크롤링 중: ${url}`);
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"User-Agent": navigator.userAgent,
"Accept": "text/html"
},
onload: function(response) {
if (response.status !== 200) {
log(`❌ 페이지 ${currentPage} 로드 실패: 상태 ${response.status}`);
handleCrawlError(currentPage, retryAttempts);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
return;
}
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
let items = extractItems(doc);
if (currentPage === 1) {
totalPages = determineTotalPages(doc);
totalPagesElem.innerText = totalPages;
}
log(`📊 페이지 ${currentPage}에서 ${items.length}개 항목 발견.`);
document.getElementById('totalRequests').innerText = parseInt(document.getElementById('totalRequests').innerText) + items.length;
items.forEach(item => {
processItem(item);
});
updateChart();
updateLoadTimeChart();
updateDomainDistributionChart();
updateErrorRateChart();
updateCumulativeVisitsChart();
updateKeywordDistributionChart();
updateLanguageDistributionChart();
updateTechStackChart();
updateDomainAgeChart();
updateProgress();
currentPage++;
currentPageElem.innerText = currentPage;
if (currentPage <= totalPages && isRunning && !isPaused) {
setTimeout(crawlPage, 100); // 고정된 100ms 크롤링 속도
} else {
finalizeCrawling();
}
},
onerror: function() {
log(`❌ 페이지 ${currentPage} 요청 중 오류 발생.`);
handleCrawlError(currentPage, retryAttempts);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
});
}
// 크롤링 완료 시 처리 함수
function finalizeCrawling() {
isRunning = false;
clearInterval(elapsedInterval);
log('✅ 크롤링이 완료되었습니다.');
GM_notification({
text: '크롤링이 완료되었습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
}
// 크롤링 오류 처리 함수
function handleCrawlError(page, retriesLeft) {
if (retriesLeft > 0 && isRunning) {
log(`🔄 페이지 ${page} 재시도 (${retriesLeft}회 남음).`);
setTimeout(() => {
const url = constructURL(page);
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"User-Agent": navigator.userAgent,
"Accept": "text/html"
},
onload: function(response) {
if (response.status === 200) {
log(`✅ 페이지 ${page} 재시도 성공.`);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
crawlPage();
} else {
handleCrawlError(page, retriesLeft - 1);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
},
onerror: function() {
handleCrawlError(page, retriesLeft - 1);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
}
});
}, retryInterval);
} else {
log(`❌ 페이지 ${page} 로드 실패: 최대 재시도 횟수 초과.`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
currentPage++;
currentPageElem.innerText = currentPage;
if (currentPage <= totalPages && isRunning && !isPaused) {
setTimeout(crawlPage, 100);
} else {
finalizeCrawling();
}
}
}
// URL 구성 함수
function constructURL(page) {
switch(searchEngine) {
case 'google':
return `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}&start=${(page-1)*10}`;
case 'bing':
return `https://www.bing.com/search?q=${encodeURIComponent(searchQuery)}&first=${(page-1)*10 +1}`;
case 'daum':
return `https://search.daum.net/search?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'naver':
return `https://search.naver.com/search.naver?query=${encodeURIComponent(searchQuery)}&start=${(page-1)*10 +1}`;
case 'yahoo':
return `https://search.yahoo.com/search?p=${encodeURIComponent(searchQuery)}&b=${(page-1)*10 +1}`;
case 'duckduckgo':
return `https://duckduckgo.com/html/?q=${encodeURIComponent(searchQuery)}&s=${(page-1)*30}`;
case 'startpage':
return `https://www.startpage.com/do/search?query=${encodeURIComponent(searchQuery)}&b=${(page-1)*10 +1}`;
case 'ecosia':
return `https://www.ecosia.org/search?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'qwant':
return `https://www.qwant.com/?q=${encodeURIComponent(searchQuery)}&t=web&s=${page}`;
case 'ahmia':
return `http://ahmia.fi/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'torch':
return `http://torchsearch.de/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
case 'notracker':
return `http://notracker.org/search/?q=${encodeURIComponent(searchQuery)}&page=${page}`;
default:
return '';
}
}
// 항목 추출 함수
function extractItems(doc) {
let items = [];
switch(searchEngine) {
case 'google':
items = doc.querySelectorAll('div.g');
break;
case 'bing':
items = doc.querySelectorAll('li.b_algo');
break;
case 'daum':
items = doc.querySelectorAll('div.wrap_cont');
break;
case 'naver':
items = doc.querySelectorAll('div.basic1');
break;
case 'yahoo':
items = doc.querySelectorAll('div.compTitle');
break;
case 'duckduckgo':
items = doc.querySelectorAll('div.result');
break;
case 'startpage':
items = doc.querySelectorAll('div.w-gl__result');
break;
case 'ecosia':
items = doc.querySelectorAll('div.result');
break;
case 'qwant':
items = doc.querySelectorAll('div.results div.result');
break;
case 'ahmia':
items = doc.querySelectorAll('div#main div.result');
break;
case 'torch':
items = doc.querySelectorAll('div.result');
break;
case 'notracker':
items = doc.querySelectorAll('div.result');
break;
default:
break;
}
return items;
}
// 총 페이지 수 결정 함수
function determineTotalPages(doc) {
let pages = 1;
switch(searchEngine) {
case 'google':
const googlePager = doc.querySelectorAll('a[href*="start="]');
pages = Math.min(googlePager.length, 10) || 1;
break;
case 'bing':
const bingPager = doc.querySelectorAll('a.sb_pagS');
pages = bingPager.length || 1;
break;
case 'daum':
const daumPager = doc.querySelectorAll('a.pagination__link');
pages = daumPager.length || 1;
break;
case 'naver':
const naverPager = doc.querySelectorAll('a.nav');
pages = naverPager.length || 1;
break;
case 'yahoo':
const yahooPager = doc.querySelectorAll('a.fz-l.tco-pagination__anchor');
pages = yahooPager.length || 1;
break;
case 'duckduckgo':
const ddgPager = doc.querySelectorAll('a.result--more__btn');
pages = ddgPager.length || 1;
break;
case 'startpage':
const spPager = doc.querySelectorAll('a.paginator__link');
pages = spPager.length || 1;
break;
case 'ecosia':
const ecosiaPager = doc.querySelectorAll('a.pagination__link');
pages = ecosiaPager.length || 1;
break;
case 'qwant':
const qwantPager = doc.querySelectorAll('a.pagination__link');
pages = qwantPager.length || 1;
break;
case 'ahmia':
const ahmiaPager = doc.querySelectorAll('a.pagination__link');
pages = ahmiaPager.length || 1;
break;
case 'torch':
const torchPager = doc.querySelectorAll('a.pagination__link');
pages = torchPager.length || 1;
break;
case 'notracker':
const ntPager = doc.querySelectorAll('a.pagination__link');
pages = ntPager.length || 1;
break;
default:
pages = 1;
break;
}
return pages;
}
// 항목 처리 함수
function processItem(item) {
let title = 'N/A';
let link = 'N/A';
let description = 'N/A';
let image = 'N/A';
let htmlLink = 'N/A';
let whoisInfo = 'N/A';
let pageLoadTime = 'N/A';
let language = 'N/A';
let metaKeywords = 'N/A';
let metaRobots = 'N/A';
let openGraph = 'N/A';
let securityHeaders = 'N/A';
let domainAge = 'N/A';
let techStack = 'N/A'; // 추가된 기술 스택 정보
switch(searchEngine) {
case 'google':
const gTitleElem = item.querySelector('h3');
const gLinkElem = item.querySelector('a');
const gDescElem = item.querySelector('.IsZvec') || item.querySelector('.VwiC3b');
if (gTitleElem) title = gTitleElem.innerText;
if (gLinkElem) link = gLinkElem.href;
if (gDescElem) description = gDescElem.innerText;
const gImgElem = item.querySelector('img');
if (gImgElem && gImgElem.src) {
image = `<img src="${gImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'bing':
const bTitleElem = item.querySelector('h2 a');
const bDescElem = item.querySelector('p');
if (bTitleElem) {
title = bTitleElem.innerText;
link = bTitleElem.href;
}
if (bDescElem) description = bDescElem.innerText;
const bImgElem = item.querySelector('img');
if (bImgElem && bImgElem.src) {
image = `<img src="${bImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'daum':
const dTitleElem = item.querySelector('a.link_fax');
const dDescElem = item.querySelector('div.cont_inner');
if (dTitleElem) {
title = dTitleElem.innerText.trim();
link = dTitleElem.href;
}
if (dDescElem) description = dDescElem.innerText.trim();
const dImgElem = item.querySelector('img');
if (dImgElem && dImgElem.src) {
image = `<img src="${dImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'naver':
const nTitleElem = item.querySelector('a.tit');
const nDescElem = item.querySelector('div.desc');
if (nTitleElem) {
title = nTitleElem.innerText;
link = nTitleElem.href;
}
if (nDescElem) description = nDescElem.innerText;
const nImgElem = item.querySelector('img.thumb');
if (nImgElem && nImgElem.src) {
image = `<img src="${nImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'yahoo':
const yTitleElem = item.querySelector('h3');
const yLinkElem = item.querySelector('a');
const yDescElem = item.querySelector('div.compText');
if (yTitleElem) title = yTitleElem.innerText;
if (yLinkElem) link = yLinkElem.href;
if (yDescElem) description = yDescElem.innerText;
const yImgElem = item.querySelector('img');
if (yImgElem && yImgElem.src) {
image = `<img src="${yImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'duckduckgo':
const ddgTitleElem = item.querySelector('a.result__a');
const ddgDescElem = item.querySelector('a.result__snippet');
if (ddgTitleElem) {
title = ddgTitleElem.innerText;
link = ddgTitleElem.href;
}
if (ddgDescElem) description = ddgDescElem.innerText;
const ddgImgElem = item.querySelector('img.result__image');
if (ddgImgElem && ddgImgElem.src) {
image = `<img src="${ddgImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'startpage':
const spTitleElem = item.querySelector('a.w-gl__result-title');
const spDescElem = item.querySelector('p.w-gl__result-description');
if (spTitleElem) {
title = spTitleElem.innerText;
link = spTitleElem.href;
}
if (spDescElem) description = spDescElem.innerText;
const spImgElem = item.querySelector('img.w-gl__result-image');
if (spImgElem && spImgElem.src) {
image = `<img src="${spImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'ecosia':
const ecosiaTitleElem = item.querySelector('h2.result-title a');
const ecosiaDescElem = item.querySelector('p.result-description');
if (ecosiaTitleElem) {
title = ecosiaTitleElem.innerText;
link = ecosiaTitleElem.href;
}
if (ecosiaDescElem) description = ecosiaDescElem.innerText;
const ecosiaImgElem = item.querySelector('img.result-image');
if (ecosiaImgElem && ecosiaImgElem.src) {
image = `<img src="${ecosiaImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'qwant':
const qwantTitleElem = item.querySelector('a.result__title');
const qwantDescElem = item.querySelector('p.result__description');
if (qwantTitleElem) {
title = qwantTitleElem.innerText;
link = qwantTitleElem.href;
}
if (qwantDescElem) description = qwantDescElem.innerText;
const qwantImgElem = item.querySelector('img.result__image');
if (qwantImgElem && qwantImgElem.src) {
image = `<img src="${qwantImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'ahmia':
const ahmiaTitleElem = item.querySelector('h2.result-title a');
const ahmiaDescElem = item.querySelector('p.result-description');
if (ahmiaTitleElem) {
title = ahmiaTitleElem.innerText;
link = ahmiaTitleElem.href;
}
if (ahmiaDescElem) description = ahmiaDescElem.innerText;
const ahmiaImgElem = item.querySelector('img.result-image');
if (ahmiaImgElem && ahmiaImgElem.src) {
image = `<img src="${ahmiaImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'torch':
const torchTitleElem = item.querySelector('h3.result-title a');
const torchDescElem = item.querySelector('p.result-description');
if (torchTitleElem) {
title = torchTitleElem.innerText;
link = torchTitleElem.href;
}
if (torchDescElem) description = torchDescElem.innerText;
const torchImgElem = item.querySelector('img.result-image');
if (torchImgElem && torchImgElem.src) {
image = `<img src="${torchImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
case 'notracker':
const ntTitleElem = item.querySelector('h3.result-title a');
const ntDescElem = item.querySelector('p.result-description');
if (ntTitleElem) {
title = ntTitleElem.innerText;
link = ntTitleElem.href;
}
if (ntDescElem) description = ntDescElem.innerText;
const ntImgElem = item.querySelector('img.result-image');
if (ntImgElem && ntImgElem.src) {
image = `<img src="${ntImgElem.src}" alt="이미지" style="max-width: 80px; max-height: 80px;">`;
}
if (toggleHTML.checked && link !== 'N/A') {
whoisInfo = `<button class="whois-view-btn" data-domain="${encodeURIComponent(extractDomain(link))}">보기</button>`;
htmlLink = `<button class="html-view-btn" data-link="${encodeURIComponent(link)}">보기</button>`;
}
break;
default:
break;
}
// 추가 메타 정보 수집
try {
const metaKeywordElem = item.querySelector('meta[name="keywords"]');
if (metaKeywordElem) {
metaKeywords = metaKeywordElem.getAttribute('content');
if (metaKeywords) {
metaKeywords = metaKeywords.split(',').map(k => k.trim()).join(', ');
} else {
metaKeywords = 'N/A';
}
}
const metaRobotsElem = item.querySelector('meta[name="robots"]');
if (metaRobotsElem) {
metaRobots = metaRobotsElem.getAttribute('content') || 'N/A';
}
const ogTitle = item.querySelector('meta[property="og:title"]')?.getAttribute('content') || 'N/A';
const ogDescription = item.querySelector('meta[property="og:description"]')?.getAttribute('content') || 'N/A';
const ogImage = item.querySelector('meta[property="og:image"]')?.getAttribute('content') || 'N/A';
openGraph = `Title: ${ogTitle}, Description: ${ogDescription}, Image: ${ogImage}`;
// 보안 헤더 추출 (간단한 예시)
const securityHeadersArr = [];
const hsts = item.querySelector('meta[http-equiv="Strict-Transport-Security"]');
if (hsts) securityHeadersArr.push('HSTS');
const contentSecurityPolicy = item.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (contentSecurityPolicy) securityHeadersArr.push('CSP');
securityHeaders = securityHeadersArr.length > 0 ? securityHeadersArr.join(', ') : 'N/A';
const domainInfo = getDomainInfo(link);
domainAge = domainInfo ? domainInfo.age : 'N/A';
const htmlLang = item.querySelector('html');
if (htmlLang && htmlLang.lang) {
language = htmlLang.lang;
}
// 기술 스택 정보 수집 (예시: 간단한 스택 감지)
const techs = [];
if (item.querySelector('.wordpress')) techs.push('WordPress');
if (item.querySelector('.shopify')) techs.push('Shopify');
if (item.querySelector('.react')) techs.push('React');
if (item.querySelector('.angular')) techs.push('Angular');
if (item.querySelector('.vue')) techs.push('Vue.js');
techStack = techs.length > 0 ? techs.join(', ') : 'N/A';
} catch (e) {
log(`⚠️ 메타 정보 수집 중 오류 발생: ${e.message}`);
}
// 광고 제외 필터링
if (toggleAds.checked) {
const combinedText = `${title} ${description}`.toLowerCase();
const isAd = combinedText.includes('advertisement') || combinedText.includes('광고') || combinedText.includes('ad ');
if (isAd) {
log(`🚫 광고 항목 제외: "${title}"`);
return;
}
}
// 필터 패턴 적용
if (filterPatterns.length > 0) {
const combinedText = `${title} ${description}`;
const isFiltered = filterPatterns.some(regex => regex.test(combinedText));
if (isFiltered) {
log(`❌ 필터 단어 포함 항목 제외: "${title}"`);
return;
}
}
// 도메인 추출 및 제외 도메인 적용
const domain = extractDomain(link);
if (!domain) return;
if (excludeDomains.includes(domain.toLowerCase())) {
log(`🚫 제외 도메인 항목 제외: "${link}"`);
return;
}
// 중복 방문 제한
let visitCount = visitedLinks.get(domain) || 0;
if (visitCount >= duplicateThreshold) {
log(`🔄 중복 방문 제한: "${domain}"`);
return;
}
visitedLinks.set(domain, visitCount + 1);
visitedCountElem.innerText = visitedLinks.size.toString();
// 페이지 로딩 시간 측정
const startLoad = performance.now();
fetch(link, { mode: 'no-cors' })
.then(response => {
const endLoad = performance.now();
pageLoadTime = Math.round(endLoad - startLoad);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
updateRow();
})
.catch(err => {
pageLoadTime = '계산 불가';
log(`❌ 페이지 로딩 오류: "${link}"`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
updateRow();
});
// 언어, 키워드, 로봇 메타 정보 저장
const itemLanguage = language;
const itemKeywords = metaKeywords;
const itemRobots = metaRobots;
// WHOIS 정보 버튼 이벤트 추가 (이미 처리됨)
// 이미지 옵션 적용
if (!toggleImages.checked) {
image = '비활성화';
}
// OpenGraph 옵션 적용
if (!toggleOpenGraph.checked) {
openGraph = '비활성화';
}
// 보안 헤더 옵션 적용
if (!toggleSecurityHeaders.checked) {
securityHeaders = '비활성화';
}
// 도메인 연령 옵션 적용
if (!toggleDomainAge.checked) {
domainAge = '비활성화';
}
// 결과 배열에 추가
results.push({ title, link, description, image, htmlLink, whoisInfo, pageLoadTime, language, metaKeywords, metaRobots, openGraph, securityHeaders, domainAge, techStack });
// 결과 테이블에 행 추가
const row = document.createElement('tr');
row.innerHTML = `
<td>${escapeHTML(title)}</td>
<td><a href="${escapeHTML(link)}" target="_blank">${escapeHTML(link)}</a></td>
<td>${escapeHTML(description)}</td>
<td>${image}</td>
<td>${htmlLink}</td>
<td>${whoisInfo}</td>
<td>${pageLoadTime}</td>
<td>${escapeHTML(itemLanguage)}</td>
<td>${escapeHTML(itemKeywords)}</td>
<td>${escapeHTML(itemRobots)}</td>
<td>${escapeHTML(openGraph)}</td>
<td>${escapeHTML(securityHeaders)}</td>
<td>${escapeHTML(domainAge)}</td>
<td>${escapeHTML(techStack)}</td>
`;
resultsBody.appendChild(row);
// 미리보기 이벤트 추가
addPreviewEvents(row, link);
// WHOIS 버튼 이벤트 추가
const whoisBtn = row.querySelector('.whois-view-btn');
if (whoisBtn) {
whoisBtn.addEventListener('click', () => {
const encodedDomain = whoisBtn.getAttribute('data-domain');
const decodedDomain = decodeURIComponent(encodedDomain);
openWhoisModal(decodedDomain);
});
}
function updateRow() {
// 업데이트된 로딩 시간을 테이블에 반영
row.cells[6].innerText = pageLoadTime;
row.cells[10].innerText = openGraph;
row.cells[11].innerText = securityHeaders;
row.cells[12].innerText = domainAge;
updateChart();
updateLoadTimeChart();
updateDomainDistributionChart();
updateErrorRateChart();
updateCumulativeVisitsChart();
updateKeywordDistributionChart();
updateLanguageDistributionChart();
updateTechStackChart();
updateDomainAgeChart();
updateProgress();
}
}
// HTML 이스케프 함수
function escapeHTML(str) {
if (!str) return '';
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 도메인 추출 함수
function extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace('www.', '');
} catch (e) {
return null;
}
}
// 도메인 연령 정보 가져오기 함수
function getDomainInfo(url) {
try {
const urlObj = new URL(url);
// 실제 WHOIS 데이터를 통해 도메인 연령을 가져와야 하지만, 샘플 데이터로 대체
const creationYear = 2010; // 임의의 생성 연도
const currentYear = new Date().getFullYear();
const age = currentYear - creationYear;
return { age };
} catch (e) {
return null;
}
}
// 경과 시간 업데이트 함수
function updateElapsedTime() {
if (!startTime) return;
const now = Date.now();
const elapsed = Math.floor((now - startTime) / 1000);
elapsedTimeElem.innerText = formatTime(elapsed);
const estimatedTotal = totalPages * ((elapsed > 0) ? (elapsed / (currentPage - 1)) : 0.5);
const remaining = Math.max(Math.round(estimatedTotal - elapsed), 0);
remainingTimeElem.innerText = formatTime(remaining);
}
// 시간 형식화 함수
function formatTime(seconds) {
const hrs = String(Math.floor(seconds / 3600)).padStart(2, '0');
const mins = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
const secs = String(seconds % 60).padStart(2, '0');
return `${hrs}:${mins}:${secs}`;
}
// 차트 업데이트 함수
function updateChart() {
crawlData.labels = Array.from(visitedLinks.keys());
crawlData.datasets[0].data = Array.from(visitedLinks.values());
// 평균 페이지 로딩 시간 계산
let loadTimes = results.filter(r => typeof r.pageLoadTime === 'number').map(r => r.pageLoadTime);
let averageLoadTime = loadTimes.length > 0 ? Math.round(loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length) : 0;
crawlData.datasets[1].data = Array.from(visitedLinks.keys()).map(domain => {
const times = results.filter(r => extractDomain(r.link) === domain && typeof r.pageLoadTime === 'number').map(r => r.pageLoadTime);
return times.length > 0 ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : 0;
});
if (crawlChart) {
crawlChart.update();
}
GM_setValue('crawlerData', JSON.stringify(crawlData));
}
// 추가 차트 업데이트 함수
function updateLoadTimeChart() {
loadTimeData.labels = results.map(r => r.title).slice(-20); // 최근 20개
loadTimeData.datasets[0].data = results.map(r => r.pageLoadTime).slice(-20);
if (loadTimeChart) {
loadTimeChart.update();
}
}
function updateDomainDistributionChart() {
domainDistributionData.labels = Array.from(visitedLinks.keys());
domainDistributionData.datasets[0].data = Array.from(visitedLinks.values());
if (domainDistributionChart) {
domainDistributionChart.update();
}
}
function updateErrorRateChart() {
// 오류 발생 수를 도메인별로 집계
const errorCounts = {};
results.forEach(r => {
if (r.pageLoadTime === '계산 불가') {
const domain = extractDomain(r.link);
if (domain && !excludeDomains.includes(domain.toLowerCase())) {
errorCounts[domain] = (errorCounts[domain] || 0) + 1;
}
}
});
errorRateData.labels = Object.keys(errorCounts);
errorRateData.datasets[0].data = Object.values(errorCounts);
if (errorRateChart) {
errorRateChart.update();
}
}
function updateCumulativeVisitsChart() {
let cumulative = 0;
cumulativeVisitsData.labels = results.map((_, index) => index + 1);
cumulativeVisitsData.datasets[0].data = results.map(() => { cumulative += 1; return cumulative; });
if (cumulativeVisitsChart) {
cumulativeVisitsChart.update();
}
}
function updateKeywordDistributionChart() {
const keywordCounts = {};
results.forEach(r => {
if (r.metaKeywords && r.metaKeywords !== 'N/A') {
const keywords = r.metaKeywords.split(',').map(k => k.trim());
keywords.forEach(k => {
if (k) {
keywordCounts[k] = (keywordCounts[k] || 0) + 1;
}
});
}
});
keywordDistributionData.labels = Object.keys(keywordCounts);
keywordDistributionData.datasets[0].data = Object.values(keywordCounts);
if (keywordDistributionChart) {
keywordDistributionChart.update();
}
}
function updateLanguageDistributionChart() {
const languageCounts = {};
results.forEach(r => {
if (r.language && r.language !== 'N/A') {
languageCounts[r.language] = (languageCounts[r.language] || 0) + 1;
}
});
languageDistributionData.labels = Object.keys(languageCounts);
languageDistributionData.datasets[0].data = Object.values(languageCounts);
if (languageDistributionChart) {
languageDistributionChart.update();
}
}
function updateTechStackChart() {
const techCounts = {};
results.forEach(r => {
if (r.techStack && r.techStack !== 'N/A') {
const techs = r.techStack.split(',').map(t => t.trim());
techs.forEach(t => {
if (t) {
techCounts[t] = (techCounts[t] || 0) + 1;
}
});
}
});
techStackData.labels = Object.keys(techCounts);
techStackData.datasets[0].data = Object.values(techCounts);
if (techStackChart) {
techStackChart.update();
}
}
function updateDomainAgeChart() {
const ageCounts = {};
results.forEach(r => {
if (r.domainAge && r.domainAge !== 'N/A') {
const age = r.domainAge;
if (age >= 0 && age <= 1) {
ageCounts['0-1'] = (ageCounts['0-1'] || 0) + 1;
} else if (age <= 3) {
ageCounts['2-3'] = (ageCounts['2-3'] || 0) + 1;
} else if (age <= 5) {
ageCounts['4-5'] = (ageCounts['4-5'] || 0) + 1;
} else if (age <= 10) {
ageCounts['6-10'] = (ageCounts['6-10'] || 0) + 1;
} else {
ageCounts['10+'] = (ageCounts['10+'] || 0) + 1;
}
}
});
domainAgeData.labels = Object.keys(ageCounts);
domainAgeData.datasets[0].data = Object.values(ageCounts);
if (domainAgeChart) {
domainAgeChart.update();
}
}
// 크롤 데이터 로드 함수
function loadCrawlData() {
const storedData = GM_getValue('crawlerData', null);
if (storedData) {
const parsedData = JSON.parse(storedData);
crawlData = parsedData;
if (crawlChart) {
crawlChart.data = crawlData;
crawlChart.update();
}
}
}
// 크롤 데이터 저장 함수
function saveCrawlData() {
GM_setValue('crawlerData', JSON.stringify(crawlData));
}
// 주기적으로 크롤 데이터 저장
setInterval(saveCrawlData, 5000);
// 검색어 및 검색 엔진 감지 함수
function getSearchQueryAndEngine() {
const url = window.location.href;
if (url.includes('google.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'google';
} else if (url.includes('bing.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'bing';
} else if (url.includes('daum.net/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'daum';
} else if (url.includes('search.naver.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('query') || '';
searchEngine = 'naver';
} else if (url.includes('search.yahoo.com/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('p') || '';
searchEngine = 'yahoo';
} else if (url.includes('duckduckgo.com/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'duckduckgo';
} else if (url.includes('startpage.com/do/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('query') || '';
searchEngine = 'startpage';
} else if (url.includes('ecosia.org/search')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'ecosia';
} else if (url.includes('qwant.com/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'qwant';
} else if (url.includes('ahmia.fi/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'ahmia';
} else if (url.includes('torchsearch.de/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'torch';
} else if (url.includes('notracker.org/')) {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') || '';
searchEngine = 'notracker';
} else {
searchQuery = '';
searchEngine = '';
}
log(`🔍 검색 엔진: ${capitalizeFirstLetter(searchEngine)}, 검색어: "${searchQuery}"`);
}
// 로그 기록 함수
function log(message) {
const time = new Date().toLocaleTimeString();
const entry = `[${time}] ${message}`;
logMessages.push(entry);
logContainer.innerHTML += `${entry}<br>`;
logContainer.scrollTop = logContainer.scrollHeight;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
}
// 결과 내보내기 함수 (중복 제거 및 정렬 추가)
function exportResults() {
if (results.length === 0) {
alert('내보낼 결과가 없습니다.');
return;
}
const format = prompt('내보낼 형식을 선택하세요 (xlsx, csv, json):', 'xlsx');
if (!format) return;
if (format.toLowerCase() === 'xlsx') {
const ws = XLSX.utils.json_to_sheet(results);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "결과");
XLSX.writeFile(wb, "크롤링_결과.xlsx");
log('💾 결과를 "크롤링_결과.xlsx" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.xlsx" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else if (format.toLowerCase() === 'csv') {
const csv = results.map(row => `"${row.title.replace(/"/g, '""')}","${row.link.replace(/"/g, '""')}","${row.description.replace(/"/g, '""')}","${row.image.replace(/"/g, '""')}","${row.htmlLink}","${row.whoisInfo}","${row.pageLoadTime}","${row.language}","${row.metaKeywords}","${row.metaRobots}","${row.openGraph}","${row.securityHeaders}","${row.domainAge}","${row.techStack}"`).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '크롤링_결과.csv';
a.click();
log('💾 결과를 "크롤링_결과.csv" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.csv" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else if (format.toLowerCase() === 'json') {
const json = JSON.stringify(results, null, 2);
const blob = new Blob([json], { type: 'application/json;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '크롤링_결과.json';
a.click();
log('💾 결과를 "크롤링_결과.json" 파일로 내보냈습니다.');
GM_notification({
text: '결과를 "크롤링_결과.json" 파일로 내보냈습니다.',
title: '웹 크롤러 8.6',
timeout: 3000
});
} else {
alert('지원하지 않는 형식입니다.');
}
}
// 결과 테이블 정렬 함수
function sortTable(n) {
const table = document.getElementById("resultsTable");
let switching = true;
let dir = "asc";
let switchcount = 0;
while (switching) {
switching = false;
const rows = table.rows;
for (let i = 1; i < (rows.length - 1); i++) {
let shouldSwitch = false;
const x = rows[i].getElementsByTagName("TD")[n];
const y = rows[i + 1].getElementsByTagName("TD")[n];
let xContent = x.textContent || x.innerText;
let yContent = y.textContent || y.innerText;
// 숫자 비교
if (n === 6 || n === 12) { // 페이지 로딩 시간과 도메인 연령
xContent = parseInt(xContent);
yContent = parseInt(yContent);
}
if (dir === "asc") {
if (xContent > yContent) {
shouldSwitch = true;
break;
}
} else if (dir === "desc") {
if (xContent < yContent) {
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount === 0 && dir === "asc") {
dir = "desc";
switching = true;
}
}
}
}
// 사용자 경험 향상을 위한 함수 추가
function capitalizeFirstLetter(string) {
if (!string) return '';
return string.charAt(0).toUpperCase() + string.slice(1);
}
// 항목 검색 기능
function filterResults() {
const filter = searchResultsInput.value.toLowerCase();
const rows = resultsBody.getElementsByTagName('tr');
for (let i = 0; i < rows.length; i++) {
const cells = rows[i].getElementsByTagName('td');
let match = false;
for (let j = 0; j < cells.length - 9; j++) { // 제외 항목 인덱스 조정
if (cells[j].innerText.toLowerCase().includes(filter)) {
match = true;
break;
}
}
rows[i].style.display = match ? '' : 'none';
}
}
// 로그 초기화 함수
function clearLogs() {
if(confirm('로그를 초기화하시겠습니까?')) {
logMessages = [];
logContainer.innerHTML = `<strong>로그:</strong><br>`;
GM_setValue('crawlerLogs', JSON.stringify(logMessages));
log('🧹 로그를 초기화하였습니다.');
}
}
// 진행률 업데이트 함수
function updateProgress() {
if (totalPages === 0) {
progressBar.value = 0;
progressText.innerText = '0%';
return;
}
const progress = Math.min((currentPage - 1) / totalPages * 100, 100);
progressBar.value = progress;
progressText.innerText = `${Math.round(progress)}%`;
}
// 크롤링 오류가 발생한 행에 대한 미리보기 추가
function addPreviewEvents(row, link) {
const htmlBtn = row.querySelector('.html-view-btn');
if (htmlBtn) {
htmlBtn.addEventListener('click', () => {
const decodedLink = decodeURIComponent(htmlBtn.getAttribute('data-link'));
openPreview(decodedLink);
});
// HTML 버튼에 마우스 호버 시 미리보기 표시
htmlBtn.addEventListener('mouseover', (e) => {
const decodedLink = decodeURIComponent(htmlBtn.getAttribute('data-link'));
showPreview(e, decodedLink);
});
htmlBtn.addEventListener('mouseout', hidePreview);
}
}
// 미리보기 표시 함수
function showPreview(event, link) {
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY + 10) + 'px';
tooltip.style.display = 'block';
tooltip.innerHTML = `<iframe src="${link}" title="미리보기"></iframe>`;
}
function hidePreview() {
tooltip.style.display = 'none';
tooltip.innerHTML = '';
}
// 사이트 미리보기 열기 함수
function openPreview(link) {
const analyticsTab = container.querySelector('#analytics');
const previewContainerId = 'site-preview-container';
let previewContainer = container.querySelector(`#${previewContainerId}`);
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.id = previewContainerId;
previewContainer.className = 'chart-container';
previewContainer.style.height = '400px';
analyticsTab.appendChild(previewContainer);
}
previewContainer.innerHTML = `
<h3>사이트 미리보기: <a href="${link}" target="_blank">${link}</a></h3>
<iframe src="${link}" style="width: 100%; height: 350px; border: none; border-radius: 5px;"></iframe>
`;
// 분석 탭으로 이동
const analyticsTabBtn = container.querySelector('.tab[data-tab="analytics"]');
analyticsTabBtn.click();
}
// WHOIS 모달 열기 함수
function openWhoisModal(domain) {
whoisContent.innerHTML = `<p>로딩 중...</p>`;
whoisModal.style.display = 'block';
fetchWhoisData(domain);
}
// WHOIS 정보 가져오기 및 표시 함수
function fetchWhoisData(domain) {
log(`📡 WHOIS 정보를 가져오는 중: ${domain}`);
const apiUrl = `https://jsonwhois.com/api/v1/whois?domain=${encodeURIComponent(domain)}`;
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Content-Type": "application/json",
"Authorization": `Token token=${WHOIS_API_KEY}`
},
onload: function(response) {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
displayWhoisData(data, domain);
log(`✅ WHOIS 정보를 성공적으로 가져왔습니다: ${domain}`);
document.getElementById('successfulRequests').innerText = parseInt(document.getElementById('successfulRequests').innerText) + 1;
} else {
log(`❌ WHOIS 정보 가져오기 실패: ${domain}, 상태 코드: ${response.status}`);
whoisContent.innerHTML = `<p>WHOIS 정보를 가져오는 데 실패했습니다.</p>`;
}
},
onerror: function() {
log(`❌ WHOIS 정보 요청 중 오류 발생: ${domain}`);
document.getElementById('failedRequests').innerText = parseInt(document.getElementById('failedRequests').innerText) + 1;
whoisContent.innerHTML = `<p>WHOIS 정보를 가져오는 데 오류가 발생했습니다.</p>`;
}
});
}
// WHOIS 데이터 표시 함수
function displayWhoisData(data, domain) {
if (!data) {
whoisContent.innerHTML = `<p>No WHOIS data available.</p>`;
return;
}
const whoisHtml = `
<h3>${domain}의 WHOIS 정보</h3>
<pre>${JSON.stringify(data, null, 2)}</pre>
`;
whoisContent.innerHTML = whoisHtml;
}
// *******************************
// 시작
// *******************************
})();
```