Files
Unimarc/unimarc/unimarc/SearchModel/BukguLibSearcher.cs
Arin(asus) b364ffc054 feat: 크롤링 1차 완료 - 400개 이상 도서관 목록 완성
추가된 도서관 시스템:
• 광주남구도서관 (5개관)
• 광주시교육청통합도서관 (6개관)
• 전남교육청통합도서관 (25개관)
• 전남교육청행정자료실 (1개관)
• 여수시립도서관 (34개관)
• 고흥군립도서관 (7개관)
• 광주북구통합도서관 (3개관)
• 광주북구작은도서관 (23개관)
• 광주북구공공도서관 (5개관)
• 전북교육청도서관 (18개관)
• 광주광산구통합도서관 (17개관)
• 목포시립도서관 (23개관)
• 순천시립도서관 (10개관)
• 광주시립도서관 (4개관)
• 완도군립도서관 (6개관)
• 익산시통합도서관 (33개관)
• 안산시중앙도서관 (27개관)
• 광주서구구립도서관 (4개관)
• 광주서구스마트도서관 (4개관)
• 광주서구작은도서관 (5개관)
• 광주동구도서관 (5개관)
• 경남대표도서관 (1개관)
• 무안군립도서관 (1개관)
• 조선대학교중앙도서관 (1개관)
• 조선이공대학교도서관 (1개관)
• KCM통합도서관 (33개관)

총 400개 이상 도서관 복본조사 시스템 완성
HTTP API 방식 및 Selenium 크롤링 방식 혼용
브라우저 헤더 최적화 및 80% 화면배율 적용

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 16:23:46 +09:00

422 lines
15 KiB
C#

