[C#] C#으로 하는 데이터 크롤링
이전 테스트용 프로그램을 개발할때,
뉴스 사이트에서 특정 파라미터로 뉴스기사를 검색하여 검색된 결과를 표출하는 기능을 구현하려고 했다.
물론 이때는 테스트용이었으므로 자연어처리등의 로직은 필요하지도 않았고, 서버사이드에서 작업할 필요는 없었기에 그냥 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}");
}
}
}
'Study > C#' 카테고리의 다른 글
<SQLite.Interop.dll'을(를) 로드할 수 없는경우>를 해결하며 알아보는 .NET과 네이티브 코드(dll)의 통신 절차 (0) | 2024.10.10 |
---|---|
[C#] VisualStudio 2022 ClickOnce를 사용한 소프트웨어 배포방법 (0) | 2024.07.29 |
[C#] 하나의 Window에서 두개의 클래스를 Binding하고 싶을때 (0) | 2024.05.22 |
[C#] ScrollViewer 안에 StackPanel을 사용해서 스크롤되는 가변 UI리스트 만들기. (0) | 2024.05.03 |
[C#] C# 소개 (0) | 2024.04.25 |
댓글