<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>需求變更｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/%E9%9C%80%E6%B1%82%E8%AE%8A%E6%9B%B4/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>需求變更｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/%E9%9C%80%E6%B1%82%E8%AE%8A%E6%9B%B4/</link><width>144</width><height>144</height></image>
    <description>林鼎淵（Dean Lin）的部落格：生成式 AI 企業內訓講師、外商資安公司軟體專家、7 本書作者。400 多篇文章用白話分享生成式 AI 實戰、Claude Code 與 Vibe Coding 教學、團隊合作與職場成長。</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <copyright>© 2020–2026 林鼎淵 Dean Lin</copyright>
    <lastBuildDate>Thu, 22 Feb 2024 13:57:29 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/%E9%9C%80%E6%B1%82%E8%AE%8A%E6%9B%B4/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>為什麼有些小變更需要等一段時間才能更新？ ── 工程師血淚史</title>
      <link>https://deanlin.net/blog/202402/why-small-changes-take-time/</link>
      <pubDate>Thu, 22 Feb 2024 13:57:29 +0800</pubDate>
      <guid>https://deanlin.net/blog/202402/why-small-changes-take-time/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>團隊合作</category>
      <description>在工程師的職涯中，「修改文字」應該是我遇過最多次的需求變更。</description>
      <media:content url="https://deanlin.net/blog/202402/why-small-changes-take-time/images/why-small-changes-take-time-og_hu_401252e20ce381df.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>在工程師的職涯中，「修改文字」應該是我遇過最多次的需求變更。</p>
