[python] 파이썬 웹 크롤링(6): 데이터 읽기

업데이트:

파이썬 웹 크롤링(6): 데이터 읽기

본 포스팅은 Web Scraping with Python by Ryan Mitchell을 참고하였습니다.

참고 링크

Chapter 6: 데이터 읽기

6.1 데이터 인코딩

데이터 인코딩이 의미하는 것은 여러분의 컴퓨터 운영체제 혹은 파이썬 코드가 자료를 어떻게 읽을지를 결정하는 것을 의미합니다. 인코딩은 파일 확장자를 보고 추론할 수 있습니다. 물론 파일 확장자만 보고 절대적으로 판단할수는 없지만 예를 들어 myImage.jpg 파일을 myImage.txt로 저장하는 것은 문제가 없습니다. 적어도 텍스트 에디터가 해당 파일을 열기 전까지는요. 이런 경우는 드문 케이스 이지만 파일 확장자만 알면 데이터를 읽는데는 문제 없습니다.

좀 더 근본적인 레벨로 내려가면 모든 자료는 0 또는 1로 인코딩 되어 있습니다. 뿐만 아니라, 문자당 몇 비트를 쓸 것인지 혹은 각 픽셀 마다 색깔을 표현하는데 몇비트를 결정하는 인코딩 알고리즘이 존재합니다. 또한 PNG 파일과 같이 압축관련된 알고리즘도 있습니다. 비록 HTML 파일이 아닌 경우에는 겁이 날 수 있지만 우리에게는 파이썬 라이브러리가 있습니다. 파이썬은 어떤 종류의 포맷이라도 처리할 수있도록 라이브러리가 잘 갖춰져 있습니다. 텍스트파일, 비디오 파일, 이미지파일과 같은 서로다른 포맷의 파일들의 유일한 차이는 0과 1을 어떻게 해석하느냐 입니다. 이번 챕터에서는 text, pdf, png, gif 파일에 대해 알아보겠습니다.

6.2 Text

텍스트 파일을 평문 그대로 저장하는 것은 흔하지는 않지만 오래된 사이트에서는 이렇게 저장하는 경우도 있습니다. 대부분의 웹 브라우저에서는 텍스트 파일을 크롤링하는데 문제 없습니다.

from urllib.request import urlopen
textPage = urlopen(
        "http://www.pythonscraping.com/pages/warandpeace/chapter1.txt")
print(textPage.read())

보통, urlopen을 통해 페이지를 검색할때, HTML을 파싱하기 위해 BeautifulSoup 오브젝트로 변환 시키는데. 이 경우, 우리는 페이지를 다이렉트로 읽습니다. 파싱할 HTML 파일이 없다면 라이브러리는 쓸모가 없습니다. 텍스트 파일을 string으로 읽는다면 여러분은 이를 파이썬으로 읽기위해 또다른 분석이 필요할 것입니다. 이것이 단점인데, 여러분은 HTML 태그를 문맥상 단서로 사용할 수 없습니다. 이는 여러분이 원하는것과 원하지 않는 것을 집어내기 어렵다는 뜻입니다. 결국 텍스트 파일 내부에서 여러분이 원하는 정보를 얻기 어렵다는 뜻입니다.

6.2.1 텍스트 인코딩에 관하여

앞서 파일 확장자만 알면 파일을 읽는데 문제가 없다고 했습니다. 하지만 그게 모두에게 해당하는 말은 아닙니다. 예를 들어 txt 파일이라고 하죠. 10번 중 9번은 텍스트 파일을 읽는데 문제가 없습니다. 하지만 인터넷에 있는 텍스트를 읽는건 꽤 까다로울 수 있습니다. 이번 장에서는 영어를 비롯한 각종 언어들을 인코딩하는 기본 방법인 ASCII 부터 유니코드, ISO까지 한번 알아봅시다.

1990년대 초반, 유니코드 콘소시움은 어떤 언어이던지, 어떤 텍스트 문서이던지에 상관없이 모든 문자를 인코딩할 수 있는 전세계적인 텍스트 인코더를 만드는 것을 시도합니다. 목표는 라틴 알파벳부터 중국 한문, 수학기호, 특수기호를 포함합니다. 결과적으로 만들어진 인코더는 UTF-8 입니다. UTF-8은 Universal Character Set - Transformation Format 8 bit을 의미합니다. UTF-8에 대한 흔한 오해는 모든 문자를 8비트로 저장한다고 생각하는 것입니다. 하지만 8비트는 문자를 저장하고 보여주기 위한 가장 작은 사이즈이지 최대 사이즈가 아닙니다. 만약 UTF-8이 모든 문자를 8비트에 저장한다면 표현 가능한 문자는 오직 2의 8승 개 입니다. 이는 고작 256개 밖에 되지 않습니다.

