모니터링 자동화 - 웹 크롤링

2023. 12. 17. 11:42개발/C#

2023.12.17 - [개발 기록/C#] - 모니터링 자동화 - 구상

 

모니터링 자동화 - 구상

재직 과정에서, 비효율적인 업무를 진행하게 됐다. 수용가 관리 업무인데, 수용가 별 데이터 수신율, 장비 상태 등을 보고서로 작성하는 업무다. 가장 불만이었던 건, 업무 소요시간이 수용가 수

iruk.tistory.com

 

구상에 이어, 웹 데이터 크롤링 로직 추가

C# Selenium 라이브러리를 활용해 

모니터링 페이지의 데이터를 크롤링 테스트한다.


크롤링 함수

public bool GetMonitoringSiteData()
{
    bool flag = false;

    #region 리스트 초기화
    companyListMgr.companyIdList.         Clear();
    companyListMgr.companyNameList.       Clear();
    companyListMgr.offlineCompanyNameList.Clear();
    
    receptionList.      Clear();
    deviceTotalList.    Clear();
    deviceConnectedList.Clear();
    receptionDoubleList.Clear();
    #endregion

    #region 크롬 브라우저 설정

    var options = new ChromeOptions();                                  // ChromeOptions 인스턴스 생성
    options.AddArgument("--start-maximized");                           // 창을 최대화하는 옵션 설정

    var driverService = ChromeDriverService.CreateDefaultService();
    driverService.HideCommandPromptWindow = true;                       // ChromeDriver 명령 프롬프트 창 숨김

    #endregion

    try
    {
        IWebDriver driver = new ChromeDriver(driverService,options);

        #region 사이트 이동 및, 로그인할때까지 대기

        driver.Navigate().GoToUrl("사내 모니터링 페이지");

        IWebElement idInput = driver.FindElement(By.XPath(idInputXpath));
        IWebElement pwInput = driver.FindElement(By.XPath(pwInputXpath));

        idInput.SendKeys(id);   // id / pw 자동입력
        pwInput.SendKeys(pw);

        IWebElement secretInput = driver.FindElement(By.XPath(secretInputXpath));   // 부정방지 문자 입력란으로 이동
        secretInput.Click();

        WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromMinutes(1));
        wait.Until(d => d.FindElement(By.TagName("body")).Text.Contains(loginPageTitle)); // 1분동안 로그인 대기

        #endregion

        #region companyId, 수용가 명 크롤링 후 List로 저장

        driver.Manage().Window.Minimize();
        driver.Navigate().GoToUrl("수용가 별 데이터 페이지");

        IWebElement factoryList = driver.FindElement(By.XPath(companySelectXpath));
        IReadOnlyCollection<IWebElement> optionElements = factoryList.FindElements(By.TagName("option")); // element "option" 값에 companyId가 저장되어있음

        foreach (IWebElement option in optionElements)
        {
            companyListMgr.companyIdList.Add(option.GetAttribute("value"));   // element "option" 텍스트에 수용가 명 저장되어있음
            companyListMgr.companyNameList.Add(option.Text);
        }

        #endregion

        #region 수용가 별 수신율, 장비 수 크롤링

        for (int i = 0; i < companyListMgr.companyIdList.Count; i++)
        {
            fullUrl = url + companyListMgr.companyIdList[i];
            driver.Navigate().GoToUrl(fullUrl);
            Thread.Sleep(300);

            IWebElement reception = driver.FindElement(By.XPath(receptionXpath));         // 수신율
            IWebElement deviceTotal = driver.FindElement(By.XPath(deviceTotalXpath));       // 전체 장비 수
            IWebElement deviceConnected = driver.FindElement(By.XPath(deviceConnectedXpath));   // 연결된 장비 수

            receptionList.Add(reception.Text);              // 수신율 List에 추가
            deviceTotalList.Add(deviceTotal.Text);          // 전체 장비 수 List에 추가
            deviceConnectedList.Add(deviceConnected.Text);  // 연결된 장비 수 List에 추가
        }

        #endregion

        driver.Quit();      // 크롬 드라이버 종료, 메모리 해제
        driver.Dispose();

        flag = true;
        return flag;
    }
    catch (Exception) // 기타 예외처리
    {
        return flag;
    }
}

 

전체적인 흐름은 이렇다.

var options = new ChromeOptions();                                  // ChromeOptions 인스턴스 생성
options.AddArgument("--start-maximized");                           // 창을 최대화하는 옵션 설정

 

ChromeOptions() 를 추가한다.

크롬 드라이버를 통해 브라우저를 실행할 때 조건이다.

 

난 브라우저를 최대화면으로 실행했다.