<p>對不了解系統上線流程的人來說，他們會覺得這不過就是個小調整，為什麼每次都要我三催四請的？</p>
<p>對開發的工程師來說，他們會覺得這些人有完沒完啊？這些文字不是都已經討論過了，為什麼每隔幾天，甚至幾小時就要改一次？</p>
<p>實際上，改動文字對工程師來說可能花不到 1 分鐘的時間。</p>
<p>但對有上版流程的公司來說，每次更新可能都要花 1 小時以上（就算只有改一個字）。</p>
<p>今天這篇文章不是要談誰對誰錯，或是如何用技巧來迴避問題；而是要分享面對問題時，可以如何一步步「解決」他。</p>
<p>文章寫完後，感覺自己在寫技術發展史；另外，這是朋友向我分享的經驗，絕對不是我自己的。</p>
<p><img src="https://deanlin.net/blog/202402/why-small-changes-take-time/images/why-small-changes-take-time-01.webp" width="982" height="550" alt="為什麼有些小變更需要等一段時間才能更新？ ── 工程師血淚史（圖 1）" fetchpriority="high" decoding="async"></p>
<h2 id="石器時代寫死在程式碼">石器時代：寫死在程式碼<a class="h-anchor" href="#%e7%9f%b3%e5%99%a8%e6%99%82%e4%bb%a3%e5%af%ab%e6%ad%bb%e5%9c%a8%e7%a8%8b%e5%bc%8f%e7%a2%bc" aria-label="這個段落的連結"></a></h2>
<p>在最遠古的時期，網頁上的文字是寫死在程式碼的。</p>
<p>雖然直觀，但維護性極低。</p>
<p>因為面對「修改文字」的需求變更，開發人員可能改了 A 頁面，卻忘了還有 B、C、D 等頁面也用到一樣的詞彙。</p>
<hr>
<h2 id="大航海時代建立語言包維護">大航海時代：建立語言包維護<a class="h-anchor" href="#%e5%a4%a7%e8%88%aa%e6%b5%b7%e6%99%82%e4%bb%a3%e5%bb%ba%e7%ab%8b%e8%aa%9e%e8%a8%80%e5%8c%85%e7%b6%ad%e8%ad%b7" aria-label="這個段落的連結"></a></h2>
<p>如果系統只用同一種語言，那寫死在程式還勉強能維護。</p>
<p>但如果接下來要進軍國際市場，有中、日、英、法等多國語系的需求時，我想工程師就會崩潰了。</p>
<p>因為寫死在程式碼是完全行不通的，雖然很痛苦，但開發人員會需要把系統中所有的文字抽出來，建立「語言包」來維護才能解決這個需求。</p>
<p>這樣未來在面對「修改文字」的需求變更時，開發人員只要在單一檔案處理，透過模組化的管理，可以大幅降低遺漏的可能性。</p>
<hr>
<h2 id="工業時代提供-google-sheet-讓產品方填寫">工業時代：提供 Google Sheet 讓產品方填寫<a class="h-anchor" href="#%e5%b7%a5%e6%a5%ad%e6%99%82%e4%bb%a3%e6%8f%90%e4%be%9b-google-sheet-%e8%ae%93%e7%94%a2%e5%93%81%e6%96%b9%e5%a1%ab%e5%af%ab" aria-label="這個段落的連結"></a></h2>
<p>把文字集中到語言包維護後，其實面對修改文字的需求就很容易處理了。</p>
<p>但…接下來要討論的是「人」的問題。</p>
<p>因為產品方會一直看系統的文字，看著看著可能就覺得哪裡不順眼，不順眼就要改！</p>
<p>所以在新版本上線後，可能一週內會收到多次文字變更的需求。</p>
<p>在這樣的狀況下，現實生活中最容易遇到的就是「漏訊息」；當訊息有前後因果關係、重複資訊的時候，失誤在所難免。</p>
<p>為了減少產品方打擾 PM 與工程師的頻率，以及減少訊息遺漏的問題。</p>
<p>提供一份格式律定好的 Google Sheet，讓產品方在線上「修改文字」是一個不錯的方案。</p>
<p>這樣雙方約定一個時間一次性更新即可（會跑一個將 Google Sheet 轉成語言包的腳本）。</p>
<hr>
<h2 id="科技時代直接透過管理介面修改文字">科技時代：直接透過管理介面修改文字<a class="h-anchor" href="#%e7%a7%91%e6%8a%80%e6%99%82%e4%bb%a3%e7%9b%b4%e6%8e%a5%e9%80%8f%e9%81%8e%e7%ae%a1%e7%90%86%e4%bb%8b%e9%9d%a2%e4%bf%ae%e6%94%b9%e6%96%87%e5%ad%97" aria-label="這個段落的連結"></a></h2>
<p>改成 Google Sheet 後，雙方和平了好一段時間。</p>
<p>但還有一個問題，那就是這種修改沒有「即時性」。</p>
<p>因為程式每次上版都需要 Build Docker Image、跑回歸測試、上多個環境確認…</p>
<p>你可以理解為這些步驟至少要花費一小時以上的時間，而且會涉及到不同團隊的人（開發、QA、PM…）。</p>
<p>如果今天只是要修改文字，這樣的操作完全不符合成本效益。</p>
<p>所以目前的最新方案，就是在後台管理介面中，新增修改文字的功能，讓產品方可以依照自己的需求即時進行調整（搭配預覽頁）。</p>
<hr>
<h2 id="總結">總結<a class="h-anchor" href="#%e7%b8%bd%e7%b5%90" aria-label="這個段落的連結"></a></h2>
<p>修改文字是一個常見的需求變更，而且很多時候是在系統上線後才發生。</p>
<p>對工程師來說，修改文字這件事的確很簡單，但背後所需花費的時間與人力成本其實很高。</p>
<p>相信看完這篇文章後，大家可以理解為什麼有些小變更需要等一段時間才能更新。</p>
<p>儘管這篇不是純粹的技術文，但還是提醒一下讀者，每個時代的演進都需要不少時間成本。</p>
<p>這些改進所用到的技術並不困難，但問題在於：</p>
<ul>
<li>有足夠多的時間來做優化嗎？</li>
<li>這個問題真的有優化的必要性嗎？</li>
<li>優化帶來的效益符合成本嗎？</li>
</ul>
<p>因此千萬不要看完這篇文章後，就拿他去跟工程師吵架，我可不背這個黑鍋。</p>
<p>也許最後的方案在不久後會遇到其他挑戰，但這不就是「有問題我才有工作嗎？」</p>
]]></content:encoded>
    </item>
    <item>
      <title>需求不斷變更，到底哪個環節出錯了 ── 工程師血淚史</title>
      <link>https://deanlin.net/blog/202207/constantly-changing-requirements/</link>
      <pubDate>Thu, 07 Jul 2022 21:42:20 +0800</pubDate>
      <guid>https://deanlin.net/blog/202207/constantly-changing-requirements/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>團隊合作</category>
      <description>有些 PM 覺得跟工程師溝通很痛苦，甚至大腦會不時冒出這樣的想法：「明明只是修改一個小地方，為什麼總有各式各樣的理由拒絕跟拖延，就算答應修改，做起來也是一副心不甘情不願的樣子。」</description>
      <media:content url="https://deanlin.net/blog/202207/constantly-changing-requirements/images/constantly-changing-requirements-og_hu_3e60401d29677765.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>有些 PM 覺得跟工程師溝通很痛苦，甚至大腦會不時冒出這樣的想法：「明明只是修改一個小地方，為什麼總有各式各樣的理由拒絕跟拖延，就算答應修改，做起來也是一副心不甘情不願的樣子。」</p>