using AR;
using AR.Dialog;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Chromium;
using OpenQA.Selenium.Support.UI;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using UniMarc.SearchModel;
using UniMarc.;
using WebDriverManager;
using WebDriverManager.DriverConfigs.Impl;
namespace BokBonCheck
{
public class BukguLibSearcher : ILibrarySearcher
{
public string AreaCode { get; set; } = string.Empty;
public string SiteName { get; protected set; }
public string SiteUrl { get; protected set; } = "https://lib.bukgu.gwangju.kr/main/bookSearchSmartlib.do?PID=0301";
public bool HttpApiMode { get; set; } = false;
public int No { get; set; }
private ChromiumDriver _driver;
public BukguLibSearcher(int no, string areaCode, string areaName)
{
this.No = no;
this.AreaCode = areaCode;
this.SiteName = $"광주북구스마트도서관({areaName})";
}
public void StopDriver()
{
if (_driver != null)
{
_driver.Quit();
_driver.Dispose();
_driver = null;
}
}
public async Task StartDriver(bool showdriver = false)
{
if (_driver == null)
{
try
{
if (SeleniumHelper.IsReady == false) await SeleniumHelper.Download();
_driver = await SeleniumHelper.CreateDriver(ShowBrowser: showdriver);
Console.WriteLine("BukguLibSearcher Driver 초기화 완료");
}
catch (Exception ex)
{
Console.WriteLine($"BukguLibSearcher Driver 초기화 실패: {ex.Message}");
throw new InvalidOperationException($"BukguLibSearcher Driver 초기화에 실패했습니다: {ex.Message}", ex);
}
}
}
virtual protected bool SelectLibrary(WebDriverWait wait)
{
try
{
// 특정 도서관 선택인 경우
var targetCheckboxId = $"lib{AreaCode}";
var targetCheckbox = wait.Until(d => d.FindElement(By.Id(targetCheckboxId)));
if (targetCheckbox == null)
{
Console.WriteLine($"도서관 체크박스를 찾을 수 없습니다: {targetCheckboxId}");
return false;
}
// 다른 체크박스들 해제
var allCheckboxes = wait.Until(d => d.FindElements(By.CssSelector("input[name='libCode']")));
foreach (var checkbox in allCheckboxes)
{
if (checkbox.GetAttribute("id") != targetCheckboxId && checkbox.Selected)
{
SafeClick(checkbox);
Thread.Sleep(100);
}
}
// 대상 체크박스 선택
if (!targetCheckbox.Selected)
{
SafeClick(targetCheckbox);
Thread.Sleep(300);
Console.WriteLine($"{AreaCode} 도서관으로 변경됨");
}
else
{
Console.WriteLine($"{AreaCode} 도서관이 이미 선택되어 있음");
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"도서관 선택 실패: {ex.Message}");
return false;
}
}
protected void SafeClick(IWebElement element)
{
// 안정적인 클릭을 위한 여러 방법 시도
try
{
// 1. JavaScript로 클릭 시도
var driver = ((IWrapsDriver)element).WrappedDriver;
((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click();", element);
}
catch
{
try
{
// 2. 요소가 보이도록 스크롤 후 클릭
var driver = ((IWrapsDriver)element).WrappedDriver;
((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].scrollIntoView(true);", element);
Thread.Sleep(500);
element.Click();
}
catch
{
try
{
// 3. Actions 클래스 사용
var driver = ((IWrapsDriver)element).WrappedDriver;
var actions = new OpenQA.Selenium.Interactions.Actions(driver);
actions.MoveToElement(element).Click().Perform();
}
catch
{
// 4. 마지막 방법: JavaScript로 직접 체크 상태 변경
var driver = ((IWrapsDriver)element).WrappedDriver;
((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].checked = !arguments[0].checked;", element);
}
}
}
}
public async Task<BookSearchResult> SearchAsync(string searchTerm)
{
var result = new BookSearchResult
{
SiteName = SiteName,
SearchTerm = searchTerm,
SearchTime = DateTime.Now
};
try
{
// 드라이버가 없으면 자동으로 시작
if (_driver == null)
{
await StartDriver();
}
var cururl = _driver.Url;
if (cururl.Equals(SiteUrl) == false)
_driver.Navigate().GoToUrl(SiteUrl);
// 페이지 로딩 대기
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(15));
// 도서관 선택
if (SelectLibrary(wait) == false)
{
result.ErrorMessage = "도서관선택실패";
result.BookCount = -1;
result.IsSuccess = false;
return result;
}
// 검색어 입력 - 다양한 선택자로 시도
try
{
IWebElement searchInput = null;
try
{
searchInput = wait.Until(d => d.FindElement(By.CssSelector("input[name='searchText']")));
}
catch { }
if (searchInput == null)
{
result.ErrorMessage = "검색창을 찾을 수 없음";
result.BookCount = -1;
result.IsSuccess = false;
return result;
}
searchInput.Clear();
searchInput.SendKeys(searchTerm);
}
catch (Exception ex)
{
result.ErrorMessage = $"검색어입력실패({ex.Message})";
result.BookCount = -1;
result.IsSuccess = false;
return result;
}
// 검색 버튼 클릭 - 다양한 선택자로 시도
try
{
IWebElement searchButton = null;
try
{
searchButton = wait.Until(d => d.FindElement(By.CssSelector("button[type='submit']")));
}
catch { }
if (searchButton == null)
{
result.ErrorMessage = "검색버튼을 찾을 수 없음";
result.BookCount = -1;
result.IsSuccess = false;
return result;
}
searchButton.Click();
}
catch (Exception ex)
{
result.ErrorMessage = $"검색버튼클릭실패({ex.Message})";
result.BookCount = -1;
result.IsSuccess = false;
return result;
}
// 페이지 변경을 감지하는 메서드
await WaitForPageChange(new WebDriverWait(_driver, TimeSpan.FromSeconds(15)));
// 검색 결과 수 추출
var resultCount = ExtractBookCount(_driver, searchTerm, out string ermsg);
if (resultCount == -1)
{
result.BookCount = 0;
result.IsSuccess = false;
result.ErrorMessage = ermsg;
}
else
{
result.BookCount = resultCount;
result.IsSuccess = true;
result.ErrorMessage = ermsg;
}
}
catch (Exception ex)
{
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.BookCount = 0;
}
return result;
}
private int ExtractBookCount(IWebDriver driver, string searchTerm, out string errmessage)
{
errmessage = string.Empty;
try
{
// 1. totalCount 요소에서 직접 추출 시도
try
{
var totalCountElement = driver.FindElement(By.CssSelector("p.totalCount"));
if (totalCountElement != null)
{
// strong 태그에서 직접 숫자 추출 시도
try
{
var strongElement = totalCountElement.FindElement(By.TagName("strong"));
if (strongElement != null)
{
var countText = strongElement.Text.Trim();
if (int.TryParse(countText, out int strongCount))
{
if (strongCount == 0)
{
errmessage = "검색결과없음";
return 0;
}
errmessage = $"검색성공({strongCount}권)";
return strongCount;
}
}
}
catch { }
// 전체 텍스트에서 정규식으로 추출
var totalCountText = totalCountElement.Text;
var match = Regex.Match(totalCountText, @"전체\s*(\d+)\s*건", RegexOptions.IgnoreCase);
if (match.Success)
{
if (int.TryParse(match.Groups[1].Value, out int count))
{
if (count == 0)
{
errmessage = "검색결과없음";
return 0;
}
errmessage = $"검색성공({count}권)";
return count;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"totalCount 요소 검색 중 오류: {ex.Message}");
}
// 2. 페이지 소스에서 정규식으로 추출 시도
var pageSource = driver.PageSource;
// 검색 결과가 없다는 메시지 확인
if (pageSource.Contains("검색결과가 없습니다") ||
pageSource.Contains("검색된 자료가 없습니다") ||
pageSource.Contains("자료가 없습니다") ||
pageSource.Contains("전체 0 건"))
{
errmessage = "검색결과없음";
return 0;
}
// HTML에서 다양한 패턴 찾기
var htmlPatterns = new[]
{
@"전체\s*<strong>\s*(\d+)\s*</strong>\s*건",
@"전체\s+(\d+)\s+건",
@"총\s*(\d+)\s*권",
@"총\s*(\d+)\s*건",
@"검색결과\s*:\s*(\d+)"
};
foreach (var pattern in htmlPatterns)
{
var match = Regex.Match(pageSource, pattern, RegexOptions.IgnoreCase);
if (match.Success)
{
if (int.TryParse(match.Groups[1].Value, out int count))
{
if (count == 0)
{
errmessage = "검색결과없음";
return 0;
}
errmessage = $"검색성공({count}권)";
return count;
}
}
}
errmessage = "결과수량을찾을수없음";
return -1;
}
catch (Exception ex)
{
errmessage = ex.Message;
return -1;
}
}
// 페이지 변경을 감지하는 메서드
public async Task WaitForPageChange(WebDriverWait wait)
{
try
{
await Task.Delay(500);
// 페이지 로딩 상태 확인
wait.Until(d =>
{
var readyState = ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState");
return readyState.Equals("complete");
});
// 검색 결과 페이지가 로드될 때까지 대기
wait.Until(d =>
{
try
{
var pageSource = d.PageSource;
// 검색 결과나 관련 요소가 나타나면 로드 완료
return pageSource.Contains("searchTitle") ||
pageSource.Contains("totalCount") ||
pageSource.Contains("검색결과") ||
pageSource.Contains("전체") ||
pageSource.Contains("자료가");
}
catch
{
return false;
}
});
}
catch (Exception ex)
{
// 모든 감지 방법이 실패하면 최소한의 대기 시간 적용
await Task.Delay(3000);
Console.WriteLine($"페이지 변경 감지 실패: {ex.Message}");
}
}
}
}