try
{
    IWebDriver driver = new ChromeDriver(options);

    #region 사이트 이동 및, 로그인할때까지 대기

    driver.Navigate().GoToUrl("사내 모니터링 페이지");
    .
    .
    .

driver 인스턴스를 생성해서, 크롬 드라이버를 실행한다. ( 미리 설정한 option 에 맞게 )

Navigate().GoToUrl() 메소드를 활용해서

특정 URL 로 이동한다.

 

그럼 위와 같이, 별도의 크롬 브라우저가 실행되면서

지정한 URL 로 이동이 된다.

 

근데 좀 불편한건

크롬 브라우저가 실행될 때, 별도의 커맨드 창이 함께 자동실행된다.

 

이걸 끄는 옵션을 찾아봤는데, 도저히 못찾아서

일단 크롤링부터 성공하고, 이후에 수정한다.


XPath 추출

 

모니터링 페이지의 html 구조다.

F12 개발자 도구를 통해 볼 수 있는데,

내가 원하는 데이터들의 XPath를 따로 저장한다.

 

 

원하는 element 에 우클릭해서, 추출 가능

해당 XPath string Path를 클래스 내에 전역변수로 선언한다.

 

#region 웹사이트 관련 Property

static string url = "사내 모니터링 URL";
static string fullUrl = "";

static string id = "admin";
static string pw = "1234";

static string loginPageTitle        = "Cloud FEMS 운영 MAP";          // 로그인 상태를 check하기 위한 element값
static string idInputXpath          = "//*[@id=\"id\"]";              // 로그인 창의 id 입력란
static string pwInputXpath          = "//*[@id=\"password\"]";        // 로그인 창의 pw 입력란
static string secretInputXpath      = "//*[@id=\"answer\"]";          // 로그인 창의 부정방지문자 입력란
static string companySelectXpath    = "//*[@id='factoryList']";       // 수용가 선택 창
static string receptionXpath        = "//*[@id=\"receptionRate\"]";   // 수신율 XPath
static string deviceTotalXpath      = "//*[@id=\"totalEquipment\"]";  // 전체 장비 수 XPath
static string deviceConnectedXpath  = "//*[@id=\"connect\"]";         // 현재 연결된 장비 수 XPath
static string companyId             = "//*[@id=\"factoryCode\"]";     // 수용가 별 companyId 
static string channelTableXPath     = "//*[@id=\"connectionStatusGrid\"]/div/div/div/div[2]/div/div/div[2]/div[1]"; // 채널 표 전체 XPath
static string channelStatusXPath    = "";
static string channelReceptionXPath = "";

public List<string> receptionList       = new List<string>();          // 수용가의 수신율 담을 List
public List<string> deviceTotalList     = new List<string>();          // 수용가의 전체 장비 수 담을 List
public List<string> deviceConnectedList = new List<string>();          // 수용가의 현재 연결된 장비 수 담을 List
public List<double> receptionDoubleList = new List<double>();          // 수신율 string을 double로 변환

#endregion

 

나는 위와 같이, 고정 사용되는 Property 들을

따로 선언 후 Region으로 묶어뒀다.


로그인 처리

자동화 프로그램의 큰 문제와 봉착했다.

로그인 처리를 어떻게 할 것이냐.

 

아이디/비밀번호 는 고정값이지만

화면에 보이는 부정방지 문자는 수시로 변하며,

로그인 버튼을 클릭해도

Google OTP를 사용해 한 번 더 로그인해야한다.

 

아이디/비밀번호는 뭐 config.ini 파일로 따로 빼서 관리하거나

소스 내에 직접 저장해도 되긴 하는데

부정방지문자, OTP 2개가 문제다

 

고민고민고민 끝에

업무 담당자가 최초 1회 직접 로그인 하고,

이후 과정을 자동화 ( 로그인 유지 후 크롤링 주기적으로 반복 ) 하기로 구상했다.

 

뭐 웹파트에 로그인 로직을 스킵하는 API를 요청해도 되긴 하지만

일단 웹 크롤링 방법을 선택한게, 나 혼자 쭉 해보는 것이었으니까

 

근데 문제가 하나 더 있다.

해당 크롬 브라우저에서, 프로그램 사용자가 로그인을 했는지, 안했는지

상태 확인을 어떻게 할 것이냐 이건데

 

1. javascript 이벤트 전송하여 확인

2. 기타?

 

1번을 되게 고민하다가, 코드가 너무 복잡해질 것 같고,

로그인 하나때문에 javascript까지 건들자니

배보다 배꼽이 큰 것 같아

다른 방법을 생각해봤다.

 

로그인 성공했을 때, 화면에 뿌려주는 데이터가 있을거고,

그 데이터가 로그인 전 화면에 없던 데이터라면,

소스 내에서, 주기적으로 로그인 후 데이터가 있는지 확인하게 하면

로그인 여부를 판단할 수 있다! 가 내 생각이었다.

 

다시 말해서, "로그인 성공 시 뿌려주는 데이터" 를 확인하는 로직을 추가한다.

IWebElement idInput = driver.FindElement(By.XPath(idInputXpath));
IWebElement pwInput = driver.FindElement(By.XPath(pwInputXpath));

idInput.SendKeys(id);   // id / pw 자동입력
pwInput.SendKeys(pw);

IWebElement secretInput = driver.FindElement(By.XPath(secretInputXpath));   // 부정방지 문자 입력란으로 이동
secretInput.Click();

WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromMinutes(1));
wait.Until(d => d.FindElement(By.TagName("body")).Text.Contains(loginPageTitle)); // 1분동안 로그인 대기

 

