파이썬으로 제일 많이 하는 것이 웹 크롤링이 아닐까 생각한다.
웹사이트 크롤링 자체는 큰 문제가 없긴 한데.. 관련 정보를 확인해두면 좋다.
해결방법
해결방법은 보통 2가지인데, 개인적으로 사용하는 방법까지 총 3가지가 있다.
- replace를 이용한 유니코드 제거
- 일반적으로 가장 많이 사용하는 방법이 아닐까 생각한다. - unicodedata.normalize를 이용한 유니코드 변환
- 파이썬에서 제공하는 모듈을 통해 한 번에 유니코드를 제거할 수 있다. - split과 join을 이용한 제거
- 개인적으로 애용하는 방법이다.
설명
개인적으로 여러 사이트들을 크롤링하며 모은 유니코드와 대응하는 문자는 다음과 같다.
- \xa0 : 1칸의 공백
- : 1칸의 공백
- \u3000 : 탭(\t)
- \u200b : 이모티콘(이모지)
- \u200d : 이모티콘(이모지)
문제 상황
위 문자들을 무시한 채로 beautifulSoup 객체로 변환하면 다음과 같은 결과를 확인할 수 있다.
import bs4
string = '1,''\xa0'',2,' ' '',3,' '\u3000' ',4,''\u200b'',5,''\u200d'',6'
soup = bs4.BeautifulSoup(string, 'html.parser')
print('string')
print(f' {soup=}')
print(f' {str(soup)=}')
print(f' {soup.get_text()=}')
>> string
soup=1, ,2, ,3, ,4,,5,,6
str(soup)='1,\xa0,2,\xa0,3,\u3000,4,\u200b,5,\u200d,6'
soup.get_text()='1,\xa0,2,\xa0,3,\u3000,4,\u200b,5,\u200d,6'
단순히 beautifulSoup 객체를 출력하면 공백처럼 보이지만, str 객체로 변환하거나 get_text() 명령을 사용하면 유니코드가 숨어있다는 것을 확인할 수 있다.
replace 사용하기
replace를 사용하는 경우 각 유니코드를 확실하게 제거할 수 있다.
이 방법의 유일한 단점은 새로운 유니코드가 나타날 때마다 replace 명령을 추가해줘야 한다는 것이다.
코드
import bs4
string = '1,''\xa0'',2,' ' '',3,' '\u3000' ',4,''\u200b'',5,''\u200d'',6'
soup = bs4.BeautifulSoup(string.replace('\xa0', ' ').replace(' ', ' ').replace('\u3000', '\t'), 'html.parser')
print('string.replace')
print(f' {soup=}')
print(f' {str(soup)=}')
print(f' {soup.get_text()=}')
>> string.replace
soup=1, ,2, ,3, ,4,,5,,6
str(soup)='1, ,2, ,3,\t,4,\u200b,5,\u200d,6'
soup.get_text()='1, ,2, ,3,\t,4,\u200b,5,\u200d,6'
[" ".join(soup.get_text().split())]=['1, ,2, ,3, ,4,\u200b,5,\u200d,6']
unicodedata.normalize 사용하기
파이썬 내장모듈인 unicodedata.normalize 명령을 사용할 수도 있다.
여기서는 NFKC와 NFKD를 사용해볼 것이다.
자세한 사용방법은 파이썬 공식문서에서 확인할 수 있다.
이 방법의 단점은 유니코드가 아닌 문자는 변환되지 않는다는 것이다.
와 같이 unescape시 유니코드로 변환되는 문자열을 따로 제거해주거나, 변환 후 다시 한 번 unicodedata.normalize 작업을 수행해줘야 한다.
코드
# unicodedata.normalize('NFKC')
import unicodedata
import bs4
string = '1,''\xa0'',2,' ' '',3,' '\u3000' ',4,''\u200b'',5,''\u200d'',6'
soup = bs4.BeautifulSoup(unicodedata.normalize('NFKC', string), 'html.parser')
print("bs4.BeautifulSoup(unicodedata.normalize('NFKC', string)")
print(f' {soup=}')
print(f' {str(soup)=}')
print(f' {soup.get_text()=}')
print(f' {[" ".join(soup.get_text().split())]=}')
>> bs4.BeautifulSoup(unicodedata.normalize('NFKC', string)
soup=1, ,2, ,3, ,4,,5,,6
str(soup)='1, ,2,\xa0,3, ,4,\u200b,5,\u200d,6'
soup.get_text()='1, ,2,\xa0,3, ,4,\u200b,5,\u200d,6'
[" ".join(soup.get_text().split())]=['1, ,2, ,3, ,4,\u200b,5,\u200d,6']
# unicodedata.normalize('NFKD')
import unicodedata
import bs4
string = '1,''\xa0'',2,' ' '',3,' '\u3000' ',4,''\u200b'',5,''\u200d'',6'
print("unicodedata.normalize('NFKD', string)")
print(f' {soup=}')
print(f' {str(soup)=}')
print(f' {soup.get_text()=}')
print(f' {[" ".join(soup.get_text().split())]=}')
>> unicodedata.normalize('NFKD', string)
soup=1, ,2, ,3, ,4,,5,,6
str(soup)='1, ,2,\xa0,3, ,4,\u200b,5,\u200d,6'
soup.get_text()='1, ,2,\xa0,3, ,4,\u200b,5,\u200d,6'
[" ".join(soup.get_text().split())]=['1, ,2, ,3, ,4,\u200b,5,\u200d,6']
unicodedata.normalize는 왜 \xa0를 남겼을까?
이런 결과를 보게 되면 당황스럽지만.. 납득할만한 이유가 있다. 변환되기 전 \xa0는 유니코드가 아니었기 때문이다.
무슨 말이냐면, 유니코드 변환 후에도 남아있는 \xa0는 가 unescape된 문자라는 것이다.
다음 코드를 확인해보자.
코드
import html
print(f"{html.unescape(' ')=}")
>> html.unescape(' ')='\xa0'
split과 join을 사용하는 방법
각각의 유니코드로 이루어진 문자다.
그런데 재미있는 부분이 있는데, 이모티콘인 \u200b, \u200d을 제외한 나머지(\xa0, , \u3000)는 파이썬에서 공백 문자로 인정된다는 것이다.
다음 코드를 보면 split 명령으로 인해 남아있던 유니코드가 제거된 것을 확인할 수 있다.
웹페이지의 특정 문구만을 사용할 때 이 방법을 사용하면 별다른 작업없이 유니코드를 제거할 수 있다.
\u200b, \u200d와 같은 문자는 이모티콘이기 때문에 unicodedata.normalize를 사용하더라도 잔존하기 때문에, 제거하려면 replace나 다른 방법을 사용해야한다.
코드
import bs4
string = '1,''\xa0'',2,' ' '',3,' '\u3000' ',4,''\u200b'',5,''\u200d'',6'
soup = bs4.BeautifulSoup(string, 'html.parser')
print('string')
print(f' {soup=}')
print(f' {str(soup)=}')
print(f' {soup.get_text()=}')
print(f' {[" ".join(soup.get_text().split())]=}')
>> string
soup=1, ,2, ,3, ,4,,5,,6
str(soup)='1,\xa0,2,\xa0,3,\u3000,4,\u200b,5,\u200d,6'
soup.get_text()='1,\xa0,2,\xa0,3,\u3000,4,\u200b,5,\u200d,6'
[" ".join(soup.get_text().split())]=['1, ,2, ,3, ,4,\u200b,5,\u200d,6']