<p>其實大部分的工程師都是可以理性溝通的，但我們無法接受一直做「白工」和「重工」，今天筆者想透過一個案例來跟大家分享「客戶、PM、工程師」之間思維的差異，並提出可以改善的方案。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202207/constantly-changing-requirements/images/constantly-changing-requirements-01.webp" width="1158" height="605" alt="圖片來自：123RF" fetchpriority="high" decoding="async">
  <figcaption>圖片來自：123RF</figcaption>
</figure>

<blockquote>
<p>本文提及之專案、人物、事件純屬虛構，如有雷同，實屬巧合，請勿對號入座。</p>
</blockquote>
<pre tabindex="0"><code>大綱
一、工程師說 Loading 太重，可以請你幫忙分擔嗎？
二、需求不斷變更，緩衝時間再多也不夠用！
三、可是客戶態度很強硬啊！東西真的不符合他們需求
四、整理不完的例外，變更不斷的需求
五、從源頭解決問題，不是每個問題都要靠程式解決
六、客戶未必具備工程背景，要協助他們表達需求
七、我們要如何調整，讓未來的合作更加順利
</code></pre><h2 id="一工程師說-loading-太重可以請你幫忙分擔嗎">一、工程師說 Loading 太重，可以請你幫忙分擔嗎？<a class="h-anchor" href="#%e4%b8%80%e5%b7%a5%e7%a8%8b%e5%b8%ab%e8%aa%aa-loading-%e5%a4%aa%e9%87%8d%e5%8f%af%e4%bb%a5%e8%ab%8b%e4%bd%a0%e5%b9%ab%e5%bf%99%e5%88%86%e6%93%94%e5%97%8e" aria-label="這個段落的連結"></a></h2>
<p>PM 反饋這個問題後，我打開專案管理系統，看那位抱怨 Loading 太重的工程師手上有哪些 Ticket，是否需要做適當的調整。</p>
<p>結果仔細一看才發現，儘管那位工程師在這個 Sprint 中只剩下 2 張 Ticket 尚未完成，但有一張「匯出 Excel 報表」的 Ticket 預估時程遠遠超過之前在 Sprint Planning 的預估（4➝15）。</p>
<p>因為剩下的 2 個 Ticket 都是這次 Sprint 的核心功能，且距離結束只剩不到 2 個工作天，因此 PM 希望我可以接手那個估時已經到達 15 小時的 Ticket，讓工程師專注處理剩下的 Ticket。</p>
<p>跟 PM 談到這裡的時候我覺得有點奇怪，之所以將匯出 Excel 報表的 Ticket 分配給那位工程師，就是因為他之前有實作的經驗，沒道理在這個地方卡這麼久。</p>
<hr>
<h2 id="二需求不斷變更緩衝時間再多也不夠用">二、需求不斷變更，緩衝時間再多也不夠用！<a class="h-anchor" href="#%e4%ba%8c%e9%9c%80%e6%b1%82%e4%b8%8d%e6%96%b7%e8%ae%8a%e6%9b%b4%e7%b7%a9%e8%a1%9d%e6%99%82%e9%96%93%e5%86%8d%e5%a4%9a%e4%b9%9f%e4%b8%8d%e5%a4%a0%e7%94%a8" aria-label="這個段落的連結"></a></h2>
<p>為了釐清這個問題，我跟 PM、工程師開了一個臨時會議，去了解是什麼原因導致一個不困難的 Ticket 花了這麼多時間。</p>
<p>此時我才從工程師那邊了解到另一個事實，其實對他而言，匯出 Excel 報表的功能並不複雜，<strong>Loading 太重的原因是客戶不斷變更需求，導致這個 Ticket 的估時不斷增加，但預計交付的日期並沒有延長，因此時間壓力會全部壓到工程師身上</strong>。</p>
<p>我在詢問這個需求變更的頻率有多高時，工程師表示把新版交付出去後，可能再過 3、4 個小時就會收到新的需求變更；儘管每次修改都不會花太多時間，但架不住這麼頻繁的更動啊！</p>
<blockquote>
<p>當需求變更導致一直做白工、重工，就可能讓工程師工作效率下降，甚至可能在完成功能後故意拖一段時間再交付；畢竟提早交付沒好事，只是讓自己的工作量變更多而已。</p>
</blockquote>
<hr>
<h2 id="三可是客戶態度很強硬啊東西真的不符合他們需求">三、可是客戶態度很強硬啊！東西真的不符合他們需求<a class="h-anchor" href="#%e4%b8%89%e5%8f%af%e6%98%af%e5%ae%a2%e6%88%b6%e6%85%8b%e5%ba%a6%e5%be%88%e5%bc%b7%e7%a1%ac%e5%95%8a%e6%9d%b1%e8%a5%bf%e7%9c%9f%e7%9a%84%e4%b8%8d%e7%ac%a6%e5%90%88%e4%bb%96%e5%80%91%e9%9c%80%e6%b1%82" aria-label="這個段落的連結"></a></h2>
<p>客戶花錢請我們做系統，當然是希望做出符合他們實際業務需求的產品；站在客戶的立場上，他們當然會用最嚴格的標準在看待每一個功能。</p>
<p>但站在我們的立場上，在人力資源固定的狀態下，每多一次需求變更，我們就需要花更多的時間與精力去完成，甚至導致部分同仁需要加班。</p>
<p>為了解決這個問題，我請 PM 跟客戶約一個時間，一起了解需求不停變更的原因是什麼，不然這樣一直改下去其實是在浪費彼此的時間。</p>
<hr>
<h2 id="四整理不完的例外變更不斷的需求">四、整理不完的例外，變更不斷的需求<a class="h-anchor" href="#%e5%9b%9b%e6%95%b4%e7%90%86%e4%b8%8d%e5%ae%8c%e7%9a%84%e4%be%8b%e5%a4%96%e8%ae%8a%e6%9b%b4%e4%b8%8d%e6%96%b7%e7%9a%84%e9%9c%80%e6%b1%82" aria-label="這個段落的連結"></a></h2>
<p>客戶是一個專門提供材料出貨的公司，在深入訪談後，才了解需求之所以會一直變更，是因為這個 Excel 報表始終沒有決定一個固定的版本，會這樣是因為過去客戶在填寫出貨單時沒有律定格式，同一個材料可能有 10 幾種表達方式。</p>
<p>過去沒有遇到這個問題，是因為之前的出貨單都是 Case by Case，所以他們材料名稱儘管不統一，但依舊能夠看得懂；可是如果把好幾張出貨單放在一起看，就會因為資料很難整合而產生許多問題，這也是客戶找我們做系統化的原因。</p>
<p>下面用手機來舉個例子，我們手機有前鏡頭，假使在出貨單上顯示：「前鏡頭、前置相機、front camera、Front Camera、frontCamera」我相信大家都看得懂這是指同一個東西；但如果管理階層想要知道公司這個月到底賣了多少個「前鏡頭」，下屬就需要畫很多時間做比對跟整合。</p>
<p>除了相機的案例外，這邊再舉一個螢幕的例子，每個手機螢幕的尺寸大小不同，因此出貨單上有各自的命名；當管理階層想了解這個月總共賣了多少個「螢幕」，下屬會因為出貨單的材料沒有階層的概念而整理到崩潰。</p>
<p>在了解客戶的痛點後，我們就知道這是因為過去「命名不統一、材料缺乏階層概念」導致的問題。</p>
<p>PM 一開始也是從這個方向去處理問題的，他先請客戶將期望的命名、材料的階層整理出來，然後請工程師依照規格去統整；儘管這個方案看似沒有問題，但因為客戶當時在填寫出貨單時，命名太過狂野，因此有非常多的例外需要處理，而每發現一個例外就要請工程師調整程式。</p>
<hr>
<h2 id="五從源頭解決問題不是每個問題都要靠程式解決">五、從源頭解決問題，不是每個問題都要靠程式解決<a class="h-anchor" href="#%e4%ba%94%e5%be%9e%e6%ba%90%e9%a0%ad%e8%a7%a3%e6%b1%ba%e5%95%8f%e9%a1%8c%e4%b8%8d%e6%98%af%e6%af%8f%e5%80%8b%e5%95%8f%e9%a1%8c%e9%83%bd%e8%a6%81%e9%9d%a0%e7%a8%8b%e5%bc%8f%e8%a7%a3%e6%b1%ba" aria-label="這個段落的連結"></a></h2>
<p>了解來龍去脈後，我想也許我們都把問題搞複雜了，既然我們都知道問題是在出貨單上，為什麼不要從源頭來解決這個問題呢？</p>
<p>如果出貨單的格式沒有律定，在未來同樣的問題還是會發生；因為出貨單上每天都可能會誕生新的名詞，而新的名詞會無法自動歸類，可以料想到就算現在符合客戶的需求，在未來還是會有許多衍生問題。</p>
<p>當然過去的歷史業障很難要求客戶調整，「統一名詞、確定階層」還是一個必要的工作；但也許我們可以先用過去的出貨單模擬客戶期望的 Excel，而不是不斷請工程師修改程式。</p>
<blockquote>
<p>若發現有缺少、錯誤的材料先記錄下來，累積到一定的量後再請工程師修正，不然他會改到瘋。</p>
</blockquote>
<p>同時為了讓錯誤的歷史在這個階段就停下，我們需要請客戶與填寫出貨單的部門溝通，律定「出貨單」填寫的格式。</p>
<p>如果涉及到客戶公司內部，盡可能讓他們「自行協調」，我們做好「引導的角色」就夠了，如果由我們向客戶的其他部門提出需求，那破局的機率非常高。</p>
<hr>
<h2 id="六客戶未必具備工程背景我們要協助他們表達需求">六、客戶未必具備工程背景，我們要協助他們表達需求<a class="h-anchor" href="#%e5%85%ad%e5%ae%a2%e6%88%b6%e6%9c%aa%e5%bf%85%e5%85%b7%e5%82%99%e5%b7%a5%e7%a8%8b%e8%83%8c%e6%99%af%e6%88%91%e5%80%91%e8%a6%81%e5%8d%94%e5%8a%a9%e4%bb%96%e5%80%91%e8%a1%a8%e9%81%94%e9%9c%80%e6%b1%82" aria-label="這個段落的連結"></a></h2>
<p>其實就算是公司內部的溝通也是不容易的，起初填寫出貨單的部門對律定格式這件事反彈極大，因為他們原本都做得好好的，現在變得要依規範行事。</p>
<p>在了解到客戶推行的困難點後，我們製作了一張高達數百個欄位的 Excel，並請客戶向填寫出貨單的部門展示，讓他們體會到混亂的格式造成什麼後果。</p>
<p>在這之後他們才理解為什麼格式需要律定了，也了解公司導入這個系統就是希望把流程標準化，減少報表製作的時間，不是想要刁難他們。</p>
<hr>
<h2 id="七我們要如何調整讓未來的合作更加順利">七、我們要如何調整，讓未來的合作更加順利<a class="h-anchor" href="#%e4%b8%83%e6%88%91%e5%80%91%e8%a6%81%e5%a6%82%e4%bd%95%e8%aa%bf%e6%95%b4%e8%ae%93%e6%9c%aa%e4%be%86%e7%9a%84%e5%90%88%e4%bd%9c%e6%9b%b4%e5%8a%a0%e9%a0%86%e5%88%a9" aria-label="這個段落的連結"></a></h2>
<p>儘管這個問題最後在多方的協作下圓滿解決，但其實中間有許多摩擦是可以在初期避免的。</p>
<h3 id="-tech-lead">Tech Lead<a class="h-anchor" href="#-tech-lead" aria-label="這個段落的連結"></a></h3>
<p>若發現某個 Ticket 的估時逐漸增加，要主動向工程師詢問實際狀況，因為有些細節在 Scrum Meeting 並不會提及。</p>
<p>若發現工程師在做重工、白工，要先釐清問題來源；不是每個問題都要靠程式解決，我們可以先嘗試跟客戶溝通。</p>
<h3 id="-pm">PM<a class="h-anchor" href="#-pm" aria-label="這個段落的連結"></a></h3>
<p>如果客戶提出的需求變更太過零碎、頻繁，可以先記錄下來，等累積到一定的量，或是明確需求後再交給工程師處理。</p>
<p>如果發現需求能用模擬的就先用模擬的，盡可能不要讓工程師把時間耗在有高機率做白工的需求變更上。</p>
<h3 id="-工程師">工程師<a class="h-anchor" href="#-%e5%b7%a5%e7%a8%8b%e5%b8%ab" aria-label="這個段落的連結"></a></h3>
<p>如果發現需求變更的內容不合理、次數太多，可以即時與 Tech Lead 反應，而不是用加班默默把需求的吃下來。</p>
<blockquote class="pullquote"><p>希望這個案例對讀者日後執行專案時有幫助，如果還有其他建議也歡迎留言分享喔～</p>
</blockquote>
<hr>
<pre tabindex="0"><code>工程師血淚史
▋ 團隊在系統開發上遇到了哪些問題
▋ 需求不斷變更，到底哪個環節出錯了(本篇)
▋ 當 Scrum Master 跟 PM 同時存在，權力與責任該如何區分？
▋ 如果沒有明確的開發流程，團隊將會無所適從
▋ 你知道對專案來說，README.md 有多麼重要嗎？
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>【Day22】Google Sheets - 客戶：我希望新資料插在最前面 &amp; 談需求變更</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-22/</link>
      <pubDate>Wed, 07 Oct 2020 09:03:30 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-22/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>3.1 插入一個空白欄位：insertEmptyCol3.2 把空白欄位更新成最新的追蹤人數資訊</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-22/images/cover_hu_4fbd6832f3c4da62.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>在驗收階段你就知道一個好的需求規格書有多麼重要</p>
