[C#] Gecko 라이브러리 (웹 스크래핑)


Development note/C#  2019. 6. 6. 22:58

안녕하세요. 명월입니다.

 

이 글은 C# 환경에서 웹 스크래핑을 하는 데 MSHTML이 아닌 Gecko(Firefox) 레이아웃 엔진을 이용해 스크래핑을 하는 방법에 대한 글입니다.

제가 예전에 약 6년전 쯤에 afxWebBrowser와 C#의 기본 객체인 Webbrowser를 이용해서 스크래핑 하는 방법에 대해 소개한 적있습니다.

 

링크 - [C#] AxWebBrowser 로 자동 로그인 소스 (웹 스크래핑)

링크 - [C#] 확장형 브라우져 컨트롤 AxWebBrowser Extended Browser control(확장형 webbrowser)

 

사실 위 Browser객체로 스크래핑이 되지 않는 건 아니지만 MS에서 제공하는 라이브러리는 MSHTML 7.0인가 (버전까지는 정확하지 않습니다.) 인데, 이 것은 예전 IExplorer 8.0의 레이아웃 엔진입니다.

즉, 거의 10년전의 레이아웃 엔진으로써 기본 객체 Browser 엔진으로 프로그램을 만든다거나 스크래핑을 만들면 많은 에러가 발생할 것입니다.

 

그런데 C# Nuget에 확인하면 mozilla 재단의 Gecko 레이아웃이 있습니다. 이 Gecko version은 60까지 나온 상태로 가장 최신의 레이아웃 엔진입니다. 그래서 아마 스크래핑을 만든다고 하시면 이 Gecko 버전을 통해서 만드는 게 에러없이 가능할 것이라 생각됩니다.

 

먼저 브라우져 객체는 Window 환경에서 움직이는 컨트롤이니 프로젝트를 윈도우 프로젝트로 엽니다.

그리고 Nuget을 통해서 Gecko 브라우져를 연결합니다.

위 이미지를 보시면 예전 45버전도 있는데 필요하시면 45버전을 사용해도 방법은 같습니다. 저는 60를 사용합니다. 제 PC가 64비트이니 64로 된 것을 사용합니다. 혹시나 32비트이신 분들은 86을 사용하면 되는데 아직 32비트가 사용하시는 분이 있을까요?

 

윈도우 디자인 폼에 돌아와서 옆에 toolbar를 보면 Gecko-browser 컨트롤이 생긴것을 확인할 수 있습니다.

이걸 윈도우 폼에 Drap & Drop을 하신 후 프로퍼티의 dock을 fill로 맞춤니다.

이제 Firefox dll 파일을 주입해야 합니다.

오른쪽 Program.cs 파일을 열어서 다음 소스를 추가합니다.

Xpcom.EnableProfileMonitoring = false;
var app_dir = Path.GetDirectoryName(Application.ExecutablePath);
Xpcom.Initialize(Path.Combine(app_dir, "Firefox64"));

그리고 오른쪽 Form1.cs 파일을 마우스 오른쪽 클릭을 해서 소스 보기로 들어갑니다.

using System;
using System.Windows.Forms;
using Gecko;
using Gecko.DOM;
 
namespace GeckoTest
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }
    // Form의 Onload를 오버라이드 헀다.
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);
      // 네이버에 접속하라.
      this.geckoWebBrowser1.Navigate("www.naver.com");
      // 접속이 완료되는 시점의 이벤트
      this.geckoWebBrowser1.DocumentCompleted += GeckoWebBrowser1_DocumentCompleted;
    }
 
    private void GeckoWebBrowser1_DocumentCompleted(object sender, Gecko.Events.GeckoDocumentCompletedEventArgs e)
    {      
      GeckoWebBrowser browser = (GeckoWebBrowser)sender;
      // 네이버의 경우.
      if (browser.Url.Equals("https://www.naver.com/"))
      {
        var doc = browser.Document;
        // 검색창
        (doc.GetElementById("query") as GeckoInputElement).Value = "명월일지";
        // 검색창 옆 버튼
        (doc.GetElementById("search_btn") as GeckoButtonElement).Click();
      }
    }
  }
}

여기에 위처럼 스크래핑 소스를 입력해 보았습니다.

