본문 바로가기

[C#] C#으로 하는 데이터 크롤링

I'm 영서 2024. 7. 22.
반응형

이전 테스트용 프로그램을 개발할때, 

 

뉴스 사이트에서 특정 파라미터로 뉴스기사를 검색하여 검색된 결과를 표출하는 기능을 구현하려고 했다.

 

물론 이때는 테스트용이었으므로 자연어처리등의 로직은 필요하지도 않았고, 서버사이드에서 작업할 필요는 없었기에 그냥 C#에서 바로 짜서 사용했다.

 

물론 MVVM 패턴을 적극적으로 사용한 예제가 될 것이다.

 

간략하게 요약하면

News 데이터를 담을 NewsContents 클래스

NewsContents를 가지고 ViewModel을 만들기 위한 NewsContentsViewModel

이 ViewModel과 연결된 NewsPopup, NewsContentSelector , 3개의 코드를 작성할 예정이다.

 

먼저 뉴스정보를 담을 클래스가 필요한데, 나같은 경우 날짜, 제목, preview, 작성자, Image, 그리고 리다이렉트로 연결해줄 PageUrl 정도가 필요했다.

사용한 라이브러리는 

HtmlAgilityPack 

 

풀코드는 아래에 있고, 크롤링 절차를 설명해본다.

 

먼저 HTML을 가져오기 위한 HttpClinet 객체를 생성해줘야 한다.

var client = new HttpClient();

 

그리고 특정 url로부터 html을 가져오고, html을 파싱한다.

var html = await client.GetStringAsync(url);
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(html);

 

이제부터 노가다 시작이다.

 

요소를 선택하거나 노드선택, 노드의 attribute선택을 해야한다.

내가 주로 사용한건 총 3가지로 

SelectSingleNode : 하나의 노드만 가져옴. /div/div/a 이런경우 div의 div속 a를 가져오는데, 이 a가 여러개인 경우에 반드시 처음것만 가져온다.

returnType은 HtmlNode이다.

 

SelectNodes : 여러개의 노드를 가져온다. 여러개의 노드를 가져와서 리스트로 사용할 수 있다.  

returnType은 HtmlNodeCollection 인데, 찾아보면 List<HtmlNode> 이다.

 

 

GetAttributeValue : 선택된 노드의 어트리뷰트를 가져온다. 인자가 두개인데 구현은 아래와 같이 되어있다.

name은 가져올 속성의 이름, def는 속성이 없을때 반환할 기본값이다.

이미지 url을 가져오도록 하고, 없으면 기본이미지를 넣는 방식등으로 사용하는건 꿀팁..

public string GetAttributeValue(string name, string def)

 

먼저 NewsContents 클래스는 다음과 같이 작성했다.

 public class NewsContents
 {
     private string _date;
     private string _title;
     private string _spec;
     private string _preview;
     private string _writer;
     private string _imageurl;
     private string _pageurl;
     public string Date
     {
         get => _date;
         set => _date = value;
     }
     public string Title
     {
         get => _title;
         set => _title = value;
     }
     public string Spec
     {
         get => _spec;
         set => _spec = value;
     }
     public string Preview
     {
         get => _preview;
         set => _preview = value;
     }
     public string Writer
     {
         get => _writer;
         set => _writer = value;
     }
     public string ImageURL
     {
         get => _imageurl;
         set => _imageurl = value;
     }
     public string PageURL
     {
         get => _pageurl;
         set => _pageurl = value;
     }

 }

 

NewsContetnsViewModel을 싱글톤패턴을 적용하여 작성했는데, 객체생성을 한번만 할것이며, 이 뉴스 데이터를 다른곳에서도 사용하기 때문에 하나의 객체로 관리하기 위해서 사용하였다.

 public class NewsContentsViewModel : ViewModelBase
 {
     public static NewsContentsViewModel Instance { get {
             if (_instance == null)
                 _instance = new NewsContentsViewModel();
             return _instance;
         } }

     private static NewsContentsViewModel _instance = null;

     private List<NewsContents> _contentsList = new List<NewsContents>();
     private List<cNewsSelector> _selectorList = new List<NewsSelector>();

     public List<NewsContents> ContentsList
     {
         get => _contentsList;
         set => GetProperty(ref _contentsList, value);
     }

     public List<NewsSelector> NewsContentsSelector {
         get =>  _selectorList;
         set => GetProperty(ref _selectorList, value);
     }


     protected T GetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
     {
         if (!EqualityComparer<T>.Default.Equals(field, value))
         {
             field = value;
             RaisePropertyChanged(propertyName);
         }
         return field;
     }
 }

 

 

그리고 이제 이 NewsContents를 담아줄 xaml을 만들어줘야하는데,

 

각 뉴스를 담을 Selector부터 작성해보자

대충 이런모양으로 여러개 쌓을것..

 

xaml

더보기
<Grid >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="3*"></ColumnDefinition> <!-- 이미지 구역 -->
        <ColumnDefinition Width="7*"></ColumnDefinition> <!-- 콘텐츠 구역 -->
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <dxe:ImageEdit x:Name="imageEdit" Grid.Row="1" Grid.RowSpan="5" Margin="0,0,10,0"  />
    <TextBlock Grid.Column="1" Grid.Row="1"  x:Name="txtdate">11111</TextBlock>
    <TextBlock Grid.Column="1" Grid.Row="2" x:Name="txtSpec">2222222</TextBlock>
    <TextBlock Grid.Column="1" Grid.Row="3" x:Name="txtTitle">3333333</TextBlock>
    <TextBlock Grid.Column="1" Grid.Row="4" x:Name="txtPreview">444444</TextBlock>
    <TextBlock Grid.Column="1" Grid.Row="5" x:Name="txtWriter">555555</TextBlock>
    <Line Margin="0, 10,0,0" x:Name="myLine" Grid.ColumnSpan="2" Grid.Row="6" VerticalAlignment="Center" StrokeThickness="3" X1="0" Y1="0" X2="800" Y2="0" Stroke="Blue" >
        <Line.StrokeDashArray>
            <DoubleCollection>5, 5</DoubleCollection>
            <!-- 점선 스타일 설정 -->
        </Line.StrokeDashArray>
    </Line>
</Grid>

 

.cs

더보기
public partial class NewsSelector : UserControl
{
    public NewsSelector(NewsContents contents)
    {
        InitializeComponent();
        txtdate.Text = contents.Date;
        txtSpec.Text = contents.Spec;
        txtTitle.Text = contents.Title;
        txtPreview.Text = contents.Preview;
        txtWriter.Text = contents.Writer;

        try
        {

            BitmapImage bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.UriSource = new Uri(contents.ImageURL);
            bitmap.EndInit();

            // Image 컨트롤에 이미지 설정
            imageEdit.Source = bitmap;

        }
        catch
        {

        }
    }
    public NewsSelector()
    {
        InitializeComponent();
    }

    private void UserControl_MouseDown(object sender, MouseButtonEventArgs e)
    {
        //무브페이지
    }
}

 

그리고 마지막으로 ScrollViewer를 통한 뉴스를 보여주는 

NewsPopup을 만들면 된다.

 

xaml

더보기
<Window.DataContext>
    <viewmodel:NewsContentsViewModel/>
</Window.DataContext>
<Grid>
    <ScrollViewer x:Name="scrollViewer"  VerticalScrollBarVisibility="Auto">
        
    </ScrollViewer>
</Grid>

cs

더보기
 public partial class NewsPopup : Window
 {
     
     public NewsPopup(string _country)
     {
         InitializeComponent();

         HtmlMapping($@"myurl...");


     }


     public async void HtmlMapping(string url)
     {
         var myStackPanel = new StackPanel();

         myStackPanel.VerticalAlignment = VerticalAlignment.Top;

         try
         {
             // HTML을 가져오기 위한 HttpClient 생성
             var client = new HttpClient();

             // URL에서 HTML 가져오기
             var html = await client.GetStringAsync(url);

             // HtmlDocument 객체 생성하여 HTML 파싱
             var doc = new HtmlAgilityPack.HtmlDocument();
             doc.LoadHtml(html);

             // ol 태그의 data-testid가 "search-results"인 요소를 선택합니다.
             var olElement = doc.DocumentNode.SelectSingleNode("//ol[@data-testid='search-results']");

             // ol 태그의 자식인 li 태그들을 선택합니다.
             var liElements = olElement.SelectNodes("./li");


             // 각 li 요소에 대한 반복문
             foreach (var li in liElements)
             {
             
                 // li 요소 내부의 div > div > p를 선택하여 값 추출
                 var pValue = li.SelectNodes(".//p")[0]?.InnerText.Trim();
                 if (pValue.Contains("Adv"))
                     continue;
                 NewsContents newsContents = new NewsContents();

                 newsContents.Spec = pValue;


                 // li 요소 내부의 div > span을 선택하여 값 추출
                 var spanValue = li.SelectSingleNode("./div/span")?.InnerText?.Trim();
                 newsContents.Date = spanValue;

                 // li 요소 내부의 div > div > a를 선택하여 값 추출
                 var anchorHref = li.SelectSingleNode("./div/div/a")?.GetAttributeValue("href", "");
                 newsContents.PageURL = anchorHref;

                 // li 요소 내부의 div > div > a > h4를 선택하여 값 추출
                 var h4Value = li.SelectSingleNode(".//h4")?.InnerText.Trim();
                 newsContents.Title = h4Value;


                 // li 요소 내부의 div > div > a > p 중 첫 번째를 선택하여 값 추출
                 var firstParagraphValue = li.SelectNodes(".//p").Count > 1 ? li.SelectNodes(".//p")[1]?.InnerText?.Trim() : "";
                 newsContents.Preview = firstParagraphValue;

                 // li 요소 내부의 div > div > a > p 중 두 번째를 선택하여 값 추출
                 var secondParagraphValue = li.SelectNodes(".//p")[0]?.InnerText.Trim();
                 newsContents.Writer = secondParagraphValue;

                 // li 요소 내부의 figure > div > img 태그의 src 속성 값을 가져옵니다.
                 var imageUrl = li.SelectSingleNode("./div/div/figure/div/img")?.GetAttributeValue("src", "");
                 newsContents.ImageURL = imageUrl;


                 NewsContentsViewModel.Instance.ContentsList.Add(newsContents);
                 NewsContentsViewModel.Instance.NewsContentsSelector.Add(new NewsSelector(newsContents));
                 myStackPanel.Children.Add(new NewsSelector(newsContents));


             }

             scrollViewer.Content = myStackPanel;

             // 처리된 HTML 반환
         }
         catch (Exception ex)
         {
             Console.WriteLine($"Error: {ex.Message}");
         }
     }



 }

 

 

반응형

댓글