<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.nuridol.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.nuridol.net/" rel="alternate" type="text/html" /><updated>2026-07-07T11:03:01+09:00</updated><id>https://www.nuridol.net/feed.xml</id><title type="html">NuRi’s 몰라도 되는 개발 이야기</title><subtitle>이런 저런 개발 관련 이야기를 하는 곳</subtitle><author><name>NuRi</name></author><entry><title type="html">번역한 책이 나왔습니다 - 문제 해결력을 높이는 알고리즘과 자료 구조</title><link href="https://www.nuridol.net/book/algorithm-book/" rel="alternate" type="text/html" title="번역한 책이 나왔습니다 - 문제 해결력을 높이는 알고리즘과 자료 구조" /><published>2022-02-17T21:10:00+09:00</published><updated>2022-02-17T21:10:00+09:00</updated><id>https://www.nuridol.net/book/algorithm-book</id><content type="html" xml:base="https://www.nuridol.net/book/algorithm-book/"><![CDATA[<p>일 끝나고 틈틈이 번역한 책이 나왔습니다. 이번에는 알고리즘 관련 책입니다.</p>

<h2 id="표지-및-링크">표지 및 링크</h2>

<p><a href="https://link.coupang.com/a/jP8fT"><img src="https://image.aladin.co.kr/product/28927/42/cover/k422836210_1.jpg" alt="img" /></a><br />
<a href="https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=289274245&amp;ttbkey=ttbnuridol1505002&amp;COPYPaper=1">문제 해결력을 높이는 알고리즘과 자료 구조</a><br />
오츠키 켄스케 지음, 서수환 옮김, 아키바 타쿠야 감수/길벗</p>

<h2 id="책소개">책소개</h2>

<p>프로그래밍하면서 문제를 풀다 보면 좀 더 빠르게, 좀 더 효율적으로 풀고 싶을 때가 생깁니다. 그런 방법을 찾아보면 등장하는 단어가 알고리즘이지요. 이 책은 지금까지 알려진 알고리즘이 왜 그렇게 만들어졌는지, 어떤 방식으로 동작하는지, 어떻게 응용 가능한지 알려주면서 함께 생각해 보는 책입니다.<br />
초보자를 비롯해 중급 이상 실력인 분에게 좋은 내용이 많습니다. 한번 읽어 보세요.</p>

<p>출판일이 2022-02-22이라서 재미있네요. ^^</p>

<p><img src="https://user-images.githubusercontent.com/1617715/154499651-f2837986-35ed-419d-b24a-46a64b8b8aa1.jpg" alt="책소개" /></p>]]></content><author><name>NuRi</name></author><category term="book" /><category term="book" /><category term="algorithm" /><summary type="html"><![CDATA[문제 해결력을 높이는 알고리즘과 자료 구조 - 코딩 테스트, 프로그래밍 경진대회 전 필독서!]]></summary></entry><entry><title type="html">Selenium + Python를 사용할 때 유용한 코드 몇 개</title><link href="https://www.nuridol.net/python/selenium-tips/" rel="alternate" type="text/html" title="Selenium + Python를 사용할 때 유용한 코드 몇 개" /><published>2021-02-20T08:20:00+09:00</published><updated>2021-02-20T08:20:00+09:00</updated><id>https://www.nuridol.net/python/selenium-tips</id><content type="html" xml:base="https://www.nuridol.net/python/selenium-tips/"><![CDATA[<p><a href="https://www.selenium.dev">Selenium</a>은 브라우저 자동화 테스트를 지원하는 프레임워크입니다. 만든 웹사이트 동작을 확인하거나 반복 작업을 줄이고 싶을 때 좋은 도구이지요. 그런 Selenium을 사용할 때 알아두면 좋은 코드를 몇 개 소개합니다.</p>

<h2 id="시작하기-전에">시작하기 전에</h2>

<p>Selenium은 여러 프로그래밍 언어를 사용할 수 있지만 여기서는 파이썬 코드를 소개합니다.</p>

<h2 id="마우스-휠-동작-흉내내기">마우스 휠 동작 흉내내기</h2>