결과처럼 스크래핑이 되는 것을 확인할 수 있습니다.

 

-- 만약 에러가 나오는 경우 --

1. 64비트를 설정했는데도 64비트로 만들어 지지 않을 경우가 있습니다.

위와 같은 에러가 나오면 디버그 설정을 64로 변경하시면 됩니다.

먼저 Debug 메뉴의 Property 메뉴로 갑니다.

메뉴로 들어가면 build 탭으로 이동해서 플랫폼을 x64로 변경하시면 해결됩니다.

 

2. dll 파일을 읽어 들일 수 없다라는 에러가 발생하는 경우도 있습니다.

이 경우에는 debug 폴더의 dll 경로 설정이 잘 못 되어 있는 경우입니다.

var app_dir = Path.GetDirectoryName(Application.ExecutablePath);
Xpcom.Initialize(Path.Combine(app_dir, "Firefox64"));

위 소스를 보면 실행하는 폴더의 Firefox64 폴더를 참조하라고 설정 되어있습니다.

소스의 Root 폴더로 이동한 뒤 bin -> debug 폴더로 이동하면 Firefox 모듈이 있는 폴더가 있습니다.

여기 폴더 명이랑 위의 Firefox64를 일치시키면 문제없이 실행됩니다.

개인적으로 스크래핑할 때 Gecko Browser를 많이 사용합니다. 이전 afx나 WebBrowser는 10년 전까지만 해도 사용했었는데 최근에는 script 에러가 너무 많이 발생해서 사용할 수가 없더라고요. Gecko Browser와 HttpWebRequest을 적절히 잘 섞어 쓰면 괜찮은 스크래핑 프로그램이 많들어지지 않을까 싶네요.

 

-- 참고 --

GeckoBrowser에서 HttpWebRequest와 세션을 유지하고 싶으면 아래와 같이 cookie를 일치시키면 동기화가 됩니다.

HttpWebRequest request = (HttpWebRequest)WebRequest.Create();
request.Headers["Cookie"] = browser.Document.Cookie;

테스트해 볼 사이트가 마땅히 없어서 참고사항으로 적어 놨습니다.

 

-- 추가 2020년 3월 2일 --

Gecko 브라우저에서 Javascript를 사용하고 싶다는 질문이 있어서 남깁니다.

 

먼저 원리를 설명하면 GeckoWebBrowser1_DocumentCompleted의 이벤트를 보면 GeckoDocument를 취득하는 부분이 있습니다.

즉, GeckoWebBrowser1_DocumentCompleted의 이벤트는 웹 서버로부터 Reponse(응답)이 완료되면 호출되는 이벤트로 GeckoDocument에는 렌더링 전의 Http 프로토콜의 Body내용이 있습니다.

예를 들면, 브라우저에서 개발자 모드와 같습니다.

 

여기에 우리가 태그를 삽입하거나 요소의 값을 바꾸거나 class를 바꾸면 브라우저에는 그대로 적용이 되는 것입니다.

Script를 실행하고 싶다고 요청을 하셨으니, Script 태그에 javascript를 입력할 수 있는 함수를 만들면 되겠네요.

// 자바스트립트 를 넣기 위한 함수
protected void ExcuteJavascript(GeckoDocument document, string script)
{
  // documentㄹ 부터 요소를 생성한다. 태그명은 script이다.
  GeckoElement scriptelement = document.CreateElement("script");
  // 속성을 추가할 필요는 없지만.. 그래도 
  scriptelement.SetAttribute("type", "text/javascript");
  scriptelement.TextContent = script;
  // Body에 추가해도 되는고 Head에 추가해도 된다.
  // 단 Jquery코드 식을 사용하게 되면 참조되어 있는 위치를 고려해서 추가한다.
  document.Head.AppendChild(scriptelement);
}

위 소스를 테스트하기 위한 예제 소스입니다.

<!DOCTYPE html>
<html>
<head>
  <title>title</title>
  <!-- cdn 참조 -->
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
    <!-- 기본 localhost로 들어가면 빈페이지가 나온다. -->
    <div style="display:none;" class="test">Hello world</div>
    <script>
      // test함수를 호출하면 위 div 태그는 Hello world가 보일것이다.
      function test(){
          $(".test").show();
      }
    </script>
  </div>
</body>
</html>

