NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET 매크로 프로그램 만들기. (웹페이지에서 엘리먼트 탐색하는 방법)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 오늘은 내용이 좀 복잡한데요. 웹페이지에서 내가 어떤 동작을 수행하려면, 명령을 수행할 엘리먼트(Element: 요소)를 찾아야 합니다. 일반적으로 Selenium의 FindElement를 사용하면 원하는 기능을 만들 수 있는데요. 일부 웹페이지 환경에서는 정상적으로 동작하지 않습니다. 이 문제를 해결하기 위한 다양한 방법들이 존재하지만, 약간 어려운 부분들이 있어서 코드량이 많아지고 복잡해졌습니다.

     

    FindElementModel을 하나 추가 해줍니다. 명령을 수행할 브라우저 정보를 담고 있는 WebBaseModel을 상속 받아야 합니다. 그리고, 아래 프라이빗 맴버는 NonSerialized 특성(Attribute)을 부여 해줍니다. 제가 직접 만든 클래스는 직렬화와 역직렬화가 가능하지만, 외부 라이브러리를 가져다 쓰는 경우 해당 클래스를 변경할 수 없기 때문에 가급적이면 직렬화하지 않는게 좋습니다. 물론, 오픈소스이기 때문에 코드를 수정해도 됩니다. 이렇게하면 업데이트에 문제가 생기게 됩니다.

    public class FindElementsModel : WebBaseModel
    {
        [NonSerialized]
        private IWebElement? _externalElement = null;
    
        [NonSerialized]
        private IWebElement? _currentElement = null;
    
        [NonSerialized]
        private List<IWebElement>? _elements = null;

     

    엘리먼트를 찾으면 정보를 표시할 속성들입니다. 참고로, 엘리먼트는 하나일수도 있지만 대부분은 다수의 엘리먼트를 찾아서 처리해야 합니다.

    /// <summary>
    /// 현재 선택된 엘리먼트의 태그 이름입니다.
    /// </summary>
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("WebTagName")]
    [LocalizedDescription("WebTagName")]
    [Browsable(true)]
    [DefaultValue(null)]
    [ReadOnly(true)]
    public string? TagName { get; set; }
    
    /// <summary>
    /// 엘리먼트의 값을 가져오거나 설정합니다.
    /// </summary>
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("ElementData")]
    [LocalizedDescription("ElementData")]
    [Browsable(true)]
    [DefaultValue(null)]
    [EditorAttribute(typeof(MultilineStringEditor), typeof(UITypeEditor))]
    public string? ElementData { get; set; }
    
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("ElementDatas")]
    [LocalizedDescription("ElementDatas")]
    [Browsable(true)]
    [DefaultValue(null)]
    public string[]? ElementDatas { get; set; }
    
    /// <summary>
    /// 엘리먼트 목록의 수를 가져오거나 설정합니다.
    /// </summary>
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("ElementCount")]
    [LocalizedDescription("ElementCount")]
    [Browsable(true)]
    [DefaultValue(0)]
    [ReadOnly(true)]
    public int ElementCount { get; set; }
    
    /// <summary>
    /// 현재 선택되어 있는 엘리먼트입니다.
    /// </summary>
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("WebCurrentElement")]
    [LocalizedDescription("WebCurrentElement")]
    [Browsable(true)]
    [DefaultValue(null)]
    public IWebElement? CurrentElement
    {
        get { return _currentElement; }
        set { _currentElement = value; }
    }
    
    /// <summary>
    /// 웹드라이브에서 가져온 엘리먼트의 전체 목록입니다.
    /// </summary>
    [LocalizedCategory("Data")]
    [LocalizedDisplayName("WebElements")]
    [LocalizedDescription("WebElements")]
    [Browsable(true)]
    [DefaultValue(null)]
    [XmlIgnore]
    public List<IWebElement>? Elements
    {
        get { return _elements; }
        set { _elements = value; }
    }

     

     

    쉐도우 돔을 처리할 수 있는 옵션도 하나 추가 해줍니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("UseShadowDOM")]
    [LocalizedDescription("UseShadowDOM")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool UseShadowDOM { get; set; }

     

    HTML 엘리먼트는 같은 요소라도 브라우저와 운영체제에 따라 다르게 보입니다. 또한 HTML5 이외에도 다양한 엘리먼트가 필요하기도 합니다. 그래서 JS 컴포넌트를 사용하기도 하지만, 적용이 힘들고 느리다는 단점이 있습니다. 

     

    따라서 W3C에서는 이러한 이슈를 개선하고자 웹 컴포넌트 (Web Component)라는 명세를 만들었습니다. 개발자가 자체적으로 HTML 엘리먼트를 만드는 기술을 말합니다. 쉐도우 돔(Shadow DOM)을 사용하면 독립적으로 페이지에 컴포넌트를 심을 수 있고, 페이지 내의 다른 CSS에 영향을 받지 않게 됩니다. 이외에도 다양한 이유로 커스텀 또는 쉐도우 돔을 사용하고 있습니다.

    Bn5sdI7.jpeg

     

     

    현재 페이지에서 쉐도우 돔을 테스트할만한 내용이 없어서 아마도 다음 시간에 알아볼텐데요. 일단, 기능은 미리 만들어 둘께요. 웹업무자동화 보다는 광고 마케팅과 홍보 그리고, 무한타등등... 다양한 곳에서 쉐도우 돔을 사용해야 합니다. 단순 웹업무의 경우에는 거의 필요 없는 기능이긴 합니다. 오래된 사이트들은 쉐도우 돔이나 컴포넌트 자체가 없기 때문입니다. 전에 웹 역사로 보면 거의 최근 기술이기도 하고 컴포넌트는 SPA와 같은 서비스에서 주로 이용되고 있기 때문입니다. 오래된 사이트 또는 포털이나 쇼핑몰의 경우에는 이미 서비스한지 오래되서 이런 기술이 필요하지는 않을듯 하네요^^

     

    엘리먼트를 찾는 방법은 아래 2개의 속성으로 처리할 수 있습니다.

    /// <summary>
    /// DOM에서 찾을 엘리먼트의 형식을 가져오거나 설정합니다.
    /// </summary>
    [LocalizedCategory("Find")]
    [LocalizedDisplayName("ElementFindOption")]
    [LocalizedDescription("ElementFindOption")]
    [Browsable(true)]
    [DefaultValue(typeof(Definition.ElementFindOption), "XPath")]
    public Definition.ElementFindOption ElementFindOption { get; set; } = Definition.ElementFindOption.XPath;
    
    /// <summary>
    /// DOM에서 찾을 엘리먼트의 이름을 가져오거나 설정합니다.
    /// </summary>
    [LocalizedCategory("Find")]
    [LocalizedDisplayName("ElementFindValue")]
    [LocalizedDescription("ElementFindValue")]
    [Browsable(true)]
    [DefaultValue(null)]
    [EditorAttribute(typeof(MultilineStringEditor), typeof(UITypeEditor))]
    public string? ElementFindValue { get; set; }

     

    여기서 핵심은 "엘리먼트를 어떤 방식으로 찾을것인가?"입니다. 아래와 같은 옵션을 제공할 수 있습니다.

    /// <summary>
    /// 웹 엘리멘트를 탐색하는 방법을 나타냅니다.
    /// </summary>
    public enum ElementFindOption
    {
        /// <summary>
        /// 클래스 이름으로 탬색합니다.
        /// </summary>
        ClassName = 0,
        /// <summary>
        /// CSS 셀렉터로 탬색합니다.
        /// </summary>
        CssSelector = 1,
        /// <summary>
        /// 아이디로 탐색합니다.
        /// </summary>
        Id = 2,
        /// <summary>
        /// 링크 텍스트로 탐색합니다.
        /// </summary>
        LinkText = 3,
        /// <summary>
        /// 이름으로 탐색합니다.
        /// </summary>
        Name = 4,
        /// <summary>
        /// 파셜 링크 텍스트로 탐색합니다.
        /// </summary>
        PartialLinkText = 5,
        /// <summary>
        /// 태그 이름으로 탬색합니다.
        /// </summary>
        TagName = 6,
        /// <summary>
        /// XPath로 탐색합니다.
        /// </summary>
        XPath = 7
    }

     

    대략적인 기본 속성들은 추가 했습니다. 이제 구현부가 남았는데요. 구현부는 아래와 같이 단순합니다. 이미 셀레니움이라는 막강한 패키지가 있고, 셀레니움이 제공해주는 기능을 잘 사용하기만 하면 되기 때문입니다.

    List<string?> values = new List<string?>();
    
    foreach (var element in elements)
    {
        string ev = string.Empty;
    
        switch (ElementValueType)
        {
            case Definition.ElementValueType.Text:
                ev = element.Text;
                break;
            case Definition.ElementValueType.Attribute:
                ev = element.GetAttribute(ElementValueName);
                break;
            case Definition.ElementValueType.CssValue:
                ev = element.GetCssValue(ElementValueName);
                break;
            case Definition.ElementValueType.Property:
                ev = element.GetDomProperty(ElementValueName);
                break;
        }
    
        if (!(EmptyValueDelete && string.IsNullOrEmpty(ev)))
        {
            if (!string.IsNullOrEmpty(ev.Trim()))
            {
                if (!(RemoveDuplicates && values.Contains(ev)))
                {
                    values.Add(ev);
                    Elements.Add(element);
                }
            }
        }
    }
    
    ElementDatas = values.ToArray();
    ElementData = ElementDatas[ElementIndex];
    ElementCount = Elements.Count;
    CurrentElement = Elements[ElementIndex];
    TagName = CurrentElement.TagName;

     

    이제 다 만들어진 엘리먼트 찾기 액션을 테스트 해볼께요. 테스트 내용이 좀 복잡한데요. 액션은 몇개 안됩니다.

    ULgIp4x.jpeg

     

     

    완성된 매크로를 테스트 해볼께요. 테스트는 아래 동영상을 참고 해주세요.

     

     

    이 글이 도움이 되셨다면~ 커피 한잔이라도 후원 부탁드립니다^^

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.