현실적으로, UTF-8의 각 문자는 이렇게 시작했습니다. 오직 1 바이트가 이 문자를 인코딩 하는데 사용된다, 혹은, 그 다음 2 바이트는 single character를 인코딩합니다. 최대 4바이트가 사용됩니다. 왜냐하면 이러한 4바이트는 해당 문자를 인코딩하는데 얼마나 많은 바이트가 필요한지에 대한 정보를 포함합니다. full 32비트가 사용되지는 않고 정보의 21비트가 사용될 수 있습니다. 이는 전체 2,097,152개의 가능한 문자에 해당하는 것입니다. 현재 사용중인 문자는 1,113,112개 입니다.

비록 유니코드 다양한 분야에 사용되지만 ASCII 또한 여전히 많은 사람들이 사용중입니다. ASCII(이하 아스키)는 1960년대 이래로 쭉 사용중인 텍스트 인코딩 방법입니다. 아스키는 문자하나를 인코딩하는데 오직 7비트를 사용합니다. 따라서 총 사용가능한 문자는 2의 7승개, 128개의 문자입니다. 이것은 라틴 알파벳(대, 소문자), 마침표, 또는 필수적인 문자를 포함합니다. 물론 1960년대에는 문자를 7비트로 저장하느냐와 8비트로 저장하느냐는 엄청난 차이가 있었습니다. 왜냐하면 그 당시에는 저장 장치가 매우 비쌌기 때문입니다. 컴퓨터 사이언스는 저장하는데 비트를 더 써서 개발하기가 편하지느냐, 아니면 용량을 아껴 쓰서 효율적으로 개발하느냐와 싸워왔습니다. 결국 7비트가 승리했습니다. 하지만 현대에는 7비트의 시퀀스는 초반에 0으로 패딩되어있습니다.

6.2.2 인코딩 인 액션

이전에 우리는 인터넷에 존재하는 .txt파일을 읽기 위해 urlopen을 이용했는데 디폴트 세팅을 사용했습니다.
이는 영어 문서를 읽을 때는 효과적이지만 러시아어 등과 같은 다른 언어를 만났을 때는 문제가 발생할 수 있습니다. 예를 들어

from urllib.request import urlopen
textPage = urlopen(
              "http://www.pythonscraping.com/pages/warandpeace/chapter1-ru.txt")
print(textPage.read())

위 코드는 War and Peace라는 글의 첫 챕터를 읽는 것인데, 러시아어와 프랑스어로 작성된 글입니다. 이를 프린트하면 아래와 같은 화면을 보여줍니다.

b"\xd0\xa7\xd0\x90\xd0\xa1\xd0\xa2\xd0\xac 
\xd0\x9f\xd0\x95\xd0\xa0\xd0\x92\xd0\
x90\xd0\xaf\n\nI\n\n\xe2\x80\x94 Eh bien, mon prince.

실제 웹 사이트를 방문해봐도 원어민도 읽기 어려울정도로 글이 이상하다는 것을 볼 수 있습니다. 왜냐하면 브라우저는 이를 ISO-8859-1로 인코딩된 문서로 읽으려했던 반면 파이썬은 이 문서를 ASCII 문서로 읽기 떄문입니다. 실상은 양쪽 다 정답이 아니고 UTF-8 문서입니다.

따라서 우리는 아래와 같이 문자를 UTF-8로 정의해야 합니다.

from urllib.request import urlopen
textPage = urlopen(
              "http://www.pythonscraping.com/pages/warandpeace/chapter1-ru.txt")
print(str(textPage.read(), 'utf-8'))

BeautifulSoup을 사용했을 때는 아래와 같습니다.

html = urlopen("http://en.wikipedia.org/wiki/Python_(programming_language)")
bsObj = BeautifulSoup(html)
content = bsObj.find("div", {"id":"mw-content-text"}).get_text()
content = bytes(content, "UTF-8")
content = content.decode("UTF-8")

