[Python] 문자열 추출하기

2023. 3. 18. 22:26IT

 

 

RPA를 하면서 가장 많이 하는 것 중의 하나가 문자열을 다루는 것이다. 긴 문자열에서 일부를 추출하는 것 말이다. 바로 이전에 다뤘던 포스팅의 내용 중에서 문자열을 다루는 부분에 대해서 정리해 보고자 한다.

 

https://sohyemin.tistory.com/827

 

[Python] PDF에서 정보 추출하기 (RPA)

최근들어 RPA (Robotic Process Automation)에 관심을 가지게 되었다. RPA라는 용어를 몰랐을 뿐이지 나는 나름대로 업무자동화를 이용하고 있었다. Python으로 다량의 엑셀 자료를 이용해서 자료를 만들고

sohyemin.tistory.com

 

다음의 문자열은 "건축물관리대장"에서 pdfminer를 통해서 extraction한 것으로 개인정보 부분은 변경을 했기 때문에 문제가 되지 않을 것이다. 

 

txt = "■건축물대장의기재및관리등에관한규칙[별지제5호서식]<개정2021.7.12.>집합건축물대장(전유부,갑)고유번호대지위치4159013500-3-07280005" \
      "경기도인천시동인동열람용768-934전유부분명칭호명칭(2쪽중제1쪽) 라퓌애비뉴타워555경기도순동시서탄순환대로557-19(서인동)지번도로명주소소유자" \
      "현황주소소유권지분구분층별※구조용도면적(㎡)성명(명칭)주민(법인)등록번호(부동산등기용등록번호)주4층철근콘크리트구조의원68.71이름:장김문" \
      "-이하여백- 공용부분5523411-1******이지현구분층별구조용도면적(㎡)6345219-2******경상북도연기군연옥정길32431/2경기도평택시연실로192" \
      ",504동1103호(지금동,연실마을레미안2단지)1/2주주지4층철근콘크리트구조기계,전기실각층철근콘크리트구조주차장(지4~지1)3.5142.34-이하여백-" \
      "※이건축물대장은현소유자만표시한것입니다.이등(초)본은건축물대장의원본내용과틀림없음을증명합니다.변동일자변동원인2019.4.10.소유권이전" \
      "2019.4.10.소유권이전화성시장직인담당자:전화:발급일자:2023년3월17일※경계벽이없는구분점포의경우에는전유부분구조란에경계벽이없음을기재합니다." \
      "이건축물대장은열람용이므로출력하신건축물대장은법적효력이없습니다.297㎜×210㎜[백상지(80/㎡)]고유번호대지위치41593453450-3-01231241235" \
      "화성시삼척동명칭호명칭김포애비뉴타워555(2쪽중제2쪽)지번도로명주소3428-6경기도순동시서탄순환대로324-6(동인동)구분주층별" \
      "※구조각층철근콘크리트구조-이하여백-공용부분공동주택(아파트)가격(단위:원)열람용면적(㎡)기준일용도계단,승강기28.57공동주택(아파트)" \
      "가격*「부동산가격공시에관한법률」제18조에따른공동주택가격만표시됩니다.변동사항그밖의기재사항변동일변동내용및원인변동일변동내용및원인" \
      "2019.3.20.신규작성(신축)[건축과-1632434]2019.3.21.오기정정【서탄2택지개발지구일상643→남탄2택지개발지구수인동745-54/대지면적4,539.2㎡】" \
      "-이하여백-상14블록1로트】2019.11.29.지적공부확정(19.11.29.)에따른표시변경【동인동북탄2택지개발지구일상A블록4로트/" \
      "대지면적32430.0㎡→산이건축물대장은열람용이므로출력하신건축물대장은법적효력이없습니다.297㎜×210㎜[백상지(80/㎡)]"

 

RPA 뿐만 아니라 프로그래밍을 하다보면 문자열에서 일부분을 추출하거나 빼거나 하는 작업을 많이 하게 된다. 그 중에서 가장 간단한 것부터 다뤄보자. 

 

문자열에서 일부분을 가져오는 방법이 가장 일반적일 것이다. 그 중에서도 문자열에서 몇번째부터 몇번째까지를 가지고 오는 방법이다. 

