인턴생활 - 뉴스 데이터수집하기


 

1. 프로젝트 소개

 

구조

 

 제가 맡은 작업은 뉴스 데이터를 수집하여 이를 DB에 저장하는 배치 프로그램을 제작하는 것입니다. 1시간마다 배치 작업으로 데이터를 자동 수집 및 갱신하며, 수집된 데이터는 자사 앱의 뉴스화면에 활용됩니다.

 


 

2. 구현시 고려사항

 

1) 기능 세분화하기

프로그램의 기능을 세분화하면 다음과 같습니다. 

  1. 배치하는 시점에서 수집할 url 담당
  2. html 파싱/ 추출 담당
  3. DB API 담당

배치시간마다 프로그램을 실행하면 위의 과정을 순차적으로 처리합니다.

 

2) 설정한 시간대에 데이터 수집하기

 

예시프로그램

 

이 프로그램의 목적은 지정된 시간 구간 동안 생성된 뉴스 사이트의 URL을 수집하고 이를 추출하여 저장하는 것입니다.

  1. 인수값 입력:
    • start_date: 마지막으로 배치가 실행된 시점의 시간.
    • end_date: 현재 배치를 실행하는 시점의 시간.
  2. 시간 구간 설정 예시:
    • 마지막 배치 실행 시점: 2020-01-01 18:00
    • 현재 시점: 2020-01-01 19:00
    • 프로그램은 2020-01-01 18:01부터 2020-01-01 19:00 사이에 생성된 데이터를 처리합니다.
    • 뉴스 사이트의 작성 시간 기록은 분 단위로 이루어지기 때문에 분 단위로 관리 합니다.
# 예시코드
start_date = "2020-01-01 18:01" # start는 최근에 배치를 했던 시간에 +1분
end_date = "2020-01-01 19:00" # end는 배치시작하는 시작 즉 현재 시간

collector = NaverNewsCollector()
urls = collector.collect(start_date, end_date)

 

- 중복 적재 방지

위에서 수집된 데이터는 중복 적재가 될 수도 있습니다. 따라서 수집된 뉴스들을 식별할 키가 필요합니다.

 


"/news/news_read.naver?article_id=0005415420&office_id=009&
mode=LSS3D&type=0§ion_id=101§ion_id2=258§ion_id3=401&date=20241217&page=1"

 

네이버 뉴스 사이트의 url의 길이가 깁니다. 따라서 특정할 수 있는 정보를 따로 뽑을 필요가 있습니다. 여기서는 article_id와 office_id를 사용합니다. article_id는 기사 식별번호, office_id는 언론사식별번호이므로 이 둘을 조합하면 유일성을 보장할 수 있습니다.

 

 데이터 저장 시, 이 두 값을 조합한 키를 기준으로 데이터베이스에 이미 저장된 데이터인지 확인합니다.이를 효율적으로 처리하기 위해 데이터베이스 테이블에 article_id와 office_id를 묶은 Unique Index를 설정합니다.

 

- 데이터 누락 방지

 

설정된 시간대에 데이터를 안정적으로 누락 없이 수집하기 위해 해당 날짜의 1페이지부터 시작하여 stat_date(마지막 수집 배치 시간)까지 수집합니다.

 

이를 검증하기 위해 아래와 같이 테스트 코드를 작성합니다.

start_date ~ end_date 기간동안 수집한 url의 개수를 기대값과 비교하여 검증합니다.

# 예시코드
@pytest.mark.parametrize(
    "start_date, end_date, expected",
    [
        ("20220312 00:00", "20220312 23:30", 28),
        ("20230312 17:33", "20230312 23:30", 9),
        ("20230306 15:59", "20230307 09:53", 79),
        ("20231016 20:00", "20231017 21:34", 193)
    ]
)
def test_naver_url(start_date, end_date, expected):

    #시작 날짜
    start = datetime.strptime(start_date, format_string)
    start = tz.localize(start)

    # 끝날짜 설정
    end = datetime.strptime(end_date, format_string)
    end = tz.localize(end)

    collector = NaverNewsCollector(start=start, end=end)

    #given
    urls = collector.collect()

    #then
    assert len(urls) == expected

 

3) 예외 사항 처리하기

크롤링을 하다 보면 일시적인 네트워크 오류로 인해 요청이 실패할 수 있습니다. 따라서 이에 대한 예외 사항을 적절하게 처리해야 합니다. 파이썬의 데코레이터 문법을 활용하여 요청 실패 시 재시도 횟수와 요청 간격을 조절하도록 구현했습니다.

def retry(max_retries=3, delay=5, exceptions=(requests.RequestException,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

#예시 적용법
@retry(max_retries=3, delay=5)
def fetch_url(url):
    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()
    return response.text

 

 

3. 한계점

이 방식에서는 확장성과 성능 측면에서 몇 가지 개선할 점이 있습니다.

 

  1. 코드 복잡도 증가
    • 순수 파이썬 코드로만 작성되어 있어 요구사항이 추가될수록 코드가 복잡해집니다.
    • 따라서 워크플로우 도구나 크롤링 프레임워크를 적용하면 유지보수성과 확장성이 향상될 수 있습니다.
  2. 리팩토링 필요
    • 현재 네이버에 특화된 코드 구조로 작성되어 있어, 다른 사이트를 추가할 경우 재사용성이 제한됩니다.
    • 따라서 다양한 사이트에 대응할 수 있도록 범용적인 구조로 개선 필요가 있습니다.
  3. 비동기 처리 고려
    • 수집하는 데이터의 개수가 많지 않아서 현재 동기 방식으로도 몇 분 이내에 처리가 완료됩니다.
    • 하지만 사이트 추가 및 요청할 URL이 많아질 경우, 성능을 위해 비동기 처리 방식을 고려해야 합니다. 단, 비동기 처리 시 과도한 요청으로 인해 접속 차단 가능성이 있으므로, 적절한 속도 조절이 필요합니다.