문자열을 자른다 — 슬라이싱과 반복문
Article
AI 서비스를 만든다고 하면 보통 모델부터 떠올린다. 그런데 실제로 문서를 다루는 AI 서비스에서 가장 먼저 하는 일은 모델과 아무 상관이 없다. 긴 문서를 짧은 조각으로 자르는 것이다.
이번 강의에서는 그 자르는 일을 파이썬으로 직접 해본다. 라이브러리를 하나도 안 쓰고, 설치할 것도 없다. 파이썬만 있으면 된다.
왜 자르나
모델에게 문서를 통째로 넘길 수는 없다. 넣을 수 있는 글자 수에 한계가 있기 때문이다. 그리고 한계가 없다 해도 통째로 넣으면 안 된다. 질문이 "연차 이월"인데 회사 규정 50장을 전부 넘기면, 모델은 그 안에서 답을 찾느라 헤매고 비용도 50장어치가 나간다.
- 긴 문서규정집 · 매뉴얼 · 가이드
- 자른다이번 강의
- 숫자로 바꾼다임베딩
- 질문과 가까운 조각을 찾는다유사도
- 그 조각만 모델에게 넘긴다답변 생성
첫 칸이 무너지면 나머지가 다 무너진다. 조각이 엉뚱하게 잘리면 그 뒤로 아무리 좋은 모델을 써도 답이 안 나온다.
파이썬 켜기
터미널에 이렇게 친다.
python
Windows에서 python이 안 먹으면 py를 친다. 이렇게 나오면 성공이다.
- Python 3.11.15 (main, ...) [MSC v.1937 64 bit (AMD64)] on win32
- Type "help", "copyright", "credits" or "license" for more information.
- >>>
>>> 가 보이면 파이썬이 한 줄씩 받아먹을 준비가 된 상태다. 여기서는 코드를 한 줄 치면 결과가 바로 나온다. 이번 강의는 이 화면에서 한다.
문자열은 글자를 한 줄로 세운 것이다
문자열을 하나 만든다. 아래를 그대로 복사해 붙여넣는다.
# 이 시리즈에서 계속 쓸 예제 문서. IT 직무 가이드를 짧게 줄인 것이다
text = "백엔드 개발자는 서버와 데이터베이스를 다룬다. 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다. 주로 쓰는 언어는 자바, 파이썬, 노드다. 요즘은 클라우드 지식도 함께 요구된다."
len(text)
- 110
len() 은 길이를 재는 함수다. 이 문서는 110글자다. 파이썬에서 한글 한 글자는 그냥 한 글자로 센다. 영어든 한글이든 공백이든 똑같이 1이다.
문자열은 글자를 한 줄로 세워놓은 것이고, 각 자리에 번호가 붙어 있다. 그 번호로 글자 하나를 꺼낼 수 있다.
text[0] # 첫 글자
- '백'
번호는 0부터 시작한다. 첫 글자가 1번이 아니라 0번이다. 프로그래밍을 처음 하면 여기서 한 번씩 걸린다.
text = "백엔드 개발자는 ..."
- text[0]
- '백'
- text[1]
- '엔'
- text[2]
- '드'
- text[109]
- '.' — 마지막 글자
- 길이가 110이니 마지막 번호는 109다
마지막 글자를 꺼내려면 109를 세고 있을 필요가 없다. 음수를 쓰면 뒤에서부터 센다.
text[-1] # 뒤에서 첫 번째
- '.'
슬라이싱 — 여러 글자를 한 번에 꺼낸다
글자 하나가 아니라 구간을 꺼내는 것을 슬라이싱이라고 한다. 대괄호 안에 콜론을 넣고 시작과 끝을 적는다.
text[0:20] # 0번부터 20번 「앞」까지
- '백엔드 개발자는 서버와 데이터베이스를'
세어보면 정확히 20글자다. 여기서 중요한 규칙이 하나 있다.
앞뒤를 비워두면 「처음부터」와 「끝까지」가 된다.
text[:20] # 처음부터 20번 앞까지 — 위와 같다
text[90:] # 90번부터 끝까지
- '백엔드 개발자는 서버와 데이터베이스를'
- '즘은 클라우드 지식도 함께 요구된다.'
끝 번호가 길이를 넘어가도 에러가 안 난다. 있는 만큼만 준다. 이건 나중에 문서를 자를 때 마지막 조각을 따로 처리하지 않아도 되게 해준다.
text[90:9999] # 9999번 같은 건 없는데도 에러가 아니다
- '즘은 클라우드 지식도 함께 요구된다.'
문서를 30글자씩 자른다
이제 본론이다. 110글자짜리 문서를 30글자씩 잘라보자. 손으로 하면 이렇게 된다.
text[0:30]
text[30:60]
text[60:90]
text[90:120]
네 줄을 치면 네 조각이 나온다. 그런데 문서가 5,000글자면 이 짓을 167번 해야 한다. 그래서 반복문을 쓴다.
# range(시작, 끝, 건너뛰기) — 0, 30, 60, 90 을 차례로 만든다
for start in range(0, len(text), 30):
print(text[start:start + 30])
- 백엔드 개발자는 서버와 데이터베이스를 다룬다. 사용자
- 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을
- 한다. 주로 쓰는 언어는 자바, 파이썬, 노드다. 요
- 즘은 클라우드 지식도 함께 요구된다.
네 조각이 나왔다. range(0, 110, 30) 이 0, 30, 60, 90 네 개를 만들었고, 마지막 text[90:120] 은 110에서 끊겨 20글자만 나왔다.
코드를 한 줄씩 뜯어보면 이렇다.
- 01
range(0, len(text), 30) 이 시작 번호를 만든다
0에서 시작해 30씩 더하면서 110을 넘기 전까지. 0, 30, 60, 90 네 개.
- 02
start 라는 이름에 그 값이 하나씩 들어간다
첫 바퀴에 0, 둘째 바퀴에 30. 이름은 아무거나 써도 되는데 뜻이 보이는 이름이 낫다.
- 03
text[start:start + 30] 으로 30글자를 꺼낸다
start 가 30이면 text[30:60] 이 된다. 앞 조각의 끝과 다음 조각의 시작이 같은 숫자라 겹치지 않는다.
- 04
print() 로 화면에 찍는다
print 를 안 쓰면 for 문 안에서는 화면에 안 나온다. 한 줄짜리 명령일 때만 자동으로 보여준다.
그런데 문제가 보인다
출력을 다시 보자. 첫 조각이 이렇게 끝난다.
1번 조각 끝
text[0:30]
- ... 다룬다. 사용자
- 「사용자」 뒤에서 뚝 끊겼다
- 문장이 안 끝났다
2번 조각 시작
text[30:60]
- 눈에 보이지 않는 곳에서 ...
- 앞 조각과 이어야 뜻이 산다
- 「사용자 눈에」가 두 조각에 걸쳤다
「사용자 눈에 보이지 않는 곳」이라는 한 덩어리가 두 조각으로 찢어졌다. 이 상태로 검색을 하면 어떻게 될까. 누가 "사용자 눈에 안 보이는 일이 뭔가요"라고 물으면, 답이 두 조각에 나뉘어 있어서 어느 쪽도 온전한 답이 못 된다.
숫자로만 자르면 반드시 이 일이 생긴다. 글자 수는 문장의 사정을 모르기 때문이다.
문단으로 자르기 — split()
문장 끝에 마침표가 있으니, 마침표를 기준으로 자르면 문장이 안 찢어진다. split() 이 그 일을 한다.
# 마침표 뒤 공백을 기준으로 쪼갠다
parts = text.split(". ")
parts
- ['백엔드 개발자는 서버와 데이터베이스를 다룬다',
- '사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다',
- '주로 쓰는 언어는 자바, 파이썬, 노드다',
- '요즘은 클라우드 지식도 함께 요구된다.']
대괄호로 감싸인 이것이 리스트다. 문자열 여러 개를 순서대로 담아둔 상자다. 이번엔 문장이 하나도 안 찢어졌다.
len(parts) # 몇 조각인가
parts[0] # 첫 조각 — 리스트도 0번부터다
- 4
- '백엔드 개발자는 서버와 데이터베이스를 다룬다'
리스트도 문자열처럼 0번부터 번호가 붙고, 슬라이싱도 똑같이 된다. 문자열은 글자를 담은 줄이고 리스트는 아무거나 담은 줄인데, 다루는 방식이 같다.
parts[:2] # 앞의 두 조각만
- ['백엔드 개발자는 서버와 데이터베이스를 다룬다', '사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다']
조각에 번호를 붙여 보기 좋게 찍는다
조각이 여러 개면 몇 번째인지 알아야 한다. enumerate() 를 쓰면 번호와 값을 같이 준다.
for i, part in enumerate(parts):
print(i, part)
- 0 백엔드 개발자는 서버와 데이터베이스를 다룬다
- 1 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다
- 2 주로 쓰는 언어는 자바, 파이썬, 노드다
- 3 요즘은 클라우드 지식도 함께 요구된다.
번호가 0부터 붙었다. 사람이 볼 화면이니 1번부터 세고, 길이도 같이 보고 싶다. 이럴 때 f-string 을 쓴다.
# 따옴표 앞에 f 를 붙이면 중괄호 안이 값으로 바뀐다
for i, part in enumerate(parts):
print(f"{i + 1}번 조각 ({len(part)}글자) : {part}")
- 1번 조각 (24글자) : 백엔드 개발자는 서버와 데이터베이스를 다룬다
- 2번 조각 (37글자) : 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다
- 3번 조각 (22글자) : 주로 쓰는 언어는 자바, 파이썬, 노드다
- 4번 조각 (21글자) : 요즘은 클라우드 지식도 함께 요구된다.
f-string 은 문자열 안에서 계산까지 된다. {i + 1} 처럼 식을 그냥 넣으면 결과가 들어간다.
| 쓴 것 | 하는 일 | 예 |
|---|---|---|
len(x) | 길이를 잰다 | len(text) → 110 |
text[3] | 3번 글자 하나 | '개' |
text[-1] | 뒤에서 첫 글자 | '.' |
text[0:20] | 0번부터 19번까지 | 20글자 |
text[:20] | 처음부터 | 위와 같다 |
text[90:] | 끝까지 | 나머지 전부 |
range(0, n, 30) | 0, 30, 60 ... 을 만든다 | 반복문의 재료 |
text.split(". ") | 기준 문자로 쪼개 리스트로 | 문장 4개 |
enumerate(리스트) | 번호와 값을 같이 준다 | for i, x in ... |
f"{값}" | 문자열 안에 값을 넣는다 | f"{n}번" |
스스로 해보기
붙여넣지 말고 직접 쳐본다. 5분이면 된다.
- 01
text 의 가운데 20글자를 꺼내본다
110글자니까 가운데는 45번쯤이다. 숫자를 바꿔가며 원하는 대목이 나올 때까지 해본다.
- 02
30이 아니라 50글자씩 잘라본다
조각 수가 어떻게 달라지나. 문장이 찢어지는 자리도 달라졌을 것이다.
- 03
조각 중에 가장 긴 것이 몇 번인지 눈으로 찾아본다
f-string 으로 길이를 찍어놓고 보면 금방 보인다. 다음 강의에서 이걸 코드로 찾는 방법을 배운다.
정리하면
문자열은 번호가 붙은 글자의 줄이고, 슬라이싱으로 구간을 꺼낸다. 반복문과 합치면 긴 문서를 일정한 크기로 자를 수 있다. 다만 글자 수로만 자르면 문장이 찢어지고, 그래서 문단 단위 자르기와 오버랩이 필요해진다.
이 문제를 눈으로 확인한 게 이번 강의의 핵심이다. 다음 강의에서는 조각을 담아둔 리스트를 함수로 다루고, 점수를 매겨 상위 3개만 뽑는 법을 배운다. 검색의 마지막 단계다.