<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>jiwoo.lee</title>
    <link>https://jiwoo.tistory.com/</link>
    <description>jiwoo 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 6 Aug 2026 09:54:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jiwoo.lee</managingEditor>
    <image>
      <title>jiwoo.lee</title>
      <url>https://tistory1.daumcdn.net/tistory/8882092/attach/2949711451f04fa18dd651468c5c4b87</url>
      <link>https://jiwoo.tistory.com</link>
    </image>
    <item>
      <title>[Git &amp;amp; Github] 아주 기초</title>
      <link>https://jiwoo.tistory.com/entry/%EA%B9%83</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codingapple.com/course/git-and-github/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이분&lt;/a&gt;에게 감사합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; git을 왜 쓰는 건가요? &amp;gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 작업한 코드들을 기록하고 보관할 수 있음&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 과거로 빠꾸 가능. 과거에 작업한 내용 열람 가능. 등등 안정적인 개발이 가능 &amp;rarr; 멋있음&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; git으로 파일 기록하기 &amp;gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 작업폴더에서 git을 사용하려면 폴더 열고 터미널에서 git init부터 입력&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-07 오후 9.29.39.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RQRui/dJMcaalRMOC/Ciiam2nzYbkI14yKJM3FKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RQRui/dJMcaalRMOC/Ciiam2nzYbkI14yKJM3FKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RQRui/dJMcaalRMOC/Ciiam2nzYbkI14yKJM3FKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRQRui%2FdJMcaalRMOC%2FCiiam2nzYbkI14yKJM3FKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;124&quot; data-filename=&quot;스크린샷 2026-01-07 오후 9.29.39.png&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스테이징 하는 법(스테이징 = 어떤걸 기록할지 고르기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git add &amp;lt;파일1&amp;gt; &amp;lt;파일2&amp;gt; ...&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 파일 지정해서 스테이징&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git add .&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 폴더 내 모든 파일 한번에 스테이징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋하는 법(커밋 = 고른 걸(스테이징) repository(저장소)에 기록하기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git commit -m 커밋메시지&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;요즘은 웬만한 에디터들(VS Code 같은 것들)은 git 기본 기능이 내장되어 있어서 버튼 몇개만 딸깍하면 스테이징, 커밋같은 git 기본 기능들은 사용할 수 있음. 다만 스테이징/커밋 개념을 알고, 터미널에서 직접 쓸 줄도 알아야 에디터에 의존하지 않을 수 있음&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋 전에 파일 차이점 확인하는 건 좋은 습관&lt;/li&gt;