해당 소스의 마지막 줄에서 로그인 여부를 체크한다.

loginPageTitle 이, 로그인 시 뿌려주는 특정 데이터의 XPath 값이다.

 

해당 값이 1분동안 들어오지 않으면, 로그인 실패

1분 안에 값 확인되면 로그인 성공 으로 판단한다.

괜찮은듯


데이터 크롤링

로그인 성공 URL 이 test.co.kr/ 이라고 가정하면,

수용가 A 의 데이터는 test.co.kr/A에

수용가 B 의 데이터는 test.co.kr/B에

수용가 C 의 데이터는 test.co.kr/C에 저장되어 있다.

 

따라서, 로그인 성공 후,

수용가 리스트( A,B,C... ) 를 먼저 저장 후,

test.co.kr/ + 수용가 id (A, B, C... ) 로 이동하여 크롤링 하도록 로직을 만들었다.

 

로그인 성공 후, 수용가 목록을 볼 수 있는 목록창이 있는데,

해당 값의 XPath를 가져와, 내부 데이터들을 모두 가져와봤다.

 

element의 'option' 값에 수용가 별 code ( A,B,C... ) 가 저장되어있고

value 에 수용가의 실제 이름 ( 공장1, 공장2, 공장3 ) 가 저장되어있었다.

 

따라서, test.co.kr/ + code 를 쭉~ 순회하면

모든 수용가의 데이터를 가져올 수 있는 것이다.

for (int i = 0; i < companyListMgr.companyIdList.Count; i++)
{
    fullUrl = url + companyListMgr.companyIdList[i];
    driver.Navigate().GoToUrl(fullUrl);
    Thread.Sleep(300);

    IWebElement reception       = driver.FindElement(By.XPath(receptionXpath));         // 수신율
    IWebElement deviceTotal     = driver.FindElement(By.XPath(deviceTotalXpath));       // 전체 장비 수
    IWebElement deviceConnected = driver.FindElement(By.XPath(deviceConnectedXpath));   // 연결된 장비 수

    receptionList.      Add(reception.Text);        // 수신율 List에 추가
    deviceTotalList.    Add(deviceTotal.Text);      // 전체 장비 수 List에 추가
    deviceConnectedList.Add(deviceConnected.Text);  // 연결된 장비 수 List에 추가
}

for문을, companyIdList 크기만큼 돌면서

url + companyIdList [ i ] 를 사용해 수용가 별 데이터 화면으로 이동한다.

 

해당 화면 이동 후, 수신율 및 필요한 데이터를 가져와

각각 List에 저장한다.

 

Thread.Sleep(300); 을 넣은 이유는

URL 이동 후, 바로 데이터 크롤링을 가져오면

 

몇개 누락되는 경우가 발생해,

0.1초 ~ 1초 사이값으로 딜레이 테스트 해본 결과

300초가 데이터 누락 없는, 최단시간이었다.

debug 로 확인한, 수신율 & 장비 수 List 내부 값이다.

수용가 별로, 전부 저장되는걸 확인했다.

 

데이터 크롤링이 됐으니, 후처리는 뭐 문제없을 것 같다.

driver.Quit();      // 크롬 드라이버 종료, 메모리 해제
driver.Dispose();

#region 크롤링된 string 수신율 값 double로 변경

foreach (string str in receptionList)
{
    if (double.TryParse(str, out double doubleValue))
    {
        receptionDoubleList.Add(doubleValue);
    }
}
#endregion

flag = true;
return flag;

 

데이터 크롤링 완료 후,

크롬 드라이버 메모리 해제 및

 

string 으로 가져온 수신율 ( 99, 100, 99,,, ) 등의 값을

double 형으로 변경해주는 로직 추가 후

 

flag 를 반환한다.