<p>출처: <a href="https://stackoverflow.com/questions/47274852/mouse-scroll-wheel-with-selenium-webdriver-on-element-without-scrollbar">Mouse scroll wheel with selenium webdriver, on element without scrollbar?</a></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">wheel_element</span><span class="p">(</span><span class="n">element</span><span class="p">,</span> <span class="n">deltaY</span><span class="o">=</span><span class="mi">120</span><span class="p">,</span> <span class="n">offsetX</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">offsetY</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
    <span class="n">error</span> <span class="o">=</span> <span class="n">element</span><span class="p">.</span><span class="n">_parent</span><span class="p">.</span><span class="n">execute_script</span><span class="p">(</span><span class="s">"""
        var element = arguments[0];
        var deltaY = arguments[1];
        var box = element.getBoundingClientRect();
        var clientX = box.left + (arguments[2] || box.width / 2);
        var clientY = box.top + (arguments[3] || box.height / 2);
        var target = element.ownerDocument.elementFromPoint(clientX, clientY);

        for (var e = target; e; e = e.parentElement) {
        if (e === element) {
            target.dispatchEvent(new MouseEvent('mouseover', {view: window, bubbles: true, cancelable: true, clientX: clientX, clientY: clientY}));
            target.dispatchEvent(new MouseEvent('mousemove', {view: window, bubbles: true, cancelable: true, clientX: clientX, clientY: clientY}));
            target.dispatchEvent(new WheelEvent('wheel',     {view: window, bubbles: true, cancelable: true, clientX: clientX, clientY: clientY, deltaY: deltaY}));
            return;
        }
        }    
        return "Element is not interactable";
        """</span><span class="p">,</span> <span class="n">element</span><span class="p">,</span> <span class="n">deltaY</span><span class="p">,</span> <span class="n">offsetX</span><span class="p">,</span> <span class="n">offsetY</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">error</span><span class="p">:</span>
        <span class="c1">#raise WebDriverException(error)
</span>        <span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">"fail to scroll"</span><span class="p">)</span>

    <span class="k">return</span>

<span class="c1"># 마우스 휠 위로
</span><span class="n">wheel_element</span><span class="p">(</span><span class="n">elm</span><span class="p">,</span> <span class="o">-</span><span class="mi">120</span><span class="p">)</span>
<span class="c1"># 마우스 휠 아래로
</span><span class="n">wheel_element</span><span class="p">(</span><span class="n">elm</span><span class="p">,</span> <span class="mi">120</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="브라우저에-저장된-blob-저장하기">브라우저에 저장된 blob 저장하기</h2>

<p>출처: <a href="https://stackoverflow.com/questions/47424245/how-to-download-an-image-with-python-3-selenium-if-the-url-begins-with-blob">How to download an image with Python 3/Selenium if the URL begins with “blob:”?</a></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_file_content_chrome</span><span class="p">(</span><span class="n">driver</span><span class="p">,</span> <span class="n">uri</span><span class="p">):</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">driver</span><span class="p">.</span><span class="n">execute_async_script</span><span class="p">(</span><span class="s">"""
        var uri = arguments[0];
        var callback = arguments[1];
        var toBase64 = function(buffer){for(var r,n=new Uint8Array(buffer),t=n.length,a=new Uint8Array(4*Math.ceil(t/3)),i=new Uint8Array(64),o=0,c=0;64&gt;c;++c)i[c]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charCodeAt(c);for(c=0;t-t%3&gt;c;c+=3,o+=4)r=n[c]&lt;&lt;16|n[c+1]&lt;&lt;8|n[c+2],a[o]=i[r&gt;&gt;18],a[o+1]=i[r&gt;&gt;12&amp;63],a[o+2]=i[r&gt;&gt;6&amp;63],a[o+3]=i[63&amp;r];return t%3===1?(r=n[t-1],a[o]=i[r&gt;&gt;2],a[o+1]=i[r&lt;&lt;4&amp;63],a[o+2]=61,a[o+3]=61):t%3===2&amp;&amp;(r=(n[t-2]&lt;&lt;8)+n[t-1],a[o]=i[r&gt;&gt;10],a[o+1]=i[r&gt;&gt;4&amp;63],a[o+2]=i[r&lt;&lt;2&amp;63],a[o+3]=61),new TextDecoder("ascii").decode(a)};
        var xhr = new XMLHttpRequest();
        xhr.responseType = 'arraybuffer';
        xhr.onload = function(){ callback(toBase64(xhr.response)) };
        xhr.onerror = function(){ callback(xhr.status) };
        xhr.open('GET', uri);
        xhr.send();
        """</span><span class="p">,</span> <span class="n">uri</span><span class="p">)</span>
    <span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="o">==</span> <span class="nb">int</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="s">"Request failed with status %s"</span> <span class="o">%</span> <span class="n">result</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>

<span class="c1"># 저장하기
</span><span class="n">bytes2</span> <span class="o">=</span> <span class="n">get_file_content_chrome</span><span class="p">(</span><span class="n">driver</span><span class="p">,</span> <span class="n">res_url</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="s">'wb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">binary_file</span><span class="p">:</span>
    <span class="n">binary_file</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">bytes2</span><span class="p">)</span>
    <span class="n">binary_file</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span>

</code></pre></div></div>

<h2 id="iframe-전환하기">iframe 전환하기</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">target_iframe</span><span class="o">=</span><span class="s">"전환하기 원하는 iframe 엘리먼트 ID"</span>
<span class="k">for</span> <span class="n">iframe</span> <span class="ow">in</span> <span class="n">driver</span><span class="p">.</span><span class="n">find_elements_by_tag_name</span><span class="p">(</span><span class="s">'iframe'</span><span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">id_info</span> <span class="o">=</span> <span class="n">iframe</span><span class="p">.</span><span class="n">get_attribute</span><span class="p">(</span><span class="s">'id'</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">target_iframe</span> <span class="o">==</span> <span class="n">id_info</span><span class="p">:</span>
            <span class="n">driver</span><span class="p">.</span><span class="n">switch_to</span><span class="p">.</span><span class="n">frame</span><span class="p">(</span><span class="n">iframe</span><span class="p">)</span>
            <span class="k">break</span>
    <span class="k">except</span><span class="p">:</span>
        <span class="c1"># back to original frame
</span>        <span class="n">driver</span><span class="p">.</span><span class="n">switch_to_default_content</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="크롬-퍼포먼스-로그-확인하기">크롬 퍼포먼스 로그 확인하기</h2>

<p>브라우저에서 주고 받는 네트워크 트래픽 정보를 확인할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 필요 정보만 추출
</span><span class="k">def</span> <span class="nf">process_browser_log_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">):</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">entry</span><span class="p">[</span><span class="s">'message'</span><span class="p">])[</span><span class="s">'message'</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">response</span>

<span class="c1"># 로그 기능 활성화
</span><span class="n">caps</span> <span class="o">=</span> <span class="n">DesiredCapabilities</span><span class="p">.</span><span class="n">CHROME</span>
<span class="n">caps</span><span class="p">[</span><span class="s">'loggingPrefs'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="s">'performance'</span><span class="p">:</span> <span class="s">'ALL'</span><span class="p">}</span>
<span class="n">caps</span><span class="p">[</span><span class="s">'goog:loggingPrefs'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="s">'performance'</span><span class="p">:</span> <span class="s">'ALL'</span><span class="p">}</span>

<span class="n">driver</span> <span class="o">=</span> <span class="n">webdriver</span><span class="p">.</span><span class="n">Chrome</span><span class="p">(</span><span class="n">CHROME_DRV_PATH</span><span class="p">,</span> <span class="n">desired_capabilities</span><span class="o">=</span><span class="n">caps</span><span class="p">)</span>

<span class="c1"># get performance logs
</span><span class="n">browser_log</span> <span class="o">=</span> <span class="n">driver</span><span class="p">.</span><span class="n">get_log</span><span class="p">(</span><span class="s">'performance'</span><span class="p">)</span>
<span class="n">events</span> <span class="o">=</span> <span class="p">[</span><span class="n">process_browser_log_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span> <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">browser_log</span><span class="p">]</span>
<span class="k">for</span> <span class="n">event</span> <span class="ow">in</span> <span class="n">events</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="json-보기-좋게-출력하기">JSON 보기 좋게 출력하기</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ensure_ascii=False로 하면 유니코드로 제대로 출력
</span><span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">json_data</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">))</span>
</code></pre></div></div>]]></content><author><name>NuRi</name></author><category term="python" /><category term="python" /><category term="code" /><category term="selenium" /><summary type="html"><![CDATA[자동화 테스트를 할 때 필요할지도 모를 코드를 모아 봤습니다.]]></summary></entry><entry><title type="html">Git 자동 커밋하기 스크립트</title><link href="https://www.nuridol.net/command/auto-git-commit/" rel="alternate" type="text/html" title="Git 자동 커밋하기 스크립트" /><published>2021-02-11T21:00:00+09:00</published><updated>2021-02-11T21:00:00+09:00</updated><id>https://www.nuridol.net/command/auto-git-commit</id><content type="html" xml:base="https://www.nuridol.net/command/auto-git-commit/"><![CDATA[<p>요즘은 Git이 거의 표준인 소스 코드 관리 프로그램이지요. 개인 프로젝트를 할 때라도 3일 뒤 나를 위해서라도 수정한 코드를 잊지 않고 커밋하는 습관을 들이면 좋지만 매번 하려니 귀찮다면 <code class="language-plaintext highlighter-rouge">cron</code>에 등록해서 매일 알아서 커밋하는 방법도 있습니다.</p>

<h2 id="시작하기-전에">시작하기 전에</h2>

<p>crontab 설정법은 <a href="https://crontab.guru">crontab guru</a> 페이지를 살펴 보세요.</p>

<h2 id="shell-script-파일">shell script 파일</h2>

<script src="https://gist.github.com/nuridol/e5b6690ac22e148d5646f03c1f73b39f.js"> </script>]]></content><author><name>NuRi</name></author><category term="command" /><category term="git" /><category term="command" /><summary type="html"><![CDATA[개발 환경에서 잊지 않고 커밋하려면]]></summary></entry><entry><title type="html">mitmproxy로 스트리밍 다운로드 해보기</title><link href="https://www.nuridol.net/python/mitmproxy/" rel="alternate" type="text/html" title="mitmproxy로 스트리밍 다운로드 해보기" /><published>2021-01-18T09:00:00+09:00</published><updated>2021-01-18T09:00:00+09:00</updated><id>https://www.nuridol.net/python/mitmproxy</id><content type="html" xml:base="https://www.nuridol.net/python/mitmproxy/"><![CDATA[<p>모바일앱 디버깅에 자주 사용하는 mitmproxy 기능을 활용해서 지금 폰으로 보고 있는 영상을 다운로드하는 스크립트 예제를 만들어 봤습니다.</p>

<h2 id="시작하기-전에">시작하기 전에</h2>

<p>예전에 사용하던 Burp 인증서를 그대로 활용하려면 변환 과정이 필요합니다.
<code class="language-plaintext highlighter-rouge">openssl</code> 명령어로 pem 형식으로 변환합니다.</p>

<h2 id="소스-코드">소스 코드</h2>

<p>요청과 응답에서 각각 필요한 정보를 따서 스트리밍을 저장하는 명령어를 호출하는 스크립트입니다.
자세한 설명은 생략합니다.</p>

<script src="https://gist.github.com/nuridol/0decdece8c5bf86789b7229a9d7a7c58.js"> </script>]]></content><author><name>NuRi</name></author><category term="python" /><category term="mitmproxy" /><category term="debugging" /><category term="proxy" /><category term="python" /><category term="command" /><summary type="html"><![CDATA[mitmproxy용 스크립트로 모바일 기기에서 본 스트리밍 영상을 자동으로 저장해봅시다.]]></summary></entry><entry><title type="html">Mac OS의 숨은 설정 옵션을 지정하는 defaults 명령어</title><link href="https://www.nuridol.net/command/mac-setting/" rel="alternate" type="text/html" title="Mac OS의 숨은 설정 옵션을 지정하는 defaults 명령어" /><published>2020-12-14T15:00:00+09:00</published><updated>2020-12-14T15:00:00+09:00</updated><id>https://www.nuridol.net/command/mac-setting</id><content type="html" xml:base="https://www.nuridol.net/command/mac-setting/"><![CDATA[<p>Mac OS는 깔끔하고 정돈된 GUI가 유명하지만 그런 만큼 환경 설정 화면에서 찾을 수 없는 숨은 옵션도 꽤 있는 편입니다. 그런 옵션 중에 유용할 수도 있는 옵션을 몇 가지 알아 봅시다.</p>

<h2 id="기본앱-관련-설정">기본앱 관련 설정</h2>

<h3 id="숨긴-앱을-독에서-반투명하게-표시하기">숨긴 앱을 독에서 반투명하게 표시하기</h3>

<p><img src="https://user-images.githubusercontent.com/1617715/102049573-75bd0a00-3e24-11eb-8858-4df29c305c89.png" alt="Dim jidden mac apps on the Dock" /></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.Dock showhidden <span class="nt">-bool</span> <span class="nb">yes</span><span class="p">;</span> killall Dock
</code></pre></div></div>

<p>이 명령어를 실행하면 <code class="language-plaintext highlighter-rouge">⌘+H</code> 키를 눌러 화면에서 숨긴 앱이 독에 반투명하게 표시됩니다.</p>

<h3 id="재부팅해도-전에-열었던-파인더-탭을-유지하기">재부팅해도 전에 열었던 파인더 탭을 유지하기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.finder NSQuitAlwaysKeepsWindows <span class="nt">-bool</span> <span class="nb">true</span>
</code></pre></div></div>

<p>환경 설정의 General에 있는 <strong>Close windows when quitting an app</strong> 체크를 해제해도 되지만 다른 앱들은 모두 닫아도 파인더는 유지하고 싶으면 위 명령어를 실행하면 됩니다. 때에 따라서는 제대로 저장이 안될 때도 있지만요.</p>

<p><code class="language-plaintext highlighter-rouge">com.apple.finder</code> 부분에 상태를 저장하고 싶은 앱 ID를 대신 넣어도 사용 가능합니다.</p>

<h3 id="파일-다이얼로그를-늘-확장한-형태로-열기">파일 다이얼로그를 늘 확장한 형태로 열기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode <span class="nt">-bool</span> <span class="nb">true</span> <span class="o">&amp;&amp;</span> defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 <span class="nt">-bool</span> <span class="nb">true</span>
</code></pre></div></div>

<p>파일을 열거나 저장할 때 열리는 창을 늘 확장한, 즉 경로나 폴더 위치를 목록으로 표시하는 상태로 만들어 주는 옵션입니다.</p>

<h3 id="네트워크-드라이브나-usb에-맥용-숨은-파일-만들지-않기">네트워크 드라이브나 USB에 맥용 숨은 파일 만들지 않기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.desktopservices DSDontWriteNetworkStores <span class="nt">-bool</span> <span class="nb">true
</span>defaults write com.apple.desktopservices DSDontWriteUSBStores <span class="nt">-bool</span> <span class="nb">true</span>
</code></pre></div></div>

<p>맥에서 작업하던 USB를 윈도에 가져가서 확인해보면 만든 적도 없는 .DS_store 같은 파일이나 폴더가 생겨 있는 걸 경험해본 적이 있을 겁니다.</p>

<p>이런 파일은 메타 정보를 관리하기 위해 사용하지만 다른 OS에서는 필요하지 않고, 없어도 사용에는 큰 지장이 없으므로 다른 OS에서 사용하는 빈도가 잦은 네트워크 드라이브나 USB에 그런 파일을 작성하지 않도록 설정하는 명령어입니다.</p>

<h3 id="배터리-남은-잔량--표시하기">배터리 남은 잔량 % 표시하기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.menuextra.battery ShowPercent <span class="nt">-string</span> <span class="s2">"YES"</span>
</code></pre></div></div>

<p>최신 OS에서도 가능한지는 테스트해보지 않았습니다.</p>

<h3 id="충돌-보고서-사용하지-않기">충돌 보고서 사용하지 않기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>defaults write com.apple.CrashReporter DialogType none
</code></pre></div></div>

<p>앱이 갑자기 충돌이 나서 실행 종료되고 나면 보고서를 보내겠냐고 물어보는 창이 나오는데 매번 표시되면 좀 귀찮습니다. 그런 창이 출력되지 않게 하는 명령어입니다.</p>

<h2 id="정리">정리</h2>

<p>그외에도 유용한 옵션이 있으니 다음 링크를 확인해 보거나 <em>mac os defaults write</em> 키워드로 검색해 보세요.</p>

<ul>
  <li><a href="https://qiita.com/REGO350/items/959d6a7f767021acaf42">https://qiita.com/REGO350/items/959d6a7f767021acaf42</a></li>
  <li><a href="https://gist.github.com/atadams/3085530">https://gist.github.com/atadams/3085530</a></li>
</ul>]]></content><author><name>NuRi</name></author><category term="command" /><category term="macos" /><category term="shell" /><category term="command" /><summary type="html"><![CDATA[defaults 명령어로 숨은 설정 지정해보기]]></summary></entry><entry><title type="html">자주 쓰는 FFmpeg 명령어들</title><link href="https://www.nuridol.net/command/ffmpeg/" rel="alternate" type="text/html" title="자주 쓰는 FFmpeg 명령어들" /><published>2020-12-13T15:00:00+09:00</published><updated>2020-12-13T15:00:00+09:00</updated><id>https://www.nuridol.net/command/ffmpeg</id><content type="html" xml:base="https://www.nuridol.net/command/ffmpeg/"><![CDATA[<p><a href="https://ffmpeg.org/download.html">FFmpeg</a>는 동영상 관련 처리를 할 때 많이 사용하는 오픈소스 소프트웨어입니다. 동영상 플레이어인 Movist나 팟플레이어도 내부적으로 사용하는 걸로 알려져 있습니다. 요즘은 동영상 추출이나 스트리밍, 다운로드 등에서도 자주 사용하지요.</p>

<p>그런데 GUI 화면을 보면서 설정하는 게 아니라 옵션을 키보드로 입력해서 사용하는 방식이라 사용법이 조금 까다롭습니다. 하지만 한 번 익혀두면 이래저래 유용하게 일괄 처리하는데 편리합니다. 그런 편리한 FFmpeg 명령어 중에 개인적으로 종종 사용하는 것을 정리해 보았습니다.</p>

<p>명령어는 Mac OS를 기준으로 설명합니다.</p>

<h2 id="설치하기">설치하기</h2>

<p><code class="language-plaintext highlighter-rouge">brew</code>를 사용해서 설치하는 것이 일반적입니다. 설치 방법은 다른 좋은 글이 많으니 검색해 보세요.</p>

<h2 id="alias-설정">alias 설정</h2>

<p>FFmpeg를 그냥 사용할 수도 있지만, 다음처럼 alias를 사용해서 공통 옵션을 설정해두면 조금 더 편리합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">ffmpegu</span><span class="o">=</span><span class="s1">'ffmpeg -hide_banner -loglevel info -stats -analyzeduration 10M -probesize 50M -user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15" -allowed_extensions ALL -protocol_whitelist "file,http,https,tcp,tls,crypto" '</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-hide_banner -loglevel info -stats</code>는 실행할 때 나오는 FFmpeg 정보를 숨기고 로그 레벨은 info, 진행 상황 정보는 출력하겠다는 내용입니다. <code class="language-plaintext highlighter-rouge">-loglevel warning</code>이나 <code class="language-plaintext highlighter-rouge">-loglevel error</code>로 바꾸면 어떤 이상이 생겼을 때만 로그가 출력되어서 조금 더 화면이 깔끔해집니다.</p>

<p><code class="language-plaintext highlighter-rouge">-analyzeduration 10M -probesize 50M</code>는 입력 파일을 분석할 버퍼를 설정하는 옵션입니다. 이 옵션을 사용하면 초반에 음성 트랙이 여러 개이거나 비디오 ID가 바뀌는 경우도 정상적으로 처리할 수 있게 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">-user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15"</code>는 마치 브라우저에서 보는 것처럼 유저 정보를 서버에 보내고 싶을 때 사용합니다. 가끔 이 정보를 확인하기도 하므로 미리 추가해 둡시다.</p>

<p><code class="language-plaintext highlighter-rouge">-allowed_extensions ALL -protocol_whitelist "file,http,https,tcp,tls,crypto"</code>는 보안상 기본적으로 사용하지 못하게 하는 프로토콜이나 확장 기능을 사용할 수 있도록 하는 옵션입니다. 파일 목록으로 합치기 등등의 기능을 사용하려면 필요한 옵션입니다.</p>

<p>이 내용을 <code class="language-plaintext highlighter-rouge">.zshrc</code>나 <code class="language-plaintext highlighter-rouge">.bashrc</code>에 추가해 두면 앞으로 설명하는 명령어 예문에서 <code class="language-plaintext highlighter-rouge">ffmpeg</code> 대신에 <code class="language-plaintext highlighter-rouge">ffmpegu</code>를 사용하면 됩니다.</p>

<h2 id="인코딩">인코딩</h2>

<p>아마도 FFmpeg를 사용하는 가장 주된 목적은 인코딩이겠지요. FFmpeg가 지원하는 인코딩 코덱은 <code class="language-plaintext highlighter-rouge">ffmpeg -encoders</code>로 확인 가능한데 그중에서도 요즘 자주 쓰이는 MP4로 인코딩하는 명령어 예를 가지고 살펴 봅시다. FFmpeg 옵션은 지정 순서가 무척 중요해서 순서를 잘못 지정하면 에러가 발생하기도 하니 주의해야 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 입력 파일을 디인터페이스해서 1280x720 해상도인 mp4로 저장하기</span>
ffmpeg <span class="nt">-fflags</span> +discardcorrupt <span class="nt">-i</span> <span class="s1">'입력 파일'</span> <span class="nt">-vf</span> <span class="s2">"yadif=0:-1:0,scale=1280:720"</span> <span class="nt">-c</span>:v libx264 <span class="nt">-preset</span>:v veryfast <span class="nt">-crf</span> 22 <span class="nt">-movflags</span> +faststart <span class="nt">-c</span>:a aac <span class="nt">-b</span>:a 160k <span class="s1">'출력 파일.mp4'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-fflags +discardcorrupt</code>는 입력 파일에 있는 깨진 패킷은 무시하겠다는 옵션입니다. 불안정한 인터넷 상태나 저장 실패 때문에 손상된 파일을 처리할 때 유용합니다. 이런 옵션은 <a href="https://ffmpeg.org/ffmpeg-formats.html">FFmpeg 공식 도움말</a>에서 확인 가능합니다.</p>

<p><code class="language-plaintext highlighter-rouge">-i '입력 파일'</code>은 <code class="language-plaintext highlighter-rouge">input</code>을 지정하는 옵션입니다. 변환할 원본 파일 경로를 지정하면 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">-vf "yadif=0:-1:0,scale=1280:720"</code> 이 옵션은 디인터레이스(yadif=0:-1:0), 해상도 변환(scale=1280:720)을 실시하는 video filter를 지정하는 부분입니다. 더욱 자세한 내용은 <a href="https://ffmpeg.org/ffmpeg-filters.html">도움말</a> 참조하세요.</p>

<p><code class="language-plaintext highlighter-rouge">-c:v libx264 -preset:v veryfast -crf 22</code>는 libx264 인코더로 영상을 mpeg-4 코덱으로 인코딩하겠다는 옵션입니다. crf 22로 화질을 정하고(숫자가 작을수록 고화질, 고용량) preset:v veryfast으로 인코딩 시간과 압축 효율을 결정합니다. 경험상 veryfast가 일반적인 사용에 적당합니다.</p>

<p><code class="language-plaintext highlighter-rouge">-movflags +faststart</code>는 MP4 영상의 크기, 재생 시간 같은 메타 정보가 든 moov atom을 파일 첫부분으로 옮겨서 스트리밍 등으로 볼 때 빠르게 재생할 수 있도록 만들어 주는 옵션입니다.</p>

<p><code class="language-plaintext highlighter-rouge">--c:a aac -b:a 160k</code> 옵션은 음성 코덱 지정 부분으로 aac 코덱으로 인코딩한다는 의미입니다. 비트레이트로 160k를 지정했는데 보다 고음질을 원하면 256k 등을 지정하면 됩니다.</p>

<p>복잡해 보이지만 FFmpeg는 이렇게 영상 관련 옵션, 음성 관련 옵션을 조합해서 사용하는 것이 기본 방식입니다.</p>

<h2 id="일부분-자르기">일부분 자르기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 영상을 00:01:00~00:03:00(2분간) 잘라서 저장하기</span>
ffmpeg <span class="nt">-ss</span> 00:01:00 <span class="nt">-i</span> <span class="s2">"in.ts"</span> <span class="nt">-to</span> 00:02:00 <span class="nt">-c</span> copy <span class="s2">"cut.ts"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-ss 00:01:00</code>은 영상 자르기 시작점을 지정하는 옵션입니다. ss 뒤에 오는 숫자는 <code class="language-plaintext highlighter-rouge">시:분:초</code> 형식입니다. 따라서 1분이 시작점이 됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">-to 00:02:00</code>은 시작점으로부터 얼마나 영상을 자를지 지정하는 옵션입니다. -to 뒤에 오는 시간은 영상 전체의 플레이 시간이 아니라 시작점으로부터 <strong>재생 시간(2분)</strong>이라는 점에 주의해야 합니다. 따라서 위 예제에서는 영상의 1분부터 3분 사이의 2분간(00:02:00)을 <code class="language-plaintext highlighter-rouge">cut.ts</code>라는 이름으로 저장하겠다는 뜻이 됩니다.</p>

<h2 id="영상을-프레임-단위로-이미지-저장하기">영상을 프레임 단위로 이미지 저장하기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 초당 10 프레임 단위로 디인터페이스, 720 해상도로 변환한 jpg 파일 저장하기</span>
<span class="nv">FPS</span><span class="o">=</span>10 ffmpeg <span class="nt">-i</span> <span class="s2">"in.ts"</span> <span class="nt">-vf</span> <span class="s2">"yadif=0:-1:0,scale=1280:720,fps=</span><span class="k">${</span><span class="nv">FPS</span><span class="k">}</span><span class="s2">"</span> <span class="nt">-qscale</span>:v 2 <span class="s2">"thumb_%04d.jpg"</span>
</code></pre></div></div>

<p>영상에 어떤 내용이 들어 있는지 빠르게 훑어 보고 싶거나 프레임 단위로 확인해 보고 싶을 때 유용한 명령어입니다.</p>

<p><code class="language-plaintext highlighter-rouge">-vf "yadif=0:-1:0,scale=1280:720,fps=${FPS}"</code>에서 앞의 두 옵션은 인코딩 옵션 때 본 것과 같습니다. fps 옵션이 초당 몇 프레임을 추출할지 지정하는 값인데, 여기서는 환경변수 옵션 <code class="language-plaintext highlighter-rouge">FPS</code>에 10을 지정해서 사용했습니다. <code class="language-plaintext highlighter-rouge">-vf "yadif=0:-1:0,scale=1280:720,fps=10"</code> 처럼 직접 숫자를 지정할 수도 있지만 이렇게 상황에 따라 바뀔 수도 있는 값은 변수를 사용하는 것이 조금 더 편리합니다.</p>

<p>` -qscale:v 2`는 jpg 파일 화질을 지정하는 옵션입니다. 숫자가 클수록 화질이 떨어지는데 2~5 값을 추천합니다.</p>

<p><code class="language-plaintext highlighter-rouge">"thumb_%04d.jpg"</code>는 출력할 파일명을 지정하는 옵션입니다. 추출한 프레임이 <code class="language-plaintext highlighter-rouge">thumb_0001.jpg</code>, <code class="language-plaintext highlighter-rouge">thumb_0002.jpg</code>, <code class="language-plaintext highlighter-rouge">thumb_0003.jpg</code>… 식으로 자동으로 번호순서대로 저장됩니다.</p>

<h2 id="영상-파일-간편-합치기">영상 파일 간편 합치기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> <span class="s2">"concat:in1.mp4|in2.mp4"</span> <span class="nt">-c</span> copy <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p>재인코딩 없이 두 영상을 하나로 합쳐서 저장하는 명령어입니다. in1.mp4와 in2.mp4가 입력 파일, out.mp4가 출력 파일이 됩니다. 입력의 두 파일은 같은 해상도, 같은 코덱을 사용해야 합니다.</p>

<h2 id="음성-파일에-섬네일을-지정해서-영상-파일-만들기">음성 파일에 섬네일을 지정해서 영상 파일 만들기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-loop</span> 1 <span class="nt">-i</span> <span class="s2">"thumb.jpg"</span> <span class="nt">-i</span> <span class="s2">"in.aac"</span> <span class="nt">-c</span>:v libx264 <span class="nt">-tune</span> stillimage <span class="nt">-acodec</span> copy <span class="nt">-pix_fmt</span> yuv420p <span class="nt">-shortest</span> <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p>유튜브 등에 음성을 올릴 때 사용할 수 있는 방법입니다. <code class="language-plaintext highlighter-rouge">thumb.jpg</code>라는 이미지를 이용해서 <code class="language-plaintext highlighter-rouge">in.aac</code> 음성 파일이 재생이 끝날 때까지 같은 이미지가 계속 출력되는 mp4 영상 파일을 작성할 수 있습니다.</p>

<h2 id="영상-회전하기">영상 회전하기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 90도 회전</span>
ffmpeg <span class="nt">-i</span> <span class="s2">"in.mp4"</span> <span class="nt">-metadata</span>:s:v <span class="s2">"rotate=90"</span> <span class="nt">-c</span> copy <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p>어쩌다 보니 옆으로 누워서 출력되는 영상을 회전시키고 싶을 때 사용하는 명령어입니다. 위 옵션을 사용하면 재인코딩 없이 90도 각도로 회전시킬 수 있습니다. <code class="language-plaintext highlighter-rouge">90</code>에 회전시키고 싶은 각도 값을 지정하면 됩니다.</p>

<h2 id="다른-영상과-음성을-합쳐서-새로운-파일로-저장하기">다른 영상과 음성을 합쳐서 새로운 파일로 저장하기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-stream_loop</span> <span class="nt">-1</span> <span class="nt">-i</span> <span class="s2">"video.mp4"</span> <span class="nt">-i</span> <span class="s2">"audio.aac"</span> <span class="nt">-c</span> copy <span class="nt">-shortest</span> <span class="nt">-map</span> 0:v:0 <span class="nt">-map</span> 1:a:0 <span class="nt">-y</span> <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p>기존에 존재하는 영상에 있는 음성을 좀 더 고음질 음성으로 교체하거나, 음성 출력 시간 동안 영상을 반복 재생하는 영상을 만들고 싶을 때 유용한 명령어입니다.</p>

<h2 id="자막-파일을-내장하는-mp4-파일-만들기">자막 파일을 내장하는 MP4 파일 만들기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> <span class="s2">"in.mp4"</span> <span class="nt">-i</span> <span class="s2">"sub.srt"</span> <span class="nt">-map</span> 0:v:0 <span class="nt">-c</span>:v copy <span class="nt">-map</span> 0:a:0 <span class="nt">-c</span>:a copy <span class="nt">-map</span> 1 <span class="nt">-c</span>:s:0 mov_text <span class="nt">-metadata</span>:s:s:0 <span class="nv">title</span><span class="o">=</span>srt <span class="nt">-metadata</span>:s:s:0 <span class="nv">language</span><span class="o">=</span>kor <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-map</code> 옵션은 여러 입력 파일이 있을 때 사용하는 옵션입니다. <code class="language-plaintext highlighter-rouge">-map 입력 인덱스:스트림 종류:스트림 인덱스</code> 형식으로 지정하는데 자세한 내용은 <a href="https://trac.ffmpeg.org/wiki/Map">위키</a>를 참조하기 바랍니다. 복잡하지만 익숙해지면 활용법이 무궁무진한 옵션입니다.</p>

<p><code class="language-plaintext highlighter-rouge">-map 0:v:0 -c:v copy -map 0:a:0 -c:a copy</code>은 0번 입력 파일(in.mp4)에서 영상과 음성을 복사해서 0번 출력(out.mp4)으로 보낸다는 의미입니다.</p>

<p><code class="language-plaintext highlighter-rouge">-map 1 -c:s:0 mov_text -metadata:s:s:0 title=srt -metadata:s:s:0 language=kor</code>은 1번 입력 파일(sub.srt)에서 자막을 복사해서 0번 출력(out.mp4)으로 보낸다는 의미입니다. FFmpeg는 srt 형식 자막을 지원합니다.</p>

<h2 id="번외편-nvidia-하드웨어-인코딩-사용하기">번외편: Nvidia 하드웨어 인코딩 사용하기</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-y</span> <span class="nt">-hide_banner</span> <span class="nt">-analyzeduration</span> 30M <span class="nt">-probesize</span> 100M <span class="nt">-fflags</span> +discardcorrupt  <span class="nt">-i</span> <span class="s2">"in.ts"</span> <span class="nt">-vf</span> <span class="s2">"yadif=0:-1:0,scale=1280:720"</span> <span class="nt">-map</span> 0:v:0 <span class="nt">-c</span>:v h264_nvenc <span class="nt">-preset</span>:v fast <span class="nt">-rc</span> constqp <span class="nt">-qp</span> 29.5 <span class="nt">-profile</span>:v high <span class="nt">-movflags</span> +faststart <span class="nt">-map</span> 0:a:0 <span class="nt">-c</span>:a:0 aac <span class="nt">-b</span>:a:0 160k <span class="nt">-metadata</span>:s:a:0 <span class="nv">title</span><span class="o">=</span><span class="s2">"Audio#1"</span>  <span class="s2">"out.mp4"</span>
</code></pre></div></div>

<p>맥에서는 더이상 사용할 수 없는 Nvidia이지만 윈도나 리눅스라면 이런 방식으로 하드웨어 인코딩을 사용할 수 있습니다. 하드웨어 인코딩은 CPU 인코딩에 비해 훨씬 빠르게 인코딩할 수 있는데 사용하는 그래픽 카드 성능에 따라 10배 이상 차이가 나기도 합니다. 훨씬 다양한 옵션이 존재하므로 자세한 내용은 검색해 보세요.</p>

<p>해당 인코더를 사용하려면 공식 홈페이지에서 배포하는 파일이 아니라 직접 빌드해서 사용해야 합니다.</p>

<h2 id="정리">정리</h2>

<p>이렇게 간단히 상황에 따라 유용한 FFmpeg 명령어를 살펴 보았습니다. 더욱 자세한 내용은 공식 사이트나 위키 페이지를 참조하세요.</p>

<ul>
  <li><a href="https://ffmpeg.org/documentation.html">FFmpeg - Document</a></li>
  <li><a href="https://trac.ffmpeg.org/wiki">FFmpeg Wiki</a></li>
  <li><a href="https://gist.github.com/Brainiarc7/3f7695ac2a0905b05c5b">compile-ffmpeg-nvenc.sh</a></li>
  <li><a href="https://gist.github.com/gfodor/16b216b354c656ea1078bbc54c364ca6">ffmpeg-minimalist-build-nvenc-static.md</a></li>
</ul>]]></content><author><name>NuRi</name></author><category term="command" /><category term="script" /><category term="shell" /><category term="ffmpeg" /><category term="command" /><summary type="html"><![CDATA[알아두면 언젠가 쓸 수 있을지도?]]></summary></entry><entry><title type="html">새 블로그</title><link href="https://www.nuridol.net/blog/first-post/" rel="alternate" type="text/html" title="새 블로그" /><published>2020-12-04T22:51:00+09:00</published><updated>2020-12-04T22:51:00+09:00</updated><id>https://www.nuridol.net/blog/first-post</id><content type="html" xml:base="https://www.nuridol.net/blog/first-post/"><![CDATA[<p>github.io 블로그로 개발 내용을 간단히 정리해 보려고 합니다.</p>]]></content><author><name>NuRi</name></author><category term="blog" /><category term="blog" /><summary type="html"><![CDATA[개발 관련 내용을 담으려고 합니다.]]></summary></entry></feed>