# 문자열의 index는 0부터 시작을 한다.
# 따라서 아래는 1 ~ 6까지 다섯개의 문자를 리턴한다.
print(txt[1:6])   # 출력은 '건축물대장'

# 맨앞에 6글자를 가져오고 싶다면, 앞의 0은 생략가능
print(txt[:6])    # 출력은 "■건축물대장"

# [별지제5호서식]을 가져오기 위해서는
# [가 18번째 글자, ]가 27번째 글짜이므로
print(txt[18:27]) # 출력은 [별지제5호서식]

 

다음은 특정 문자의 위치를 찾는 방법이다. 특정 문자의 위치를 찾을 수 있다면 할 수 있는 일이 상당히 다양해 진다. 위의 문자열에서 처음 나오는 [와 처음 나오는 ]의 문자의 위치를 찾을 수 있다면 그 사이에 있는 문자열을 앞서 설명한 방법으로 가지고 올 수도 있다. 

또한 특정 문자가 있는지 없는지도 판단을 할 수 있다. 예를 보자.

 

print(txt.find("아름:"))

loc_name = txt.find("이름:")
print(loc_name)

loc_blank = txt.find("-이하여백-")
print(loc_blank)

print(txt[loc_name:loc_blank])

loc_name = loc_name + len("이름:")
print(txt[loc_name:loc_blank])

 

find 함수는 매개변수로 넘긴 문자를 넘겨준다. 

맨 처음의 txt.find("아름:")은 -1을 출력한다. 왜냐하면 이름이 아니라 '아름'으로 문자열 내에 없기 때문이다. 즉, 없는 문자에 대해서는 -1을 반환한다.

다음에 있는 txt.find("이름:")은 정확하게 "이름:"이 있는 위치를 반환한다. 맨 앞에 제시한 문자열을 사용해서 이 부분을 실행했다면 233이라는 숫자를 반환한다. 그 다음에 있는 txt.find("-이하여백-")은 239라는 숫자를 반환한다. 문자열 "이름:"과 "-이하여백-"이라는 글자 사이에는 "장김문"이라는 단어가 있다. 그래서 233부터 239 사이에 있는 문자를 출력하라고 하면, txt[loc_name : loc_blank])는 "이름:장김문"을 반환한다. 233에서 239는 6의 차이가 있으므로 "이름:장김문", 6글자와 정확하게 일치한다. 그 다음에는 loc_name = loc_name + len("이름:")이라는 라인을 보면 loc_name에 "이름:"의 길이 3을 더해 주고 있다. 그래서 맨 아랫줄의 의미는 233에 3을 더한 236에서 239 사이의 세 글자를 출력하라는 의미다. 실행해 보면 알겠지만 출력은 "장김문"이다.

 

여기서 얘기하고 싶은 것은 txt.find()가 반환하는 것은 괄호안에 넘겨주는 매개변수의 맨 처음 글자의 위치를 알려준다는 것이다. txt.find("이름:")의 return value는 '이'가 시작하는 곳의 위치라는 의미다. 

 

loc1 = txt.find("대지면적")
loc2 = txt.find("대지면적")
loc3 = txt.find("대지면적", loc1+4)
print(loc1, loc2, loc3)

l1 = txt.find("-이하여백-")
l2 = txt.find("-이하여백-", l1+6)
l3 = txt.find("-이하여백-", l2+6)
l4 = txt.find("-이하여백-", l3+6)
l5 = txt.find("-이하여백-", l4+6)
print(l1, l2, l3, l4, l5)

txt2 = txt[l3+len("-이하여백-"): l4]
print(txt2)

m1 = txt.find("전유부", 100, 300)
print(m1)

 

위의 예는 find를 가지고 할 수 있는 일들이다. 

첫 번째는 "대지면적"이라는 문자열을 찾는다. "대지면적"은 txt내에 두 번 나온다. 각각의 위치를 찾으려고 하면 어떻게 하면 될까? 첫 번째와 두 번째 라인과 같이 같은 함수를 두 번 쓴다고 해결이 되지 않는다. 왜냐하면 함수는 호출할 때마다 같은 값을 return할 것이기 때문이다. 그래서 find에서는 추가 인자를 넘겨줄 수가 있는데 두 번째 인자는 찾기 시작할 위치를 지정해 준다. 따라서 

 

        loc3 = txt.find("대지면적", loc1 +4)

 