</blockquote>
<h2 id="今日目標">今日目標<a class="h-anchor" href="#%e4%bb%8a%e6%97%a5%e7%9b%ae%e6%a8%99" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="1-了解驗收過程中需求規格書的重要性">1. 了解驗收過程中需求規格書的重要性<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3%e9%a9%97%e6%94%b6%e9%81%8e%e7%a8%8b%e4%b8%ad%e9%9c%80%e6%b1%82%e8%a6%8f%e6%a0%bc%e6%9b%b8%e7%9a%84%e9%87%8d%e8%a6%81%e6%80%a7" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="2-談需求變更">2. 談需求變更<a class="h-anchor" href="#2-%e8%ab%87%e9%9c%80%e6%b1%82%e8%ae%8a%e6%9b%b4" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="3-將新的爬蟲資料寫到最前面日期為由近而遠的排序">3. 將新的爬蟲資料寫到最前面，日期為由近而遠的排序<a class="h-anchor" href="#3-%e5%b0%87%e6%96%b0%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%af%ab%e5%88%b0%e6%9c%80%e5%89%8d%e9%9d%a2%e6%97%a5%e6%9c%9f%e7%82%ba%e7%94%b1%e8%bf%91%e8%80%8c%e9%81%a0%e7%9a%84%e6%8e%92%e5%ba%8f" aria-label="這個段落的連結"></a></h3>
<p>3.1 插入一個空白欄位：<code>insertEmptyCol</code></p>
<p>3.2 把空白欄位更新成最新的追蹤人數資訊</p>
<hr>
<h2 class="h-nomark" id="1-了解驗收過程中需求規格書的重要性-1">1. 了解驗收過程中需求規格書的重要性<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3%e9%a9%97%e6%94%b6%e9%81%8e%e7%a8%8b%e4%b8%ad%e9%9c%80%e6%b1%82%e8%a6%8f%e6%a0%bc%e6%9b%b8%e7%9a%84%e9%87%8d%e8%a6%81%e6%80%a7-1" aria-label="這個段落的連結"></a></h2>
<p>驗收的過程就是按造需求規格上的驗收項目一一測試，所以<mark>如果沒有需求規格書或是需求規格書寫得太不完善，你在驗收的過程會一直被凹新功能</mark>，而這些功能基本上只能忍氣吞聲吃下去，畢竟這是因為你在一開始沒談好規格的歷史業障，這就是我在<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-01/">Day 1</a>提到需求規格書的重要性</p>
<hr>
<h2 class="h-nomark" id="2-談需求變更-1">2. 談需求變更<a class="h-anchor" href="#2-%e8%ab%87%e9%9c%80%e6%b1%82%e8%ae%8a%e6%9b%b4-1" aria-label="這個段落的連結"></a></h2>
<p>反過來如果你需求規格寫得夠好，在驗收的階段你會有很多賺外快的機會，<mark>因為大部分的客戶在驗收階段才想到他們實際上還需要哪些功能</mark></p>
<p>對於這些多出來的功能我一律列入<strong>需求變更</strong>，因為這些需求並不在原合約內所以你可以<mark>將客戶的新需求記錄下來依功能報價讓客戶自己決定需要新增哪些功能</mark></p>
<p>根據個人經驗，在結案後你會發現這些多出來的<mark>需求變更可能占總外包費的 20%~40% 不等</mark>，所以各位在接案時千萬不要因為覺得麻煩沒有好好寫需求規格書啊！你這樣的做法完全在跟自己的錢包過不去，讓自己成為免費勞工。</p>
<h3 id="下面用今天要完成的需求變更舉例">下面用今天要完成的需求變更舉例：<a class="h-anchor" href="#%e4%b8%8b%e9%9d%a2%e7%94%a8%e4%bb%8a%e5%a4%a9%e8%a6%81%e5%ae%8c%e6%88%90%e7%9a%84%e9%9c%80%e6%b1%82%e8%ae%8a%e6%9b%b4%e8%88%89%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<p><strong>客戶</strong>：我測試了幾天覺得爬蟲蠻穩定的，但最新的爬蟲資料都寫在最後面，這讓我要滑到最後面才知道最新資料，他能不能新資料都寫在最前面啊 <code>(客戶提出需求變更)</code></p>
<p><strong>我</strong>：您的意思是要<strong>將新的爬蟲資料寫到最前面，Google Sheet 上呈現的是日期由近而遠的排序</strong>嗎？ <code>(確認需求變更的邏輯)</code></p>
<p><strong>客戶</strong>：對，麻煩了</p>
<p><strong>我</strong>：不過這項需求並不在原本的需求規格書上面，原本的需求規格書只提到<strong>爬蟲日期為欄，粉專標題為列</strong>，並沒提到排序方面的需求；經過評估這個功能需要額外的費用XXX，您可以接受嗎？ <code>(提出需求規格書上面的規格做佐證，並說明這個新功能需要加收多少費用)</code></p>
<p><strong>客戶</strong>：恩&hellip;可以</p>
<p><strong>我</strong>：我把這項規則加到需求規格書的後面，您確認完後我就會開始實作 <code>(讓客戶確認規格及價格後在開始實作)</code></p>
<hr>
<h2 class="h-nomark" id="3-將新的爬蟲資料寫到最前面日期為由近而遠的排序-1">3. 將新的爬蟲資料寫到最前面，日期為由近而遠的排序<a class="h-anchor" href="#3-%e5%b0%87%e6%96%b0%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%af%ab%e5%88%b0%e6%9c%80%e5%89%8d%e9%9d%a2%e6%97%a5%e6%9c%9f%e7%82%ba%e7%94%b1%e8%bf%91%e8%80%8c%e9%81%a0%e7%9a%84%e6%8e%92%e5%ba%8f-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>因為沒找到可以一步完成的解法，所以我將這個需求拆成兩個步驟：</p>
<ol>
<li>先在第二欄插入一個空白欄位</li>
<li>把空白欄位更新成最新的追蹤人數資訊</li>
</ol>
</blockquote>
<h3 class="h-nomark" id="31-插入一個空白欄位insertemptycol">3.1 插入一個空白欄位：<code>insertEmptyCol</code><a class="h-anchor" href="#31-%e6%8f%92%e5%85%a5%e4%b8%80%e5%80%8b%e7%a9%ba%e7%99%bd%e6%ac%84%e4%bd%8dinsertemptycol" aria-label="這個段落的連結"></a></h3>
<p>在Google Sheets中插入空白欄位的部分請參考<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#insertdimensionrequest">官方的文件</a>，這裡有個需要注意的點：</p>
<ul>
<li>我們觀察到 Google 插入欄位的 api 在 range 的參數上需要 <code>sheet_id</code> 來能執行，而這隻函式會被 <code>writeSheet</code> 函式呼叫，所以下一步我們要對他做一些調整</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> insertEmptyCol (title, sheet_id, auth) {<span style="color:#8b949e;font-style:italic">//插入空白欄位
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> sheets <span style="color:#ff7b72;font-weight:bold">=</span> google.sheets({ version<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;v4&#39;</span>, auth });
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> request <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// The ID of the spreadsheet
</span></span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;spreadsheetId&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;resource&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#34;requests&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> [{
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;insertDimension&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {<span style="color:#8b949e;font-style:italic">//插入新欄位
</span></span></span><span style="display:flex;"><span>          <span style="color:#a5d6ff">&#34;range&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;sheetId&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> sheet_id,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;dimension&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;COLUMNS&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;startIndex&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1</span>,<span style="color:#8b949e;font-style:italic">//代表插入範圍從第一欄開始到第二欄結束
</span></span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;endIndex&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">2</span>
</span></span><span style="display:flex;"><span>          },
</span></span><span style="display:flex;"><span>          <span style="color:#a5d6ff">&#34;inheritFromBefore&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>      }]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> sheets.spreadsheets.batchUpdate(request)
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;update sheet:&#39;</span> <span style="color:#ff7b72;font-weight:bold">+</span> title <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39; new column&#39;</span>)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;The API returned an error: &#39;</span> <span style="color:#ff7b72;font-weight:bold">+</span> err);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="32-把空白欄位更新成最新的追蹤人數資訊">3.2 把空白欄位更新成最新的追蹤人數資訊<a class="h-anchor" href="#32-%e6%8a%8a%e7%a9%ba%e7%99%bd%e6%ac%84%e4%bd%8d%e6%9b%b4%e6%96%b0%e6%88%90%e6%9c%80%e6%96%b0%e7%9a%84%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8%e8%b3%87%e8%a8%8a" aria-label="這個段落的連結"></a></h3>
<p>因為<code>insertEmptyCol</code>這個函式需要用到用到 <strong>sheetId</strong> 的參數，我們需要對以下程式做調整：</p>
<ol>
<li>
<p><code>updateGoogleSheets</code>：還記得 <strong>getFBIGSheet</strong> 會回傳 sheets 的資訊嗎？現在我們就需要裡面的<strong>title、id</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> sheets <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getFBIGSheet(auth)<span style="color:#8b949e;font-style:italic">//取得線上FB、IG的sheet資訊
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 寫入各自的Sheet
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">for</span> (sheet <span style="color:#ff7b72">of</span> sheets) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (sheet.title <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#39;FB粉專&#39;</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> writeSheet(sheet.title, sheet.id, fb_result_array, auth)
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (sheet.title <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#39;IG帳號&#39;</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> writeSheet(sheet.title, sheet.id, ig_result_array, auth)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
<li>
<p><code>writeSheet</code> :</p>
<ol>
<li>
<p>傳入 <code>sheet_id</code> 參數讓 <strong>insertEmptyCol</strong> 使用</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> writeSheet (title, sheet_id, result_array, auth)
</span></span></code></pre></div></li>
<li>
<p>先插入空白欄位後再寫入追蹤人數</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 插入空欄位
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> insertEmptyCol(title, sheet_id, auth)
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 寫入追蹤人數
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> writeTrace(title, trace_array, auth)
</span></span></code></pre></div></li>
</ol>
<blockquote>
<p>因為取得最後一欄的函式用不到了，請將<code>getLastCol</code>、<code>toColumnName</code>刪除</p>
</blockquote>
</li>
</ol>
<hr>
<h3 id="執行程式">執行程式<a class="h-anchor" href="#%e5%9f%b7%e8%a1%8c%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>在專案資料夾的終端機(Terminal)執行指令</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn start
</span></span></code></pre></div></li>
<li>
<p>在終端機我們可以看到事先插入新欄位再更新資料</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-22/images/fb-ig-crawler-day-22-01.webp" width="1024" height="562" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>確認新的爬蟲資料有正確插入到 Google Sheets 第二欄</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-22/images/fb-ig-crawler-day-22-02.webp" width="1024" height="348" alt="執行程式（圖 2）" loading="lazy" decoding="async"></p>
<blockquote>
<p>如果你在同一天測試，你可以把之前爬蟲日期改成<code>old</code>方便辨識</p>
</blockquote>
</li>
</ol>
<hr>
<h3 id="ℹ-專案原始碼">ℹ️ 專案原始碼<a class="h-anchor" href="#%e2%84%b9-%e5%b0%88%e6%a1%88%e5%8e%9f%e5%a7%8b%e7%a2%bc" aria-label="這個段落的連結"></a></h3>
<ul>
<li>今天的完整程式碼可以在<a href="https://github.com/dean9703111/ithelp_30days/tree/master/day22">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day21_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來完成業主要求的需求變更吧
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>windows需下載與你chrome版本相同的<a href="http://chromedriver.storage.googleapis.com/index.html">chrome driver</a>放在專案根目錄</li>
<li>要在tools/google_sheets資料夾放上自己的憑證，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/">Day17</a></li>
<li>調整fanspages資料夾內目標爬蟲的粉專網址</li>
<li>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
<li>填上SPREADSHEET_ID</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-22/images/fb-ig-crawler-day-22-03.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>在許多人的幫助下，本系列文章已出版成書，並添加了新的篇章與細節補充：</strong></p>
<ul>
<li>加入更多實務經驗，用完整的開發流程讓讀者了解專案每個階段要注意的事項</li>
<li>將爬蟲的步驟與技巧做更詳細的說明，讓讀者可以輕鬆入門</li>
<li>調整專案架構
<ul>
<li>優化爬蟲程式，以更廣的視角來擷取網頁資訊</li>
<li>增加資料驗證、錯誤通知等功能，讓爬蟲執行遇到問題時可以第一時間通知使用者</li>
<li>排程部分改用 node-schedule &amp; pm2 的組合，讓讀者可以輕鬆管理專案程序並獲得更精確的 log 資訊</li>
</ul>
</li>
</ul>
<p><strong>有興趣的朋友可以到天瓏書局選購，感謝大家的支持。</strong></p>
<p><strong>購書連結</strong>：<a href="https://www.tenlong.com.tw/products/9789864348008">https://www.tenlong.com.tw/products/9789864348008</a></p>
</blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