div의 display가 none으로 되어있기 때문에 빈페이지가 나옵니다.

여기서 test()함수를 호출해서 display를 block으로 바꾸는 프로그램을 만들겠습니다.

using System;
using System.Windows.Forms;
using Gecko;
 
namespace GeckoTest
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
    }
    // Form이 로드되면 호출된다.
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);
      // localhost를 탐색한다.
      this.geckoWebBrowser1.Navigate("http://localhost");
      // Response이 완료되면 호출되는 이벤트 등록
      this.geckoWebBrowser1.DocumentCompleted += GeckoWebBrowser1_DocumentCompleted;
    }
    // Reponse 완료되면 호출된다.
    private void GeckoWebBrowser1_DocumentCompleted(object sender, Gecko.Events.GeckoDocumentCompletedEventArgs e)
    {
      // GeckoBrowser 객체를 받아온다.
      GeckoWebBrowser browser = (GeckoWebBrowser)sender;
      // Document 를 받아온다.
      var doc = browser.Document;
      // Javascript를 추가한다. test();를 추가한다.
      // 
      ExcuteJavascript(browser.Document, "test();");
    }
    
    // 자바스트립트 를 넣기 위한 함수
    protected void ExcuteJavascript(GeckoDocument document, string script)
    {
      // documentㄹ 부터 요소를 생성한다. 태그명은 script이다.
      GeckoElement scriptelement = document.CreateElement("script");
      // 속성을 추가할 필요는 없지만.. 그래도 
      scriptelement.SetAttribute("type", "text/javascript");
      scriptelement.TextContent = script;
      // Body에 추가해도 되는고 Head에 추가해도 된다.
      // 단 Jquery코드 식을 사용하게 되면 참조되어 있는 위치를 고려해서 추가한다.
      document.Head.AppendChild(scriptelement);
    }
  }
}


-- 2021/10/22 추가--

어떤 분이 Javascript에서 이벤트를 호출하면 C#에서 응답되는 방법을 알고 싶다고 해서 추가 글을 남깁니다.

티스토리가 개편이 되면서, 작성된 글을 수정할려면 글을 처음부터 다시 써야하는 번거러움이 있습니다. 맘 같아서는 블로그 옮기고 싶은데.....


그래도 조단님께서 워낙 고생하시는 거 같아서, 제가 조금 수고를 하는 걸로...


먼저 C#에서 javascipt의 이벤트를 받아오려면 GeckoWebBrowser에 이벤트를 등록해야 합니다.

using System;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
  // Window Form 클래스
  public partial class Form1 : Form
  {
    // 생성자
    public Form1()
    {
      InitializeComponent();
    }
    // 화면이 로드되면 발생되는 함수
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);
      // localhost를 탐색한다.
      this.geckoWebBrowser1.Navigate("http://localhost");
      // 제가 생성한 이벤트를 등록하는 것입니다.
      // 즉, javascript에서 이벤트를 등록하고 myFunction 날리면 여기 함수가 호출되겠습니다.
      geckoWebBrowser1.AddMessageEventListener("myFunction", (string p) =>
      {
        MessageBox.Show("Call!!! " + p);
      });
    }
  }
}

웹 서버에서 작성될 html 파일입니다.

<!DOCTYPE html>
<html>
<head>
  <title>title</title>
</head>
<body>
    hello world
    <script>
        function sendEvent() {
          // 교차 메시지 이벤트 등록
          // 이벤트 이름은 myFunction
          // data는 hello world - C#의 string p로 받을 값
          var event = new MessageEvent('myFunction',{'data': 'hello world'});
          // 이벤트 등록
          document.dispatchEvent(event);
        }
        // 실행
        sendEvent();
    </script>
  </div>
</body>
</html>

사실 저는 그냥 addeventlistener로 등록하면 될 줄 알았는데.. 그건 아니네요..

링크 - https://developer.mozilla.org/ko/docs/Web/API/MessageEvent

Javasciprt에서 sendEvent함수를 호출하니 C#에서 응답했네요..

데이터는 json의 data항목에 넣어서 보내면, C#의 람다식 파라미터에서 받을 것입니다.


여기까지 Gecko 라이브러리 (Firefox 브라우져로 스크래핑 하기)에 대한 설명이었습니다.

 

궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.