파이썬으로 HTML의 태그들을 제거하고 내용만 확인해 보도록 하겠습니다. HTML의 태그는 종류도 다양하고 대소문자도 구분하지 않지만 기본적으로 < 로 시작하고 > 로 끝나는 규칙이 있습니다. 이런 일정한 규칙들을 갖는 문자열들을 다루는 데는 정규 표현식(Regular Expression)이 효율적이니 이를 가지고 태그들을 제거해 보겠습니다.
일단은 샘플 HTML으로 간단히 테스트를 하고 마지막에는 원하는 주소의 HTML을 가져와서 적용해보도록 하겠습니다. 다음은 일반적인 html의 예제입니다. <html><head></head><body></body></html>으로 진행되며 그 안에 script도 있고 주석도 있고 여러 태그들도 있습니다. 한글도 있고 영어도 있고 여러 줄(\n)로 이루어져 있습니다.
1 2 3 4 5 | html = "<html><head>some header information</head> \ <Body>it's start. <script src='..'>some script</script> \ <!-- some comments -->some <b>body</b> contents.. \n<a href='some link'>gogo</a> \ 그리고 다른 것들.. \ <script>another</script></Body></html>" |
1) import re
파이썬에서 정규 표현식을 사용하기 위해서 re를 import 합니다. re lib에 대한 자세한 내용은 https://docs.python.org/3.6/library/re.html?highlight=re#module-re와 https://docs.python.org/3.6/howto/regex.html#regex-howto 를 참조하세요.
1 | import re |
2) <body></body>의 내용만 잘라내기
HTML의 컨텐츠들만 알아내는 것이 목적이기 때문에 <body>와 </body> 사이의 내용만을 추려내도록 합니다.
re에는 찾는 기능을 하는 2종류의 함수가 있는데 match()는 대상 문자열의 처음부터 찾기 시작하고, search()는 대상 문자열의 임의의 위치에 원하는 문자열이 있으면 찾을 수 있습니다. <body>는 HTML 문서의 중간에 위치하니 여기서는 search() 메소드를 사용합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
search() 메소드의 첫번째 인수 '<body.*</body>' 는 <body로 시작하고 어떤 글자가(.) 없을 수도 여러개 있을수도 (*) 있고 그 이후에 </body>로 끝난다는 정규 표현식입니다. re.I는 re.IGNORECASE와 동일한데 대소문자를 구분하지 않겠다는 의미입니다. (실제 샘플 HTML을 보면 <Body></Body> 로 되어 있습니다.) 마지막으로 re.S는 re.DOTALL과 동일한데 의미는 여러 줄에 걸쳐서 정규 표현식을 찾으라는 의미입니다. (.(dot)의 범위가 모든 문자(\n)를 포함하라는 의미)
검색 결과가 없으면 프로그램을 종료하고, 그렇지 않으면 정규 표현식에 부합하는 문자열(body.group())을 다시 body에 넣습니다.
실행 결과는 다음과 같이 <Body> ~ </Body>만 남았습니다.
<Body>it's start. <script src='..'>some script</script> <!-- some comments -->some <b>body</b> contents..
<a href='some link'>gogo</a> 그리고 다른 것들.. <script>another</script></Body>
[Finished in 0.1s]
3) <body> 안의 <script>...</script> 삭제
대부분의 <script>는 <head>내에 위치하지만, <body> 내에도 존재할 수 있습니다. 이 <script> 태그는 다른 태그와 달리 <script>와 </script> 사이의 내용도 지우는 것이 컨텐츠만 남기는 목적으로는 맞다고 생각합니다.
문자열 내의 정규 표현식에 부합되는 내용을 지우기 위해서는 sub() 함수를 사용합니다. '<script.*?>.*?</script>'의 이미는 <script로 시작하고 그 뒤에 어떤 글자가 없을 수도 있고 여러개 있을 수도 있으며 (*? - ex, src='...'), 그 뒤에는 >가 나오고 다시 어떤 내용이 있을 수 있으며 끝은 </script>로 끝난다는 내용입니다.
여기서 .* 와 .*?의 차이는 Greedy 하게 탐색을 하느냐 안하느냐 입니다. 즉, .*는 조건에 가능한 많이 (Greedy) 매칭을 시키고, .*?는 조건에 가능한 적게 매칭을 시킵니다. 만약 <script.*? 부분을 <script.* 까지만 사용하면 .*의 조건을 가능한 크게 매칭시키기 때문에 첫번째 <script 부터 두번째 </script>를 만날 때까지 .*로 매칭시킬 수 있습니다.
?(non-greedy qualifier)는 *뿐만 아니라 +, ?, {m,n}에도 적용하여 *?, +?, ??, {m,n}? 로 사용할 수 있습니다.
1 2 | body = re.sub('<script.*?>.*?</script>', '', body, 0, re.I|re.S) print (body) |
3번째 인수 body는 정규 표현식을 적용할 문자열이고, 4번째 0은 부합되는 모든 문자열을 지우라는 의미입니다. 1을 적게 되면 첫번째 <script>만 삭제됩니다. 기본으로 안적으면 모든 것을 지우게 되지만, 마지막 인수에서 다시 대소문자 구분을 하지 않고, 여러 줄 대상으로 검색을 해야 하기 때문에 0을 명시해 줬습니다.
실행 결과는 다음과 같습니다. 이제 <script> 태그와 그 내용이 지워진 것을 확인할 수 있습니다.
<Body>it's start. <!-- some comments -->some <b>body</b> contents..
<a href='some link'>gogo</a> 그리고 다른 것들.. </Body>
[Finished in 0.1s]
4) 태그 및 주석 삭제
태그들은 전부 <로 시작하고 >로 끝나며 이것은 주석도 마찬가지입니다. 이번에는 이들 태그와 주석을 지워보도록 하겠습니다.
동일하게 sub() 함수를 사용하며 '<.+?>'는 <로 시작하고 뭔가가 한글자 이상 있으며(.+?) 그 뒤에 >로 끝나는 것의 의미입니다.
1 2 | text = re.sub('<.+?>', '', body, 0, re.I|re.S) print (text) |
이제 실행해보면 <body> 내의 텍스트만 출력되는 것을 볼 수 있습니다.
it's start. some body contents..
gogo 그리고 다른 것들..
[Finished in 0.1s]
5) 공백 제거
만약에 모든 공백까지 제거하려면 다음처럼 한번 더 sub() 함수를 호출해 주면 됩니다. 나 스페이스, 탭(\t), 엔터(유닉스 : \r, 윈도우즈 : \n)에 대해 삭제하는 기능을 하니 필요하면 적용하면 됩니다.
1 2 | nospace = re.sub(' | |\t|\r|\n', '', text) print (nospace) |
다음은 실행 결과입니다.
it'sstart.somebodycontents..gogo그리고다른것들..
[Finished in 0.1s]
6) 글자수 세기
원래 html과 text, nospace의 각각의 글자수를 세어보도록 하겠습니다.
1 | print ('html = ', len(html), ', text = ', len(text), ', nospace = ', len(nospace)) |
실행 결과는 다음과 같습니다.
html = 236 , text = 58 , nospace = 41
[Finished in 0.1s]
7) 특정 URL의 HTML을 가져오기
URL의 내용을 읽어오기 위해서는 urllib.request lib를 사용합니다. 해당 라이브러리의 urlopen()을 이용하여 원하는 주소의 HTML을 가져옵니다. 이후 response.read()로 읽은 후 반드시 utf-8로 decode를 해주도록 합니다. 만약 str(response.read())로 하게 되면 대부분 제대로 된 HTML은 utf-8로 인코딩이 되어있지만 str() 메소드에서는 utf-8로 디코딩을 하지 않아서 한글이 깨지는 문제가 발생하게 됩니다.
1 2 3 4 5 6 7 8 | import urllib.request path = 'http://zeany.net/' with urllib.request.urlopen(path) as response: html = response.read().decode("utf-8") print (html) |
출력 결과는 생략합니다.
8) 인수로 URL 받기
path를 소스내에 정의하지 말고 인수로 받도록 수정해 보겠습니다. sys 라이브러리의 argv에 인수들은 들어가며 이 때 첫번째 값(argv[0])은 프로그램 이름이고, 두번째 값(argv[1])이 입력할 URL입니다. 만약 2개의 인수를 입력하지 않았다면 잘못된 것이니 프로그램을 종료하고, 정상적으라면 path에 argv[1]를 복사합니다.
1 2 3 4 5 6 7 8 9 | import sys argc = len(sys.argv) if argc != 2: print('usage: python count.py url') exit() path = sys.argv[1] |
9) 최종 소스
사용자에게 URL을 입력 받아 해당 HTML의 텍스트의 글자수를 세는 프로그램은 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import urllib.request import sys import re argc = len(sys.argv) if argc != 2: print('usage: python count.py url') exit() path = sys.argv[1] with urllib.request.urlopen(path) as response: html = response.read().decode("utf-8") body = re.search('<body.*</body>', html, re.I|re.S) if (body is None) : print ("No <body> in html") exit() body = body.group() body = re.sub('<script.*?>.*?</script>', '', body, 0, re.I|re.S) text = re.sub('<.+?>', '', body, 0, re.I|re.S) nospace = re.sub(' | |\t|\r|\n', '', text) print (nospace) print ('html = ', len(html), ', text = ', len(text), ', nospace = ', len(nospace)) |
이제 이 글(http://zeany.net/46)의 글자수를 세어보도록 하겠습니다.
... (생략) iveCommonsAttribution-NonCommercial4.0InternationalLicense.
html = 57240 , text = 8949 , nospace = 5921
'Python' 카테고리의 다른 글
파이썬으로 파일을 읽어서 특정 부분 찾기 (1) | 2017.01.02 |
---|