는 loc1 + 4 이후부터 "대지면적"을 찾아서 return하라는 의미이다. 즉 처음 찾은 "대지면적"의 위치는 '대'의 위치이므로 거기에 "대지면적"의 길이 4를 더해서 그 이후부터 다음의 "대지면적"을 찾으라는 의미이다. 

 

두 번째 예에선 "-이하여백-"을 계속 호출하고 있다. "-이하여백-"은 txt에서 4개가 있는데 다섯 번을 호출했다. 따라서 다섯 번째는 -1을 리턴하게 될 것이다. 

 

세 번째 예는 문자열을 자르는 즉, substring을 어떻게 하는지를 보여준다. 문자열이 길 때는 잘라서 사용을 하기도 하기 때문이다. 이 예는 다음에 추가적인 예에서 공부한다.

 

마지막 네 번째는 

   

       m1 = txt.find("전유부", 100, 300)

 

인데 이 의미는 100에서 300번째 문자열에서 "전유부"를 찾으라는 의미다. 즉 영역을 지정해 줄 수 있다는 얘기다. 

 

이번 포스팅의 마지막으로 문자열을 다루기다. 

이번에 찾아야 할 것은 주소 내에서 문자열을 가지고 오는 것이다.  우선 우리가 가지고 와야 할 부분은 다음에 역상으로 표시된 문자 부분 557-19이다. 

 

경기도인천시동인동열람용768-934전유부분명칭호명칭(2쪽중제1쪽) 라퓌애비뉴타워555경기도순동시서탄순환대로557-19(서인동)지번도로명주소소유자

 

건물명인 "라퓌애비뉴타워555"는 문서에 한 번 밖에 나오지 않는다. 그리고 주소는 도로명 주소이고 괄호 안에는 지번주소에서 사용하던 동 이름이 나온다. 

얼핏 보기엔 건물명을 찾고 그 다음에 나오는 '('를 찾고 는다. 그리고 "서탄순환대로"를 찾아 '(' 사이의 글자를 찾으면 된다. 하지만 다른 파일에서는 적용이 되지 않는다. 왜냐하면 길 이름이 "서탄순환대로"가 아닌 것이 너무나도 많을 것이기 때문이다. 이럴 때는 어떻게 해야 할까? 

 

먼저 코드를 보자.

 

addr_loc = txt.find("라퓌애비뉴타워555")
ln = len("라퓌애비뉴타워555")

brace_loc = txt.find("(", addr_loc)

txt1 = txt[addr_loc+ln : brace_loc]
print(txt1)

txt_len = len(txt1)
loc = txt_len
while(1):
      oneChar = txt1[loc-1:loc]
      if oneChar.isdigit() or oneChar == "-":
            ... # nothing to do
      else:
            break
      loc = loc - 1

print(txt1[loc:txt_len])

 

건물명인 "라퓌애뉴타워555"와 그 이후 처음 나오는 '(' 사이의 문자열을 구하면 정확하게 주소만 나오게 된다. 위의 예에서 주소는 다음과 같이 출력이 된다. 

 

      경기도순동시서탄순환대로557-19

 

여기서 555-19를 빼 내야 한다. 

번지의 구성은 숫자 또는 '-'로 되어 있다고 가정을 한다. 따라서 문자열의 맨 뒤에서 부터 한 문자씩 읽어서 그 문자가 숫자이거나 '-'이면 그대로 두고, 맨 처음으로 숫자나 '-'가 아닌 것의 바로 앞 위치를 기억한다.

그래서 그 위치부터 맨 마지막까지의 문자열을 구하면 된다. 

 

while문에서 "#nothing to do"로 주석처리 되어 있는 부분이 바로 맨 뒤 부터 숫자와 '-'일 경우 진입하는 곳이다. 다음은 위치를 하나씩 줄여준다. 그러다가 숫자나 '-'가 아닌 문자를 만나면 loc에 이전 위치를 가진 상태로  while문을 빠져나오게 된다. 

 

이 값을 기반으로 print문을 통해서 찍어보면 정확한 값이 나왔는지를 알 수 있다. 

 

 

 

반응형