&lt;li&gt;(커밋하려는)현재 파일 vs 최근 commit 파일 차이점을 확인하고 수정하려는 부분이 맞는지 보고&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이걸 자동으로 해주는 게 git diff&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 근데 사실 더 가시성 좋은건 git difftool (vi 모드로 들어가기때문에 hjkl이 방향키임/:q,:qa가 종료)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; git difftool 커밋아이디1 커밋아이디2 : 커밋 두개 비교 가능&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 근데 사실 이것도 에디터 확장프로그램 사용하셈&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 브랜치는 뭘까? &amp;gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;계속 커밋하면서 코드를 짜다보면 갑자기 새로운 기능을 추가해야할 때가 있음. 그럴때 지금까지 잘 커밋해오던 파일에 코드를 추가해서 커밋해도 되지만, 혹시나 잘못되면 어떻게 하죠..? 그럴때 안전하게 새로운 기능을 추가해보고 싶으면 프로젝트의 복사본을 만들어서 거기에서 먼저 해보면 될것임.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그럴때 브랜치 기능을 사용. 마치 나뭇가지를 치는 것처럼 파일을 사용!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQtOlh/dJMcadQqGj7/ougyrGI0fDauhb9hmPlap1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQtOlh/dJMcadQqGj7/ougyrGI0fDauhb9hmPlap1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQtOlh/dJMcadQqGj7/ougyrGI0fDauhb9hmPlap1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQtOlh%2FdJMcadQqGj7%2FougyrGI0fDauhb9hmPlap1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;406&quot; height=&quot;135&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git branch (브랜치이름)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ㄴ 이러면 설정한 브랜치 이름을 가진 사본(새로운 나뭇가지 버전)이 생기는거임&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 만든 브랜치로 이동하고 싶으면&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git switch (이동하려는브랜치이름) 하면 됨.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(너무 왔다갔다 해서 내가 어느 브랜치에 있는지 모르겠으면 git status로 확인하면 될것)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다시 main 브랜치로 돌아가려면? 당연히 git switch main 하면 됨&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아무튼 새로만든 브랜치에서 커밋을 하면 당연히 원래 브랜치(main)에는 영향 x&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;역시 main으로 이동해서 커밋하면 다른 브랜치(새로 만든거)에도 영향 x&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;브랜치를 왔다갔다하면서 커밋한 내역을 깔끔하게 보고 싶으면&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;log 명령어를 사용하면 됨. git log --oneline --all --graph&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(그래프로 깔끔하게 볼 수 있게 해주고, git log 했을때 나오는 HEAD는 내 현재 위치를 가리키는 것임)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 다시 돌아와서.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;새로 만든 브랜치에서 수정한 내용이 마음에 들어서 원래 작업하던 main 브랜치로 합치고 싶으면 &amp;rarr; 일일이 복사해야됨? &amp;rarr; 아님 merge를 사용하면 쉽게 합칠 수 있음!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eS7IOI/dJMcajwrIo8/DB2LkkW9NTK9juku2eKftK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eS7IOI/dJMcajwrIo8/DB2LkkW9NTK9juku2eKftK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eS7IOI/dJMcajwrIo8/DB2LkkW9NTK9juku2eKftK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeS7IOI%2FdJMcajwrIo8%2FDB2LkkW9NTK9juku2eKftK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;118&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; merge(합치기)하는 법 &amp;gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;git switch main 으로 돌아와서&lt;/li&gt;
&lt;li&gt;git merge 브랜치이름 입력하면 합쳐짐&lt;/li&gt;
&lt;li&gt;늘 그랬듯이 git log 로 확인하면 예쁘게 합쳐졌다고 알려줌.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 여기서 충돌(conflict)이 날 수도 있음..&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ex. main 브랜치에서 1번 파일의 1번줄을 수정했었고,&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;새 브랜치에서도 1번 파일의 1번줄을 수정했으면, 합칠때 뭘로 반영하는게 맞을지 컴퓨터는 모름.(나도 모름)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그럴땐 컴퓨터가 지 맘대로 더 좋은 코드로 합치..는게 아니고, merge conflict가 발생했다고 알려줌&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 에디터로 이동해서 해당 파일을 보면 충돌사항이 적혀있고, 어떤 코드를 남겨서 합칠지 고르면됨&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;( &amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD &amp;gt;&amp;gt;&amp;gt;&amp;gt; //// ==== ) 이런 쓸데없는거 지우고 원하는것만 남기면 됨&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 어떤 거 남길지 결정하고서 add, commit 해주면 merge conflict를 해결하고 브랜치를 합친것임&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 브랜치를 쓰면 좋은 점 &amp;gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;나뭇가지를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;협업할때 branch를 만들어서 하면 편리함 &amp;rarr; 프로그램 하나 만드는데 10명이서 똑같은 파일 수정하고 저장하려면 여기저기서 난리나지 않겠음? 그러니까 기능을 추가하고 싶으면
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선 브랜치로 프로젝트 사본을 만들어서 거기서 열심히 개발해보셈&lt;/li&gt;
&lt;li&gt;그리고 테스트해봤는데 너무 완벽하게 만들었으면 main branch로 합쳐서&lt;/li&gt;
&lt;li&gt;협업하는 모두가 기능을 확인할 수 있게 해주셈. 그리고 계쏙 개발하셈&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;고백을 하나 하자면, 브랜치를 합치는 방법은 사실 여러개였음.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfe9vn/dJMcabdZapm/KWjedhiTkhROoom6lymrq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfe9vn/dJMcabdZapm/KWjedhiTkhROoom6lymrq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfe9vn/dJMcabdZapm/KWjedhiTkhROoom6lymrq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfe9vn%2FdJMcabdZapm%2FKWjedhiTkhROoom6lymrq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;339&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;821&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;브랜치에 각각 신규 commit이 1회 이상 있는 경우&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;merge 명령을 내리면 두 브랜치의 코드를 합쳐서 새로운 commit을 자동으로 생성해주는데&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 3-way merge라고 부름(기본 동작방식)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;727&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckJDMD/dJMcagfnkdc/j60yWxePiCGEjvfYnkL6Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckJDMD/dJMcagfnkdc/j60yWxePiCGEjvfYnkL6Zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckJDMD/dJMcagfnkdc/j60yWxePiCGEjvfYnkL6Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckJDMD%2FdJMcagfnkdc%2Fj60yWxePiCGEjvfYnkL6Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;526&quot; height=&quot;248&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;727&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그런데, 가끔은&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;브랜치 파서 개발했는데 그냥 그걸로 쭉 가면 되는 상황이 있음. 그럴땐 기존거랑 합칠 필요가 없음!&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그냥 개발하던 새로운 브랜치를 main이라고 불러주면 됨.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 merge를 하게 되면 fast-forward merge 된거라고 알려줌.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &amp;lsquo;기준이 되는 브랜치(main)에 신규 commit이 없으면&amp;rdquo; 자동으로 &amp;ldquo;fast forward merge&amp;rdquo; 발동!&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 하기 싫으면 강제로 git merge &amp;mdash;no &amp;mdash;ff (브랜치이름) 하면 강제로 3-way merge 할 수도 있음.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;아무튼 merge를 잘 했음.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;파일 복구, 커밋 복구, 과거로 이동!! &amp;larr; 많이 쓰는 건 아니기때문에 이런게 있다 정도를 알아두고 찾아서 쓰기&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git restore 파일명&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ㄴ 파일명이 최근 커밋상태로 되돌아감&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git restore --source 커밋아이디&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ㄴ 커밋아이디 시점으로 되돌아감&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git restore --staged 파일명&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ㄴ 스테이징을 취소할 수 있음(add를 해제)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; git flow 전략 &amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5v9Y1/dJMcagT0TaH/BbgzAgUKFB3NpYrC4ZQ9D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5v9Y1/dJMcagT0TaH/BbgzAgUKFB3NpYrC4ZQ9D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5v9Y1/dJMcagT0TaH/BbgzAgUKFB3NpYrC4ZQ9D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5v9Y1%2FdJMcagT0TaH%2FBbgzAgUKFB3NpYrC4ZQ9D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;276&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;main
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포용으로 가장 중심이 되는 브랜치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;develop
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신기능을 개발해서 넣어보고 싶을때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;feature
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 중에 특정한 기능을 추가할때 feature/기능1 이런 이름으로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;release
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신기능을 추가한 코드를 배포하기 전에 테스트용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;hotfix
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메인 버전에서 긴급한 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; trunk-based 전략(github-basedeh 유사) &amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y52ob/dJMcabdZat1/n4ecIyMSsmkJlnKAxuteM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y52ob/dJMcabdZat1/n4ecIyMSsmkJlnKAxuteM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y52ob/dJMcabdZat1/n4ecIyMSsmkJlnKAxuteM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy52ob%2FdJMcabdZat1%2Fn4ecIyMSsmkJlnKAxuteM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;174&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중심이 되는 하나의 브랜치만 운영하면서&lt;br /&gt;기능 하나하나를 추가할때는 그냥 그때그때 브랜치를 만들어서 사용&lt;br /&gt;ㄴ 대신 테스트를 많이 해봐야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;방금 쓰레기같이 짜놓은 코드가 있다고 칩시다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 코드를 잠깐 치워놓고 개발하고 싶으면 주석처리해도 되겠지만&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;git stash 명령어를 이용해도 잠깐 코드를 치울 수 있음!&lt;/li&gt;
&lt;li&gt;git stash save &amp;lsquo;메모&amp;rsquo;&lt;/li&gt;
&lt;li&gt;git stash list&lt;/li&gt;
&lt;li&gt;git stash drop 번호 &amp;larr; 번호는 stash@{번호} 이걸 말하는 거임&lt;/li&gt;
&lt;li&gt;git stash pop &amp;larr; 가장 최근 stash를 불러옴&lt;/li&gt;
&lt;li&gt;git stash는 커밋할때 쓸때없는 코드를 숨기는 용도로 사용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; github를 알아보자 &amp;gt; 허브.? 옥천허브도 아니고 이게 뭐냐.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git &amp;lt; 파일 버전 관리해주는 프로그램&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git이 파일 기록해두는 장소 : repository(커맨드 쉬프트 닷 &amp;lt; 숨김파일 볼 수 있음)&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 이걸 내가 쓰는 데스크탑에만 만들어온라인 repository만들면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴퓨터 고장나도 안심&lt;/li&gt;
&lt;li&gt;협업도 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;온라인 repository = 원격 저장소인 셈 &amp;larr; 그걸 github에서 하는 거임&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 그러면 작업하던걸 온라인 repository = github에서 관리하는 방법을 알아봅시다 &amp;gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;repository(저장소)를 만들자&lt;/li&gt;
&lt;li&gt;github.com에 들어가서 + 버튼 누르고 레포지토리 하나 만드세요!!&lt;/li&gt;
&lt;li&gt;작업폴더를 만든다음 git으로 commit 몇번 해보세요&lt;/li&gt;
&lt;li&gt;git init &amp;larr; 사실 이게 로컬 저장소(repository) 생성하는 거였던 것임&lt;/li&gt;
&lt;li&gt;로컬 저장소 &amp;rarr; 원격 저장소로 업로드를 해봅시다git push -u 원격저장소주소 main &amp;larr; 여기서 -u 옵션은 지금 입력한 주소 기억해두라는 뜻.참고로 주소는 &lt;a href=&quot;https://~.git&quot;&gt;https://~.git&lt;/a&gt; 형태인거 찾으면 됨.&lt;/li&gt;
&lt;li&gt;다음부터는 주소를 길게 입력안하고 git push만 입력해도 됩니다.&lt;/li&gt;
&lt;li&gt;작업폴더에서 터미널켜서&lt;/li&gt;
&lt;li&gt;그랬더니 진짜로 로컬(컴터)에 있던 파일과 commit 내역이 원격 저장소에 올라갔습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주소 입력하는게 귀찮은데요. git remote add 변수명 저장소주소예를 들어보면은..이렇게 입력하면 어쩌구 주소가 필요할때마다 origin 이라는 변수를 쓸 수 있습니다.git push -u origin main(근데 사실 아까 처음 했던 대로 -u 옵션으로 push하면 그냥 git push만 해도 됨)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;근데 변수를 너무 많이 저장해서 까묵었는데요..&lt;/li&gt;
&lt;li&gt;git remote -v를 치면 목록이 나와요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이렇게 쓸 수 있다는 이야기입니다.&lt;/li&gt;
&lt;li&gt;그러니까..&lt;/li&gt;
&lt;li&gt;git remote add origin &lt;a href=&quot;https://github.com/어쩌구/저쩌구.git&quot;&gt;https://github.com/어쩌구/저쩌구.git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ㄴ 이래 하면 됩니다.&lt;/li&gt;
&lt;li&gt;그럴땐 주소를 변수에 저장해서 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 원격 저장소에 있던거를 그대로 내려받는 법을 알아봅시다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;갑자기 개발을 시작하고 싶어질때 쓰면 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git clone &amp;lt;&lt;a href=&quot;https://github.com/어쩌구/저쩌구.git&quot;&gt;https://github.com/어쩌구/저쩌구.git&lt;/a&gt;&amp;gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;을 입력하면 그 주소에 있는걸 그대로 내려받을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원격 저장소에 안올리고 싶은데 그러면 스테이징하고 커밋할때마다 귀찮은데요..그럴땐 .gitignore 파일을 하나 만들면 저장소에 올리지 않을 파일들을 쉽게 명시할 수 있어요&lt;/li&gt;
&lt;li&gt;ignore(무시)하면 됨&lt;/li&gt;
&lt;li&gt;개인정보가 들어있는.. 또는 쓸데없는 파일들로.. 등등으로 올릴 필요가 없는 파일들이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;원격 저장소의 장점은 남들과 협업할 수 있다는 겁니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;일이 너무 많아서 코딩노예 2명을 고용했다고 해봅시다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;코딩노예들의 github 아이디를 탈취해서 그 아이디를 collaborators 메뉴에 등록해놓으면 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;코딩노예가 코딩 작업을 해서 git push 어쩌구를 했다고 치면 그게 협업 끝입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 git push는 사실 마음대로 할 수 있는게 아닙니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;다른 노예가 작업한 파일이 원격저장소에 생기면 git push를 못합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면&amp;hellip;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;노예1 이 hello.c 라는 파일을 만들어서 push 해놨다고 칩시다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 노예 2가 hi.c라는 파일을 만들어서 push 하려고 하면, 에러납니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;원격 vs 로컬 내용이 다르다면 로컬 저장소에서 git push가 안됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;대충 git push 해버리면 꼬이기 때문에 미리 예방을 해주는 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 git pull 원격저장소주소를 해서 합쳐주면, 최신상태가 되기때문에 git push 가능합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결론 &amp;rarr; git pull 하고 git push 하면 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;근데 git pull 은 git fetch + git merge 축약어입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git fetch 는 원격 저장소에 있는 commit 중에 로컬에 없는 신규 commit을 가져오라는 뜻이고&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;git merge는 그걸 합치라는 뜻입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결론 &amp;rarr; git push 하기 전에 뭔가 에러가 나면 git pull 을 하면 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션에 있던걸 그대로 복사해서 올려놓는거라.. 좀 말투가 이상합니다.&lt;/p&gt;</description>
      <category>짧은 호흡/Git</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/15</guid>
      <comments>https://jiwoo.tistory.com/entry/%EA%B9%83#entry15comment</comments>
      <pubDate>Fri, 31 Jul 2026 23:07:49 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.13 (2) gemma/llama 챗봇 + 기사 번역 웹</title>
      <link>https://jiwoo.tistory.com/entry/Python-Ch13-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 여러가지 LLM 사용해서 비교답변 얻기 &amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jiwoo.tistory.com/entry/Python-Ch13-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(1)에서&lt;/a&gt; 이어서&amp;hellip;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구글의 gemma4(26년 4월 출시) 모델과, 메타의 llama3.1(24년 7월 출시)을 동시에 사용하여,&lt;/li&gt;
&lt;li&gt;사용자가 한번 메시지를 입력하면 두 LLM 으로 받은 답변을 비교할 수 있는 웹앱으로 바꿔보자.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.21.17.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0jF7X/dJMcaiqxzE3/yJBuNjfdmk2eFU9hsxexvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0jF7X/dJMcaiqxzE3/yJBuNjfdmk2eFU9hsxexvk/img.png&quot; data-alt=&quot;이런 모양으로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0jF7X/dJMcaiqxzE3/yJBuNjfdmk2eFU9hsxexvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0jF7X%2FdJMcaiqxzE3%2FyJBuNjfdmk2eFU9hsxexvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;329&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.21.17.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이런 모양으로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import streamlit as st

# 모듈 불러오기
from step_2_1 import (
    chat_message_llm,
    chat_message_user,
    init_session_state,
)

if __name__ == &quot;__main__&quot; :
    st.set_page_config( layout = &quot;wide&quot; )
    st.title(&quot;이지우의 챗봇(Gemma4 + Llama3.1)&quot;)

    init_session_state( dict ( gemma = [], llama = [] ) )

    gemma : list = st.session_state[ &quot;gemma&quot; ]
    llama : list = st.session_state[ &quot;llama&quot; ]

    for msg_gemma, msg_llama in zip( gemma, llama ) :

        if msg_gemma[&quot;role&quot;] == &quot;user&quot; : # 사용자 메시지는 1회만 출력 
            with st.chat_message( &quot;user&quot; ) :
                st.markdown( msg_gemma [ &quot;content&quot; ] )
        
        else : # LLM 메시지는 두개의 열로 나누어 각각 출력
            col_gemma, col_llama = st.columns(2)

            with col_gemma :
                with st.chat_message(&quot;Gemma&quot;) :
                    st.markdown(msg_gemma[&quot;content&quot;])
                
            with col_llama :
                with st.chat_message(&quot;Llama&quot;) :
                    st.markdown(msg_llama[&quot;content&quot;])
        
    if prompt := st.chat_input( &quot;여기에 대화를 입력하시옵소서...&quot; ) :

        msg_user = chat_message_user( prompt )

        gemma.append( msg_user )
        llama.append( msg_user )

        col_gemma, col_llama = st.columns(2)

        # gemma2 세션에 LLM 메시지
        with col_gemma :
            msg_gemma = chat_message_llm( &quot;Gemma&quot;, &quot;gemma4:e2b&quot;, gemma )
            gemma.append(msg_gemma)
        
        # llama3.1 세션에 LLM 메시지
        with col_llama :
            msg_llama = chat_message_llm( &quot;Llama&quot;, &quot;llama3.1:8b&quot;, llama )
            llama.append(msg_llama)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.59.08.png&quot; data-origin-width=&quot;2382&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lKFUm/dJMcadQfVm9/VcrS65a5A0j5WiUYure69K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lKFUm/dJMcadQfVm9/VcrS65a5A0j5WiUYure69K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lKFUm/dJMcadQfVm9/VcrS65a5A0j5WiUYure69K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlKFUm%2FdJMcadQfVm9%2FVcrS65a5A0j5WiUYure69K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2382&quot; height=&quot;2048&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.59.08.png&quot; data-origin-width=&quot;2382&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://namu.wiki/w/%EC%98%A4%ED%83%80%EB%8B%88%20vs%20%EC%9D%80%EA%B0%80%EB%88%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배트를 든 오타니 vs 은가누 대전&lt;/a&gt;에서 누가 이길지 물어봤는데, 확실히 최신모델인 Gemma4의 답변이 훨씬 일리있는 모습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 기사 번역 웹 앱 만들기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 크롤링(web crawling)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트의 링크를 따라 여러 페이지를 자동으로 방문하고, 페이지의 위치와 내용을 수집하는 과정.&lt;br /&gt;검색 엔진이 웹페이지를 발견하고 색인하기 위해 사용하는 방식이 대표적이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 스크레이핑(web scraping)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 페이지에서 필요한 정보를 선별적으로 추출. 예를 들어 상품명, 가격, 뉴스 제목, 리뷰 등의 데이터를 HTML에서 가져오는 작업이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;trafilatura 패키지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 크롤링과 웹 스크레이핑에 사용되며, 특히 HTML 문서에서 텍스트를 효율적으로 추출한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch_url() : 주어진 URL에 접속하여 HTML을 수집&lt;/li&gt;
&lt;li&gt;extract() : 주어진 HTML에서 텍스트를 추출&lt;/li&gt;
&lt;li&gt;extract_metadata() 주어진 HTML에서 제목, 작성자, 이미지 등 메타 데이터를 추출&lt;/li&gt;
&lt;li&gt;기사를 하나 골라주자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월드컵 시즌에 걸맞는 &lt;a href=&quot;https://www.koreaherald.com/article/10811183&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;월드컵 관련 기사&lt;/a&gt;를 가져왔다. 현대차 아틀라스 넘 멋있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; HTML에서 텍스트 추출 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import trafilatura

def extract_text_img( url : str ) -&amp;gt; tuple[ str, str ] :
    
    html = trafilatura.fetch_url(url)

    text = trafilatura.extract(
        html,
        output_format = &quot;markdown&quot;,
        include_comments = False, # 댓글은 제외할게요
    )

    img_url = trafilatura.extract_metadata(html).image

    return text, img_url

if __name__ == &quot;__main__&quot; :
    
    url = &quot;&amp;lt;https://www.koreaherald.com/article/10811183&amp;gt;&quot;

    text, img_url = extract_text_img( url )

    print(f&quot;{text=}&quot;)
    print(f&quot;{img_url=}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 URL에서 텍스트와 이미지 URL을 추출하는 extract_text_img 함수를 정의하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면,&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;text='Hyundai Motor Group is set to take full control of Boston Dynamics after SoftBank exercised a put option on its remaining 9.65 percent stake in the US robotics company, as the Korean auto group accelerates its push into physical artificial intelligence.\n\nHyundai said Thursday that SoftBank exercised the option under a 2020 agreement that allows the Japanese technology investor to sell its common shares in Boston Dynamics.\n\nBoston Dynamics is currently 28 percent owned by Hyundai Motor, 22.6 percent by Hyundai Motor Group Executive Chair Chung Euisun, 17.2 percent by Kia, 11.3 percent by Hyundai Mobis and 11.25 percent by Hyundai Glovis. Once the transaction is completed, the Hyundai affiliates and Chung will collectively own the company in full.\n\nThe group is reviewing the purchase price and how SoftBank&amp;rsquo;s shares will be divided among the existing shareholders.\n\nHyundai acquired an 80 percent controlling stake in Boston Dynamics from SoftBank for $880 million in 2021. SoftBank retained a 20 percent stake at the time, but its ownership was diluted to below 10 percent through subsequent capital increases.\n\n&amp;ldquo;As part of our long-term robotics strategy, we have been reviewing ways to expand our investment in Boston Dynamics,&amp;rdquo; a Hyundai Motor Group official said. &amp;ldquo;We will continue to strengthen the competitiveness of our robotics business and pursue greater synergies.&amp;rdquo;\n\nFull ownership would give Hyundai greater control over a key part of its physical AI strategy and allow it to more closely coordinate robot development, production and deployment with its manufacturing operations.\n\nThe group plans to introduce Boston Dynamics&amp;rsquo; Atlas humanoid robots at Hyundai Motor Group Metaplant America and Kia&amp;rsquo;s plant in Georgia, starting with tests on individual production processes.\n\nAtlas is scheduled to begin parts-sequencing work in 2028, with its role expanding to component assembly from 2030 once the robot&amp;rsquo;s operational reliability has been established.\n\nBoston Dynamics has also stepped up public demonstrations of Atlas, most recently at a FIFA World Cup match on July 5, where the humanoid delivered the match ball at halftime and mimicked goal celebrations by soccer players including Son Heung-min.\n\nHyundai aims to build annual production capacity for 30,000 robots by 2028 and deploy 25,000 units at Hyundai Motor and Kia plants before supplying outside customers.\n\nThe group also plans to establish production capacity for 350,000 actuators, which function as a robot&amp;rsquo;s joints and muscles.\n\nyeeun@heraldcorp.com'
img_url='&amp;lt;https://wimg.heraldcorp.com/news/cms/2026/07/16/news-p.v1.20260716.72e50109423041ac9618a062d8b8864e_T1.jpg&amp;gt;'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 추출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용을 생성형 AI를 사용해서 번역하고 streamlit 웹 앱에 출력해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 번역 프롬프트를 넣어준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;* system.txt 파일
당신은 많은 언어에 대한 전문 지식을 갖춘 매우 숙련된 번역가입니다.
내가 제공하는 텍스트의 언어를 식별한 다음, 원문의 의미, 어조, 뉘앙스를
유지하면서 한국어로 정확하게 번역하는 것이 당신의 임무입니다.
번역된 버전에서는 문법, 철자, 구두점, 줄바꿈, 마크다운 서식을 정확하게 유지해 주세요.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import ollama

from step_1_1 import IN_DIR
from step_3_1 import extract_text_img

SYSTEM_PROMPT = ( IN_DIR / &quot;system.txt&quot; ).read_text( encoding = &quot;utf-8&quot; )

url = &quot;&amp;lt;https://www.koreaherald.com/article/10811183&amp;gt;&quot;
text, img_url = extract_text_img(url)

msgs = [
    { &quot;role&quot; : &quot;system&quot; , &quot;content&quot; : SYSTEM_PROMPT },
    { &quot;role&quot; : &quot;user&quot;, &quot;content&quot; : text },
]

resp = ollama.chat(

    model = &quot;gemma4:e2b&quot;,
    messages = msgs,
    options = dict( temperature = 0.2 ),

)

msg_llm = dict(resp.get(&quot;message&quot;, {}))
msg_llm
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;{'role': 'assistant',
 'content': '현대차그룹이 한국 자동차 그룹이 물리적 인공지능(Physical AI) 분야로의 확장을 가속화함에 따라 소프트뱅크가 미국 로봇 회사인 보스턴 다이내믹스(Boston Dynamics)의 나머지 9.65% 지분에 대한 풋옵션을 행사함에 따라 해당 회사의 완전한 통제권을 확보하게 될 예정이다.\n\n현대차는 목요일에 소프트뱅크가 일본 기술 투자자가 보스턴 다이내믹스의 보통주를 매각할 수 있도록 허용하는 2020년 계약에 따라 해당 옵션을 행사했다고 밝혔다.\n\n현재 보스턴 다이내믹스는 현대모터스가 28%, 현대자동차그룹 회장인 정의선이 22.6%, 기아차는 17.2%, 현대모비스는 11.3%, 현대글로비스는 11.25%를 소유하고 있다. 이번 거래가 완료되면 현대차 계열사들과 정 회장은 회사 전체를 공동으로 소유하게 된다.\n\n그룹은 구매 가격과 소프트뱅크의 주식이 기존 주주들 사이에 어떻게 분배될 것인지 검토하고 있다.\n\n현대차는 2021년에 소프트뱅크로부터 보스턴 다이내믹스의 80% 지분을 8억 8천만 달러에 인수했다. 당시 소프트뱅크는 20%의 지분을 보유했지만, 이후 자본 증가를 통해 그 소유권은 10% 미만으로 희석되었다.\n\n현대차그룹 관계자는 &amp;ldquo;장기적인 로봇 전략의 일환으로 보스턴 다이내믹스에 대한 투자를 확대할 방안을 검토해 왔다&amp;rdquo;라며 &amp;ldquo;우리는 로봇 사업의 경쟁력을 계속 강화하고 더 큰 시너지를 추구할 것&amp;rdquo;이라고 말했다.\n\n완전한 소유권은 현대차에게 핵심적인 물리적 AI 전략에 대한 더 큰 통제권을 부여하고, 로봇 개발, 생산 및 배포를 제조 운영과 더욱 긴밀하게 조정할 수 있게 할 것이다.\n\n그룹은 현대차그룹 메타플랜트 아메리카와 조지아주의 기아 공장에 보스턴 다이내믹스의 휴머노이드 로봇인 아틀라스(Atlas)를 도입하여 개별 생산 공정에 대한 테스트부터 시작할 계획이다.\n\n아틀라스는 2028년에 부품 시퀀싱 작업을 시작할 예정이며, 로봇의 작동 신뢰성이 확립되면 2030년부터는 부품 조립 역할로 확대될 예정이다.\n\n보스턴 다이내믹스는 또한 아틀라스에 대한 공개 시연을 강화했는데, 가장 최근에는 7월 5일 FIFA 월드컵 경기에서 휴머노이드가 하프타임에 경기공을 전달하고 손흥민을 포함한 축구 선수들의 골 세리머니를 흉내 내는 모습을 보였다.\n\n현대차는 2028년까지 연간 3만 대의 로봇 생산 능력을 구축하고 현대차 및 기아 공장에서 2만 5천 대의 로봇을 배포한 후 외부 고객에게 공급하는 것을 목표로 한다.\n\n그룹은 또한 로봇의 관절과 근육 역할을 하는 액추에이터(actuators) 35만 개의 생산 능력을 구축할 계획이다.\n\nyeeun@heraldcorp.com',
 'thinking': 'Here\'s a thinking process to arrive at the desired translation:\n\n1.  **Analyze the Request:**\n    *   **Goal:** Translate the provided English text into Korean.\n    *   **Role:** Highly skilled translator specializing in many languages, focusing on accuracy, tone, nuance, and maintaining original formatting (grammar, punctuation, line breaks, markdown).\n    *   **Source Text Analysis (Tone &amp;amp; Context):** The text is a formal news report about a corporate transaction, strategic investment (AI/robotics), and future production plans involving major automotive groups (Hyundai Motor Group) and technology investors (SoftBank). The tone is informative, business-oriented, and forward-looking.\n\n2.  **Segment and Draft Translation (Sentence by Sentence):**\n\n    *   *Original:* Hyundai Motor Group is set to take full control of Boston Dynamics after SoftBank exercised a put option on its remaining 9.65 percent stake in the US robotics company, as the Korean auto group accelerates its push into physical artificial intelligence.\n        *   *Drafting Focus:* Complex sentence structure. Need to clearly link the cause (SoftBank action) and the effect (Hyundai gaining control) within the context of the broader strategy (physical AI).\n\n    *   *Original:* Hyundai said Thursday that SoftBank exercised the option under a 2020 agreement that allows the Japanese technology investor to sell its common shares in Boston Dynamics.\n        *   *Drafting Focus:* Straightforward reporting of the action and the legal basis (2020 agreement).\n\n    *   *Original:* Boston Dynamics is currently 28 percent owned by Hyundai Motor, 22.6 percent by Hyundai Motor Group Executive Chair Chung Euisun, 17.2 percent by Kia, 11.3 percent by Hyundai Mobis and 11.25 percent by Hyundai Glovis. Once the transaction is completed, the Hyundai affiliates and Chung will collectively own the company in full.\n        *   *Drafting Focus:* Listing ownership percentages accurately. Maintaining clarity regarding the final outcome (full collective ownership).\n\n    *   *Original:* The group is reviewing the purchase price and how SoftBank&amp;rsquo;s shares will be divided among the existing shareholders.\n        *   *Drafting Focus:* Standard business language for ongoing review.\n\n    *   *Original:* Hyundai acquired an 80 percent controlling stake in Boston Dynamics from SoftBank for $880 million in 2021. SoftBank retained a 20 percent stake at the time, but its ownership was diluted to below 10 percent through subsequent capital increases.\n        *   *Drafting Focus:* Historical transaction details. Ensure financial terms are precise.\n\n    *   *Original:* &amp;ldquo;As part of our long-term robotics strategy, we have been reviewing ways to expand our investment in Boston Dynamics,&amp;rdquo; a Hyundai Motor Group official said. &amp;ldquo;We will continue to strengthen the competitiveness of our robotics business and pursue greater synergies.&amp;rdquo;\n        *   *Drafting Focus:* Direct quote translation. Maintain the formal, strategic tone.\n\n    *   *Original:* Full ownership would give Hyundai greater control over a key part of its physical AI strategy and allow it to more closely coordinate robot development, production and deployment with its manufacturing operations.\n        *   *Drafting Focus:* Explaining the strategic benefit clearly.\n\n    *   *Original:* The group plans to introduce Boston Dynamics&amp;rsquo; Atlas humanoid robots at Hyundai Motor Group Metaplant America and Kia&amp;rsquo;s plant in Georgia, starting with tests on individual production processes.\n        *   *Drafting Focus:* Specific project details (Atlas, locations).\n\n    *   *Original:* Atlas is scheduled to begin parts-sequencing work in 2028, with its role expanding to component assembly from 2030 once the robot&amp;rsquo;s operational reliability has been established.\n        *   *Drafting Focus:* Timeline and phased development plan (2028, 2030).\n\n    *   *Original:* Boston Dynamics has also stepped up public demonstrations of Atlas, most recently at a FIFA World Cup match on July 5, where the humanoid delivered the match ball at halftime and mimicked goal celebrations by soccer players including Son Heung-min.\n        *   *Drafting Focus:* Anecdotal/PR detail. Needs lively translation while remaining factual.\n\n    *   *Original:* Hyundai aims to build annual production capacity for 30,000 robots by 2028 and deploy 25,000 units at Hyundai Motor and Kia plants before supplying outside customers.\n        *   *Drafting Focus:* Production targets (capacity vs. deployment).\n\n    *   *Original:* The group also plans to establish production capacity for 350,000 actuators, which function as a robot&amp;rsquo;s joints and muscles.\n        *   *Drafting Focus:* Technical detail (actuators).\n\n    *   *Original:* yeeun@heraldcorp.com\n        *   *Drafting Focus:* Signature/Email address (keep as is).\n\n3.  **Review and Refine:** (Self-Correction Phase)\n    *   Check flow: Does the Korean read naturally, or is it a literal, awkward translation? (The business terms must be idiomatic.)\n    *   Check terminology: &quot;Put option,&quot; &quot;stake,&quot; &quot;diluted,&quot; &quot;synergies,&quot; &quot;actuators&quot; must use standard industry Korean equivalents.\n    *   Check formatting: Ensure the paragraph breaks and structure match the original markdown style.\n\n4.  **Final Output Generation.** (This leads to the final provided translation.)',
 'images': None,
 'tool_name': None,
 'tool_calls': None}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lsquo;content&amp;rsquo; 항목에 변역된 결과가 잘 출력된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과를 웹에서 보기 좋게 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import ollama
import streamlit as st

from step_1_1 import IN_DIR
from step_3_1 import extract_text_img

SYSTEM_PROMPT = ( IN_DIR / &quot;system.txt&quot; ).read_text( encoding = &quot;utf-8&quot; )

if __name__ == &quot;__main__&quot; :
    st.set_page_config( layout = &quot;wide&quot; )
    st.title(&quot;기사를 번역해드립니다..&quot;)

    with st.form(&quot;form&quot;, border = False) :

        col_url, col_submit = st.columns([9,1])

        with col_url :
            url = st.text_input(
                &quot;text_input&quot;, # 위젯 이름
                placeholder = &quot;URL을 입력하세요!&quot;,
                label_visibility = &quot;collapsed&quot;, # 위젯 이름 출력 방지
            )
        
        with col_submit :
            submitted = st.form_submit_button(&quot;번역하기&quot;, use_container_width = True )
        
    if submitted :
        st.write(f&quot;(Source) {url}&quot;)
        text, img_url = extract_text_img(url)

        col_input, col_output = st.columns(2)

        with col_input :
            st.image( img_url, width = &quot;stretch&quot; )
            st.markdown(text)
        
        with col_output :
            with st.spinner(&quot;번역하는 중입니다...&quot;) :
                msgs = [
                    { &quot;role&quot; : &quot;system&quot;, &quot;content&quot; : SYSTEM_PROMPT },
                    { &quot;role&quot; : &quot;user&quot;, &quot;content&quot; : text },
                ]

                resp = ollama.chat(
                    model = &quot;gemma4:e2b&quot;,
                    messages = msgs,
                    options = dict( temperature = 0.2 ),
                )

                msg_llm = resp.get(&quot;message&quot;, {})
                st.markdown(msg_llm[&quot;content&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-19 오후 8.44.46.png&quot; data-origin-width=&quot;3208&quot; data-origin-height=&quot;2082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd6lOR/dJMcajwgpem/FcT9AU9ixgzN1PNEK5Wh90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd6lOR/dJMcajwgpem/FcT9AU9ixgzN1PNEK5Wh90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd6lOR/dJMcajwgpem/FcT9AU9ixgzN1PNEK5Wh90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd6lOR%2FdJMcajwgpem%2FFcT9AU9ixgzN1PNEK5Wh90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;422&quot; data-filename=&quot;스크린샷 2026-07-19 오후 8.44.46.png&quot; data-origin-width=&quot;3208&quot; data-origin-height=&quot;2082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-19 오후 8.45.03.png&quot; data-origin-width=&quot;3208&quot; data-origin-height=&quot;2956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGdQPq/dJMcajwgpeA/SlCfrzRac0tR3zg0biPvd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGdQPq/dJMcajwgpeA/SlCfrzRac0tR3zg0biPvd0/img.png&quot; data-alt=&quot;와우!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGdQPq/dJMcajwgpeA/SlCfrzRac0tR3zg0biPvd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGdQPq%2FdJMcajwgpeA%2FSlCfrzRac0tR3zg0biPvd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;651&quot; data-filename=&quot;스크린샷 2026-07-19 오후 8.45.03.png&quot; data-origin-width=&quot;3208&quot; data-origin-height=&quot;2956&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;와우!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/12</guid>
      <comments>https://jiwoo.tistory.com/entry/Python-Ch13-2#entry12comment</comments>
      <pubDate>Thu, 16 Jul 2026 10:31:14 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.13 (1) gemma2로 챗봇 웹 앱 만들기</title>
      <link>https://jiwoo.tistory.com/entry/Python-Ch13-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; ollama &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ollama&lt;/b&gt;는 오픈소스 AI 모델을 관리할 수 있는 도구이다&lt;/li&gt;
&lt;li&gt;CLI 기반 실행방식으로 다양한 오픈소스 LLM 을 제공한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;pip install -U ollama streamlit trafilatura
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://ollama.com&quot;&gt;ollama.com&lt;/a&gt;에서 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 챗봇을 사용해서 한국어를 영어로 번역할 것이고, LLM 모델 중 구글의 Gemma2를 사용하겠다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ollama run gemma2:9b
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에서 gemma2 는 모델명이고, 9b 는 매개변수 9 billion(90억)개를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-16 오후 4.23.28.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EQmfE/dJMcacRkYBY/Vm8kfqCDlzWPsTPIvGhoD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EQmfE/dJMcacRkYBY/Vm8kfqCDlzWPsTPIvGhoD1/img.png&quot; data-alt=&quot;&amp;quot;학점 잘 받게 해주세요&amp;quot;를 영어로 잘 번역한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EQmfE/dJMcacRkYBY/Vm8kfqCDlzWPsTPIvGhoD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEQmfE%2FdJMcacRkYBY%2FVm8kfqCDlzWPsTPIvGhoD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;428&quot; data-filename=&quot;스크린샷 2026-07-16 오후 4.23.28.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;학점 잘 받게 해주세요&quot;를 영어로 잘 번역한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 대화 문맥을 이해하는지 확인 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫번째 대화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import ollama

resp1 = ollama.chat(
    model = &quot;gemma2:9b&quot;,
    messages = [{ &quot;role&quot; : &quot;user&quot;,  &quot;content&quot; : &quot;내 이름은 파이썬이야!&quot;}],
)

dict(resp1[&quot;message&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 대화형 창에서 실행하면,&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;{'role': 'assistant',
 'content': '안녕하세요, 파이썬님!  \n\n멋진 이름을 가지고 계시네요. 파이썬 언어를 좋아하신다면 저에게 무엇을 알려줄 수 있나요?   Python의 흥미로운 부분이나 특별히 배우고 싶은 내용이 있으신가요?  저도 당신과 함께 즐겁게 이야기 나누고 싶습니다!  ',
 'thinking': None,
 'images': None,
 'tool_name': None,
 'tool_calls': None}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 답변을 얻을 수 있다. 챗봇이 적절한 응답을 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두번째 대화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resp2 = ollama.chat(
    model = &quot;gemma2:9b&quot;,
    messages = [{ &quot;role&quot;:&quot;user&quot;, &quot;content&quot;:&quot;내 이름이 뭐라고 했지?&quot;}]
)

dict(resp2[&quot;message&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 대화시도에 이어서, resp2를 추가해서 내 첫번재 대화에서 했던 내 이름을 기억하는지 물었다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;{'role': 'assistant',
 'content': '네가 나에게 너의 이름을 말해주었는지 기억하지 않습니다. 그래서 &quot;예&quot; 또는 &quot;아니오&quot;로 대답할 수 없습니다. 내 이름은 Gemma입니다.  ',
 'thinking': None,
 'images': None,
 'tool_name': None,
 'tool_calls': None}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 직전의 대화에서 내 이름이 &amp;ldquo;파이썬&amp;rdquo;이라고 알려주었는데도 기억을 못하는 모습이다. 아직 내 일자리까지는 위협받지 않는 모습이다. 이제, 이전 대화내용을 기억하여 문맥을 형성해서 파악할 수 있도록 만들어보자 = 일자리를 위협하는 AI로 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;append() 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 응답을 추가(append)하는 방식으로 문맥을 쌓으면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import ollama

msgs = [{ &quot;role&quot;:&quot;user&quot;, &quot;content&quot;:&quot;내 이름은 파이썬이야!&quot;}]

resp1 = ollama.chat(model=&quot;gemma2:9b&quot;, messages=msgs)
msgs.append(dict(resp1[&quot;message&quot;]))

msgs.append({&quot;role&quot;:&quot;user&quot;, &quot;content&quot;:&quot;내 이름이 뭐라고 했었지?&quot;})
resp2 = ollama.chat(model=&quot;gemma2:9b&quot;, messages=msgs)
msgs.append(dict(resp2[&quot;message&quot;]))

msgs
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼, 기존 메시지를 초기화하는 방식이 아니라, 추가하는 방식으로 하면.&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;[{'role': 'user', 'content': '내 이름은 파이썬이야!'},
 {'role': 'assistant',
  'content': '안녕하세요, 파이썬! 만나서 반가워요.   \n\n무엇을 도와드릴까요? 코딩할 준비 되셨나요? 아니면 저에게 질문하실 건가요?    \n\n\n\n',
  'thinking': None,
  'images': None,
  'tool_name': None,
  'tool_calls': None},
 {'role': 'user', 'content': '내 이름이 뭐라고 했었지?'},
 {'role': 'assistant',
  'content': '네! 당신의 이름은 **파이썬** 이라고 말씀하셨어요.  \n\n반갑습니다, 파이썬!  무엇을 도와드릴까요?    \n\n\n',
  'thinking': None,
  'images': None,
  'tool_name': None,
  'tool_calls': None}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 대화에서도 첫번째 대화 내용을 기억하는 걸 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 챗봇 웹 앱 만들기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM 기반 챗봇 웹 앱을 구현&lt;/li&gt;
&lt;li&gt;streamlit 패키지에서 제공하는 GUI 위젯 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; streamlit 패키지의 채팅 위젯 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;st.chat_message() : 앱에 채팅 메시지 컨테이너를 삽입&lt;/li&gt;
&lt;li&gt;st.chat_input() : 사용자가 채팅 메시지를 입력할 수 있는 입력 위젯을 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;import streamlit as st

with st.chat_message(&quot;user&quot;) :
    st.write(&quot;hello&quot;)

prompt = st.chat_input(&quot;Say something&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gemma2를 이용한 챗봇을 만들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;import ollama
import streamlit as st

def init_session_state( keys : dict ) :

    for key, value in keys.items() :

        if key not in st.session_state :    # key가 세션 저장소에 없는 경우
            st.session_state[key] = value   # 새로운 세션 생성

def chat_message_user( prompt : str ) -&amp;gt; dict :

    with st.chat_message(&quot;user&quot;) : # 사용자 메시지 출력
        st.markdown(prompt)
        return dict( role = &quot;user&quot;, content = prompt )

def chat_message_llm( role : str , model : str , messages : list ) -&amp;gt; dict :

    with st.chat_message(role) : # LLM 메시지 출력

        with st.spinner(&quot;대화를 생성하는 중입니다...&quot;) :
            resp = ollama.chat( model = model , messages = messages )
            msg_llm = resp.get(&quot;message&quot;, {})
            st.markdown( msg_llm[&quot;content&quot;])

            return msg_llm

if __name__ == &quot;__main__&quot; :
    
    st.set_page_config( layout = &quot;wide&quot; )
    
    st.title(&quot;이지우의 챗봇입니다&quot;)

    init_session_state( dict( msgs = [] ) )
    msgs : list = st.session_state[&quot;msgs&quot;]

    # msgs에 저장된 모든 메시지 기록을 하나씩 반복 출력!
    for row in msgs : 

        with st.chat_message( row [ &quot;role&quot; ] ) :
            st.markdown( row[&quot;content&quot;])
    
    if prompt := st.chat_input( &quot;여기에 대화를 입력하시옵소서..&quot; ) :
        
        msg_user = chat_message_user( prompt )
        msgs.append( msg_user ) # 사용자 메시지 추가

        msg_llm = chat_message_llm( &quot;assistant&quot;, &quot;gemma2:9b&quot;, msgs )
        msgs.append(msg_llm) # LLM 메시지 추가

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.02.37.png&quot; data-origin-width=&quot;2238&quot; data-origin-height=&quot;2002&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6tFiA/dJMcadQew6c/WYg5IAeCOJ1UHANGeS4rak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6tFiA/dJMcadQew6c/WYg5IAeCOJ1UHANGeS4rak/img.png&quot; data-alt=&quot;손흥민의 업적을 잘 설명하는 챗봇이 완성되었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6tFiA/dJMcadQew6c/WYg5IAeCOJ1UHANGeS4rak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6tFiA%2FdJMcadQew6c%2FWYg5IAeCOJ1UHANGeS4rak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;649&quot; data-filename=&quot;스크린샷 2026-07-16 오후 5.02.37.png&quot; data-origin-width=&quot;2238&quot; data-origin-height=&quot;2002&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;손흥민의 업적을 잘 설명하는 챗봇이 완성되었다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/11</guid>
      <comments>https://jiwoo.tistory.com/entry/Python-Ch13-1#entry11comment</comments>
      <pubDate>Mon, 13 Jul 2026 22:45:38 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.8 (2) 키워드별 경쟁강도 분석 웹 앱</title>
      <link>https://jiwoo.tistory.com/entry/Python-Ch8-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;(1)에 이어서..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 키워드별 상품 개수 수집 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키워드별 상품 개수를 CSV 파일로 저장하는 함수 shop_cnt_to_cvs() 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import time
from pathlib import Path
import pandas as pd
from tqdm import tqdm
from step_1_1 import OUT_DIR
from step_1_2 import query_naver_shop
from step_2_2 import OUT_2_2

OUT_2_3 = OUT_DIR / f&quot;{Path(__file__).stem}.csv&quot;

def shop_cnt_to_csv() :

    df_raw = pd.read_csv( OUT_2_2 ) # CSV 파일에서 데이터 프레임 생성
    kwd_list = df_raw[&quot;키워드&quot;].to_list() # 키워드 열을 리스트 타입으로 변환
    item_cnt = []

    with tqdm( total = len(kwd_list)) as pbar :
        
        for kwd in kwd_list :

            resp = query_naver_shop( query = kwd )

            total = resp.get( &quot;total&quot;, 0 )
            item_cnt.append( { &quot;키워드&quot; : kwd , &quot;상품수&quot; : total } )

            time.sleep(0.5) # 0.5 초간 일시정지
            pbar.set_description(kwd) # 진행 표시줄 메시지 수정
            pbar.update() # 진행 표시줄 업데이트

    df_raw = pd.DataFrame( item_cnt )
    df_raw.to_csv( OUT_2_3 , index = False )

if __name__ == &quot;__main__&quot; :
    shop_cnt_to_csv()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.23.34.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgw9Jy/dJMcagzrq5H/8ueXY2Hdc33Kcug9ynzFXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgw9Jy/dJMcagzrq5H/8ueXY2Hdc33Kcug9ynzFXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgw9Jy/dJMcagzrq5H/8ueXY2Hdc33Kcug9ynzFXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgw9Jy%2FdJMcagzrq5H%2F8ueXY2Hdc33Kcug9ynzFXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.23.34.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 키워드별 경쟁 강도 분석 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키워드별 상품 개수 데이터에 키워드별 검색수 데이터를 결합하여 경쟁 강도를 분석할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from pathlib import Path
import pandas as pd
from step_1_1 import OUT_DIR
from step_2_2 import OUT_2_2
from step_2_3 import OUT_2_3

OUT_2_4 = OUT_DIR / f&quot;{Path(__file__).stem}.csv&quot;

def comp_lev_to_csv() :
    
    df_kwd = pd.read_csv( OUT_2_2 ) # 정제 처리된 연관 키워드
    df_shop = pd.read_csv( OUT_2_3 ) # 키워드별 상품 개수

    df_merged = pd.merge( df_kwd , df_shop , left_on = &quot;키워드&quot; , right_on = &quot;키워드&quot; )
    
    df_merged[&quot;경쟁강도&quot;] = ( df_merged[&quot;상품수&quot;] / df_merged[&quot;검색수M&quot;] ).round(6)

    df_filtered = df_merged.filter([&quot;키워드&quot;, &quot;검색수M&quot;, &quot;클릭수M&quot;, &quot;클릭률M&quot;, &quot;상품수&quot;, &quot;경쟁강도&quot;])

    df_sorted = df_filtered.sort_values( &quot;경쟁강도&quot;, ascending=False )
    df_sorted.to_csv( OUT_2_4 , index = False )

if __name__ == &quot;__main__&quot; :
    comp_lev_to_csv()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.31.13.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1b61h/dJMcage9r90/1Iov74Shj0KwyrsXWXxweK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1b61h/dJMcage9r90/1Iov74Shj0KwyrsXWXxweK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1b61h/dJMcage9r90/1Iov74Shj0KwyrsXWXxweK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1b61h%2FdJMcage9r90%2F1Iov74Shj0KwyrsXWXxweK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;498&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.31.13.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키워드별로 경쟁강도가 내림차순으로 정렬된 csv 파일&lt;/li&gt;
&lt;li&gt;&amp;lsquo;나이키신발&amp;rsquo; 키워드의 경쟁강도가 136으로 매우 높은 수준임을 알 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 연관 키워드 경쟁 강도 분석 웹 앱 만들기 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import pandas as pd
import streamlit as st

from step_2_1 import rel_kwd_to_csv
from step_2_2 import data_cleaning
from step_2_3 import shop_cnt_to_csv
from step_2_4 import OUT_2_4, comp_lev_to_csv

def init_page() :
    
    st.set_page_config( layout = &quot;wide&quot; )
    st.header( &quot;jiwoo's 연관키워드 경쟁강도 분석&quot; )

    if &quot;keywords&quot; not in st.session_state :
        st.session_state[ &quot;keywords&quot; ] = &quot;&quot;
    
    with st.form( key = &quot;my_form&quot; , border = False ) :
        
        col_1, col_2 = st.columns( [ 3 , 1 ] ) # 3 : 1 비율의 두개의 열 위젯 생성

        with col_1 :
            st.text_input( &quot;키워드&quot;, key = &quot;keywords&quot; , label_visibility = &quot;collapsed&quot; )
        
        with col_2 :
            st.form_submit_button( label = &quot;분석하기&quot; , use_container_width = True )
        

def analyze_keywords ( keywords : str = None , event : int = None ) :
    
    rel_kwd_to_csv ( keywords = keywords , event = event )

    data_cleaning() # 데이터 정제
    shop_cnt_to_csv() # 키워드별 상품 개수 수집
    comp_lev_to_csv() # 키워드별 경쟁강도 분석

def print_dataframe( keywords : str = None ) :

    if keywords :

        with st.spinner( &quot;잠시만 기다려주세요...&quot; ) :
            analyze_keywords( st.session_state[ &quot;keywords&quot; ] ) # 연관 키워드 분석
        
        df_result = pd.read_csv( OUT_2_4 ) # 분석 결과를 데이터 프레임으로 변환
        st.dataframe( df_result , use_container_width = True ) # 데이터 프레임 출력

if __name__ == &quot;__main__&quot; :
    init_page()
    keywords = st.session_state[&quot;keywords&quot;]
    print_dataframe( keywords = keywords ) # 데이터 프레임 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;streamlit run '파일경로'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LmSC5/dJMb9979rKA/Zul7QFEkTIgG2KNdNu1Oo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LmSC5/dJMb9979rKA/Zul7QFEkTIgG2KNdNu1Oo1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;1634&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.52.28.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LmSC5/dJMb9979rKA/Zul7QFEkTIgG2KNdNu1Oo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLmSC5%2FdJMb9979rKA%2FZul7QFEkTIgG2KNdNu1Oo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2056&quot; height=&quot;1634&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LpzUh/dJMb9979rLz/dwKVrGyfa6gzS7iWEKbhU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LpzUh/dJMb9979rLz/dwKVrGyfa6gzS7iWEKbhU0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;1634&quot; data-filename=&quot;스크린샷 2026-07-14 오후 12.52.34.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LpzUh/dJMb9979rLz/dwKVrGyfa6gzS7iWEKbhU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLpzUh%2FdJMb9979rLz%2FdwKVrGyfa6gzS7iWEKbhU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2056&quot; height=&quot;1634&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-14 오후 2.24.51.png&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;1844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQLoPG/dJMcafHnhz6/6DEinEjzbqtDczaRHXSnPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQLoPG/dJMcafHnhz6/6DEinEjzbqtDczaRHXSnPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQLoPG/dJMcafHnhz6/6DEinEjzbqtDczaRHXSnPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQLoPG%2FdJMcafHnhz6%2F6DEinEjzbqtDczaRHXSnPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;416&quot; data-filename=&quot;스크린샷 2026-07-14 오후 2.24.51.png&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;1844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 데이터프레임 시각화 하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 앱에서 데이터프레임의 인덱스 표시를 숨기고, 숫자 표시 부분을 막대 그래프로 바꿔서&lt;/li&gt;
&lt;li&gt;수치가 전체 데이터에서 어느정도를 차지하는지 쉽게 확인할 수 있도록 시각화 하기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import pandas as pd
import streamlit as st

from streamlit.column_config import NumberColumn, ProgressColumn
from step_2_4 import OUT_2_4
from step_3_1 import analyze_keywords, init_page

def print_dataframe_with_style( keywords : str = None , event : int = None ) :

    if keywords or event is not None :

        with st.spinner( &quot;잠시만 기다려주세요...&quot; ) :
            analyze_keywords ( keywords = keywords , event = event ) # 연관 키워드 분석
        
        df_result = pd.read_csv( OUT_2_4 ) # 분석 결과를 데이터프레임으로 변환
        max_values = df_result.max().to_dict() # 열별 최댓값을 딕셔너리로 변환

        st.dataframe(

            df_result ,
            use_container_width = True , # 너비 최대화
            hide_index = True , # 데이터프레임 인덱스 숨김

            column_config = { # 열별 스타일 설정

                &quot;검색수M&quot; : ProgressColumn( width = &quot;small&quot; ,
                                          max_value = max_values.get( &quot;검색수M&quot; , 1 ) ,
                                          format = &quot;&quot; ) ,

                &quot;클릭수M&quot; : ProgressColumn( width = &quot;small&quot; ,
                                          max_value = max_values.get( &quot;클릭수M&quot; , 1 ) ,
                                          format = &quot;&quot; ) ,
            
                &quot;클릭률M&quot; : ProgressColumn( width = &quot;small&quot; ,
                                          max_value = max_values.get( &quot;클릭률M&quot; , 1 ) ,
                                          format = &quot;&quot; ) ,

                &quot;상품수&quot; : ProgressColumn( width = &quot;small&quot; ,
                                          max_value = max_values.get( &quot;상품수&quot; , 1 ) ,
                                          format = &quot;&quot; ) ,
                &quot;경쟁강도&quot; : NumberColumn( format = &quot;%.2f&quot; ) ,

            },
        )

if __name__ == &quot;__main__&quot; :
    
    init_page()
    keywords = st.session_state[&quot;keywords&quot;]
    print_dataframe_with_style( keywords = keywords )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-14 오후 2.24.11.png&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;1844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z3grv/dJMcaasvTyp/6GNkKCegKASzeV9v6fW6b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z3grv/dJMcaasvTyp/6GNkKCegKASzeV9v6fW6b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z3grv/dJMcaasvTyp/6GNkKCegKASzeV9v6fW6b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ3grv%2FdJMcaasvTyp%2F6GNkKCegKASzeV9v6fW6b0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;446&quot; data-filename=&quot;스크린샷 2026-07-14 오후 2.24.11.png&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;1844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;열별로 최댓값 대비 실제 결괏값을 진행 표시줄로 시각화해서 경쟁강도를 직관적으로 파악할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/10</guid>
      <comments>https://jiwoo.tistory.com/entry/Python-Ch8-2#entry10comment</comments>
      <pubDate>Fri, 10 Jul 2026 22:43:42 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.8 (1) 쇼핑 키워드 데이터 수집 + 정제</title>
      <link>https://jiwoo.tistory.com/entry/Python-Ch8-1</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 네이버 API &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 서비스 API : 각종 네이버 검색, 로그인, 데이터랩 등&lt;/li&gt;
&lt;li&gt;네이버 검색광고 API : 키워드 도구, 광고 캠페인 관리 등 검색광고 특화&lt;/li&gt;
&lt;li&gt;네이버 클라우드 API : 지도, 번역, 문자 인식, 음성 인식, 음성 합성 등 최신 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 외부 패키지 설치 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;pip install -U datakart pandas streamlit tqdm
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;datakart(데이터 수집), pandas(데이터 분석), streamlit(웹 앱), tqdm(진행 표시줄)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.naver.com/main/%20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;네이버 개발자 센터&lt;/a&gt;에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색, 데이터랩(검색어트렌드, 쇼핑인사이트) API&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;선택해서 키를 생성한다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 네이버 쇼핑 상품 검색 함수 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import json
from pathlib import Path
from datakart import Naver
from step_1_1 import OUT_DIR

def query_naver_shop( query : str, display : int = 1 ) -&amp;gt; dict :
    
    NAVER_KEY = &quot; &quot; # Cient ID
    NAVER_SEC = &quot; &quot; # Client Secret

    naver = Naver( NAVER_KEY, NAVER_SEC )

    return naver.shop( query = query, display = display )

if __name__ == &quot;__main__&quot; :

    query = &quot;월드컵&quot; # 검색 키워드

    resp = query_naver_shop(query) # 네이버 쇼핑 상품 검색

    with open( OUT_DIR / f&quot;{Path(__file__).stem}.json&quot;, &quot;w&quot;, encoding = &quot;utf-8&quot;) as fp :
        json.dump( resp, fp, ensure_ascii = False, indent = 2 ) # JSON 으로 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;월드컵&amp;rdquo; 키워드로 등록된 상품 정보들을 아래 json 파일로 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;lastBuildDate&quot;: &quot;Fri, 10 Jul 2026 17:14:10 +0900&quot;,
  &quot;total&quot;: 296906,
  &quot;start&quot;: 1,
  &quot;display&quot;: 1,
  &quot;items&quot;: [
    {
      &quot;title&quot;: &quot;월드컵 기어2 남성 여성 다이얼 신발 운동화 러닝화 워킹화 조깅화 헬스화 트레킹화 등산화&quot;,
      &quot;link&quot;: &quot;&amp;lt;https://smartstore.naver.com/main/products/7382530568&amp;gt;&quot;,
      &quot;image&quot;: &quot;&amp;lt;https://shopping-phinf.pstatic.net/main_8492703/84927030890.5.jpg&amp;gt;&quot;,
      &quot;lprice&quot;: &quot;46000&quot;,
      &quot;hprice&quot;: &quot;&quot;,
      &quot;mallName&quot;: &quot;월드컵공식몰&quot;,
      &quot;productId&quot;: &quot;84927030890&quot;,
      &quot;productType&quot;: &quot;2&quot;,
      &quot;brand&quot;: &quot;월드컵&quot;,
      &quot;maker&quot;: &quot;엘유티&quot;,
      &quot;category1&quot;: &quot;패션잡화&quot;,
      &quot;category2&quot;: &quot;남성신발&quot;,
      &quot;category3&quot;: &quot;스니커즈/운동화&quot;,
      &quot;category4&quot;: &quot;러닝화&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;total : 296906 개의 상품이 등록되어 있고,&lt;/li&gt;
&lt;li&gt;items : 리스트로 상품 상세 정보가 저장되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 연관 키워드 검색하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 검색광고 API 를 사용하여 주어진 키워드에 대한 연관 키워드 검색을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;네이버 검색광고 API 는 &lt;a href=&quot;http://%20https://searchad.naver.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 받을 수 있음.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import json
from pathlib import Path
from datakart import NaverAd
from step_1_1 import OUT_DIR

def query_keywords_tool( keywords : str, event : int = None ) -&amp;gt; list :
    AD_KEY = &quot;&quot; # 엑세스 라이선스
    AD_SEC = &quot; &quot; # 비밀키
    AD_CUST_ID = &quot; &quot; # customer id

    naver_ad = NaverAd( AD_KEY, AD_SEC, AD_CUST_ID )

    resp = naver_ad.keywords_tool( keywords = keywords, event = event, show_detail = True )

    return resp.get(&quot;keywordList&quot;, [])

if __name__ == &quot;__main__&quot; :
    
    keywords = &quot;월드컵&quot; # 검색 키워드

    resp = query_keywords_tool( keywords )

    with open ( OUT_DIR / f&quot;{Path(__file__).stem}.json&quot;, &quot;w&quot;, encoding = &quot;utf-8&quot;) as fp :
        json.dump( resp, fp, ensure_ascii = False, indent = 2 )
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;월드컵 키워드로 연관 키워드, Pc/모바일 환경에서 각각 월간 검색 수, 평균 클릭수, 클릭률, 노출 광고 수 등 여러가지 데이터를 뽑아낼 수 있다!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[
  {
    &quot;relKeyword&quot;: &quot;월드컵&quot;,
    &quot;monthlyPcQcCnt&quot;: 4943600,
    &quot;monthlyMobileQcCnt&quot;: 18546200,
    &quot;plAvgDepth&quot;: 4,
    &quot;compIdx&quot;: &quot;중간&quot;,
    &quot;monthlyAvePcClkCnt&quot;: 52.3,
    &quot;monthlyAveMobileClkCnt&quot;: 783.6,
    &quot;monthlyAvePcCtr&quot;: 0.01,
    &quot;monthlyAveMobileCtr&quot;: 0.01
  },
  {
    &quot;relKeyword&quot;: &quot;월드컵운동화&quot;,
    &quot;monthlyPcQcCnt&quot;: 570,
    &quot;monthlyMobileQcCnt&quot;: 2360,
    &quot;plAvgDepth&quot;: 8,
    &quot;compIdx&quot;: &quot;중간&quot;,
    &quot;monthlyAvePcClkCnt&quot;: 20.1,
    &quot;monthlyAveMobileClkCnt&quot;: 87.8,
    &quot;monthlyAvePcCtr&quot;: 3.65,
    &quot;monthlyAveMobileCtr&quot;: 3.83
  },
  {
    &quot;relKeyword&quot;: &quot;축구&quot;,
    &quot;monthlyPcQcCnt&quot;: 1374700,
    &quot;monthlyMobileQcCnt&quot;: 5559900,
    &quot;plAvgDepth&quot;: 3,
    &quot;compIdx&quot;: &quot;높음&quot;,
    &quot;monthlyAvePcClkCnt&quot;: 6.7,
    &quot;monthlyAveMobileClkCnt&quot;: 120.3,
    &quot;monthlyAvePcCtr&quot;: 0.01,
    &quot;monthlyAveMobileCtr&quot;: 0.01
  },
# ... 
# 더 있었는데, 너무 길어지는 관계로 블로그에서는 생략하겠읍니다
]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;monthlyPc(Mobile)QcCnt 로 월간 검색수를 볼 수 있다.&lt;/li&gt;
&lt;li&gt;montlyAveClkCtr로 월 평균 클릭률을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;확실히 월드컵 시즌이라 월드컵, 축구 검색수가 엄청나다. &amp;lsquo;월드컵&amp;rsquo; 키워드로 PC와 모바일 검색수만 단순하게 합치면 2천만이 훨씬 넘어간다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;좀 특이한 점이 있다. &amp;lsquo;월드컵&amp;rsquo; 자체의 검색량은 매우 큰데, 클릭률은 PC 와 모바일 모두 0.01로 매우 낮았다. 이건 상품검색에서 얻은 데이터이기 때문에 그럴 가능성이 있다. 단순히 상품이 아니라 &amp;lsquo;월드컵&amp;rsquo;으로 검색했다는 건 상품 구매보다는 정보 탐색에 목적을 뒀을 가능성이 더 클 것이기 때문에. 구매로 연결되기에는 검색 의도가 너무 넓다는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;반면에, &amp;lsquo;월드컵 운동화&amp;rsquo; 자체 검색량은 적지만 클릭률은 PC 와 모바일 모두 높은 편이다. &amp;lsquo;운동화&amp;rsquo;로 타겟해서 검색한 것이 더 구매 의도가 있는 사용자의 검색 방식일 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;생각보다 결과도 정직하게(?) 나오고.. 실제로 이런 데이터를 볼 수 있는게 신기하다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 연관 키워드 경쟁 강도 분석하기 &amp;gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;키워드 경쟁 강도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 상품의 수요와 공급을 비율로 표시한 것.&lt;/li&gt;
&lt;li&gt;네이버쇼핑에서 검색횟수를 수요로 보고 상품 개수를 공급으로 본다.&lt;/li&gt;
&lt;li&gt;경쟁강도 = 상품개수(공급) / 검색횟수(수요)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 먼저, 연관키워드 데이터를 CSV로 저장 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pathlib import Path
import pandas as pd
from step_1_1 import OUT_DIR
from step_1_3 import query_keywords_tool

OUT_2_1 = OUT_DIR / f&quot;{Path(__file__).stem}.csv&quot;

def rel_kwd_to_csv( keywords : str = None, event : int = None ) :

    resp = query_keywords_tool( keywords=keywords, event=event )

    df_raw = pd.DataFrame(resp)

    df_raw.columns = [
    &quot;키워드&quot;,
    &quot;검색수PC&quot;,
    &quot;검색수M&quot;,
    &quot;광고수&quot;,
    &quot;경쟁정도&quot;,
    &quot;클릭수PC&quot;,
    &quot;클릭수M&quot;,
    &quot;클릭률PC&quot;,
    &quot;클릭률M&quot;,
]

    df_raw.to_csv(OUT_2_1, index = False)

if __name__ == &quot;__main__&quot; :
    rel_kwd_to_csv(&quot;나이키&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;나이키&amp;rdquo; 키워드에 대한 연관 키워드 데이터를 저장한 CSV 파일을 생성한다.&lt;/li&gt;
&lt;li&gt;결과는 아래.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;키워드,검색수PC,검색수M,광고수,경쟁정도,클릭수PC,클릭수M,클릭률PC,클릭률M
나이키,202400,652800,10,높음,160.7,2397.6,0.09,0.4
나이키런닝화,7840,51100,10,높음,37.1,930.4,0.51,1.96
슈마커나이키,&amp;lt; 10,40,7,중간,0.0,0.3,0.0,0.63
나이키온라인,680,600,10,높음,40.4,87.8,6.3,15.3
나이키스토어,450,1200,10,높음,24.4,153.0,5.69,13.71
나이키몰,390,1240,10,높음,23.5,171.6,6.24,14.99
나이키마노아레더,20,230,7,중간,0.9,6.3,4.26,2.78
TC7900,70,330,10,높음,2.0,14.3,2.94,4.75
자라세일,34500,260000,5,중간,148.6,2223.7,0.44,0.88
나이키플렉스러너,110,1220,8,높음,1.4,35.3,1.23,3.04
나이키공식홈페이지,440,3400,10,높음,23.5,503.6,5.53,15.9
나이키정품,20,170,10,높음,1.2,13.0,4.14,8.08
...
# 너무 길어서 생략&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt; 연관 키워드 데이터 정제하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 결과는 생략한 결과이지만, 실제로는 1200줄의 연관 키워드 데이터가 저장되었다.&lt;/li&gt;
&lt;li&gt;검색수가 10건 미만이면 문자열 &amp;ldquo; &amp;lt; 10 &amp;ldquo; 으로 표현된다.&lt;/li&gt;
&lt;li&gt;검색수가 숫자로만 구성된 것이 아니고 이처럼 문자가 섞여있어 데이터 분석이 어려우므로&lt;/li&gt;
&lt;li&gt;검색수가 10 미만인 = 즉, 검색수에 문자열이 포함된 행을 삭제한다.&lt;/li&gt;
&lt;li&gt;또, 모바일 검색수가 최소 1만건 + 클릭률 최소 1% 이상인 데이터만 필터링한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from pathlib import Path
import pandas as pd
from step_1_1 import OUT_DIR
from step_2_1 import OUT_2_1

OUT_2_2 = OUT_DIR / f&quot;{Path(__file__).stem}.csv&quot;

def data_cleaning() :
    
    df_raw = pd.read_csv(OUT_2_1, dtype = &quot;string&quot;)

    f_pc_cnt = df_raw[&quot;검색수PC&quot;].str.contains(&quot;&amp;lt;&quot;) # PC 검색수에 문자열 &amp;lt; 포함여부
    f_mo_cnt = df_raw[&quot;검색수M&quot;].str.contains(&quot;&amp;lt;&quot;)  # 모바일 검색수에 문자열 &amp;lt; 포함여부

    df_sliced = df_raw.loc[(~f_pc_cnt) &amp;amp; (~f_mo_cnt)]

    df_sliced = df_sliced.astype({&quot;검색수M&quot; : int, &quot;클릭률M&quot; : float})

    f_mo_cnt_cond = df_sliced[&quot;검색수M&quot;] &amp;gt;= 10_000 # 검색수 조건
    f_mo_rate_cond = df_sliced[&quot;클릭률M&quot;] &amp;gt;= 1.0 # 클릭률 조건

    df_cond = df_sliced.loc[(f_mo_cnt_cond) &amp;amp; (f_mo_rate_cond)]

    df_sorted = df_cond.sort_values([&quot;검색수M&quot;], ascending=[False])
    df_sorted.to_csv(OUT_2_2, index = False)

if __name__ == &quot;__main__&quot; :
    data_cleaning()
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 ..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 오후 9.45.00.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;1358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/de4uFf/dJMcaaTvp7J/5HU1AmGFyYvjkw4nYyzYlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/de4uFf/dJMcaaTvp7J/5HU1AmGFyYvjkw4nYyzYlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/de4uFf/dJMcaaTvp7J/5HU1AmGFyYvjkw4nYyzYlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fde4uFf%2FdJMcaaTvp7J%2F5HU1AmGFyYvjkw4nYyzYlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;599&quot; data-filename=&quot;스크린샷 2026-07-10 오후 9.45.00.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;1358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기준에 따라 정제되어 약 30건의 유의미한 데이터만 남는다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/9</guid>
      <comments>https://jiwoo.tistory.com/entry/Python-Ch8-1#entry9comment</comments>
      <pubDate>Thu, 9 Jul 2026 12:23:44 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.5 (2) 광학 문자 인식 + DeepL API 번역</title>
      <link>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch5-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; DeepL API 사용하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 서비스를 사용하면 DeepL에 직접 접속하지 않고도 번역 기능을 이용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.24.48.png&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;1412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjWJiu/dJMcaiRvJf3/tHWBVAGTMaEkT73EzHkib1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjWJiu/dJMcaiRvJf3/tHWBVAGTMaEkT73EzHkib1/img.png&quot; data-alt=&quot;Developer 무료 API 키 받기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjWJiu/dJMcaiRvJf3/tHWBVAGTMaEkT73EzHkib1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjWJiu%2FdJMcaiRvJf3%2FtHWBVAGTMaEkT73EzHkib1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;339&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.24.48.png&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;1412&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Developer 무료 API 키 받기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; DeepL 패키지를 불러와서 Translator 객체를 사용해보자 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import deepl

DEEPL_KEY = &quot;제 API KEY는 비밀입니다&quot;
tran = deepl.Translator(DEEPL_KEY)

resp = tran.translate_text( &quot;Hello, World&quot;, source_lang = &quot;EN&quot;, target_lang = &quot;KO&quot;)

resp.text&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.24.11.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UB4OO/dJMcahLO1R3/L9Nsw2pflgYMy8tuJJ2kuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UB4OO/dJMcahLO1R3/L9Nsw2pflgYMy8tuJJ2kuK/img.png&quot; data-alt=&quot;반갑다 세상아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UB4OO/dJMcahLO1R3/L9Nsw2pflgYMy8tuJJ2kuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUB4OO%2FdJMcahLO1R3%2FL9Nsw2pflgYMy8tuJJ2kuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;240&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.24.11.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;반갑다 세상아&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 외국어 번역 함수 만들기 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pathlib import Path
import deepl
from step_1 import IN_DIR
from step_2_2 import read_text

def read_text_translated(path: Path) -&amp;gt; list :
    
    text_list = read_text(path) # 문자 인식 함수

    DEEPL_KEY = &quot;API 키&quot;

    tran = deepl.Translator(DEEPL_KEY)

    result = []

    for coords, text, prob in text_list :
        resp = tran.translate_text(text, target_lang = &quot;KO&quot;)
        result.append((coords, resp.text, prob))
    
    return result

if __name__ == &quot;__main__&quot; :
    path = IN_DIR / &quot;ocr.jpg&quot;
    print(read_text_translated(path))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ocr.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ExJ54/dJMcabkBxop/ku2UnuQhjCABzjG7BIMj21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ExJ54/dJMcabkBxop/ku2UnuQhjCABzjG7BIMj21/img.jpg&quot; data-alt=&quot;예시로 쓸 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ExJ54/dJMcabkBxop/ku2UnuQhjCABzjG7BIMj21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FExJ54%2FdJMcabkBxop%2Fku2UnuQhjCABzjG7BIMj21%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;453&quot; height=&quot;279&quot; data-filename=&quot;ocr.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시로 쓸 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[ (
	[
	[85, 288], [1088, 288], [1088, 457], [85, 457]], # 문자 위치
	'채식주의자로 살기', # 번역 결과
	0.9799573460577209 # 인식률
  ),
 
	(
	[[77, 417], [1159, 417], [1159, 574], [77, 574]],
	'정말 아까운 기회다',
	0.501874285483583
	)
]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'채식주의자로 살기란 정말 아까운 기회다' 로 번역한 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.55.21.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clufki/dJMcab5VcUI/CHlxbYYMhtFqJplaJ7X3Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clufki/dJMcab5VcUI/CHlxbYYMhtFqJplaJ7X3Y1/img.png&quot; data-alt=&quot;언어유희까지도 문맥으로 파악했다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clufki/dJMcab5VcUI/CHlxbYYMhtFqJplaJ7X3Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclufki%2FdJMcab5VcUI%2FCHlxbYYMhtFqJplaJ7X3Y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;157&quot; data-filename=&quot;스크린샷 2026-07-08 오후 3.55.21.png&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;언어유희까지도 문맥으로 파악했다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 이미지에 번역된 문장까지 표시해서 함께 출력하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://jiwoo.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC-Ch5-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ch.5 (1)&lt;/a&gt;에서 만들었던 것 처럼..&lt;/li&gt;
&lt;li&gt;인식률 0.75를 기준으로 색깔 박스로 바운딩&lt;/li&gt;
&lt;li&gt;여기서는 RGBA 모드를 사용해서, 불투명도를 적용할 수 있도록 함&lt;/li&gt;
&lt;li&gt;번역된 텍스트가 바운딩 박스의 첫번째 좌표(왼쪽 상단)에 흰색 글자로 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from step_1 import IN_DIR, OUT_DIR
from step_3_2 import read_text_translated

OUT_3_3 = OUT_DIR / f&quot;{Path(__file__).stem}.jpg&quot;
PROB = 0.75 # 인식률 기준값

def read_text_and_fill_area(path : Path) :
    
    parsed = read_text_translated(path) # 문자 인식 및 번역 결과 저장

    img = Image.open(path)
    draw = ImageDraw.Draw(img, &quot;RGBA&quot;)
    font = ImageFont.truetype(IN_DIR / &quot;Pretendard-Bold.ttf&quot;, size = 60)
    
    for row in parsed :

        bbox, text, prob = row
        box = [ (x, y) for x, y in bbox ]

        draw.polygon(
            box,
            fill = ( 255, 0, 0, 100 ) if prob &amp;gt;= PROB else ( 0, 255, 0, 100 ),
        )

        draw.text(
            xy = box[0],
            text = text,
            fill = ( 255, 255, 255 ),
            font = font
        )
    
    img.save(OUT_3_3)

if __name__ == &quot;__main__&quot; :
    path = IN_DIR / &quot;ocr.jpg&quot;
    read_text_and_fill_area(path)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_3_3.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWOKA7/dJMcabkBxPl/ngKSsQB6d7pJzp4eFZEeY0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWOKA7/dJMcabkBxPl/ngKSsQB6d7pJzp4eFZEeY0/img.jpg&quot; data-alt=&quot;결과입니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWOKA7/dJMcabkBxPl/ngKSsQB6d7pJzp4eFZEeY0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWOKA7%2FdJMcabkBxPl%2FngKSsQB6d7pJzp4eFZEeY0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;281&quot; data-filename=&quot;step_3_3.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과입니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; (1) 에서처럼 이 기능을 웹 앱으로 만들어 보기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1)에서 했던 것 처럼, pillow 패키지를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pathlib import Path
import streamlit as st
from step_1 import OUT_DIR
from step_3_3 import OUT_3_3, read_text_and_fill_area

st.title(&quot;이지우의 (번역기능이 추가된) 문자 인식 웹 앱&quot;)

uploaded = st.file_uploader(&quot;인식할 이미지를 선택하세요.&quot;)

if uploaded is not None :
    
    tmp_path = OUT_DIR / f&quot;{Path(__file__).stem}.tmp&quot;
    tmp_path.write_bytes(uploaded.getvalue())

    col_left, col_right = st.columns(2)

    with col_left :  
        st.subheader(&quot;원본 이미지&quot;)
        st.image(tmp_path.as_posix())
    
    with col_right :
        st.subheader(&quot;문자 인식 + 번역 결과&quot;)
        
        # spinner
        with st.spinner(text = &quot;문자를 인식하고 번역하는 중입니다...&quot;) :
            read_text_and_fill_area(tmp_path)
        
        st.image(OUT_3_3.as_posix())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceqDIf/dJMcaiRvJHd/gLbGnugiZkDQZkdh70z721/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceqDIf/dJMcaiRvJHd/gLbGnugiZkDQZkdh70z721/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;1742&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.17.13.png&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceqDIf/dJMcaiRvJHd/gLbGnugiZkDQZkdh70z721/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceqDIf%2FdJMcaiRvJHd%2FgLbGnugiZkDQZkdh70z721%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2236&quot; height=&quot;1742&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYU1gz/dJMcahZkHOM/kkF5xOmJmEdwQCgBXGlmjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYU1gz/dJMcahZkHOM/kkF5xOmJmEdwQCgBXGlmjk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;1742&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.17.25.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYU1gz/dJMcahZkHOM/kkF5xOmJmEdwQCgBXGlmjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYU1gz%2FdJMcahZkHOM%2FkkF5xOmJmEdwQCgBXGlmjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2236&quot; height=&quot;1742&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.17.31.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;1742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k4Anw/dJMcabZcHtY/1TZJY6EGKsRBIRt4t7nly0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k4Anw/dJMcabZcHtY/1TZJY6EGKsRBIRt4t7nly0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k4Anw/dJMcabZcHtY/1TZJY6EGKsRBIRt4t7nly0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk4Anw%2FdJMcabZcHtY%2F1TZJY6EGKsRBIRt4t7nly0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;438&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.17.31.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;1742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 무한도전짤 수출을 위한, 무도짤 번역기로 바꾸기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한국어(source) &amp;rarr; 영어(target) 로 번역을 바꾸기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;    for coords, text, prob in text_list :
        resp = tran.translate_text(text, source_lang = &quot;KO&quot;, target_lang = &quot;EN-US&quot;)
        result.append((coords, resp.text, prob))
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폰트 컬러와 사이즈를 조금 수정해주면.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.30.40.png&quot; data-origin-width=&quot;2430&quot; data-origin-height=&quot;1622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3YUK/dJMcai4XUh8/4BltuOnI8TK7K8D8yVlGDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3YUK/dJMcai4XUh8/4BltuOnI8TK7K8D8yVlGDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3YUK/dJMcai4XUh8/4BltuOnI8TK7K8D8yVlGDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft3YUK%2FdJMcai4XUh8%2F4BltuOnI8TK7K8D8yVlGDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;719&quot; height=&quot;480&quot; data-filename=&quot;스크린샷 2026-07-08 오후 4.30.40.png&quot; data-origin-width=&quot;2430&quot; data-origin-height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;429b2949ceb7c5a43c07a261b4a656a4.jpg&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MWk13/dJMcage5dIE/u7mQ2enRRe19n4d3i4us51/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MWk13/dJMcage5dIE/u7mQ2enRRe19n4d3i4us51/img.jpg&quot; data-alt=&quot;영어를 잘하는 박명수로 만들 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MWk13/dJMcage5dIE/u7mQ2enRRe19n4d3i4us51/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMWk13%2FdJMcage5dIE%2Fu7mQ2enRRe19n4d3i4us51%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;510&quot; height=&quot;360&quot; data-filename=&quot;429b2949ceb7c5a43c07a261b4a656a4.jpg&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;영어를 잘하는 박명수로 만들 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/6</guid>
      <comments>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch5-2#entry6comment</comments>
      <pubDate>Tue, 7 Jul 2026 16:08:57 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.5 (1) 광학 문자 인식 + 웹 앱 만들기</title>
      <link>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch5-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; OCR은 무엇인가? &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Optical Character Recognition = 광학 문자 인식&lt;/li&gt;
&lt;li&gt;이미지에서 문자를 인식하여 추출하는 기술&lt;/li&gt;
&lt;li&gt;&amp;rarr; easyocr 패키지를 사용해서 파이썬에서 OCR 기술을 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; easyocr 패키지 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 클로바의 딥러닝 기술을 바탕으로 만든 파이썬 패키지&lt;/li&gt;
&lt;li&gt;영어를 포함한 세계 80개언어 인식 가능&lt;/li&gt;
&lt;li&gt;(1) 텍스트 감지 &amp;rarr; (2) 문자인식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; easyocr 패키지의 Reader 객체 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 readtext()를 사용하면 이미지 속 문자를 감지할 수 있음&lt;/li&gt;
&lt;li&gt;readtext() &amp;rarr; 이미지파일을 입력받으면, 이미지 속 문자의 좌표와 인식한 문자, 인식률을 튜플 타입으로 반환함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; OCR 문자 인식&amp;gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# step_2_1
import easyocr
from step_1 import IN_DIR

path = IN_DIR / &quot;ocr.jpg&quot;
reader = easyocr.Reader([ &quot;ko&quot; , &quot;en&quot; ], verbose = False)
parsed = reader.readtext(path.read_bytes())

print(parsed)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ocr.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwwjhf/dJMcaaMHIS7/e9vRsw9yE47PVL5TnvUhO1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwwjhf/dJMcaaMHIS7/e9vRsw9yE47PVL5TnvUhO1/img.jpg&quot; data-alt=&quot;예시로 사용할 사진파일(ocr.jpg)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwwjhf/dJMcaaMHIS7/e9vRsw9yE47PVL5TnvUhO1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwwjhf%2FdJMcaaMHIS7%2Fe9vRsw9yE47PVL5TnvUhO1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;605&quot; height=&quot;372&quot; data-filename=&quot;ocr.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시로 사용할 사진파일(ocr.jpg)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# ocr.jpg 파일의 이미지 정보 출력 결과
[
	# 첫번째 문자열
	([[85, 288], [1088, 288], [1088, 457], [85, 457]], # 인식한 문자1의 위치
	'Being a vegetarian', # 인식한 문자1
	0.9799573460577209), # 인식률
	
	# 두번째 문자열
	([[77, 417], [1159, 417], [1159, 574], [77, 574]], # 인식한 문자2의 위치
	'is a big missed steak', # 인식한 문자2
	0.501874285483583) # 인식률
]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reader 클래스의 readtext()함수를 사용하면 이미지 속 문자를 감지함&lt;/li&gt;
&lt;li&gt;&amp;rarr; 좌표, 문자, 인식률을 튜플타입으로 반환함(전체는 리스트로)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; step_2_1.py 의 문자인식 코드를 함수 read_text()로 만들기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 경로를 입력 받아서, 해당 이미지의 OCR 결과를 반환하는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pathlib import Path
import easyocr

def read_text(path: Path) -&amp;gt; list :
    reader = easyocr.Reader([&quot;ko&quot;, &quot;en&quot;], verbose = False)
    return reader.readtext(path.read_bytes())

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 바운딩 박스를 추가하기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지에서 어떤 부분이 문자로 인식되었는지 한눈에 쉽게 파악하기 위해서..&lt;/li&gt;
&lt;li&gt;이미지에 Bounding Box 를 그리기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 이전에 만든 read_text() 함수를 보면.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반환값은 리스트 형식의 데이터&lt;/li&gt;
&lt;li&gt;리스트 내부에 튜플 형식으로 저장된 것은
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자의 위치를 나타내는 좌표&lt;/li&gt;
&lt;li&gt;인식된 문자&lt;/li&gt;
&lt;li&gt;인식률&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터를 반복문으로 처리 &amp;rarr; 인식된 문자의 좌표와 인식률을 사용해서 인식률이 높은경우 빨간색, 낮으면 녹색 바운딩 박스를 그리기&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;from pathlib import Path
from PIL import Image, ImageDraw
from step_1 import IN_DIR, OUT_DIR
from step_2_2 import read_text

OUT_2_3 = OUT_DIR / f&quot;{Path(__file__).stem}.jpg&quot;
PROB = 0.75 # 인식률 기준값을 0.75 로 설정

def read_text_and_draw_line(path: Path) :
    parsed:list = read_text(path) # 문자 인식 결과
    img = Image.open(path) # Image 객체 생성
    draw = ImageDraw.Draw(img, &quot;RGB&quot;) # ImageDraw 객체 생성
    for row in parsed :
        bbox, text, prob = row # 문자 인식 결과를 좌표, 문자, 인식률로 각각 분리
        box = [(x,y) for x, y in bbox] # 리스트를 튜플로 변환
        draw.polygon(
            box,
            outline = (255, 0, 0) if prob &amp;gt;= PROB else (0, 255, 0),
            # 인식률 기준값보다 높으면 붉은색, 낮으면 녹색
            width = 10,
        )
    img.save(OUT_2_3)

if __name__ == &quot;__main__&quot; :
    path = IN_DIR / &quot;ocr.jpg&quot;
    read_text_and_draw_line(path)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_2_3.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT480N/dJMcag0n954/GESGxViKfjHXBozl10Usyk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT480N/dJMcag0n954/GESGxViKfjHXBozl10Usyk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT480N/dJMcag0n954/GESGxViKfjHXBozl10Usyk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT480N%2FdJMcag0n954%2FGESGxViKfjHXBozl10Usyk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;359&quot; data-filename=&quot;step_2_3.jpg&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인식률 기준값 0.75를 기준으로, 기준값보다 높은 첫번째 줄은 붉은 색으로 / 낮은 두번째 줄은 초록색 박스 바운딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 지금까지의 문자인식 기능을 웹 앱으로 만들기 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;steamlit 패키지는 위젯을 통해 입력받은 데이터를 실시간으로 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 웹 앱에 이미지 파일 업로드 &amp;rarr; (2) 업로드된 이미지에 대해서 문자를 인식하고 바운딩 박스를 그린 결과 출력&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-07 오후 6.29.44.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w9mxJ/dJMcaglOBnP/15aGHjTbbCLdg37WdI9681/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w9mxJ/dJMcaglOBnP/15aGHjTbbCLdg37WdI9681/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w9mxJ/dJMcaglOBnP/15aGHjTbbCLdg37WdI9681/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw9mxJ%2FdJMcaglOBnP%2F15aGHjTbbCLdg37WdI9681%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;614&quot; height=&quot;410&quot; data-filename=&quot;스크린샷 2026-07-07 오후 6.29.44.png&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pathlib import Path
import streamlit as st
from step_1 import OUT_DIR
from step_2_3 import OUT_2_3, read_text_and_draw_line

st.title(&quot;이지우의 문자인식 웹 앱&quot;)

uploaded = st.file_uploader(&quot;인식할 이미지를 선택하세요.&quot;)

if uploaded is not None :
    tmp_path = OUT_DIR / f&quot;{Path(__file__).stem}.tmp&quot;
    tmp_path.write_bytes(uploaded.getvalue())

    # 2개의 columns(열) 생성
    col_left, col_right = st.columns(2)

    with col_left : # 좌측 첫번째 열 &amp;lt;- 원본 이미지
        st.subheader(&quot;원본 이미지&quot;)
        st.image(tmp_path.as_posix())

    with col_right : # 우측 두번째 열 &amp;lt;- 문자 인식한 이미지
        st.subheader(&quot;문자 인식 결과&quot;)
        
        with st.spinner(text = &quot;문자를 인식하는 중입니다...&quot;) :
            read_text_and_draw_line(tmp_path)
        
        st.image(OUT_2_3.as_posix())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;streamlit run ch_05/step_2_4.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bigQHQ/dJMcadbuIWq/D2DcNGQDKhu4z4OVNnYMxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bigQHQ/dJMcadbuIWq/D2DcNGQDKhu4z4OVNnYMxk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;1484&quot; data-filename=&quot;스크린샷 2026-07-07 오후 8.57.40.png&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bigQHQ/dJMcadbuIWq/D2DcNGQDKhu4z4OVNnYMxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbigQHQ%2FdJMcadbuIWq%2FD2DcNGQDKhu4z4OVNnYMxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2036&quot; height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZQw4b/dJMcacX2VUU/1l3epqPu33sPA4gk5AP6ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZQw4b/dJMcacX2VUU/1l3epqPu33sPA4gk5AP6ok/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;1484&quot; data-filename=&quot;스크린샷 2026-07-07 오후 9.38.26.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZQw4b/dJMcacX2VUU/1l3epqPu33sPA4gk5AP6ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZQw4b%2FdJMcacX2VUU%2F1l3epqPu33sPA4gk5AP6ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2036&quot; height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사진을 업로드 한 뒤에, 함수 read_text_and_draw_line() 이 실행되는 동안 스피너(spinner)가 돌아가며 '문자를 인식하는 중입니다...'라는 문자열을 보여줌.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-07 오후 8.58.10.png&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;1484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrJd9/dJMcadbuIWr/kxOd4D6k4zsc5DFeXAyNi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrJd9/dJMcadbuIWr/kxOd4D6k4zsc5DFeXAyNi0/img.png&quot; data-alt=&quot;로컬 서버에서 웹 앱을 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrJd9/dJMcadbuIWr/kxOd4D6k4zsc5DFeXAyNi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrJd9%2FdJMcadbuIWr%2FkxOd4D6k4zsc5DFeXAyNi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;761&quot; height=&quot;555&quot; data-filename=&quot;스크린샷 2026-07-07 오후 8.58.10.png&quot; data-origin-width=&quot;2036&quot; data-origin-height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로컬 서버에서 웹 앱을 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4lvzq/dJMcah6bytc/Omssm5O1b8XZPoac9mtovk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4lvzq/dJMcah6bytc/Omssm5O1b8XZPoac9mtovk/img.png&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;1574&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-07-07 오후 9.47.46.png&quot; style=&quot;width: 33.2828%; margin-right: 10px;&quot; data-widthpercent=&quot;34.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4lvzq/dJMcah6bytc/Omssm5O1b8XZPoac9mtovk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4lvzq%2FdJMcah6bytc%2FOmssm5O1b8XZPoac9mtovk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1736&quot; height=&quot;1574&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLhR8G/dJMcabEN8t5/kpSYhuRXKsgq9cbVNLqfMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLhR8G/dJMcabEN8t5/kpSYhuRXKsgq9cbVNLqfMk/img.png&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;1574&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-07-07 오후 9.47.56.png&quot; style=&quot;width: 33.2828%; margin-right: 10px;&quot; data-widthpercent=&quot;34.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLhR8G/dJMcabEN8t5/kpSYhuRXKsgq9cbVNLqfMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLhR8G%2FdJMcabEN8t5%2FkpSYhuRXKsgq9cbVNLqfMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1736&quot; height=&quot;1574&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aopj1/dJMcabZb00c/Cq77yD3ueKoFA41K35YBz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aopj1/dJMcabZb00c/Cq77yD3ueKoFA41K35YBz1/img.png&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;1684&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2026-07-07 오후 9.48.20.png&quot; style=&quot;width: 31.1088%;&quot; data-widthpercent=&quot;31.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aopj1/dJMcabZb00c/Cq77yD3ueKoFA41K35YBz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAopj1%2FdJMcabZb00c%2FCq77yD3ueKoFA41K35YBz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1736&quot; height=&quot;1684&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;아르테타, 호날두, 고우석 짤도 잘 인식하는 모습이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1b76d7366b48a3ad409b7c9ea8fa02b3.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t817z/dJMcaiDYvNP/Iec9Y6U2EFZTgLZGjQbUYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t817z/dJMcaiDYvNP/Iec9Y6U2EFZTgLZGjQbUYk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t817z/dJMcaiDYvNP/Iec9Y6U2EFZTgLZGjQbUYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft817z%2FdJMcaiDYvNP%2FIec9Y6U2EFZTgLZGjQbUYk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;319&quot; height=&quot;418&quot; data-filename=&quot;1b76d7366b48a3ad409b7c9ea8fa02b3.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 제일 좋아하는 고우석 선수가 드디어 마이너리그를 벗어나 메이저리그에 데뷔할 것 같다! 고우석 화이팅.&lt;/p&gt;</description>
      <category>짧은 호흡/Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/5</guid>
      <comments>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch5-1#entry5comment</comments>
      <pubDate>Sat, 4 Jul 2026 22:14:07 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.2 데이터 프레임 + matplotlib 시각화</title>
      <link>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch2</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러개의 엑셀파일 불러온뒤, 하나로 취합 &amp;rarr; 취합된 데이터를 피벗 테이블로 분석하고 matplotlib를 통해 파이차트로 시각화하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.14.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;2164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GhZmT/dJMcahLKXIr/inhkaDrpGICNDtQZTMFZu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GhZmT/dJMcahLKXIr/inhkaDrpGICNDtQZTMFZu0/img.png&quot; data-alt=&quot;임의로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GhZmT/dJMcahLKXIr/inhkaDrpGICNDtQZTMFZu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGhZmT%2FdJMcahLKXIr%2FinhkaDrpGICNDtQZTMFZu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;144&quot; height=&quot;274&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.14.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;2164&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;임의로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.13.png&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;2250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VmbF6/dJMcaf8bYhs/Rhf8KR7pKE1q97wH3KFABK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VmbF6/dJMcaf8bYhs/Rhf8KR7pKE1q97wH3KFABK/img.png&quot; data-alt=&quot;사용할&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VmbF6/dJMcaf8bYhs/Rhf8KR7pKE1q97wH3KFABK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVmbF6%2FdJMcaf8bYhs%2FRhf8KR7pKE1q97wH3KFABK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;144&quot; height=&quot;265&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.13.png&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;2250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용할&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.12.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;2162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkAXjE/dJMcaalweSd/hNPQkHVWJ0k6ZrksgKKFQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkAXjE/dJMcaalweSd/hNPQkHVWJ0k6ZrksgKKFQ0/img.png&quot; data-alt=&quot;엑셀파일들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkAXjE/dJMcaalweSd/hNPQkHVWJ0k6ZrksgKKFQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkAXjE%2FdJMcaalweSd%2FhNPQkHVWJ0k6ZrksgKKFQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;144&quot; height=&quot;274&quot; data-filename=&quot;스크린샷 2026-07-02 오후 7.17.12.png&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;2162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;엑셀파일들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;opnepyxl 패키지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install -U openpyxl
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pandas 패키지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install -U pandas
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;seaborn 패키지&lt;/li&gt;
&lt;li&gt;&amp;rarr; matplotlib 패키지 기반의 시각화 패키지.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install -U seaborn
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; 데이터취합1&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# step_2_1.py
import pandas as pd
from step_1 import IN_DIR

df_raw = pd.read_excel(
    IN_DIR / &quot;2026_01.xlsx&quot;,
    sheet_name = &quot;Sheet1&quot;,
    usecols = &quot;B:E&quot;,
    skiprows = 2
    )

df_raw
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; 데이터취합2 &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# step_2_2.py
from pathlib import Path
import pandas as pd
from step_1 import IN_DIR, OUT_DIR

OUT_2_2 = OUT_DIR / f&quot;{Path(__file__).stem}.xlsx&quot;

if __name__ == &quot;__main__&quot; :
    result = []

    for xlsx_path in Path(IN_DIR).glob(&quot;2026_*.xlsx&quot;) :
        df_raw = pd.read_excel (
            xlsx_path,
            sheet_name = &quot;Sheet1&quot;,
            usecols = &quot;B:E&quot;,
            skiprows = 2
        )

        result.append(df_raw)

    df_concat = pd.concat(result)
    df_concat.to_excel(OUT_2_2, index = False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; concat으로 합쳐진 엑셀파일이, 현재파일명(step_2_2).xlsx 이름으로 합쳐진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;30B00B16-B482-4248-899B-9053F8C61238_1_206_a.jpeg&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;2122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DpKTL/dJMcacjkRlV/TMhJQ2Fz4As5gjPbK6C5yk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DpKTL/dJMcacjkRlV/TMhJQ2Fz4As5gjPbK6C5yk/img.jpg&quot; data-alt=&quot;전부 합쳐져서 엑셀로 저장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DpKTL/dJMcacjkRlV/TMhJQ2Fz4As5gjPbK6C5yk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDpKTL%2FdJMcacjkRlV%2FTMhJQ2Fz4As5gjPbK6C5yk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;438&quot; height=&quot;759&quot; data-filename=&quot;30B00B16-B482-4248-899B-9053F8C61238_1_206_a.jpeg&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;2122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전부 합쳐져서 엑셀로 저장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유니코드 문자를 저장하는 방식에는 크게 두가지가 있음.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;NFC : 완성된 글자로 저장보이는 그대로, 하나의 완성된 문자로 저장&lt;/li&gt;
&lt;li&gt;NFD : 자음+모음+받침으로 쪼개서 저장보이는 글자는 김 한글자여도, 내부적으로는 3글자로 저장됨.&lt;/li&gt;
&lt;li&gt;김 을 저장할때 ㄱ + ㅣ + ㅁ 으로 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파일이름을 2026년1월 로 했을때, glob 패턴으로 이를 인식하지 못했었음. 코드에서는 NFC로 인식하고, 실제 파일은 NFD로 저장되어 있어서 인식하지 못한것&amp;rarr; 2026_01 이런식으로 변경해서 파일을 저장하고, glob 패턴도 2026_*.xlsx로 수정&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;    for xlsx_path in Path(IN_DIR).glob(&quot;2026년*월.xlsx&quot;) :
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그래서 애초에 파일명에 한글을 포함하지 않고, 영어 기본 알파벳으로 구성하면 nfc, nfd 차이가 거의 없어서 더 편함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;영문 알파벳의 경우 악센트를 표기하는 (&amp;eacute;) 때를 제외하고는 NFC, NFD 차이 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-01 오후 6.52.24.png&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcJG5L/dJMcacXZc7L/kIF032XmggjLRFFHIk3af1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcJG5L/dJMcacXZc7L/kIF032XmggjLRFFHIk3af1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcJG5L/dJMcacXZc7L/kIF032XmggjLRFFHIk3af1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcJG5L%2FdJMcacXZc7L%2FkIF032XmggjLRFFHIk3af1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;246&quot; data-filename=&quot;스크린샷 2026-07-01 오후 6.52.24.png&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 concat()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pandas의 pd.concat() 함수는 여러 개의 DataFrame 또는 Series를 하나로 이어 붙일 때 사용&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;pd.concat([df1, df2, df3])
# concat() 안에 data frame 여러개를 리스트로 넣음.

result = [
	2026_01 데이터프레임,
	2026_02 데이터프레임,
	2026_03 데이터프레임
]

df_total = pd.concat(result)
# 을 하면, 1월/2월/3월 데이터가 세로로 이어 붙는다!
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;concatenate = 이어붙이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피벗 테이블(pivot table)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pandas 패키지의 pivot_table() 함수는 매개변수 값에 따라 옵션을 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;pd.pivot_table(
	df,
	index = &quot;row&quot;,
	columns = &quot;column&quot;,
	values = &quot;value&quot;,
	aggfuc = &quot;sum&quot;,
)

# sum : 합계
# mean : 평균
# max : 최댓값
# min : 최솟값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; 피벗테이블, 집계 기준 추가, 엑셀로 저장 &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# step_3_2.py
import pandas as pd
from step_2_2 import OUT_2_2
from pathlib import Path
from step_1 import OUT_DIR

# 피벗 테이블1
df_raw = pd.read_excel(OUT_2_2)
df_pivot_1 = pd.pivot_table(
    df_raw,
    index = &quot;분류&quot;,
    values = &quot;사용금액&quot;,
    aggfunc = &quot;sum&quot;
    )
df_pivot_1

# 집계 기준 추가하기
# str.slice()를 이용하여, YYYY-MM 부분 슬라이싱
df_raw[&quot;거래연월&quot;] = df_raw[&quot;거래일시&quot;].astype(str).str.slice(0, 7)
df_raw

df_pivot_2 = pd.pivot_table(
    df_raw,
    index = &quot;분류&quot;,
    columns = &quot;거래연월&quot;,
    values= &quot;사용금액&quot;,
    aggfunc = &quot;sum&quot;
)

# 피벗 테이블2
df_pivot_2[&quot;누적금액&quot;] = df_pivot_2.sum(axis = 1)
df_pivot_2

# 데이터 정렬
df_sort = df_pivot_2.sort_values(&quot;누적금액&quot;, ascending=False)
df_sort

# 인덱스를 열로 변환
df_reindex = df_sort.reset_index()
df_reindex

# 데이터 프레임을 엑셀 파일로 저장하기
df_reindex.to_excel(
    OUT_DIR / f&quot;{Path(__file__).stem}.xlsx&quot;,
    index = False,
    sheet_name = &quot;분류별누적금액&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-01 오후 7.25.26.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xEjkH/dJMcaiKB9mv/PvsdllKdsWKkxnvqyJ6LSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xEjkH/dJMcaiKB9mv/PvsdllKdsWKkxnvqyJ6LSk/img.png&quot; data-alt=&quot;엑셀파일로 저장된 데이터 프레임&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xEjkH/dJMcaiKB9mv/PvsdllKdsWKkxnvqyJ6LSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxEjkH%2FdJMcaiKB9mv%2FPvsdllKdsWKkxnvqyJ6LSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;540&quot; data-filename=&quot;스크린샷 2026-07-01 오후 7.25.26.png&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;엑셀파일로 저장된 데이터 프레임&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;% 연산자로 문자열 변경하기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;name = &quot;python&quot;
score = 32.45

print(&quot;이름: %s&quot; % name)
# 이름: python

print(&quot;점수: %.1f&quot; % score)
# 점수: 32.5

print(&quot;이름: %s, 점수: %.1f&quot; % (name, score))
# 이름: python, 점수: 32.5

# %s   : 문자열
# %d   : 정수
# %f   : 실수
# %.1f : 소수점 첫째 자리까지 출력
# %.2f : 소수점 둘째 자리까지 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; 파이차트(pie chart)로 시각화 &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# step_4_2.py
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
from step_1 import OUT_DIR
from step_3_2 import OUT_3_2

def load_data(n: int = 4) -&amp;gt; pd.DataFrame:
    df_raw = pd.read_excel(OUT_3_2)
    df_head, df_tail = df_raw.iloc[:n], df_raw.iloc[n:]
    df_sum = df_tail.drop(columns=[&quot;분류&quot;]).sum().to_frame().transpose()
    df_sum[&quot;분류&quot;] = &quot;기타&quot;
    
    return pd.concat([df_head, df_sum], ignore_index = True)

if __name__ == &quot;__main__&quot; :
    df_raw = load_data()
    values = df_raw[&quot;누적금액&quot;]
    fig, ax = plt.subplots(figsize = (16, 9), dpi = 100)
    ax.pie(
        values,
        textprops = dict(color = &quot;black&quot;, size = 20),
        startangle = 90,
        autopct = &quot;%.1f%%&quot;
    )

    fig.savefig( OUT_DIR / f&quot;{Path(__file__).stem}.png&quot;, bbox_inches = &quot;tight&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_4_2.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;713&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FNCtR/dJMcafmVwIF/dmVGPk79FwuydUKeGcBKsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FNCtR/dJMcafmVwIF/dmVGPk79FwuydUKeGcBKsK/img.png&quot; data-alt=&quot;pie chart로 시각화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FNCtR/dJMcafmVwIF/dmVGPk79FwuydUKeGcBKsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFNCtR%2FdJMcafmVwIF%2FdmVGPk79FwuydUKeGcBKsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;458&quot; data-filename=&quot;step_4_2.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;713&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pie chart로 시각화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; 파이 차트 꾸미기(제목, 레이블, 범례 추가하기) &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;# step_4_3.py
from pathlib import Path
import matplotlib.pyplot as plt
import seaborn as sns
from step_1 import OUT_DIR
from step_4_2 import load_data

def custom_autopct(pct, total):
    real_val = int(pct / 100 * total)
    return f&quot;{real_val:,}원\n({pct:.1f}%)&quot;

df_raw = load_data()
labels, values = df_raw[&quot;분류&quot;], df_raw[&quot;누적금액&quot;]

# ste_theme()으로 차트의 크기와 폰트를 설정
sns.set_theme(context = &quot;poster&quot;, font = &quot;Apple SD Gothic Neo&quot;)
fig, ax = plt.subplots(figsize = (16, 9), dpi = 100)

# lambda() 사용
ax.pie(
    values,
    textprops = dict(color = &quot;white&quot;, size = 20),
    startangle = 90,
    autopct = lambda pct : custom_autopct(pct, sum(values))
)

ax.legend(labels, loc = &quot;center left&quot;, bbox_to_anchor = (1, 0.5))
ax.set_title(f&quot;1분기에는 {labels[1]}에 {values[1]:,}원을 사용했습니다.&quot;)
fig.suptitle(&quot;이지우의 2026년 1분기 카드 사용 내역&quot;)
fig.savefig(OUT_DIR / f&quot;{Path(__file__).stem}.png&quot;, bbox_inches = &quot;tight&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;seaborn 패키지를 사용하여 전체적인 디자인 스타일을 지정하고, 폰트를 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_4_3.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wvryx/dJMcabLC13n/cPTFTJCxaTCQtglVsuDyP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wvryx/dJMcabLC13n/cPTFTJCxaTCQtglVsuDyP1/img.png&quot; data-alt=&quot;제목, 레이블이 추가되어 더 가독성이 좋아졌음.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wvryx/dJMcabLC13n/cPTFTJCxaTCQtglVsuDyP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwvryx%2FdJMcabLC13n%2FcPTFTJCxaTCQtglVsuDyP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;468&quot; height=&quot;383&quot; data-filename=&quot;step_4_3.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제목, 레이블이 추가되어 더 가독성이 좋아졌음.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;람다(lambda) 함수는 주로 간단한 기능을 구현하는 용도로 사용하는 1행짜리 익명함수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# lambda: 짧은 함수를 한 줄로 만드는 문법

add = lambda x, y: x + y
print(add(3, 5))
# 8

square = lambda x: x ** 2
print(square(4))
# 16

# 형태
# lambda 매개변수: 반환값
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>짧은 호흡/Python</category>
      <category>Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/4</guid>
      <comments>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch2#entry4comment</comments>
      <pubDate>Thu, 2 Jul 2026 15:56:38 +0900</pubDate>
    </item>
    <item>
      <title>[파이썬] Ch.1 폴더 크기 측정 + 데이터 시각화</title>
      <link>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; Pathlib &amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pathlib는 파일 경로를 다루기 위한 파이썬 표준 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 경로를 문자열로 다루게 되면 여러가지 불편한 점이 생길 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 리눅스/macOS 계열에서는 슬래쉬( / ), 윈도우에서는 역슬래쉬( \ )를 씀.&lt;/li&gt;
&lt;li&gt; 그래서 문자열로 직접쓰면 운영체제에 따라 경로를 다르게 써야할 수도 있음.&lt;/li&gt;
&lt;li&gt; 문자열을 이어붙여서 경로를 만들때도 복잡(슬래시가 중복될수도 있고, 빼먹을수도)&lt;/li&gt;
&lt;li&gt;문자열 자체는 파일 존재 확인, 폴더 생성, 부모 폴더 찾기 기능이 있는건 아니니까.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; pathlib를 쓰면 경로 자체를 객체로(Path 타입으로) 다룰 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1782661307150&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Path(&quot;output&quot;).mkdir()
Path(&quot;data.txt&quot;).exists()
Path(&quot;data.txt&quot;).read_text()
Path(&quot;data.txt&quot;).write_text(&quot;hello&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mkdir()의 exist_ok=True는 폴더가 이미 있어도 에러를 발생시키지 않도록 하는 옵션이다.&lt;/p&gt;
&lt;pre id=&quot;code_1782661316287&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;__file__ # 현재 파일의 절대 경로
__name__ # 확장자를 제외한 현재 파일명
print(__file__)
# &amp;gt;&amp;gt;&amp;gt; /Users/leejiwoo/Desktop/gopython/ch_01/step_2_1.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__file__은 현재 실행 중인 파이썬 파일의 경로를 담고 있는 특수 변수이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;671&quot; data-start=&quot;600&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;641&quot; data-start=&quot;600&quot; data-section-id=&quot;no4op0&quot;&gt;Path(__file__) : 현재 파일을 Path 객체로 변환&lt;/li&gt;
&lt;li data-end=&quot;671&quot; data-start=&quot;642&quot; data-section-id=&quot;1mqlbg0&quot;&gt;.parent : 현재 파일이 있는 폴더 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 폴더 크기 측정 &amp;gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# step_2_2.py
from pathlib import Path
from step_2_1 import WORK_DIR

def get_total_filesize(base_dir: Path, pattern: str = &quot;*&quot;) -&amp;gt; int:
    total_bytes = 0
    for fullpath in base_dir.glob(pattern):
        if fullpath.is_file():
            total_bytes += fullpath.stat().st_size
    return total_bytes

if __name__ == &quot;__main__&quot;:
    base_dir = WORK_DIR
    filesize = get_total_filesize(base_dir, pattern=&quot;*&quot;)
    print(f&quot;{base_dir.as_posix()=}, {filesize=} bytes&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글로브 패턴(glob pattern)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;glob() &amp;rarr; 특정 패턴과 일치하는 파일/폴더 찾기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stat()&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt; 파일 또는 폴더의 상태정보, 메타데이터를 가져오는 메서드&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;as_posix()&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr;POSIX는 리눅스 계열에서 쓰는 표준 경로 형식( .../../. 이런식으로 생긴거)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; JSON 형식으로 폴더 목록 저장 &amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782720410607&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_2_3.py
import json
from pathlib import Path
from step_2_1 import OUT_DIR

OUT_2_3 = OUT_DIR / f&quot;{Path(__file__).stem}.json&quot;

def dump_dirnames(base_dir: Path) -&amp;gt; None:
    dirs = []
    for path in base_dir.iterdir():
        if path.is_dir():
            dirs.append(path.as_posix())
    dirs_sorted = sorted(dirs)
    with open(OUT_2_3, &quot;w&quot;, encoding=&quot;utf-8&quot;) as fp:
        json.dump(dirs_sorted, fp, ensure_ascii=False, indent=2)

def load_dirnames() -&amp;gt; list[str]:
    if OUT_2_3.is_file():
        with open(OUT_2_3, encoding=&quot;utf-8&quot;) as fp:
            return json.load(fp)
    else:
        return []

if __name__ == &quot;__main__&quot;:
    dump_dirnames(Path.home())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 폴더 크기 측정 &amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782720489014&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_2_4.py
import json
from pathlib import Path
from step_2_1 import OUT_DIR
from step_2_2 import get_total_filesize
from step_2_3 import load_dirnames

OUT_2_4 = OUT_DIR / f&quot;{Path(__file__).stem}.json&quot;

def dump_filesize_from_dirnames():
    dirs = load_dirnames()
    result = {}
    for path_str in dirs:
        path = Path(path_str)
        filesize = get_total_filesize(path, pattern = &quot;**/*&quot;)
        result[path.as_posix()] = filesize
    with open(OUT_2_4, &quot;w&quot;, encoding=&quot;utf-8&quot;) as fp:
        json.dump(result, fp, ensure_ascii=False, indent=2)
    
def load_filesize_per_dir() -&amp;gt; dict[str, int]:
    if OUT_2_4.is_file():
        with open(OUT_2_4, encoding=&quot;utf-8&quot;) as fp:
            return json.load(fp)
    else:
        return {}

if __name__ == &quot;__main__&quot;:
    dump_filesize_from_dirnames()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폴더 크기 측정 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 여기까지하면 폴더 목록과 각 크기를 바이트단위로 저장한 step_2_4.json 파일이 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1782720583975&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_2_4.json
{
  &quot;/Users/leejiwoo/.Trash&quot;: 1381617009,
  &quot;/Users/leejiwoo/.adobe&quot;: 30,
  &quot;/Users/leejiwoo/.antigravity&quot;: 81603948,
  &quot;/Users/leejiwoo/.cache&quot;: 2185123022,
  &quot;/Users/leejiwoo/.codex&quot;: 2952486994,
  &quot;/Users/leejiwoo/.config&quot;: 1736,
  &quot;/Users/leejiwoo/.gemini&quot;: 4144448,
  &quot;/Users/leejiwoo/.idlerc&quot;: 80,
  &quot;/Users/leejiwoo/.ipython&quot;: 29043,
  &quot;/Users/leejiwoo/.local&quot;: 36,
  &quot;/Users/leejiwoo/.matplotlib&quot;: 378754,
  &quot;/Users/leejiwoo/.npm&quot;: 2669284701,
  &quot;/Users/leejiwoo/.oh-my-zsh&quot;: 11240386,
  &quot;/Users/leejiwoo/.vscode&quot;: 680825645,
  &quot;/Users/leejiwoo/.zsh_sessions&quot;: 312,
  &quot;/Users/leejiwoo/Applications&quot;: 241,
  &quot;/Users/leejiwoo/Desktop&quot;: 3316786160,
  &quot;/Users/leejiwoo/Documents&quot;: 1270269,
  &quot;/Users/leejiwoo/Downloads&quot;: 269070,
  &quot;/Users/leejiwoo/Library&quot;: 47344774502,
  &quot;/Users/leejiwoo/Movies&quot;: 26531,
  &quot;/Users/leejiwoo/Music&quot;: 208408,
  &quot;/Users/leejiwoo/Pictures&quot;: 19463002264,
  &quot;/Users/leejiwoo/Public&quot;: 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 데이터 전처리 &amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 분석과 시각화를 위해 수집한 원본 데이터를 가공하는 과정&lt;/li&gt;
&lt;li&gt;정확한 분석을 위해 필요&lt;/li&gt;
&lt;li&gt;수집한 데이터에서 피룡한 정보를 선별하고, 범위를 벗어나는 이상치/결측치 등을 다른 값으로 대체하거나 삭제 &amp;rarr; 데이터 품질 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-29 오후 5.15.00.png&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DSGMO/dJMcadoWVX6/vx4Ok9K9KRkdOUapejjv01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DSGMO/dJMcadoWVX6/vx4Ok9K9KRkdOUapejjv01/img.png&quot; data-alt=&quot;폴더의 크기를 가로축, 이름을 세로축에 두어 긴 막대그래프 그리기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DSGMO/dJMcadoWVX6/vx4Ok9K9KRkdOUapejjv01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDSGMO%2FdJMcadoWVX6%2Fvx4Ok9K9KRkdOUapejjv01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;245&quot; data-filename=&quot;스크린샷 2026-06-29 오후 5.15.00.png&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;폴더의 크기를 가로축, 이름을 세로축에 두어 긴 막대그래프 그리기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782720964051&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install -U matplotlib&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;matplotlib 패키지 설치하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 전처리한 데이터 만들기 &amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782721030608&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_3_1.py
import json
from pathlib import Path
from step_2_1 import OUT_DIR
from step_2_4 import load_filesize_per_dir

OUT_3_1 = OUT_DIR / f&quot;{Path(__file__).stem}.json&quot;

def dump_plot_data():
    size_per_path = load_filesize_per_dir()
    size_per_stem = { Path(path).stem : size for path,
                      size in size_per_path.items()
                      if size &amp;gt; 0 }
    plot_data = dict(
        stem = list(size_per_stem.keys()),
        size = list(size_per_stem.values()),
    )
    with open(OUT_3_1, &quot;w&quot;, encoding=&quot;utf-8&quot;) as fp:
        json.dump(plot_data, fp, ensure_ascii=False, indent=2)
    
def load_plot_data() -&amp;gt; dict :
    if OUT_3_1.is_file():
        with open(OUT_3_1, encoding=&quot;utf-8&quot;) as fp:
            return json.load(fp)
    else:
        return {}

if __name__ == &quot;__main__&quot;:
    dump_plot_data()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전처리한 데이터 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 폴더이름과, 폴더크기를 분리하여 저장한 step_3_1.json 파일이 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1782721090423&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_3_1.json
{
  &quot;stem&quot;: [
    &quot;.Trash&quot;,
    &quot;.adobe&quot;,
    &quot;.antigravity&quot;,
    &quot;.cache&quot;,
    &quot;.codex&quot;,
    &quot;.config&quot;,
    &quot;.gemini&quot;,
    &quot;.idlerc&quot;,
    &quot;.ipython&quot;,
    &quot;.local&quot;,
    &quot;.matplotlib&quot;,
    &quot;.npm&quot;,
    &quot;.oh-my-zsh&quot;,
    &quot;.vscode&quot;,
    &quot;.zsh_sessions&quot;,
    &quot;Applications&quot;,
    &quot;Desktop&quot;,
    &quot;Documents&quot;,
    &quot;Downloads&quot;,
    &quot;Library&quot;,
    &quot;Movies&quot;,
    &quot;Music&quot;,
    &quot;Pictures&quot;
  ],
  &quot;size&quot;: [
    1381617009,
    30,
    81603948,
    2185123022,
    2952486994,
    1736,
    4144448,
    80,
    29043,
    36,
    378754,
    2669284701,
    11240386,
    680825645,
    312,
    241,
    3316786160,
    1270269,
    269070,
    47344774502,
    26531,
    208408,
    19463002264
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 시각화하기 &amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782721188235&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_3_2.py
from pathlib import Path
import matplotlib.pyplot as plt
from step_2_1 import OUT_DIR
from step_3_1 import load_plot_data

plot_data = load_plot_data()
fig, ax = plt.subplots()
ax.barh(plot_data[&quot;stem&quot;], plot_data[&quot;size&quot;])
fig.savefig(OUT_DIR / f&quot;{Path(__file__).stem}.png&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시각화한 png 파일 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_3_2.png&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eQNZvV/dJMb997Yxl4/X0oIjATQmHwMD7njTrqgMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eQNZvV/dJMb997Yxl4/X0oIjATQmHwMD7njTrqgMK/img.png&quot; data-alt=&quot;step_3_2.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eQNZvV/dJMb997Yxl4/X0oIjATQmHwMD7njTrqgMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeQNZvV%2FdJMb997Yxl4%2FX0oIjATQmHwMD7njTrqgMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;400&quot; data-filename=&quot;step_3_2.png&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;step_3_2.png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프로는 나왔는데 몇가지 문제가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 해상도 변경 불가능&lt;/li&gt;
&lt;li&gt;차트 여백이 일괄적으로 설정되어 폴더명이 긴 경우(ex. Applications) 잘려서 표시됨&lt;/li&gt;
&lt;li&gt;특정 폴더 크기가 너무 크면, 다른 폴더는 상대적으로 너무 작아서 시각화한 의미가 없어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt; 보기 좋은 차트 만들기 &amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782721405632&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# step_3_3.py
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from step_2_1 import OUT_DIR
from step_3_1 import load_plot_data

plot_data = load_plot_data()
log_size = np.log(plot_data[&quot;size&quot;])
fig, ax = plt.subplots(figsize=(16, 9), dpi=100)
ax.barh(plot_data[&quot;stem&quot;], log_size)
ax.grid(True, axis = &quot;x&quot;)
ax.tick_params(labelbottom=False, length=0, labelsize=20)
fig.set_layout_engine(&quot;tight&quot;)
fig.savefig(OUT_DIR / f&quot;{Path(__file__).stem}.png&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;matplotlib 패키지와 numpy 패키지를 활용하여 바 차트를 보기 좋게 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 log() 를 사용하여, 숫자 데이터를 로그스케일로 변환하여 특정 데이터의 크기가 지나치게 큰 경우 단위를 조정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 subplots()를 사용하여 객체 Figure와 Axes를 생성하고 각각 fig, ax 변수에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 매개변수 figsize에 차트크기를 (가로, 세로) 형식으로, dpi에 해상도를 전달하여 전체 해상도를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 set_layout_engine() 사용하여 차트 여백을 조정한다. 이 함수의 입력값으로 tight 를 전달하면 차트 여백을 최소화할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;step_3_3.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ3I5t/dJMcagMSlEV/GUmYFdXMvlNyOZbwin1PY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ3I5t/dJMcagMSlEV/GUmYFdXMvlNyOZbwin1PY1/img.png&quot; data-alt=&quot;step_3_3.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ3I5t/dJMcagMSlEV/GUmYFdXMvlNyOZbwin1PY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ3I5t%2FdJMcagMSlEV%2FGUmYFdXMvlNyOZbwin1PY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;385&quot; data-filename=&quot;step_3_3.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;step_3_3.png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폴더 크기를 측정하기에 유의미한 그래프 이미지를 얻을 수 있다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Type Hint&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;x = 123 # 이것보다
x : int = 123 # 이렇게 힌트와 함께 정의하면 보다 명확하다.
y : str = &quot;python king&quot; # 이렇게

# 함수 정의에서도 똑같이 쓸 수 있다. 매개변수/리턴 타입 모두!

def get_size(path : Path) -&amp;gt; int :
# 이렇게 정의하면? 매개변수 path는 Path 타입, 함수는 int를 반환할 것이란걸 쉽게 알 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;f-string&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;print(f&quot;{filesize=}&quot;) #1
print(f&quot;filesize={filesize}&quot;) #2

# 이 둘은 서로 같은 기능!
# 1번처럼 쓰면, 변수 이름까지 자동으로 출력해주는것
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>짧은 호흡/Python</category>
      <category>Python</category>
      <author>jiwoo.lee</author>
      <guid isPermaLink="true">https://jiwoo.tistory.com/3</guid>
      <comments>https://jiwoo.tistory.com/entry/%EC%A7%A7%EC%9D%80%ED%98%B8%ED%9D%A1-Python-Ch1#entry3comment</comments>
      <pubDate>Mon, 29 Jun 2026 00:22:26 +0900</pubDate>
    </item>
  </channel>
</rss>