mhtml(mime, 이메일) 파일 분석하기 - email.parser를 통해

ilotoki·2023년 7월 8일
0

스크래핑을 하던 중 mhtml을 분석할 기회가 생겨 mhtml에 대해 살펴보게 되었다.

  • Selenium에서는 driver.execute_cdp_cmd('Page.captureSnapshot', {})이라는 커맨드를 치면 mhtml을 export해준다.

이 글을 읽기 전에

mhtml은 분석하는 것보다 페이지 저장 용도로 사용하는 것이 좋다. 웹페이지를 mthml을 사용해 분석하는 것은 시간이 상당히 소요되고 원하는 정보만 뽑아내기에도 무리가 있다. 따라서 웹페이지에서 정보를 스크래핑하고 싶다면 requests 등을 이용하는 것이 좋다. 하지만 누군가는 mhtml을 통해 정보를 뽑아내야 하는 사람이 있을 것이라 생각해 이 글을 작성한다.
또한 이 글은 코드의 주석에 많은 정보가 있다. 주석을 꼼꼼히 읽어보기 바란다.

mhtml의 구조

mhtml은 다음과 같은 구성으로 이루어져 있다.
맨 위에는 헤더라고 하는 정보란이 있다.
이 헤더에는 다음과 같은 정보들이 있다.

From: [저장한 엔진 이름, '<Saved by Blink>'라고 되어 있음.]
Snapshot-Content-Location: [mhtml을 저장한 URL]
Subject: [문서 제목]
Date: [날짜정보]
MIME-Version: 1.0
Content-Type: multipart/related;
	type="text/html";
	boundary="----MultipartBoundary--[ascii 문자열]----"

이보다 더 많은 정보가 있을 수도, 더 적을 수도 있다. 또한 mthml 자체가 원래 이메일을 보내는 포맷이다 보니 from이나 subject, date같은 것들도 가지고 있는 것을 확인할 수 있다.

헤더 다음에는 본문이 있는데, 이 본문은 boundary로 구분되어 있다.

------MultipartBoundary--[ascii 문자열]----
Content-Type: text/html
Content-ID: <frame-[16비트 수]@mhtml.blink>
Content-Transfer-Encoding: quoted-printable
Content-Location: [URL]

...내용...

맨 위에는 정보가 있고 그 아래에 내용이 있는 식이다.
이런 게 여러 개가 있다. 사이트를 이루는 요소를 모두 이런 식으로 저장한다고 생각하면 된다. html, css, js, png, jpg가 모두 이런 식으로 저장되어 있다. 특히 png나 jpg와 같은 raw format의 경우 base64로 인코딩되어 있다(이는 나중에 파이썬에서 자동으로 raw라는 것을 인식해준다)

mhtml 파싱하기

우선 mhtml 파일을 하나 준비한다. mhtml 파일은 아무 웹사이트나 가서 ctrl+S > 파일 형식 > 웹페이지, 단일 파일 > 저장을 통해 하나 만들 수 있다.
다음의 코드를 통해 파일을 불러와 parser에 보낼 수 있다.

from email.parser import Parser
from pathlib import Path
import email.policy

mhtml = Path(r"mhtml.mhtml").read_text(encoding="utf-8")
# policy가 default가 아니면 내용을 가져오지 않기 때문에 꼭 필요하다.
e = Parser(policy=email.policy.default).parsestr(mhtml)

이제 e에 parsing된 mthml이 저장되었다.
이는 Message 객체인데, 기본적으로는 헤더의 정보만 가지고 있다. 이때 쓸만한 함수는 4개 정도이다.

print(dict(e))  # header에 있는 정보를 모두 불러온다.
# 출력값(이전에 보여줬던 값이 dict로 표현된 것 뿐이다):
# {'From': '"Saved by Blink"', 'Snapshot-Content-Location': '[개인정보]', 'Subject': '[개인정보]', 'Date': '[개인정보]', 'MIME-Version': '1.0', 'Content-Type': 'multipart/related; type="text/html"; boundary="----MultipartBoundary--[개인정보]----"'}

# dict를 굳이 하지 않고 그냥 바로 get을 해도 된다.
print(e["MIME-Version"])  # '1.0'

# boundary 값을 알려준다. Content-Type에서 추출해도 상관없다.
print(e.get_boundary())

마지막 하나가 중요하다. e.walk()함수는 다음 파트로 넘어갈 수 있게 해준다(제너레이터이다). 다음 파트로 넘어가면 다음 파트의 정보와 내용을 확인할 수 있다.
예를 들어 Content-Type이 image일때는 이미지를 저장하고, html일때는 BeatifulSoup로 파싱하는 함수는 다음과 같이 짤 수 있다.

for i, body in enumerate(e.walk()):
    print(c_type := body["Content-Type"])
    # if c_type == 'image/png':
    if c_type.startswith("image/"):
        # print(dict(body))  # 이전의 e처럼 여기의 body도 dict로 만들면 정보를 준다.

        # # content를 제공함(컴퓨터 멈춤 주의!)
        # print(body.get_content())
        
        # 파일 폭탄 주의! 파일을 '수.확장자' 형태로 저장함.
        Path(f'{i}.{c_type.split("/")[1]}').write_bytes(body.get_content())
    if c_type == "text/html":
        # content를 BeatifulSoup에 넣어 해석함
        soup = BeautifulSoup(body.get_content(), "lxml")
        print(soup)

이 글이 mhtml이나 email을 해석할 때 유용하게 참고되길 빈다.

0개의 댓글