여러분은 여러분이 만드는 모든 웹 크롤러에 UTF-8 인코딩을 사용할 수 있습니다. 결국 UTF-8은 아스키 문자를 부드럽게 다룹니다. 그러나 웹 사이트 중 오직 9%만 ISO 인코딩을 사용한다는 사실을 기억해주세요. 불행히도 텍스트 문서의 경우 해당 문서가 쓰고 있는 인코딩에 대해 정확히 결정하기는 힘듭니다. 물론 해당 문서가 어떤 인코딩방식을 취하는지 검사하는 라이브러리도 있긴 하지만 대부분 틀릴 경우가 많습니다.

다행히도, HTML 페이지의 경우, 인코딩 방식은 <head> 섹선의 태그를 보면 알 수 있습니다. 대부분의 사이트, 특히 영어 사이트에서 다음과 같은 태그가 있습니다.

<meta charset="utf-8" />

6.3 CSV

여러분이 만든 크롤러가 CSV 파일을 만나거나 여러분의 동료가 CSV 포맷을 좋아하는 경우가 생길 수 있습니다. 이 경우, 파이썬은 CSV 파일을 읽고, 쓸수 있는 환상적인 라이브러리가 존재합니다. 비록 해당 라이브러리가 CSV의 다양한 형태를 다룰 수 있지만, 이번 장에서는 스탠다드 포맷에 대해서 다뤄보겠습니다.

6.3.1 CSV 파일 읽기

파이썬의 csv 라이브러리는 여러분의 로컬 파일을 여러분의 컴퓨터에 저장하는 방식으로 사용합니다. 하지만 이는 웹 크롤링에 대해서는 적용되지 않습니다. 웹 크롤링 때는 또다른 방법이 필요합니다.

  • 해당 파일을 로컬로 다운로드하고 파이썬이 해당 경로를 바라보게 합니다.
  • 파이썬 스크립트가 해당 파일을 다운로드하고, 일고, 삭제할수있게끔 합니다.
  • 웹으로부터 문자를 담아서 표현하고 StringIO 오브젝트로 감싸줍니다.

비록 처음 두개의 옵션은 가능하지만, 해당 파일이 하드 드라이브를 계속 차지하고 있는 것은 나쁜 습관 입니다. 더 나은 방법은 파일을 읽고, 오브젝트로 저장한다음 처리하는 것입니다. 이 경우 별다른 파일 저장 절차는 거치지 않습니다. 아래 코드를 봅시다.

from urllib.request import urlopen
from io import StringIO
import csv
data = urlopen("http://pythonscraping.com/files/MontyPythonAlbums.csv")
              .read().decode('ascii', 'ignore')
dataFile = StringIO(data)
csvReader = csv.reader(dataFile)

for row in csvReader:
print(row)

아웃 풋은 아래와 같습니다.

['Name', 'Year']
["Monty Python's Flying Circus", '1970']
['Another Monty Python Record', '1971']
["Monty Python's Previous Record", '1972']
...

위 코드에서 알 수 있듯, reader 오브젝트는 csv.reader가 되고, 이는 파이썬 리스트 오브젝트로 구성됩니다. 이 때문에 csvReader 오브젝트의 각 행에는 다음과 같이 접근 가능합니다.

for row in csvReader:
  print("The album \""+row[0]+"\" was released in "+str(row[1]))
The album "Name" was released in Year
The album "Monty Python's Flying Circus" was released in 1970
The album "Another Monty Python Record" was released in 1971
The album "Monty Python's Previous Record" was released in 1972
...

물론 위 결과에서 첫 행은 스킵할 수 있습니다.

from urllib.request import urlopen
from io import StringIO
import csv
data = urlopen("http://pythonscraping.com/files/MontyPythonAlbums.csv")
              .read().decode('ascii', 'ignore')
dataFile = StringIO(data)
dictReader = csv.DictReader(dataFile)

print(dictReader.fieldnames)

for row in dictReader:
print(row)

csv.DicReader는 csv 파일의 각 행을 딕셔너리 오브젝트로 리턴합니다. (이전에는 리스트 오브젝트로 리턴했었죠)

['Name', 'Year']
{'Name': "Monty Python's Flying Circus", 'Year': '1970'}
{'Name': 'Another Monty Python Record', 'Year': '1971'}
{'Name': "Monty Python's Previous Record", 'Year': '1972'}

6.4 PDF

PDF 파일은 텍스트나 이미지를 볼 때 플랫폼에 관계없이 볼 수 있다는 장점이 있습니다. PDFMiner3K는 사용하기 쉬운 라이브러리입니다.

PDFMiner3K 다운로드

6.5 Microsoft Word

python-docx library