<?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>google-sheets｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/google-sheets/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>google-sheets｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/google-sheets/</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>Wed, 10 Aug 2022 13:56:49 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/google-sheets/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>免費！提交 Google 表單後，系統自動寄送客製化 email，宣傳效果 Up！</title>
      <link>https://deanlin.net/blog/202208/google-forms-auto-email/</link>
      <pubDate>Wed, 10 Aug 2022 13:56:49 +0800</pubDate>
      <guid>https://deanlin.net/blog/202208/google-forms-auto-email/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>有時我們希望使用者提交 Google 表單後，可以提供給他更多資訊（ex：活動詳細內容、宣傳廣告、友站連結…）；但 Google 表單只有在「設定 → 簡報 → 確認訊息」給你用純文字的方式表達，除了傳送內容受限制外，許多使用者在提交表單後會下意識的關閉網頁…</description>
      <media:content url="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-og_hu_3569880bfc09ace.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>有時我們希望使用者提交 Google 表單後，可以提供給他更多資訊（ex：活動詳細內容、宣傳廣告、友站連結…）；但 Google 表單只有在「設定 → 簡報 → 確認訊息」給你用純文字的方式表達，除了傳送內容受限制外，許多使用者在提交表單後會下意識的關閉網頁，根本沒注意到裡面寫了些什麼。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-01.webp" width="1400" height="731" alt="免費！提交 Google 表單後，系統自動寄送客製化 email，宣傳效果 Up！（圖 1）" fetchpriority="high" decoding="async"></p>
<h2 id="前言需求來自於實務">前言：需求來自於實務<a class="h-anchor" href="#%e5%89%8d%e8%a8%80%e9%9c%80%e6%b1%82%e4%be%86%e8%87%aa%e6%96%bc%e5%af%a6%e5%8b%99" aria-label="這個段落的連結"></a></h2>
<p>其實會想做這個功能，是因為不久前<a href="https://www.tenlong.com.tw/products/9786263331525">出版書籍</a>後，陸續有人填寫 Google 表單回傳自己的心得。</p>
<p>而我這次有給填寫表單的讀者「30 分鐘免費諮詢」的回饋，但因為把諮詢的報名連結放在 Google 表單「確認訊息」的位置，導致有些讀者根本沒注意到它的存在，因此便想透過自動寄送 email，來解決這個囧境。</p>
<blockquote>
<p>你也可以先<a href="https://forms.gle/ADvarze9bKrwyUGV6">體驗看看筆者完成的 Demo</a>，只要填寫電子郵件，並在「是否願意收到電子報」的欄位選擇「是」，提交後便會收到自動發送的郵件嚕！</p>
</blockquote>
<hr>
<h2 id="step-1-產生-html-email">STEP 1 ：產生 HTML email<a class="h-anchor" href="#step-1-%e7%94%a2%e7%94%9f-html-email" aria-label="這個段落的連結"></a></h2>
<p>其實這個步驟並非必需，但為了讓寄送的 email 有基礎的質感，筆者還是決定製作一個 HTML email。</p>
<p>看到 HTML 先別慌，現在網路上有非常多的資源，只要拖拉點選，一行程式碼都不用寫就能產生出來，這邊筆者是使用 <a href="https://beefree.io/">Beefree</a> 這個網站提供的服務，在這裡有擁有豐富的版型供你選擇（雖然筆者還是選用空白模板）。</p>
<div class="gallery-row cols-2">

<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-02.webp" width="1400" height="736" alt="STEP 1 ：產生 HTML email（圖 2）" loading="lazy" decoding="async">
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-03.webp" width="1400" height="697" alt="STEP 1 ：產生 HTML email（圖 3）" loading="lazy" decoding="async">
</figure>


</div>

<p>它的使用非常方便，你甚至可以在不註冊的狀態下製作自己的 HTML email；<strong>不過如果你擔心自己會失手關閉網頁，還是建議註冊登入。</strong></p>
<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-04.webp" width="1400" height="998" alt="這個是 Beefree 的 UI 操作畫面" loading="lazy" decoding="async">
  <figcaption>這個是 Beefree 的 UI 操作畫面</figcaption>
</figure>

<p>在完成 HTML email 後，點擊右上角的 Download 會獲得一個壓縮檔，解壓縮後你會看到裡面的「beefree-xxx.html」檔，筆者建議重新命名成「email-content.html」方便辨識（這個 HTML 在後面的步驟會用到，請妥善保存）。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-05.webp" width="1400" height="927" alt="STEP 1 ：產生 HTML email（圖 5）" loading="lazy" decoding="async"></p>
<blockquote>
<p><strong>備註：</strong></p>
<p>1. 免費版在最下面會有 Beefree 的 Logo，它可以在下載後透過編輯 HTML 來刪除。</p>
<p>2. 如果有使用到圖片，建議先上傳到 Imgur，再把產生連結貼回來（ex：https://i.imgur.com/Oonq5Gy.jpg），詳細流程這邊不再贅述。</p>
</blockquote>
<hr>
<h2 id="setp-2建立-google-表單">SETP 2：建立 Google 表單<a class="h-anchor" href="#setp-2%e5%bb%ba%e7%ab%8b-google-%e8%a1%a8%e5%96%ae" aria-label="這個段落的連結"></a></h2>
<p>建立表單的步驟我就不再示範了，需要注意的是「電子郵件（email）」這個欄位需要設定為必填，不然你也沒有寄信的對象啊！</p>
<p>為了驗證 Google 表單的參數可以作為邏輯判斷使用，我這邊再新增一個「是否願意收到電子報」的問題，依照回答的「是/否」來決定要不要寄送 email。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-06.webp" width="1400" height="975" alt="SETP 2：建立 Google 表單（圖 6）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="setp-3從-google-表單產生-google-試算表">SETP 3：從 Google 表單產生 Google 試算表<a class="h-anchor" href="#setp-3%e5%be%9e-google-%e8%a1%a8%e5%96%ae%e7%94%a2%e7%94%9f-google-%e8%a9%a6%e7%ae%97%e8%a1%a8" aria-label="這個段落的連結"></a></h2>
<p>在「回覆」的 Tab 中，點擊「Google 試算表」的 icon 便會跳出建立 Google 試算表的詢問視窗。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-07.webp" width="1400" height="461" alt="SETP 3：從 Google 表單產生 Google 試算表（圖 7）" loading="lazy" decoding="async"></p>
<p>下面是自動產生的 Google 試算表。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-08.webp" width="1400" height="774" alt="SETP 3：從 Google 表單產生 Google 試算表（圖 8）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="step-4從-google-試算表建立-apps-script">STEP 4：從 Google 試算表建立 Apps Script<a class="h-anchor" href="#step-4%e5%be%9e-google-%e8%a9%a6%e7%ae%97%e8%a1%a8%e5%bb%ba%e7%ab%8b-apps-script" aria-label="這個段落的連結"></a></h2>
<p>點擊工具列的「擴充功能」，再點擊「Apps Script」便會產生嚕！</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-09.webp" width="1400" height="329" alt="STEP 4：從 Google 試算表建立 Apps Script（圖 9）" loading="lazy" decoding="async"></p>
<p>在 Apps Script 裡面用「新增檔案」建立一個 HTML，並將 STEP 1 產生的 HTML 程式碼貼回去。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-10.webp" width="1232" height="666" alt="STEP 4：從 Google 試算表建立 Apps Script（圖 10）" loading="lazy" decoding="async"></p>
<p>接著要撰寫 Apps Script 裡面的程式邏輯（程式碼.gs）。</p>
<p>我們要先抓取 Google Form 裡面提交的值，如果「是否願意收到電子報」的值為「否」，我們就跳出函式；若為「是」，我們便將 HTML email 寄出（相關注意事項請看備註描述）。</p>
<pre tabindex="0"><code>function onSubmit(datas) {
  
  // 取得提交的值（你的欄位名稱可能與我不同）
  var data = datas.namedValues;
  var replyEmail = data[&#39;電子郵件地址&#39;][0];
  var isReceiveEDM = data[&#39;是否願意收到電子報&#39;][0];

  if (isReceiveEDM === &#39;否&#39;){
    // 如果拒絕收到電子報就直接結束
    return;
  }

  // 取得上傳的 HTML 檔
  var edmHTML = HtmlService.createTemplateFromFile(&#39;email-content&#39;);
  
  // 設定HTML裡的變數
  edmHTML.data = data;
  
  // 產出成要發送的 HTML
  var output = edmHTML.evaluate().getContent();

  // 發送信件
  MailApp.sendEmail({
    to: replyEmail,
    subject: &#34;[系統發送] 感謝您填寫「Dean Lin 的回饋表單」&#34;,
    htmlBody: output
  });
}
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/36e9cb9e237c53936ed751d5fb986fae" rel="noopener">email-response.gs</a>（GitHub Gist）</p>
<hr>
<h2 id="step-5設定觸發條件">STEP 5：設定觸發條件<a class="h-anchor" href="#step-5%e8%a8%ad%e5%ae%9a%e8%a7%b8%e7%99%bc%e6%a2%9d%e4%bb%b6" aria-label="這個段落的連結"></a></h2>
<p>在上面的步驟完成後，我們要將 Google 表單的事件與剛剛撰寫的程式綁定；因此我們要到左側的功能列，點擊「觸發條件」。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-11.webp" width="777" height="286" alt="STEP 5：設定觸發條件（圖 11）" loading="lazy" decoding="async"></p>
<p>接著點擊右下角的「新增觸發條件」。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-12.webp" width="1400" height="892" alt="STEP 5：設定觸發條件（圖 12）" loading="lazy" decoding="async"></p>
<p>觸發條件可以參考筆者在紅框的選擇，完成後按右下角的「儲存即可」。</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-13.webp" width="1400" height="1076" alt="STEP 5：設定觸發條件（圖 13）" loading="lazy" decoding="async"></p>
<p>接下來 Google 會跳出一堆需要你授權的彈窗，順序大概是「選擇帳戶 → 驗證 app → 允許 app 權限」。</p>
<div class="gallery-row cols-3">

<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-14.webp" width="584" height="497" alt="STEP 5：設定觸發條件（圖 14）" loading="lazy" decoding="async">
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-15.webp" width="584" height="591" alt="STEP 5：設定觸發條件（圖 15）" loading="lazy" decoding="async">
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-16.webp" width="584" height="617" alt="STEP 5：設定觸發條件（圖 16）" loading="lazy" decoding="async">
</figure>


</div>

<hr>
<h2 id="final測試發信">Final：測試發信<a class="h-anchor" href="#final%e6%b8%ac%e8%a9%a6%e7%99%bc%e4%bf%a1" aria-label="這個段落的連結"></a></h2>
<p>終於走到最後一步惹 🤣</p>
<p>我們來<a href="https://forms.gle/ADvarze9bKrwyUGV6">填寫 Google 表單</a>，確認是否會順利自動寄信。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-17.webp" width="748" height="766" alt="如果選擇「否」就不會收到郵件喔！" loading="lazy" decoding="async">
  <figcaption>如果選擇「否」就不會收到郵件喔！</figcaption>
</figure>

<p>如果收到 email，就大功告成嚕！</p>
<p><img src="https://deanlin.net/blog/202208/google-forms-auto-email/images/google-forms-auto-email-18.webp" width="1400" height="1132" alt="Final：測試發信（圖 18）" loading="lazy" decoding="async"></p>
<p>教學到這邊就結束啦～希望大家按照步驟都能順利完成，提供更多的資訊給目標 TA，讓宣傳效果 Up Up！</p>
]]></content:encoded>
    </item>
    <item>
      <title>【Day23】Google Sheets - 優化格式，滿足客戶需求 &amp; 談使用者體驗</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-23/</link>
      <pubDate>Thu, 08 Oct 2020 08:10:42 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-23/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>1.1 那些不堪回首的共同回憶1.2 是什麼原因製造出這些體驗的1.3 有在使用產品的人才會給你最真實的回饋，專案還有哪些地方要改善呢？</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/images/cover_hu_c8cd0557ef90c246.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%e4%bd%bf%e7%94%a8%e8%80%85%e9%ab%94%e9%a9%97%e7%82%ba%e4%bb%80%e9%ba%bc%e9%87%8d%e8%a6%81" aria-label="這個段落的連結"></a></h3>
<p>1.1 那些不堪回首的共同回憶</p>
<p>1.2 是什麼原因製造出這些體驗的</p>
<p>1.3 有在使用產品的人才會給你最真實的回饋，專案還有哪些地方要改善呢？</p>
<h3 class="h-nomark" id="2-優化用手機來看-google-sheets-的爬蟲資料的體驗">2. 優化<mark>用手機來看 Google Sheets 的爬蟲資料</mark>的體驗<a class="h-anchor" href="#2-%e5%84%aa%e5%8c%96%e7%94%a8%e6%89%8b%e6%a9%9f%e4%be%86%e7%9c%8b-google-sheets-%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e7%9a%84%e9%ab%94%e9%a9%97" aria-label="這個段落的連結"></a></h3>
<p>2.1 如何讓手機看 Google Sheets 爬蟲資料時舒服點</p>
<p>2.2 窗口凍結：將最上方的爬蟲日期、左方的粉專名稱欄位凍結</p>
<p>2.3 欄位寬度調整：減少追蹤人數的欄位寬度</p>
<hr>
<h2 class="h-nomark" id="1-了解使用者體驗為什麼重要-1">1. 了解使用者體驗為什麼重要<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3%e4%bd%bf%e7%94%a8%e8%80%85%e9%ab%94%e9%a9%97%e7%82%ba%e4%bb%80%e9%ba%bc%e9%87%8d%e8%a6%81-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>讓客戶用的開心他們才會買單</p>
</blockquote>
<h3 class="h-nomark" id="11-那些不堪回首的共同回憶">1.1 那些不堪回首的共同回憶<a class="h-anchor" href="#11-%e9%82%a3%e4%ba%9b%e4%b8%8d%e5%a0%aa%e5%9b%9e%e9%a6%96%e7%9a%84%e5%85%b1%e5%90%8c%e5%9b%9e%e6%86%b6" aria-label="這個段落的連結"></a></h3>
<p>我想每個人都有過類似經驗：</p>
<ol>
<li><strong>操作系統時讓你感到焦慮</strong>
<ul>
<li>我Ｘ，到底這個系統要怎麼操作啊？為什麼按下去沒反應？到底哪裡弄錯了？</li>
<li>卡在 loading 的畫面已經 10 分鐘，所以我要繼續等嗎？</li>
</ul>
</li>
<li><strong>你想要用的功能會隱身術</strong>
<ul>
<li>我想要用XXX功能，到底他在哪裡才能操作？</li>
<li>為什麼要改版啦？功能都不知道跑去哪了？</li>
</ul>
</li>
<li><strong>每個按鈕都像定時炸彈</strong>
<ul>
<li>所以這個按鈕是什麼意思？是下一步還是送出？</li>
<li>痾&hellip;下單了？這不是加入購物車嗎？</li>
</ul>
</li>
<li><strong>懷疑設計者是不是嗑藥</strong>
<ul>
<li>簡訊的驗證碼有 6 碼，可是眼前能輸入的欄位只有 4 碼，是在哈嚕？</li>
<li>線上填完表格後不能儲存只能下載列印後寄給主辦單位，如果要用郵件寄出幹嘛設計成網頁?</li>
</ul>
</li>
</ol>
<hr>
<h3 class="h-nomark" id="12-是什麼原因製造出這些體驗的">1.2 是什麼原因製造出這些體驗的<a class="h-anchor" href="#12-%e6%98%af%e4%bb%80%e9%ba%bc%e5%8e%9f%e5%9b%a0%e8%a3%bd%e9%80%a0%e5%87%ba%e9%80%99%e4%ba%9b%e9%ab%94%e9%a9%97%e7%9a%84" aria-label="這個段落的連結"></a></h3>
<blockquote>
<p>相信遇到這些經驗的時候大家心裡都會想：<mark>這麼爛的東西到底是誰設計出來的啊？他自己真的有用過嗎？</mark></p>
</blockquote>
<p>總結發生這些不好的經驗通常都是因為：</p>
<ol>
<li><strong>沒有了解使用者真正的需求</strong>：這是許多外包案的真實狀況，工程師的目標是驗收通過，而承包商也只是為了把事情做完有績效，當<mark>甲乙方都不在乎終端使用者的需求時，完成的產品通常不堪入目</mark></li>
<li><strong>經驗不足</strong>：很多時候有些報價比較低的案子會交給公司的新人，如果公司也沒有讓前輩在旁邊給予指導會讓這個<mark>產品有非常多的漏洞</mark></li>
<li><strong>沒有足夠的測試</strong>：有些案子時程非常的趕，在這樣的狀態下通常<mark>產品的穩定度很低，且操作時也很容易發生錯誤</mark></li>
<li><strong>經費不足</strong>：花大錢的案子不一定做得好，但是錢少的案子基本上都不太好，一分錢一分貨，<mark>壓價格的時候一定會喪失品質</mark></li>
</ol>
<hr>
<h3 class="h-nomark" id="13-有在使用產品的人才會給你最真實的回饋專案還有哪些地方要改善呢">1.3 有在使用產品的人才會給你最真實的回饋，專案還有哪些地方要改善呢？<a class="h-anchor" href="#13-%e6%9c%89%e5%9c%a8%e4%bd%bf%e7%94%a8%e7%94%a2%e5%93%81%e7%9a%84%e4%ba%ba%e6%89%8d%e6%9c%83%e7%b5%a6%e4%bd%a0%e6%9c%80%e7%9c%9f%e5%af%a6%e7%9a%84%e5%9b%9e%e9%a5%8b%e5%b0%88%e6%a1%88%e9%82%84%e6%9c%89%e5%93%aa%e4%ba%9b%e5%9c%b0%e6%96%b9%e8%a6%81%e6%94%b9%e5%96%84%e5%91%a2" aria-label="這個段落的連結"></a></h3>
<p>沒有一開始就完美的專案，<strong>客戶的需求會在使用的過程中慢慢增加</strong>，這個專案也在業主使用的過程中增加了一些需求：</p>
<ol>
<li>下班後會<mark>用手機來看 Google Sheets 的爬蟲資料</mark>，操作起來不是很順，想問有沒有好的建議</li>
<li>上班太忙會忘記跑爬蟲程式，能不能<mark>指定爬蟲程式在某個時間點自動執行</mark></li>
<li>因為執行爬蟲程式的時候我也要做事情，能不能<mark>有個通知提醒我爬蟲執行的狀況</mark></li>
</ol>
<p>接下來的日子就讓我們把這些需求一一實踐吧！</p>
<hr>
<h2 class="h-nomark" id="2-優化用手機來看-google-sheets-的爬蟲資料的體驗-1">2. 優化<mark>用手機來看 Google Sheets 的爬蟲資料</mark>的體驗<a class="h-anchor" href="#2-%e5%84%aa%e5%8c%96%e7%94%a8%e6%89%8b%e6%a9%9f%e4%be%86%e7%9c%8b-google-sheets-%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e7%9a%84%e9%ab%94%e9%a9%97-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>原則上你想對 Google Sheets 的任何操作官方都有提供 api 給你，如果你時常會使用到的話請一定要讀懂官方文件，閱讀的技巧可以參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/">這篇文章</a>喔</p>
</blockquote>
<h3 class="h-nomark" id="21-如何讓手機看-google-sheets-爬蟲資料時舒服點">2.1 如何讓手機看 Google Sheets 爬蟲資料時舒服點<a class="h-anchor" href="#21-%e5%a6%82%e4%bd%95%e8%ae%93%e6%89%8b%e6%a9%9f%e7%9c%8b-google-sheets-%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e6%99%82%e8%88%92%e6%9c%8d%e9%bb%9e" aria-label="這個段落的連結"></a></h3>
<p>這個問題分析起來很簡單，主要就是下面兩點：</p>
<ol>
<li>因為資料存在 Google sheets 上面，資料量一大就很難觀看</li>
<li>手機螢幕小，能呈現的資料不多</li>
</ol>
<p>後來再跟客戶討論的過程中發現<mark>這個問題在使用電腦時也會有，但是使用手機時問題更嚴重</mark>，於是我在操作他 Google Sheets 的爬蟲資料時提出了優化方案，讓他在使用上方便許多：</p>
<ol>
<li><strong>將最上方的爬蟲日期、左方的粉專名稱欄位凍結</strong>
<ul>
<li>這樣在瀏覽時就不需要一直往上確認日期、往左確認他是哪個粉專了</li>
</ul>
</li>
<li><strong>減少追蹤人數的欄位寬度</strong>
<ul>
<li>我們無法改變手機的螢幕寬度，但是改變資料欄位的寬度能讓一個螢幕能呈現更多的資訊</li>
</ul>
</li>
</ol>
<blockquote>
<p>測試環境通常資料量不足，很多問題在資料量逐漸上升的正式環境才會浮現</p>
</blockquote>
<hr>
<h3 class="h-nomark" id="22-窗口凍結將最上方的爬蟲日期左方的粉專名稱欄位凍結">2.2 窗口凍結：將最上方的爬蟲日期、左方的粉專名稱欄位凍結<a class="h-anchor" href="#22-%e7%aa%97%e5%8f%a3%e5%87%8d%e7%b5%90%e5%b0%87%e6%9c%80%e4%b8%8a%e6%96%b9%e7%9a%84%e7%88%ac%e8%9f%b2%e6%97%a5%e6%9c%9f%e5%b7%a6%e6%96%b9%e7%9a%84%e7%b2%89%e5%b0%88%e5%90%8d%e7%a8%b1%e6%ac%84%e4%bd%8d%e5%87%8d%e7%b5%90" aria-label="這個段落的連結"></a></h3>
<p>我將這個功能合併到建立 sheet 的函式 <code>addSheet</code>，窗口凍結這件事在<mark>建立 sheet 時執行一次就夠了</mark></p>
<blockquote>
<p>詳細的官方文件請參考<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#gridproperties">窗口凍結</a></p>
</blockquote>
<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> addSheet (title, sheet_id, auth) {
</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:#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;addSheet&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a5d6ff">&#34;properties&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;title&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> title,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;gridProperties&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a5d6ff">&#34;frozenRowCount&#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;frozenColumnCount&#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></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></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;added sheet:&#39;</span> <span style="color:#ff7b72;font-weight:bold">+</span> title)
</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="23-欄位寬度調整減少追蹤人數的欄位寬度">2.3 欄位寬度調整：減少追蹤人數的欄位寬度<a class="h-anchor" href="#23-%e6%ac%84%e4%bd%8d%e5%af%ac%e5%ba%a6%e8%aa%bf%e6%95%b4%e6%b8%9b%e5%b0%91%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8%e7%9a%84%e6%ac%84%e4%bd%8d%e5%af%ac%e5%ba%a6" aria-label="這個段落的連結"></a></h3>
<p>為了能容納能多的資訊量，所以我們要在插入新欄位 <code>insertEmptyCol</code> 時給他一個合適的欄位寬度</p>
<ul>
<li>提醒你 <mark>Google Sheets 的 requests 是可以執行多個要求的</mark>，像下面的範例程式中的 requests 就包含 insertDimension、updateDimensionProperties 這兩個</li>
</ul>
<blockquote>
<p>詳細的官方文件請參考<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateDimensionPropertiesRequest">欄位寬度調整</a></p>
</blockquote>
<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></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:#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></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></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 style="color:#a5d6ff">&#34;updateDimensionProperties&#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;properties&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;pixelSize&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">85</span><span style="color:#8b949e;font-style:italic">//欄位寬度
</span></span></span><span style="display:flex;"><span>          },
</span></span><span style="display:flex;"><span>          <span style="color:#a5d6ff">&#34;fields&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;pixelSize&#34;</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 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>在執行程式之前請先把 Google Sheets 的&rsquo;FB粉專&rsquo;、&lsquo;IG帳號&rsquo;這兩個 sheet 刪除，因為<mark>要觸發 addSheet 才會有窗口凍結</mark>的功能</p>
</li>
<li>
<p>刪除 sheet 後在專案資料夾的終端機(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><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/images/fb-ig-crawler-day-23-01.webp" width="1024" height="604" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</blockquote>
</li>
<li>
<p>執行完畢後確認 Google Sheets 是否有符合今天設定的格式：<strong>窗口凍結、欄位寬度調整</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/images/fb-ig-crawler-day-23-02.webp" width="868" height="316" alt="執行程式（圖 2）" loading="lazy" decoding="async"></p>
</li>
</ol>
<hr>
<h3 class="h-nomark" id="-下個階段">⏭️ 下個階段<a class="h-anchor" href="#-%e4%b8%8b%e5%80%8b%e9%9a%8e%e6%ae%b5" aria-label="這個段落的連結"></a></h3>
<p>Google Sheets 對爬蟲資料處理的系列文章到這裡告一個段落，接下來我們依照今天提到專案要改善的地方繼續前進，接著來聊聊<mark>把排程加入爬蟲專案</mark>這個主題吧</p>
<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/day23">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day22_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>
<h3 id="參考資源">參考資源<a class="h-anchor" href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" aria-label="這個段落的連結"></a></h3>
<ol>
<li><a href="https://designtongue.me/%E4%BA%86%E8%A7%A3%E4%BD%BF%E7%94%A8%E8%80%85%E9%AB%94%E9%A9%97-ux-%E7%9A%84%E7%AC%AC%E4%B8%80%E5%A0%82%E8%AA%B2/">了解使用者體驗 (UX) 的第一堂課</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/images/fb-ig-crawler-day-23-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>
    <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>
    <item>
      <title>【Day21】Google Sheets - BUG!爬蟲資料塞錯位置 &amp; 專案出包怎麼辦？</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-21/</link>
      <pubDate>Tue, 06 Oct 2020 09:08:20 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-21/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>其實這份專案完成到昨天的進度時就給業主做功能上的確認準備結案了 但一個案子通常很難一次就通過，無論這隻程式在你電腦上執行的多麼穩定，放到別人的電腦上總會冒出大大小小的 bug，我大概歸類成以下幾種：</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-21/images/cover_hu_c7282654385febc2.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>客戶的操作跟你想的不一樣</p>
</blockquote>
<h2 id="為什麼寫這篇文章">為什麼寫這篇文章？<a class="h-anchor" href="#%e7%82%ba%e4%bb%80%e9%ba%bc%e5%af%ab%e9%80%99%e7%af%87%e6%96%87%e7%ab%a0" aria-label="這個段落的連結"></a></h2>
<p>其實這份專案完成到昨天的進度時就給業主做功能上的確認準備結案了</p>
<p>但一個案子通常很難一次就通過，<mark>無論這隻程式在你電腦上執行的多麼穩定，放到別人的電腦上總會冒出大大小小的 bug</mark>，我大概歸類成以下幾種：</p>
<ol>
<li>作業系統不同導致的
<ul>
<li>Windows、MacOS、Ubuntu Linux (ex：爬蟲時需要設定各自的 webdriver)</li>
</ul>
</li>
<li>套件版本不同導致
<ul>
<li>Node.js版本、npm 套件 (ex：有些 npm套件只能在特定 Node.js版本運行)</li>
</ul>
</li>
<li>電腦運行環境
<ul>
<li>網路速度、硬體規格&hellip; (ex：如果你的網路環境太差需要去調整爬蟲元件的極限等待時間)</li>
</ul>
</li>
<li><mark>客戶的操作跟你預想的不一樣</mark></li>
</ol>
<p>第 1、2、3 點的問題我在之前文章實作碰到時有提到過，今天來講講<mark>實際上你會最崩潰的第 4 點</mark></p>
<hr>
<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-將客戶發的-bug-做基礎分類">1. 將客戶發的 bug 做基礎分類<a class="h-anchor" href="#1-%e5%b0%87%e5%ae%a2%e6%88%b6%e7%99%bc%e7%9a%84-bug-%e5%81%9a%e5%9f%ba%e7%a4%8e%e5%88%86%e9%a1%9e" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="2-引導客戶說出-bug-發生原因及提出解決方案的小技巧">2. 引導客戶說出 bug 發生原因及提出解決方案的小技巧<a class="h-anchor" href="#2-%e5%bc%95%e5%b0%8e%e5%ae%a2%e6%88%b6%e8%aa%aa%e5%87%ba-bug-%e7%99%bc%e7%94%9f%e5%8e%9f%e5%9b%a0%e5%8f%8a%e6%8f%90%e5%87%ba%e8%a7%a3%e6%b1%ba%e6%96%b9%e6%a1%88%e7%9a%84%e5%b0%8f%e6%8a%80%e5%b7%a7" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="3-重現客戶發生的-bug">3. 重現客戶發生的 bug<a class="h-anchor" href="#3-%e9%87%8d%e7%8f%be%e5%ae%a2%e6%88%b6%e7%99%bc%e7%94%9f%e7%9a%84-bug" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="4-改寫程式讓新的爬蟲資料塞入正確位置">4. 改寫程式讓新的爬蟲資料塞入正確位置<a class="h-anchor" href="#4-%e6%94%b9%e5%af%ab%e7%a8%8b%e5%bc%8f%e8%ae%93%e6%96%b0%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%a1%9e%e5%85%a5%e6%ad%a3%e7%a2%ba%e4%bd%8d%e7%bd%ae" aria-label="這個段落的連結"></a></h3>
<p>4.1 取得 Google Sheets 第一欄的粉專名稱：<code>readTitle</code></p>
<p>4.2 讓新的爬蟲資料正確寫入 Google Sheets：<code>writeSheet</code></p>
<hr>
<h2 class="h-nomark" id="1-將客戶發的-bug-做基礎分類-1">1. 將客戶發的 bug 做基礎分類<a class="h-anchor" href="#1-%e5%b0%87%e5%ae%a2%e6%88%b6%e7%99%bc%e7%9a%84-bug-%e5%81%9a%e5%9f%ba%e7%a4%8e%e5%88%86%e9%a1%9e-1" aria-label="這個段落的連結"></a></h2>
<p>與你合作的客戶通常不是資工相關背景，他們所提出來的 bug 有時也會讓你啼笑皆非，我個人會將他們提出的 bug 分成 3 種</p>
<ul>
<li><strong>明顯的 bug</strong>：客戶一報 bug 你就知道哪個環節出問題了，就認命地解決吧 (ex：臉書的爬蟲登入後就卡住不動了)</li>
<li><strong>需要討論的 bug</strong>：<mark>客戶報 bug 時你不確定是哪個環節出了問題，需要與客戶討論發生 bug 的操作流程</mark></li>
<li><strong>不合理的 bug</strong>：明顯不合理的要求你可以委婉的跟他說為什麼辦不到 (ex：程式執行到一半把電腦關機問你爬蟲資料為什麼沒有寫入 Google Sheets)</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-引導客戶說出-bug-發生原因及提出解決方案的小技巧-1">2. 引導客戶說出 bug 發生原因及提出解決方案的小技巧<a class="h-anchor" href="#2-%e5%bc%95%e5%b0%8e%e5%ae%a2%e6%88%b6%e8%aa%aa%e5%87%ba-bug-%e7%99%bc%e7%94%9f%e5%8e%9f%e5%9b%a0%e5%8f%8a%e6%8f%90%e5%87%ba%e8%a7%a3%e6%b1%ba%e6%96%b9%e6%a1%88%e7%9a%84%e5%b0%8f%e6%8a%80%e5%b7%a7-1" aria-label="這個段落的連結"></a></h2>
<p>很多時候客戶對於 bug 的陳述是很模糊的，所以你<strong>需要與客戶溝通，引導他們說出自己的操作流程</strong>，以今天發出的 bug 做舉例：</p>
<p><strong>客戶</strong>：今天爬蟲資料寫入 Google Sheets 表單時塞錯位置了！</p>
<p><strong>我</strong>：請問這個問題是今天突然發生的，還是過去幾天也發生過通樣的問題呢？ <code>(釐清 bug 發生的時間點)</code></p>
<p><strong>客戶</strong>：是今天才發生的</p>
<p><strong>我</strong>：那今天你在跑爬蟲前有做過什麼調整嗎？ <code>(確認是什麼樣的操作導致 bug)</code></p>
<p><strong>客戶</strong>：我在爬蟲的json 檔裡面新增了幾個粉專，然後把 Google Sheets 上面一些沒有在維護的粉專給砍掉</p>
<p><strong>我</strong>：了解，我這邊會先重複一遍您的操作來釐清問題，最晚會在兩天後給您答覆 <code>(自己操作一遍確認有這個bug)</code></p>
<p>===我是正在重現bug的分隔線===</p>
<p><strong>我</strong>：我依照您的方式操作確實遇到這個問題；這邊我提出一個解決方案，這個方案會將你新增的爬蟲粉專寫入 Google Sheets 表單的最下面，不管你爬蟲json 如何調整，追蹤人數都會填入 Google Sheets 的正確位置，詳細邏輯你可以參考下方說明 <code>(自己提出解決方案，千萬別讓客戶天馬行空亂提)</code></p>
<blockquote>
<ul>
<li>導致錯誤發生的操作：<br>1. 人工整理 Google Sheets粉專名稱的欄位：刪除、上下置換<br>2. 調整爬蟲json 內容：新增/刪除/上下置換</li>
<li>解決錯誤的方式：<br>1. 粉專名稱：寫入前將 Google Sheets 上粉專名稱的欄位與爬蟲json 比對，如果 <strong>爬蟲json 裡面有新的粉專就會新增到最下面，不改變 Google Sheets 上粉專名稱原有排列順序</strong><br>2. 追蹤人數：以 <strong>粉專名稱+粉專網址作為寫入追蹤人數欄位的判斷</strong></li>
</ul>
</blockquote>
<p><strong>客戶</strong>：好的先這樣做吧，你什麼時候能改好？</p>
<p><strong>我</strong>：預計三個工作天，如果提早完成會儘早與您聯繫 <code>(說一個你絕對能完成功能的時間，並表達積極性)</code></p>
<hr>
<h2 class="h-nomark" id="3-重現客戶發生的-bug-1">3. 重現客戶發生的 bug<a class="h-anchor" href="#3-%e9%87%8d%e7%8f%be%e5%ae%a2%e6%88%b6%e7%99%bc%e7%94%9f%e7%9a%84-bug-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>這個步驟非常重要，<mark>如果 bug 無法在自己的環境重現時你會超級頭大</mark>，因為站在客戶的角度就是認為你沒有完成專案，這會導致你結案的日子變得遙遙無期&hellip;</p>
</blockquote>
<p>今天問題重現起來非常簡單，跟著客戶的說的方式做就好：<mark>調整爬蟲json 及 Google sheet</mark></p>
<ol>
<li>
<p>我們這裡用 fb.json 為例，將<strong>麻糬爸愛亂畫</strong>刪除，然後把<strong>寶寶不說</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-json" data-lang="json"><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:#7ee787">&#34;title&#34;</span>: <span style="color:#a5d6ff">&#34;鹿尼&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;https://www.facebook.com/%E9%B9%BF%E5%B0%BC-260670294340690/&#34;</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:#7ee787">&#34;title&#34;</span>: <span style="color:#a5d6ff">&#34;寶寶不說&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;https://www.facebook.com/baobaonevertell/&#34;</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:#7ee787">&#34;title&#34;</span>: <span style="color:#a5d6ff">&#34;好想兔&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;https://www.facebook.com/chien760608/&#34;</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:#7ee787">&#34;title&#34;</span>: <span style="color:#a5d6ff">&#34;ㄇㄚˊ幾兔&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;https://www.facebook.com/machiko324/&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div></li>
<li>
<p>執行程式 <strong>yarn start</strong> 後你就會發現excel的表格亂掉了</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-21/images/fb-ig-crawler-day-21-01.webp" width="1024" height="349" alt="3. 重現客戶發生的 bug（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
</ol>
<hr>
<h2 class="h-nomark" id="4-改寫程式讓新的爬蟲資料塞入正確位置-1">4. 改寫程式讓新的爬蟲資料塞入正確位置<a class="h-anchor" href="#4-%e6%94%b9%e5%af%ab%e7%a8%8b%e5%bc%8f%e8%ae%93%e6%96%b0%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%a1%9e%e5%85%a5%e6%ad%a3%e7%a2%ba%e4%bd%8d%e7%bd%ae-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="41-取得-google-sheets-第一欄的粉專名稱readtitle">4.1 取得 Google Sheets 第一欄的粉專名稱：<code>readTitle</code><a class="h-anchor" href="#41-%e5%8f%96%e5%be%97-google-sheets-%e7%ac%ac%e4%b8%80%e6%ac%84%e7%9a%84%e7%b2%89%e5%b0%88%e5%90%8d%e7%a8%b1readtitle" aria-label="這個段落的連結"></a></h3>
<ul>
<li>當這個 sheet 全新的時候內容是 undefine，但是因為我們等等再做對比時用的格式為 array，所以要給他一個初始型別</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> readTitle (title, auth) {
</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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        ranges<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!A:A`</span>
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        valueRenderOption<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;FORMULA&#34;</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">let</span> title_array <span style="color:#ff7b72;font-weight:bold">=</span> []
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> values <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">await</span> sheets.spreadsheets.values.batchGet(request)).
</span></span><span style="display:flex;"><span>            data.valueRanges[<span style="color:#a5d6ff">0</span>].values;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (values) {<span style="color:#8b949e;font-style:italic">//如果沒資料values會是undefine，所以我們只在有資料時塞入
</span></span></span><span style="display:flex;"><span>            title_array <span style="color:#ff7b72;font-weight:bold">=</span> values.map(value =&gt; value[<span style="color:#a5d6ff">0</span>]);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// console.log(title_array)
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> title_array
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(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="42-讓新的爬蟲資料正確寫入-google-sheetswritesheet">4.2 讓新的爬蟲資料正確寫入 Google Sheets：<code>writeSheet</code><a class="h-anchor" href="#42-%e8%ae%93%e6%96%b0%e7%9a%84%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e6%ad%a3%e7%a2%ba%e5%af%ab%e5%85%a5-google-sheetswritesheet" aria-label="這個段落的連結"></a></h3>
<p>我們依照客戶接受的<mark>解決方案</mark>來調整這隻函式</p>
<ul>
<li>將爬蟲json 中新增的粉專<mark>補到 Google Sheets 最後面</mark></li>
<li>追蹤人數用<code>粉專名稱+粉專網址</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> writeSheet (title, result_array, 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">let</span> online_title_array <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> readTitle(title, auth)
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 如果json檔有新增的粉專就補到最後面
</span></span></span><span style="display:flex;"><span>  result_array.forEach(fanpage =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>online_title_array.includes(<span style="color:#a5d6ff">`=HYPERLINK(&#34;</span><span style="color:#a5d6ff">${</span>fanpage.url<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;,&#34;</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;)`</span>)) {
</span></span><span style="display:flex;"><span>      online_title_array.push(<span style="color:#a5d6ff">`=HYPERLINK(&#34;</span><span style="color:#a5d6ff">${</span>fanpage.url<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;,&#34;</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;)`</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:#8b949e;font-style:italic">// &#34;粉專名稱+粉專網址&#34;作為寫入追蹤人數欄位的判斷
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> trace_array <span style="color:#ff7b72;font-weight:bold">=</span> []
</span></span><span style="display:flex;"><span>  online_title_array.forEach(title =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> result_array.find(fanpage =&gt; <span style="color:#a5d6ff">`=HYPERLINK(&#34;</span><span style="color:#a5d6ff">${</span>fanpage.url<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;,&#34;</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;)`</span> <span style="color:#ff7b72;font-weight:bold">==</span> title)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (fanpage) {
</span></span><span style="display:flex;"><span>      trace_array.push([fanpage.trace])
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>      trace_array.push([])
</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">const</span> datetime <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Date()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (online_title_array[<span style="color:#a5d6ff">0</span>] <span style="color:#ff7b72;font-weight:bold">!==</span> title) {<span style="color:#8b949e;font-style:italic">//如果是全新的sheet就會在開頭插入
</span></span></span><span style="display:flex;"><span>    online_title_array.unshift(title)
</span></span><span style="display:flex;"><span>    trace_array.unshift([dateFormat(datetime, <span style="color:#a5d6ff">&#34;GMT:yyyy/mm/dd&#34;</span>)])
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> {<span style="color:#8b949e;font-style:italic">//如果不是全新就取代
</span></span></span><span style="display:flex;"><span>    trace_array[<span style="color:#a5d6ff">0</span>] <span style="color:#ff7b72;font-weight:bold">=</span> [dateFormat(datetime, <span style="color:#a5d6ff">&#34;GMT:yyyy/mm/dd&#34;</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">await</span> writeTitle(title, online_title_array.map(title =&gt; [title]), auth)
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> lastCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getLastCol(title, auth)
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> writeTrace(title, trace_array, lastCol, auth)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="執行程式">執行程式<a class="h-anchor" href="#%e5%9f%b7%e8%a1%8c%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>本次目標需多次執行程式你才能確保程式運作正確</p>
<ol>
<li>
<p>依照自己的想法去編修線上的 Google Sheets、調整爬蟲json 內容</p>
</li>
<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>在爬蟲跑完後看看線上的 Google Sheets 是否有依照你的更改寫入正確位置</p>
<blockquote>
<p>下圖是我多次修改 fb.json 後執行爬蟲的結果</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-21/images/fb-ig-crawler-day-21-02.webp" width="1024" height="314" alt="執行程式（圖 2）" loading="lazy" decoding="async"></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/day21">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day20_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來解決客戶回報的bug喔
<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-21/images/fb-ig-crawler-day-21-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>
    <item>
      <title>【Day20】Google Sheets - 寫入爬蟲資料，跟 Copy &amp; Paste 的日子說掰掰</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-20/</link>
      <pubDate>Mon, 05 Oct 2020 09:01:38 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-20/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題1.2 組合回傳的資訊：以 crawlerFB 為範例</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/cover_hu_7524c32e28b30d4.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>爬蟲是一個技術，他將網頁的數據收集下來</p>
<p>Google Sheets 是一個容器，他可以儲存資料並將資料以不同面向做展示</p>
<p><mark>爬蟲 Ｘ Google Sheets ＝ 你要學習的技術整合</mark></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-改寫crawlerigcrawler這兩隻函式讓他們依照我們想要格式回傳爬蟲資料">1. 改寫<code>crawlerIG、crawler</code>這兩隻函式，讓他們依照我們想要格式回傳爬蟲資料<a class="h-anchor" href="#1-%e6%94%b9%e5%af%abcrawlerigcrawler%e9%80%99%e5%85%a9%e9%9a%bb%e5%87%bd%e5%bc%8f%e8%ae%93%e4%bb%96%e5%80%91%e4%be%9d%e7%85%a7%e6%88%91%e5%80%91%e6%83%b3%e8%a6%81%e6%a0%bc%e5%bc%8f%e5%9b%9e%e5%82%b3%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h3>
<p>1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題</p>
<p>1.2 組合回傳的資訊：以 <code>crawlerFB</code> 為範例</p>
<h3 class="h-nomark" id="2-以主程式當橋樑將爬蟲資料傳遞到updategooglesheets">2. 以主程式當橋樑，將爬蟲資料傳遞到<code>updateGoogleSheets</code><a class="h-anchor" href="#2-%e4%bb%a5%e4%b8%bb%e7%a8%8b%e5%bc%8f%e7%95%b6%e6%a9%8b%e6%a8%91%e5%b0%87%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%82%b3%e9%81%9e%e5%88%b0updategooglesheets" aria-label="這個段落的連結"></a></h3>
<p>2.1 用<code>主程式</code>傳遞爬蟲資料</p>
<p>2.2 改寫 <code>updateGoogleSheets</code> 函式來接收並處理爬蟲資料</p>
<h3 class="h-nomark" id="3-將-fbig粉專爬蟲資料寫入各自的-sheet">3. 將 FB、IG粉專爬蟲資料寫入各自的 Sheet<a class="h-anchor" href="#3-%e5%b0%87-fbig%e7%b2%89%e5%b0%88%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%af%ab%e5%85%a5%e5%90%84%e8%87%aa%e7%9a%84-sheet" aria-label="這個段落的連結"></a></h3>
<p>3.1 分析寫入 Google Sheets 的步驟：<code>writeSheet</code></p>
<p>3.2 在官方文件尋找 <strong>寫入Sheet</strong> 的範例</p>
<p>3.3 第一欄寫入 title(粉專名稱)：<code>writeTitle</code></p>
<p>3.4 取得 Sheet 最後一欄的空白欄位：<code>getLastCol</code></p>
<p>3.5 將 trace(追蹤人數)資訊寫入最後一欄：<code>writeTrace</code></p>
<hr>
<h2 class="h-nomark" id="1-改寫crawlerigcrawler這兩隻函式讓他們依照我們想要格式回傳爬蟲資料-1">1. 改寫<code>crawlerIG、crawler</code>這兩隻函式，讓他們依照我們想要格式回傳爬蟲資料<a class="h-anchor" href="#1-%e6%94%b9%e5%af%abcrawlerigcrawler%e9%80%99%e5%85%a9%e9%9a%bb%e5%87%bd%e5%bc%8f%e8%ae%93%e4%bb%96%e5%80%91%e4%be%9d%e7%85%a7%e6%88%91%e5%80%91%e6%83%b3%e8%a6%81%e6%a0%bc%e5%bc%8f%e5%9b%9e%e5%82%b3%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>過去我們只將 FB粉專、IG粉專的資訊用 console.log 輸出，現在我們要把這些資料有結構的存入 json 物件方便後續使用</p>
</blockquote>
<h3 class="h-nomark" id="11-分析-google-sheets-上要呈現的資料寫入時可能遇到的問題">1.1 分析 Google Sheets 上要呈現的資料、寫入時可能遇到的問題<a class="h-anchor" href="#11-%e5%88%86%e6%9e%90-google-sheets-%e4%b8%8a%e8%a6%81%e5%91%88%e7%8f%be%e7%9a%84%e8%b3%87%e6%96%99%e5%af%ab%e5%85%a5%e6%99%82%e5%8f%af%e8%83%bd%e9%81%87%e5%88%b0%e7%9a%84%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>Google Sheets 呈現的資料</strong>：
<ol>
<li>粉專名稱</li>
<li>追蹤人數</li>
<li>追蹤日期</li>
</ol>
</li>
<li><strong>可能會遇到的問題</strong>：
<ol>
<li>重複名稱的粉專
<ul>
<li>解決方案：粉專名稱有可能重名，但是<mark>粉專網址是唯一值</mark>，所以使用<code>粉專名稱+粉專網址作為key</code>就能解決這個問題，同時也方便使用者可以點擊連結直接前往粉專</li>
</ul>
</li>
</ol>
</li>
<li><strong>總結</strong>：
<ol>
<li>因為追蹤的 FB、IG粉專數量很多，所以 <strong>crawlerFB、crawlerIG</strong> 這兩個函式回傳的最外層用陣列(array)包起來</li>
<li>根據 Google Sheets 所需的資料，每個粉專的物件(object)需要有以下內容：
<ol>
<li>粉專名稱(title)</li>
<li>粉專網址(url)</li>
<li>追蹤人數(trace)</li>
</ol>
</li>
</ol>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="12-組合回傳的資訊以-crawlerfb-為範例">1.2 組合回傳的資訊：以 <code>crawlerFB</code> 為範例<a class="h-anchor" href="#12-%e7%b5%84%e5%90%88%e5%9b%9e%e5%82%b3%e7%9a%84%e8%b3%87%e8%a8%8a%e4%bb%a5-crawlerfb-%e7%82%ba%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>在 try-catch 的後面加上 finally，在 finally 階段才把粉專物件存入 result_array 是因為<mark>無論這個粉專爬蟲成功或是失敗我們都要記錄他的數據</mark></p>
<blockquote>
<p>finally 代表在 try-catch 結束後會執行的任務，忘記的可以回到<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/">Day14 try-catch-finally 基礎語法說明</a>複習喔</p>
</blockquote>
</li>
<li>
<p>在 FB粉專爬蟲完成後，回傳儲存所有粉專資訊的 result_array 讓主程式做資料串接</p>
</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> crawlerFB (driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> isLogin <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> loginFacebook(driver)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (isLogin) {
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`FB開始爬蟲`</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> result_array <span style="color:#ff7b72;font-weight:bold">=</span> []
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">for</span> (fanpage <span style="color:#ff7b72">of</span> fanpage_array) {
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">let</span> trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#79c0ff">null</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">const</span> isGoFansPage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> goFansPage(driver, fanpage.url)
</span></span><span style="display:flex;"><span>                <span style="color:#ff7b72">if</span> (isGoFansPage) {
</span></span><span style="display:flex;"><span>                    <span style="color:#ff7b72">await</span> driver.sleep((Math.floor(Math.random() <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">4</span>) <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">3</span>) <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)
</span></span><span style="display:flex;"><span>                    trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getTrace(driver, By, until)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#ff7b72">if</span> (trace <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#79c0ff">null</span>) {
</span></span><span style="display:flex;"><span>                    console.log(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">無法抓取追蹤人數`</span>)
</span></span><span style="display:flex;"><span>                } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>                    console.log(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">追蹤人數：</span><span style="color:#a5d6ff">${</span>trace<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>                console.error(e);
</span></span><span style="display:flex;"><span>                <span style="color:#ff7b72">continue</span>;
</span></span><span style="display:flex;"><span>            } <span style="color:#ff7b72">finally</span> {<span style="color:#8b949e;font-style:italic">// 將粉專的資訊塞入物件
</span></span></span><span style="display:flex;"><span>                result_array.push({
</span></span><span style="display:flex;"><span>                    url<span style="color:#ff7b72;font-weight:bold">:</span> fanpage.url,
</span></span><span style="display:flex;"><span>                    title<span style="color:#ff7b72;font-weight:bold">:</span> fanpage.title,
</span></span><span style="display:flex;"><span>                    trace<span style="color:#ff7b72;font-weight:bold">:</span> trace
</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:#8b949e;font-style:italic">// 回傳FB粉專爬蟲資料
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> result_array
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="2-以主程式當橋樑將爬蟲資料傳遞到updategooglesheets-1">2. 以主程式當橋樑，將爬蟲資料傳遞到<code>updateGoogleSheets</code><a class="h-anchor" href="#2-%e4%bb%a5%e4%b8%bb%e7%a8%8b%e5%bc%8f%e7%95%b6%e6%a9%8b%e6%a8%91%e5%b0%87%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%82%b3%e9%81%9e%e5%88%b0updategooglesheets-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-用主程式傳遞爬蟲資料">2.1 用<code>主程式</code>傳遞爬蟲資料<a class="h-anchor" href="#21-%e7%94%a8%e4%b8%bb%e7%a8%8b%e5%bc%8f%e5%82%b3%e9%81%9e%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h3>
<p>主程式將 crawlerIG、crawlerFB 回傳的爬蟲資訊提供給 updateGoogleSheets 當參數</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> crawler () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> driver <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> initDrive();
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>driver) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span>
</span></span><span style="display:flex;"><span>    }
</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">const</span> ig_result_array <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> crawlerIG(driver)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fb_result_array <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> crawlerFB(driver)
</span></span><span style="display:flex;"><span>    driver.quit();
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//將回傳的爬蟲資料傳遞給Google Sheets處理
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> updateGoogleSheets(ig_result_array, fb_result_array)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="22-改寫-updategooglesheets-函式來接收並處理爬蟲資料">2.2 改寫 <code>updateGoogleSheets</code> 函式來接收並處理爬蟲資料<a class="h-anchor" href="#22-%e6%94%b9%e5%af%ab-updategooglesheets-%e5%87%bd%e5%bc%8f%e4%be%86%e6%8e%a5%e6%94%b6%e4%b8%a6%e8%99%95%e7%90%86%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h3>
<ul>
<li>接受爬蟲回傳的資料：ig_result_array, fb_result_array</li>
<li>新增函式 <code>writeSheet</code> 將收到的爬蟲資料寫入對應的 Sheet</li>
<li>最後印出 Google Sheets 的網址方便查看</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> updateGoogleSheets (ig_result_array, fb_result_array) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> auth <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getAuth()
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> sheets <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getFBIGSheet(auth)
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#39;FB、IG Sheet資訊:&#39;</span>)
</span></span><span style="display:flex;"><span>        console.log(sheets)
</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">await</span> writeSheet(<span style="color:#a5d6ff">&#39;FB粉專&#39;</span>, fb_result_array, auth)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">await</span> writeSheet(<span style="color:#a5d6ff">&#39;IG帳號&#39;</span>, ig_result_array, auth)
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`成功更新Google Sheets：https://docs.google.com/spreadsheets/d/</span><span style="color:#a5d6ff">${</span>process.env.SPREADSHEET_ID<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#39;更新Google Sheets失敗&#39;</span>);
</span></span><span style="display:flex;"><span>        console.error(err);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="3-將fbig粉專爬蟲資料寫入各自的sheet">3. 將FB、IG粉專爬蟲資料寫入各自的Sheet<a class="h-anchor" href="#3-%e5%b0%87fbig%e7%b2%89%e5%b0%88%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%af%ab%e5%85%a5%e5%90%84%e8%87%aa%e7%9a%84sheet" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="31-分析寫入-google-sheets-的步驟writesheet">3.1 分析寫入 Google Sheets 的步驟：<code>writeSheet</code><a class="h-anchor" href="#31-%e5%88%86%e6%9e%90%e5%af%ab%e5%85%a5-google-sheets-%e7%9a%84%e6%ad%a5%e9%a9%9fwritesheet" aria-label="這個段落的連結"></a></h3>
<p><strong>目標</strong>：希望寫入的 Google Sheets 長得像下圖：</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/fb-ig-crawler-day-20-01.webp" width="1024" height="351" alt="3.1 分析寫入 Google Sheets 的步驟：writeSheet（圖 1）" fetchpriority="high" decoding="async"></p>
<p><strong>我們先將想要完成的功能羅列出來，再一個個去實現：</strong></p>
<ol>
<li>第一欄為 <code>粉專名稱+粉專網址</code> 組合的 <strong>HYPERLINK</strong>，每次執行爬蟲時我們會用 <code>writeTitle</code> 這個函式來更新第一欄的資料</li>
<li>因為希望新的爬蟲紀錄會寫入到下一欄，所以我們寫一個函式 <code>getLastCol</code> 來取得 Sheet 最後一欄的空白欄位</li>
<li>取得目標寫入欄位後，我們用函式 <code>writeTrace</code> 把爬蟲下來的追蹤人數寫入就完成嚕</li>
</ol>
<p><strong>實作</strong>：將上述要完成的功能各自撰寫成函式後，由 <code>writeSheet</code> 這個函式來管理寫入 Google Sheets 的步驟</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, result_array, auth) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// title可以直接超連結到粉專
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> title_array <span style="color:#ff7b72;font-weight:bold">=</span> result_array.map(fanpage =&gt; [<span style="color:#a5d6ff">`=HYPERLINK(&#34;</span><span style="color:#a5d6ff">${</span>fanpage.url<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;,&#34;</span><span style="color:#a5d6ff">${</span>fanpage.title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#34;)`</span>]);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 開頭填入填上FB粉專/IG帳號
</span></span></span><span style="display:flex;"><span>    title_array.unshift([title])<span style="color:#8b949e;font-style:italic">//unshift是指插入陣列開頭
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 在第一欄寫入title(粉專名稱)
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> writeTitle(title, title_array, auth)
</span></span><span style="display:flex;"><span>
</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">let</span> lastCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getLastCol(title, auth)
</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">let</span> trace_array <span style="color:#ff7b72;font-weight:bold">=</span> result_array.map(fanpage =&gt; [fanpage.trace]);
</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">const</span> datetime <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Date()
</span></span><span style="display:flex;"><span>    trace_array.unshift([dateFormat(datetime, <span style="color:#a5d6ff">&#34;GMT:yyyy/mm/dd&#34;</span>)])
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 再寫入trace(追蹤人數)
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> writeTrace(title, trace_array, lastCol, auth)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="32-在官方文件尋找-寫入sheet-的範例">3.2 在官方文件尋找 <code>寫入Sheet</code> 的範例<a class="h-anchor" href="#32-%e5%9c%a8%e5%ae%98%e6%96%b9%e6%96%87%e4%bb%b6%e5%b0%8b%e6%89%be-%e5%af%ab%e5%85%a5sheet-%e7%9a%84%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>有了<mark>昨天的經驗</mark>我們可以很容易找到今天所需的資源，在範例首頁的大標題<a href="https://developers.google.com/sheets/api/samples/writing">Basic Writing</a>就符合今天的需求</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/fb-ig-crawler-day-20-02.webp" width="1024" height="600" alt="3.2 在官方文件尋找 寫入Sheet 的範例（圖 2）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在閱讀 <strong>Basic Writing</strong> 描述時有兩個標題吸引我的注意</p>
<ul>
<li>
<p><a href="https://developers.google.com/sheets/api/samples/writing#append_values">Append Values</a>：在閱讀標題時我原本覺得是最好的選擇，但發現他只能插入列(row)，與我們插入欄(col)的需求不符</p>
</li>
<li>
<p><a href="https://developers.google.com/sheets/api/samples/writing#write_a_single_range">Write a single range</a>：在開頭便提供了一個連結，並說<mark>這個方法能寫入指定 Sheet 範圍內的資料</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/fb-ig-crawler-day-20-03.webp" width="1024" height="345" alt="3.2 在官方文件尋找 寫入Sheet 的範例（圖 3）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
<li>
<p>接著你可以用<code>Try this API</code>來確認 <a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update">Method: spreadsheets.values.update</a> 是否能正確更新 Sheet 的內容，相關操作方式<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/">昨天的文章</a>有示範，這裡就不再贅述了，下面我把官方範例翻譯了一下，讓大家更清楚使用的方法</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">let</span> title <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;你的sheet title&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">//Google Sheets能吃的array格式範例
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> array <span style="color:#ff7b72;font-weight:bold">=</span> [[<span style="color:#a5d6ff">&#39;test1&#39;</span>],[<span style="color:#a5d6ff">&#39;test2&#39;</span>],[<span style="color:#a5d6ff">&#39;test3&#39;</span>],[<span style="color:#a5d6ff">&#39;test4&#39;</span>]]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> writeSheet (title, array, auth) {<span style="color:#8b949e;font-style:italic">//auth為憑證通過後取得
</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>    spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>    valueInputOption<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;USER_ENTERED&#34;</span>,<span style="color:#8b949e;font-style:italic">//寫入格式的分類有：INPUT_VALUE_OPTION_UNSPECIFIED|RAW|USER_ENTERED
</span></span></span><span style="display:flex;"><span>    range<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!A:A`</span><span style="color:#8b949e;font-style:italic">//title是sheet的標題，A:A是能寫入的範圍
</span></span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    resource<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      values<span style="color:#ff7b72;font-weight:bold">:</span> array
</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.values.update(request);<span style="color:#8b949e;font-style:italic">//執行後即完成Google Sheets更新
</span></span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`updated </span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> title`</span>);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>    console.error(err);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
</ol>
<hr>
<h3 class="h-nomark" id="33-第一欄寫入-title粉專名稱writetitle">3.3 第一欄寫入 title(粉專名稱)：<code>writeTitle</code><a class="h-anchor" href="#33-%e7%ac%ac%e4%b8%80%e6%ac%84%e5%af%ab%e5%85%a5-title%e7%b2%89%e5%b0%88%e5%90%8d%e7%a8%b1writetitle" aria-label="這個段落的連結"></a></h3>
<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> writeTitle (title, title_array, auth) {
</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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        valueInputOption<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;USER_ENTERED&#34;</span>,
</span></span><span style="display:flex;"><span>        range<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!A:A`</span> <span style="color:#8b949e;font-style:italic">// 這是寫入第一欄的意思
</span></span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        resource<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        values<span style="color:#ff7b72;font-weight:bold">:</span> title_array
</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.values.update(request);
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`updated </span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> title`</span>);
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(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="34-取得-sheet-最後一欄的空白欄位getlastcol">3.4 取得 Sheet 最後一欄的空白欄位：<code>getLastCol</code><a class="h-anchor" href="#34-%e5%8f%96%e5%be%97-sheet-%e6%9c%80%e5%be%8c%e4%b8%80%e6%ac%84%e7%9a%84%e7%a9%ba%e7%99%bd%e6%ac%84%e4%bd%8dgetlastcol" aria-label="這個段落的連結"></a></h3>
<ul>
<li>在這個函式中我們取得 Sheet 的第一列來做分析，詳細的文件請參考<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchGet">官方說明</a></li>
<li>同時我們也需要用函式 <code>toColumnName</code> 把取得的欄位名稱轉換為英文，這樣 Google Sheets 才知道要寫入的欄位</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> getLastCol (title, auth) {
</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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        ranges<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!A1:ZZ1`</span>
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        majorDimension<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;COLUMNS&#34;</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">let</span> values <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">await</span> sheets.spreadsheets.values.batchGet(request)).data.valueRanges[<span style="color:#a5d6ff">0</span>].values;
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// console.log(title + &#34; StartCol: &#34; + toColumnName(values.length + 1))
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> toColumnName(values.length <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(err);
</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">function</span> toColumnName (num) {<span style="color:#8b949e;font-style:italic">//Google Sheets無法辨認數字欄位，需轉為英文才能使用
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">var</span> ret <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;&#39;</span>, a <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>, b <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">26</span>; (num <span style="color:#ff7b72;font-weight:bold">-=</span> a) <span style="color:#ff7b72;font-weight:bold">&gt;=</span> <span style="color:#a5d6ff">0</span>; a <span style="color:#ff7b72;font-weight:bold">=</span> b, b <span style="color:#ff7b72;font-weight:bold">*=</span> <span style="color:#a5d6ff">26</span>) {
</span></span><span style="display:flex;"><span>        ret <span style="color:#ff7b72;font-weight:bold">=</span> String.fromCharCode(parseInt((num <span style="color:#ff7b72;font-weight:bold">%</span> b) <span style="color:#ff7b72;font-weight:bold">/</span> a) <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">65</span>) <span style="color:#ff7b72;font-weight:bold">+</span> ret;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> ret;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="35-將-trace追蹤人數資訊寫入最後一欄writetrace">3.5 將 trace(追蹤人數)資訊寫入最後一欄：<code>writeTrace</code><a class="h-anchor" href="#35-%e5%b0%87-trace%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8%e8%b3%87%e8%a8%8a%e5%af%ab%e5%85%a5%e6%9c%80%e5%be%8c%e4%b8%80%e6%ac%84writetrace" aria-label="這個段落的連結"></a></h3>
<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> writeTrace (title, trace_array, lastCol, auth) {
</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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        valueInputOption<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;USER_ENTERED&#34;</span>,
</span></span><span style="display:flex;"><span>        range<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!</span><span style="color:#a5d6ff">${</span>lastCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">:</span><span style="color:#a5d6ff">${</span>lastCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span><span style="color:#8b949e;font-style:italic">//將追蹤人數填入最後一欄
</span></span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        resource<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        values<span style="color:#ff7b72;font-weight:bold">:</span> trace_array
</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.values.update(request);
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`updated </span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> trace`</span>);
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(err);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><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>確認執行過程中爬蟲資料抓取正常且更新 Google sheets 時沒有錯誤</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/fb-ig-crawler-day-20-04.webp" width="1024" height="563" alt="執行程式（圖 4）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>點進去連結看看線上的 Google Sheets 是不是也被成功寫入惹～</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/images/fb-ig-crawler-day-20-05.webp" width="1024" height="349" alt="執行程式（圖 5）" loading="lazy" decoding="async"></p>
</li>
</ol>
<p>目前為止將爬蟲寫入 Google Sheets 的動作已經完成了，大家可以思考一下還有什麼東西是我們忽略的呢？有什麼狀況會造成錯誤？希望大家在下方提供自己的想法喔～</p>
<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/day20">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day19_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天Google Sheets的爬蟲資料寫入喔
<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-20/images/fb-ig-crawler-day-20-06.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 6）" 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>
    <item>
      <title>【Day19】Google Sheets - 你在文件迷路了嗎？用兩個處理Sheet的範例帶你攻略官方文件</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-19/</link>
      <pubDate>Sun, 04 Oct 2020 09:23:48 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-19/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>接下來幾天的目標是將之前的爬蟲資料放入 Google Sheets，實踐上會分成幾個步驟：</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/cover_hu_bf88811cf4f86db1.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>如果說爬蟲是在鍛鍊你分析網頁的能力，那麼 Google Sheets 就是在教你閱讀官方文件的技巧</p>
</blockquote>
<h2 id="爬蟲資料如何自動化的塞入google-sheets">爬蟲資料如何自動化的塞入Google Sheets?<a class="h-anchor" href="#%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e5%a6%82%e4%bd%95%e8%87%aa%e5%8b%95%e5%8c%96%e7%9a%84%e5%a1%9e%e5%85%a5google-sheets" aria-label="這個段落的連結"></a></h2>
<p>接下來幾天的目標是將之前的爬蟲資料放入 Google Sheets，實踐上會分成幾個步驟：</p>
<ol>
<li>取得放 FB、IG爬蟲資料的 Sheet 資訊(今日目標)</li>
<li>將 FB、IG爬蟲資料寫入各自的 Sheet <a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-20/">Day20</a></li>
<li>插入新的爬蟲資料前先分析粉專欄位，再將資料放入正確的位置 <a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-21/">Day21</a></li>
<li>將新的爬蟲資料改為插入第二欄，日期由近而遠排列 <a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-22/">Day22</a></li>
<li>調整 Google Sheets 的外觀，讓他閱讀起來更友善 <a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/">Day23</a></li>
</ol>
<hr>
<h2 id="筆者有話先說">筆者有話先說<a class="h-anchor" href="#%e7%ad%86%e8%80%85%e6%9c%89%e8%a9%b1%e5%85%88%e8%aa%aa" aria-label="這個段落的連結"></a></h2>
<p><mark>使用 Google api 時你要煩惱的不是沒有功能，而是你找不到功能在哪裡</mark>，因為<strong>這份 api 文件實在是太龐大了</strong>，對新手來說非常容易在文件中迷路&hellip;</p>
<p>所以今天的文章重點在<mark>如何從官方文件找出需要的資源</mark>，希望能幫助到剛接觸巨型文件的朋友們</p>
<hr>
<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-讀取-google-sheets-內的-sheet-資訊">1. 讀取 Google Sheets 內的 Sheet 資訊<a class="h-anchor" href="#1-%e8%ae%80%e5%8f%96-google-sheets-%e5%85%a7%e7%9a%84-sheet-%e8%b3%87%e8%a8%8a" aria-label="這個段落的連結"></a></h3>
<p>1.1 在官方文件尋找 <strong>讀取Sheet</strong> 的範例</p>
<p>1.2 調整範例以符合專案需求：<code>getSheets</code></p>
<h3 class="h-nomark" id="2-取得-fb粉專ig帳號的-sheet-資訊getfbigsheet">2. 取得 FB粉專、IG帳號的 Sheet 資訊：<code>getFBIGSheet</code><a class="h-anchor" href="#2-%e5%8f%96%e5%be%97-fb%e7%b2%89%e5%b0%88ig%e5%b8%b3%e8%99%9f%e7%9a%84-sheet-%e8%b3%87%e8%a8%8agetfbigsheet" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="3-建立放-fb粉專ig帳號爬蟲資料的-sheet">3. 建立放 FB粉專、IG帳號爬蟲資料的 Sheet<a class="h-anchor" href="#3-%e5%bb%ba%e7%ab%8b%e6%94%be-fb%e7%b2%89%e5%b0%88ig%e5%b8%b3%e8%99%9f%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e7%9a%84-sheet" aria-label="這個段落的連結"></a></h3>
<p>3.1 在官方文件尋找 <strong>新增Sheet</strong> 的範例</p>
<p>3.2 調整範例以符合專案需求：<code>addSheets</code></p>
<h3 class="h-nomark" id="4-重新架構-google-sheets-程式">4. 重新架構 Google Sheets 程式<a class="h-anchor" href="#4-%e9%87%8d%e6%96%b0%e6%9e%b6%e6%a7%8b-google-sheets-%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>4.1 建立外部函式模組：<code>updateGoogleSheets</code></p>
<p>4.2 將取得 auth(認證)的步驟改寫為函式：<code>getAuth</code></p>
<h3 class="h-nomark" id="5-讓主程式呼叫外部函式updategooglesheets">5. 讓主程式呼叫外部函式：<code>updateGoogleSheets</code><a class="h-anchor" href="#5-%e8%ae%93%e4%b8%bb%e7%a8%8b%e5%bc%8f%e5%91%bc%e5%8f%ab%e5%a4%96%e9%83%a8%e5%87%bd%e5%bc%8fupdategooglesheets" aria-label="這個段落的連結"></a></h3>
<hr>
<h2 class="h-nomark" id="1-讀取-google-sheets-內的-sheet-資訊-1">1. 讀取 Google Sheets 內的 Sheet 資訊<a class="h-anchor" href="#1-%e8%ae%80%e5%8f%96-google-sheets-%e5%85%a7%e7%9a%84-sheet-%e8%b3%87%e8%a8%8a-1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>建議你所需要的功能都先從<mark>有範例的文件</mark>找起，因為沒範例的文件你還要自己瞎猜程式結構</p>
</blockquote>
<h3 class="h-nomark" id="11-在官方文件尋找-讀取sheet-的範例">1.1 在官方文件尋找 <code>讀取Sheet</code> 的範例<a class="h-anchor" href="#11-%e5%9c%a8%e5%ae%98%e6%96%b9%e6%96%87%e4%bb%b6%e5%b0%8b%e6%89%be-%e8%ae%80%e5%8f%96sheet-%e7%9a%84%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>進入官方文件<a href="https://developers.google.com/sheets/api/samples">範例首頁</a>後 <mark>先觀察大標題的描述</mark>（可用關鍵字 <strong>sheet</strong> 搜尋來輔助）</p>
<blockquote>
<p>標題 <strong>Sheet Operations</strong> 的描述上比較符合我們需求，因為我們要<mark>找出 spreadsheet 底下的 sheet 資訊</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-01.webp" width="1024" height="474" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 1）" fetchpriority="high" decoding="async"></p>
</blockquote>
</li>
<li>
<p>進入<a href="https://developers.google.com/sheets/api/samples/sheet">Sheet Operations</a>頁面後<mark>一樣先從標題下手</mark></p>
<blockquote>
<p>標題 <strong>Determine sheet ID and other properties</strong> 跟 <mark>sheet 的詳細資訊</mark>有關</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-02.webp" width="1024" height="357" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 2）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>看<a href="https://developers.google.com/sheets/api/samples/sheet#determine_sheet_id_and_other_properties">Determine sheet ID and other properties</a>的描述</p>
<blockquote>
<p>第一段給了一個連結，並說 <mark>這個方法能取得特定 spreadsheet 底下 sheet 的屬性</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-03.webp" width="1024" height="291" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 3）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>接下來要確認 <a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get">spreadsheets.get</a> 這個方法是否真的符合我們的需求</p>
<blockquote>
<ol>
<li>
<p>首先我們看到 <mark>Google 非常貼心提供線上測試 api 的地方</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-04.webp" width="1024" height="473" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 4）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>進入測試 api 全螢幕模式後，<mark>填寫自己的 spreadsheetId、憑證權限</mark> 接著按下 <strong>EXCUTE</strong> 即可在右下角看到 api 的回傳資訊</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-05.webp" width="1024" height="699" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 5）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>打開自己的 Google Sheets 來與 api 的回傳資訊做對照</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-06.webp" width="1024" height="349" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 6）" loading="lazy" decoding="async"></p>
<ul>
<li>
<p>從回傳的 json 我們就能知道所需的資訊被放在 <code>sheets</code> 這個陣列中，並且可用 <code>sheets[].properties.title</code> 這個參數判斷 &lsquo;FB粉專&rsquo;、&lsquo;IG帳號&rsquo; 的 Sheet 是否已經建立</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-07.webp" width="689" height="374" alt="1.1 在官方文件尋找 讀取Sheet 的範例（圖 7）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
</ol>
</blockquote>
</li>
<li>
<p>確認這個 api 能取得我們所需要的資訊後，我們便可參考官方提供的<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get#examples">範例程式</a>開始實作啦</p>
</li>
</ol>
<h3 class="h-nomark" id="12-調整範例以符合專案需求getsheets">1.2 調整範例以符合專案需求：<code>getSheets</code><a class="h-anchor" href="#12-%e8%aa%bf%e6%95%b4%e7%af%84%e4%be%8b%e4%bb%a5%e7%ac%a6%e5%90%88%e5%b0%88%e6%a1%88%e9%9c%80%e6%b1%82getsheets" aria-label="這個段落的連結"></a></h3>
<p>這個函式的目標是 <mark>取得指定 spreadsheetId 下 sheets 資訊</mark>，根據上面 api 回傳的 json 結構分析，我們所需要的資料就在<code>response.sheets</code>裡面</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> getSheets (auth) {<span style="color:#8b949e;font-style:italic">//取得Google Sheets所有的sheet
</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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        includeGridData<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">false</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">let</span> response <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">await</span> sheets.spreadsheets.get(request)).data;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> sheets_info <span style="color:#ff7b72;font-weight:bold">=</span> response.sheets
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> sheets_info
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(err);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="2-取得-fb粉專ig帳號的-sheet-資訊getfbigsheet-1">2. 取得 FB粉專、IG帳號的 Sheet 資訊：<code>getFBIGSheet</code><a class="h-anchor" href="#2-%e5%8f%96%e5%be%97-fb%e7%b2%89%e5%b0%88ig%e5%b8%b3%e8%99%9f%e7%9a%84-sheet-%e8%b3%87%e8%a8%8agetfbigsheet-1" aria-label="這個段落的連結"></a></h2>
<p>這隻函式是今天功能的核心，我們將<mark>取得 FB粉專、IG帳號的 sheet 資訊</mark>分成幾個步驟</p>
<ol>
<li>先取得線上的 sheets：<code>getSheets</code></li>
<li>判斷 &lsquo;FB粉專&rsquo;、&lsquo;IG帳號&rsquo; 是否在<code>getSheets</code>回傳的資料裡面
<ul>
<li>存在就將 sheetId 資訊儲存</li>
<li>不存在就用函式新增一個 Sheet：<code>addSheet</code></li>
</ul>
</li>
<li>回傳更新後的 sheets 資訊</li>
</ol>
<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> getFBIGSheet (auth) {<span style="color:#8b949e;font-style:italic">// 取得FB粉專、IG帳號的Sheet資訊
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> sheets <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#8b949e;font-style:italic">//我們Google Sheets需要的sheet
</span></span></span><span style="display:flex;"><span>        { title<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;FB粉專&#39;</span>, id<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">null</span> },
</span></span><span style="display:flex;"><span>        { title<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;IG帳號&#39;</span>, id<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">null</span> }
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> online_sheets <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getSheets(auth)<span style="color:#8b949e;font-style:italic">//抓目前存在的sheet
</span></span></span><span style="display:flex;"><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>        online_sheets.forEach(online_sheet =&gt; {
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">if</span> (sheet.title <span style="color:#ff7b72;font-weight:bold">==</span> online_sheet.properties.title) {
</span></span><span style="display:flex;"><span>                <span style="color:#8b949e;font-style:italic">// 如果線上已經存在相同的sheet title就直接使用相同id
</span></span></span><span style="display:flex;"><span>                sheet.id <span style="color:#ff7b72;font-weight:bold">=</span> online_sheet.properties.sheetId
</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">if</span> (sheet.id <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#79c0ff">null</span>) {<span style="color:#8b949e;font-style:italic">//如果該sheet尚未被建立，則建立
</span></span></span><span style="display:flex;"><span>            console.log(sheet.title <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;:not exsit&#39;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>                sheet.id <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> addSheet(sheet.title, auth)        
</span></span><span style="display:flex;"><span>            } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>                console.error(e)
</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">return</span> sheets;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="3-建立放-fb粉專ig帳號爬蟲資料的-sheet-1">3. 建立放 FB粉專、IG帳號爬蟲資料的 Sheet<a class="h-anchor" href="#3-%e5%bb%ba%e7%ab%8b%e6%94%be-fb%e7%b2%89%e5%b0%88ig%e5%b8%b3%e8%99%9f%e7%88%ac%e8%9f%b2%e8%b3%87%e6%96%99%e7%9a%84-sheet-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="31-在官方文件尋找-新增sheet-的範例">3.1 在官方文件尋找 <code>新增Sheet</code> 的範例<a class="h-anchor" href="#31-%e5%9c%a8%e5%ae%98%e6%96%b9%e6%96%87%e4%bb%b6%e5%b0%8b%e6%89%be-%e6%96%b0%e5%a2%9esheet-%e7%9a%84%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>由<mark>讀取Sheet的經驗</mark>可以知道想要操作 Sheet 就要在<a href="https://developers.google.com/sheets/api/samples/sheet">Sheet Operations</a>這個頁面尋找資源</p>
<blockquote>
<p>標題 <strong>Add a sheet</strong> 超級直觀，就是他惹！</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-08.webp" width="1024" height="357" alt="3.1 在官方文件尋找 新增Sheet 的範例（圖 8）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>看<a href="https://developers.google.com/sheets/api/samples/sheet#add_a_sheet">Add a sheet</a>的描述</p>
<blockquote>
<ul>
<li>
<p>第一段提供一個連結，並說<mark>這方法能新增 sheet 到 spreadsheet</mark></p>
</li>
<li>
<p>下方提供了 request 的範例，讓我們能大概了解<mark>新增 Sheet 可以調整哪些參數</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-09.webp" width="1024" height="695" alt="3.1 在官方文件尋找 新增Sheet 的範例（圖 9）" loading="lazy" decoding="async"></p>
</li>
</ul>
</blockquote>
</li>
<li>
<p>接著來確認<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate">Method: spreadsheets.batchUpdate</a>是否符合需求</p>
<blockquote>
<ol>
<li>
<p>打開 Try this API(在最下方)，<mark>Request body 請依下圖設定</mark>，title 的內容可以自行調整</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-10.webp" width="1024" height="800" alt="3.1 在官方文件尋找 新增Sheet 的範例（圖 10）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>按下 <strong>EXCUTE</strong> 按鈕後，確認自己的 Google Sheets<mark>真的成功新增一個 title 為 test1 的 Sheet</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-11.webp" width="1024" height="319" alt="3.1 在官方文件尋找 新增Sheet 的範例（圖 11）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>從回傳資料中找出 sheetId 的位置：<code>replies[0].addSheet.properties.sheetId</code></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-12.webp" width="672" height="357" alt="3.1 在官方文件尋找 新增Sheet 的範例（圖 12）" loading="lazy" decoding="async"></p>
</li>
</ol>
</blockquote>
</li>
<li>
<p>確認這個 api 能新增 Sheet 後，我們便可參考官方提供的<a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate#examples">範例程式</a>開始實作啦</p>
</li>
</ol>
<h3 class="h-nomark" id="32-調整範例以符合專案需求addsheets">3.2 調整範例以符合專案需求：<code>addSheets</code><a class="h-anchor" href="#32-%e8%aa%bf%e6%95%b4%e7%af%84%e4%be%8b%e4%bb%a5%e7%ac%a6%e5%90%88%e5%b0%88%e6%a1%88%e9%9c%80%e6%b1%82addsheets" aria-label="這個段落的連結"></a></h3>
<p>這個函式的目標是 <mark>用提供的 title 建立一個新 Sheet</mark>，Google Sheets 在新增一個 Sheet 時會自動產生一個不重複的 sheetId，我們<mark>將 sheetId 回傳代表我們新增成功</mark>(sheetId 在後面的章節會使用到)</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> addSheet (title, auth) {<span style="color:#8b949e;font-style:italic">//新增一個sheet到指定的Google Sheets
</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;addSheet&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {<span style="color:#8b949e;font-style:italic">//這個request的任務是addSheet
</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:#a5d6ff">&#34;properties&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a5d6ff">&#34;title&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> title
</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></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> response <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">await</span> sheets.spreadsheets.batchUpdate(request)).data;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> sheetId <span style="color:#ff7b72;font-weight:bold">=</span> response.replies[<span style="color:#a5d6ff">0</span>].addSheet.properties.sheetId
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#39;added sheet:&#39;</span> <span style="color:#ff7b72;font-weight:bold">+</span> title)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> sheetId
</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>
<h2 class="h-nomark" id="4-重新架構-google-sheets-程式-1">4. 重新架構 Google Sheets 程式<a class="h-anchor" href="#4-%e9%87%8d%e6%96%b0%e6%9e%b6%e6%a7%8b-google-sheets-%e7%a8%8b%e5%bc%8f-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="41-建立外部函式模組updategooglesheets">4.1 建立外部函式模組：<code>updateGoogleSheets</code><a class="h-anchor" href="#41-%e5%bb%ba%e7%ab%8b%e5%a4%96%e9%83%a8%e5%87%bd%e5%bc%8f%e6%a8%a1%e7%b5%84updategooglesheets" aria-label="這個段落的連結"></a></h3>
<p>別忘記我們學習 Google Sheets 是為了寫入爬蟲的資料，所以要建立一個給主程式<mark>用來更新 Google Sheets 的外部函式模組</mark>，這個函式今天有兩個任務：</p>
<ol>
<li>取得 Google Sheets 授權：<code>getAuth</code></li>
<li>取得 FB粉專、IG帳號的 sheet 資訊：<code>getFBIGSheet</code></li>
</ol>
<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>exports.updateGoogleSheets <span style="color:#ff7b72;font-weight:bold">=</span> updateGoogleSheets;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> updateGoogleSheets () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> auth <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getAuth()
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</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>        console.log(sheets)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#39;更新Google Sheets失敗&#39;</span>);
</span></span><span style="display:flex;"><span>        console.error(err);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 class="h-nomark" id="42-將取得-auth認證的步驟改寫為函式getauth">4.2 將取得 auth(認證)的步驟改寫為函式：<code>getAuth</code><a class="h-anchor" href="#42-%e5%b0%87%e5%8f%96%e5%be%97-auth%e8%aa%8d%e8%ad%89%e7%9a%84%e6%ad%a5%e9%a9%9f%e6%94%b9%e5%af%ab%e7%82%ba%e5%87%bd%e5%bc%8fgetauth" aria-label="這個段落的連結"></a></h3>
<p>考慮到 <mark>Google Sheets 的 api 都需要通過憑證取得授權才能操作，所以我把這個步驟獨立成一個函式</mark>，由於取得授權這塊採用 callback 的函式，所以我們要用 Promise 的方式來處理。</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">function</span> getAuth () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#ff7b72">new</span> Promise((resolve, reject) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> content <span style="color:#ff7b72;font-weight:bold">=</span> JSON.parse(fs.readFileSync(<span style="color:#a5d6ff">&#39;tools/google_sheets/credentials.json&#39;</span>))
</span></span><span style="display:flex;"><span>        authorize(content, auth =&gt; {
</span></span><span style="display:flex;"><span>            resolve(auth)
</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.error(<span style="color:#a5d6ff">&#39;憑證錯誤&#39;</span>);
</span></span><span style="display:flex;"><span>        reject(err)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="5-讓主程式呼叫外部函式updategooglesheets-1">5. 讓主程式呼叫外部函式：<code>updateGoogleSheets</code><a class="h-anchor" href="#5-%e8%ae%93%e4%b8%bb%e7%a8%8b%e5%bc%8f%e5%91%bc%e5%8f%ab%e5%a4%96%e9%83%a8%e5%87%bd%e5%bc%8fupdategooglesheets-1" aria-label="這個段落的連結"></a></h2>
<p>要爬蟲完成後才會把資料寫入 Google Sheet，所以將 updateGoogleSheets 這個函式放在最後執行</p>
<blockquote>
<p>為了方便測試今天的功能先把爬蟲的功能暫時註解</p>
</blockquote>
<h3 id="indexjs">index.js<a class="h-anchor" href="#indexjs" aria-label="這個段落的連結"></a></h3>
<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>require(<span style="color:#a5d6ff">&#39;dotenv&#39;</span>).config(); <span style="color:#8b949e;font-style:italic">//載入.env環境檔
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { initDrive } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./tools/initDrive.js&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { crawlerFB } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./tools/crawlerFB.js&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { crawlerIG } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./tools/crawlerIG.js&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { updateGoogleSheets } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./tools/google_sheets&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> crawler () {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// const driver = await initDrive();
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// if (!driver) {//driver不存在就結束程式
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//     return
</span></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:#8b949e;font-style:italic">// //因為有些人是用FB帳號登入IG，為了避免增加FB登出的動作，所以採取先對IG進行爬蟲
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// await crawlerIG(driver)
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// await crawlerFB(driver)
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// driver.quit();
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//處理Google Sheets相關動作
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> updateGoogleSheets()
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>crawler()
</span></span></code></pre></div><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><blockquote>
<p>你會遇到如下的錯誤：</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-13.webp" width="1024" height="238" alt="執行程式（圖 13）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p><strong>The API returned an error: Error: Insufficient Permission</strong> 的錯誤是<code>因為 Google Sheets 權限不足(原本只有readonly)</code>，所以我們要重新申請 token</p>
<ol>
<li>
<p><strong>刪除原本的 token.json</strong></p>
</li>
<li>
<p><mark>修改憑證的執行權限</mark></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">// 原本的範本是有readonly，這樣只有讀取權限，將它拿掉後才擁有修改的權限
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// const SCOPES = [&#39;https://www.googleapis.com/auth/spreadsheets.readonly&#39;];
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> SCOPES <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;https://www.googleapis.com/auth/spreadsheets&#39;</span>];
</span></span></code></pre></div></li>
</ol>
</li>
<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>Google 會要求你再點一次連結重新取得授權碼，貼上後你會看到線上的 Google Sheets 成功新增 sheets 了</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-14.webp" width="1024" height="309" alt="執行程式（圖 14）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-15.webp" width="936" height="368" alt="執行程式（圖 15）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>如果你重複執行指令 <code>yarn start</code> ，只會回傳建立好的 sheetId 給你，不會再重新建立</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-16.webp" width="1024" height="112" alt="執行程式（圖 16）" loading="lazy" decoding="async"></p>
</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/day19">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day18_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天Google Sheets的Sheet處理喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>要在tools/google_sheets資料夾放上自己的憑證，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/">Day17</a></li>
<li>調整你.env檔填上SPREADSHEET_ID</li>
</ul>
</li>
</ul>
<hr>
<h3 id="參考資源">參考資源<a class="h-anchor" href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" aria-label="這個段落的連結"></a></h3>
<p><a href="https://noob.tw/js-async/">callback、Promise 和 async/await 那些事兒</a></p>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-19/images/fb-ig-crawler-day-19-17.webp" width="1024" height="233" alt="參考資源（圖 17）" 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>
    <item>
      <title>【Day18】Google Sheets - 加入版控後被github警告了，官方範例做了什麼？</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-18/</link>
      <pubDate>Sat, 03 Oct 2020 09:08:54 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-18/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>在跟著官方步驟完成後，這些也是我們要注意的事情</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/cover_hu_cc571fbc635b6b52.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%bd%a0%e7%9f%a5%e9%81%93%e5%ae%98%e6%96%b9%e7%af%84%e4%be%8b%e5%9c%a8%e5%81%9a%e4%bb%80%e9%ba%bc%e4%ba%8b%e6%83%85%e5%97%8e" aria-label="這個段落的連結"></a></h2>
<p>在跟著官方步驟完成後，這些也是我們要注意的事情</p>
<ol>
<li><strong>我們安裝了什麼套件?</strong>
<ul>
<li>如果你有用 github 版控在做這份專案，<mark>你應該會收到 github 給你的警告通知</mark>，今天會說明如何閱讀這個通知並解決他</li>
</ul>
</li>
<li><strong>官方的範例程式在做什麼?</strong>
<ul>
<li>會教你如何觀察官方的範例程式，知道他運行的邏輯，<mark>學會改寫，拒絕只當 copycat</mark></li>
</ul>
</li>
</ol>
<hr>
<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-了解-github-警告通知的原因並處理">1. 了解 github 警告通知的原因並處理<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3-github-%e8%ad%a6%e5%91%8a%e9%80%9a%e7%9f%a5%e7%9a%84%e5%8e%9f%e5%9b%a0%e4%b8%a6%e8%99%95%e7%90%86" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="2-改寫官方範例讀取自己指定的-google-sheets">2. 改寫官方範例讀取自己指定的 Google Sheets<a class="h-anchor" href="#2-%e6%94%b9%e5%af%ab%e5%ae%98%e6%96%b9%e7%af%84%e4%be%8b%e8%ae%80%e5%8f%96%e8%87%aa%e5%b7%b1%e6%8c%87%e5%ae%9a%e7%9a%84-google-sheets" aria-label="這個段落的連結"></a></h3>
<p>2.1 分析官方範例程式</p>
<p>2.2 取得自己的 spreadsheetId 並加入程式</p>
<p>2.3 撰寫函式 <code>listMySheet</code> 來讀取自己的 sheet</p>
<hr>
<h2 class="h-nomark" id="1-了解-github-警告通知的原因並處理-1">1. 了解 github 警告通知的原因並處理<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3-github-%e8%ad%a6%e5%91%8a%e9%80%9a%e7%9f%a5%e7%9a%84%e5%8e%9f%e5%9b%a0%e4%b8%a6%e8%99%95%e7%90%86-1" aria-label="這個段落的連結"></a></h2>
<ul>
<li>
<p>昨天雖然我們按照官方給的範例完成了，但你把 code 上傳到 github 後應該會收到警告通知如下</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-01.webp" width="1024" height="313" alt="1. 了解 github 警告通知的原因並處理（圖 1）" fetchpriority="high" decoding="async"></p>
<ol>
<li>
<p>我們先點開 <strong>high severity(高嚴重性)</strong> 的通知，閱讀後發現這是<mark>相依套件版本過低造成的警告</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-02.webp" width="1024" height="484" alt="1. 了解 github 警告通知的原因並處理（圖 2）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>接著我們以 <strong>node-forge</strong> 為關鍵字在 <strong>yarn.lock</strong> 檔案裡面搜尋，發現真的版本過低</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-03.webp" width="1024" height="100" alt="1. 了解 github 警告通知的原因並處理（圖 3）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>同時發現這個套件是跟 <strong>google</strong> 有關聯性的，一路順藤摸瓜往上查會發現<mark>根源就是昨天安裝的 googleapis</mark> 這個套件</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-04.webp" width="1024" height="150" alt="1. 了解 github 警告通知的原因並處理（圖 4）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>來回顧一下昨天<mark>官方是指定我們安裝 39</mark> 這個版本的</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-05.webp" width="1024" height="187" alt="1. 了解 github 警告通知的原因並處理（圖 5）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>接著我們直接搜尋 <strong>googleapis npm</strong> 來看看套件的資訊，你就會發現<mark>最新的套件已經來到了 59</mark> 這個版本</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-06.webp" width="1024" height="497" alt="1. 了解 github 警告通知的原因並處理（圖 6）" loading="lazy" decoding="async"></p>
</li>
</ol>
</li>
<li>
<p>官方建議安裝的套件版本跟最新的套件版本<mark>整整差了 20 個大版</mark>，雖然每次更新版本都會有 release note，但我相信會認真看的沒有幾個，所以這20個大版本改了多少功能也是我們所不清楚的，<mark>固定的版本可以給你穩定的功能，但是潛在的漏洞你無法知曉，所以如果在確認最新版本功能穩定的狀態下我會更新套件</mark></p>
<ol>
<li>
<p>輸入指令安裝最新版本的 googleapis 套件</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 add googleapis@59
</span></span></code></pre></div></li>
<li>
<p><mark>每次更新版本後記得執行程式確認有無異常</mark></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>node tools/google_sheets/index.js
</span></span></code></pre></div><ul>
<li>
<p>可能遇到的的錯誤</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-07.webp" width="1024" height="134" alt="1. 了解 github 警告通知的原因並處理（圖 7）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在 google 搜尋 <strong>googleapis export { default as v1 } from &lsquo;./v1.js&rsquo;;</strong> 就能找到問題是出在 node.js 版本上面，輸入指令安裝並套用新版本(請選用<a href="https://nodejs.org/en/">LTS版本</a>)就能解決嚕</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>nvm install 12.19
</span></span><span style="display:flex;"><span>nvm use 12.19
</span></span></code></pre></div></li>
</ul>
</li>
</ol>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-改寫官方範例讀取自己指定的-google-sheets-1">2. 改寫官方範例讀取自己指定的 Google Sheets<a class="h-anchor" href="#2-%e6%94%b9%e5%af%ab%e5%ae%98%e6%96%b9%e7%af%84%e4%be%8b%e8%ae%80%e5%8f%96%e8%87%aa%e5%b7%b1%e6%8c%87%e5%ae%9a%e7%9a%84-google-sheets-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-分析官方範例程式">2.1 分析官方範例程式<a class="h-anchor" href="#21-%e5%88%86%e6%9e%90%e5%ae%98%e6%96%b9%e7%af%84%e4%be%8b%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>以下說明建議你<a href="https://github.com/dean9703111/ithelp_30days/blob/master/day17/tools/google_sheets/index.js">打開昨日的範例程式</a>方便相互印證</p>
<ul>
<li>首先要理解 Google 提供範例程式，<mark>理解程式最快的方式的就是從輸出結果的位置開始回推</mark>，所以我反推的順序會是：
<ol>
<li>找到輸出結果 <strong>console.log()</strong> 的位置 → <em>console.log(&lsquo;Name, Major:&rsquo;);</em></li>
<li>這個結果是在哪個 <strong>function</strong> 輸出的 → <em>listMajors(auth)</em></li>
<li>程式在哪裏呼叫 <strong>listMajors</strong> 這個 function → <em>authorize(JSON.parse(content), listMajors)</em></li>
<li><mark>程式的根源</mark>：<strong>要先要取得憑證才能 authorize(授權)</strong> → <em>fs.readFile(&rsquo;tools/google_sheets/credentials.json&rsquo;&hellip;)</em></li>
</ol>
</li>
<li>根據上面的架構分析，我們先觀察輸出console.log()的函式： <code>listMajors</code>，裡面有兩個參數我們要特別注意：
<ol>
<li><strong>spreadsheetId</strong>：你的Google Sheets id</li>
<li><strong>range</strong>：你指定讀取的範圍</li>
</ol>
</li>
<li>知道要處理的參數後我們就能開始改寫了，步驟：
<ol>
<li>取得自己的 <strong>spreadsheetId</strong> 並加入程式</li>
<li>撰寫函式：<code>listMySheet</code> 讀取自己的sheet</li>
</ol>
</li>
</ul>
<h3 class="h-nomark" id="22-取得自己的-spreadsheetid-並加入程式">2.2 取得自己的 spreadsheetId 並加入程式<a class="h-anchor" href="#22-%e5%8f%96%e5%be%97%e8%87%aa%e5%b7%b1%e7%9a%84-spreadsheetid-%e4%b8%a6%e5%8a%a0%e5%85%a5%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>我們先觀察官方對 <code>listMajors</code> 這隻函式的註解</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:#8b949e;font-style:italic"> * Prints the names and majors of students in a sample spreadsheet:
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @see https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {google.auth.OAuth2} auth The authenticated Google OAuth client.
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">*/</span>
</span></span></code></pre></div></li>
<li>
<p>再觀察裡面 <code>spreadsheetId</code> 的參數</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>spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms&#39;</span>
</span></span></code></pre></div><p>就會發現 <code>spreadsheetId</code> 這個參數在範例網址中間：<br>docs.google.com/spreadsheets/d/ <code>1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms</code> /edit</p>
</li>
<li>
<p>我們建立好 Google Sheets 後<mark>把 spreadsheetId 替換成自己的</mark>(實際網頁位置如下圖紅框處)</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-08.webp" width="1024" height="45" alt="2.2 取得自己的 spreadsheetId 並加入程式（圖 8）" loading="lazy" decoding="async"></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>spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID
</span></span></code></pre></div><ul>
<li>
<p>因為 spreadsheetId 並不適合公開放到git上面(你應該不會想公布這份 Google Sheets 給全世界吧)，所以我們要把<mark>spreadsheetId 複製起來放到 .env 裡面設定為環境變數</mark>，以下是目前環境檔需要做的設定</p>
<h4 id="envexmaple">.env.exmaple<a class="h-anchor" href="#envexmaple" aria-label="這個段落的連結"></a></h4>
<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><span style="color:#8b949e;font-style:italic">#填寫自己登入IG的真實資訊(建議開小帳號來實驗，因為帳號使用太頻繁會被官方鎖住)</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">IG_USERNAME</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;ig username&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">IG_PASSWORD</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;ig password&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">#填寫自己登入FB的真實資訊(建議開小帳號來實驗，因為帳號使用太頻繁會被官方鎖住)</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">FB_USERNAME</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;fb username&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">FB_PASSWORD</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;fb password&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">#FB跑經典版還是新版(classic/new)</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">FB_VERSION</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;new&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">#填寫你目標放入的spreadsheetId</span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">SPREADSHEET_ID</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#39;your spreadsheetId&#39;</span>
</span></span></code></pre></div></li>
</ul>
</li>
</ol>
<h3 class="h-nomark" id="23-撰寫函式-listmysheet-來讀取自己的-sheet">2.3 撰寫函式 <code>listMySheet</code> 來讀取自己的 sheet<a class="h-anchor" href="#23-%e6%92%b0%e5%af%ab%e5%87%bd%e5%bc%8f-listmysheet-%e4%be%86%e8%ae%80%e5%8f%96%e8%87%aa%e5%b7%b1%e7%9a%84-sheet" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>你可以由官方範例的<a href="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit">Google Sheets</a> 發現昨天程式印出來的是這個 Sheets <strong>第一欄、第五欄</strong> 的值</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-09.webp" width="1024" height="638" alt="2.3 撰寫函式 listMySheet 來讀取自己的 sheet（圖 9）" loading="lazy" decoding="async"></p>
<ul>
<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>console.log(<span style="color:#a5d6ff">&#39;Name, Major:&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Print columns A and E, which correspond to indices 0 and 4.
</span></span></span><span style="display:flex;"><span>rows.map((row) =&gt; {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>row[<span style="color:#a5d6ff">0</span>]<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">, </span><span style="color:#a5d6ff">${</span>row[<span style="color:#a5d6ff">4</span>]<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>接下來我們<strong>把原本 Google 範例程式的 listMajors() 刪除</strong>，<code>改寫成非同步函式listMySheet()</code></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> listMySheet (auth) {
</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> title <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;我的sheet&#39;</span><span style="color:#8b949e;font-style:italic">//請你更改成自己設定的sheet(工作表)名稱
</span></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>        spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> process.env.SPREADSHEET_ID,
</span></span><span style="display:flex;"><span>        range<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#8b949e;font-style:italic">//這是指抓取的範圍，你也可以改寫成A1:A300(抓第1欄的第1列到第300列)
</span></span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">`&#39;</span><span style="color:#a5d6ff">${</span>title<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">&#39;!A:ZZ`</span>
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        valueRenderOption<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;FORMULA&#34;</span><span style="color:#8b949e;font-style:italic">//FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA
</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:#8b949e;font-style:italic">//這裡改寫為await，之後會有順序執行的需求
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> values <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">await</span> sheets.spreadsheets.values.get(request)).data.values;
</span></span><span style="display:flex;"><span>        console.log(values)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>        console.error(err);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><blockquote>
<p><strong>valueRenderOption</strong> 這個參數是把資料抓出來時的類型，感興趣可參考<a href="https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption">Google官方文件</a></p>
</blockquote>
</li>
</ul>
<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>先在自己的 Google Sheets 表格上隨機輸入文字</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-10.webp" width="1024" height="428" alt="執行程式（圖 10）" loading="lazy" decoding="async"></p>
</li>
<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>node tools/google_sheets/index.js
</span></span></code></pre></div></li>
<li>
<p>看看輸出的結果是否與你 Google Sheets 上的一致呢～</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-11.webp" width="1024" height="165" alt="執行程式（圖 11）" loading="lazy" decoding="async"></p>
</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/day18">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day17_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來改寫Google Sheets的範例喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>要在tools/google_sheets資料夾放上自己的憑證，今天的程式才能執行喔，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/">Day17</a></li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-18/images/fb-ig-crawler-day-18-12.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 12）" 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>
    <item>
      <title>【Day17】Google Sheets - 免費儲存資料的好選擇，一起進入省錢起手式</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-17/</link>
      <pubDate>Fri, 02 Oct 2020 09:14:06 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-17/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>一般來說爬蟲的資料都是儲存到資料庫，但是我相信絕大多數的人不太可能隨時隨地打開資料庫觀看，而且存在資料庫除非你請網頁工程師幫你完成前端視覺化，不然直接看資料庫絕對會讓你懷疑人生XD</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/cover_hu_4591ab322b33238b.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>專案最基本的就是：<strong>符合客戶預算與實際使用需求</strong></p>
</blockquote>
<h2 id="為什麼選擇-google-sheets-作為資料載體">為什麼選擇 Google Sheets 作為資料載體?<a class="h-anchor" href="#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%81%b8%e6%93%87-google-sheets-%e4%bd%9c%e7%82%ba%e8%b3%87%e6%96%99%e8%bc%89%e9%ab%94" aria-label="這個段落的連結"></a></h2>
<p>一般來說爬蟲的資料都是儲存到資料庫，但是我相信絕大多數的人不太可能隨時隨地打開資料庫觀看，而且存在資料庫除非你請網頁工程師幫你完成前端視覺化，不然直接看資料庫絕對會讓你懷疑人生XD</p>
<p>經過多方思慮我最後採用 Google Sheets 的服務，主要理由：</p>
<ol>
<li>只要有網路我就能看</li>
<li>絕大多數人都有使用 Google Sheets 的經驗</li>
<li>使用者能輕易的將 Google Sheets 轉化成圖表</li>
<li>你不需要一台 Server 專門來存這些資料</li>
<li><strong><code>免費</code></strong></li>
</ol>
<p>儘管 Google Sheets 並不完美，但他真的很適合用在這個專案上面</p>
<hr>
<h2 id="今日目標">今日目標<a class="h-anchor" href="#%e4%bb%8a%e6%97%a5%e7%9b%ae%e6%a8%99" aria-label="這個段落的連結"></a></h2>
<ol>
<li>取得 Google Sheets 憑證(credentials)</li>
<li>在專案加入官方提供的範例程式做測試</li>
<li>將 Google Sheets 的憑證、授權檔加入 .gitignore</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-取得-google-sheets-憑證credentials">1. 取得 Google Sheets 憑證(credentials)<a class="h-anchor" href="#1-%e5%8f%96%e5%be%97-google-sheets-%e6%86%91%e8%ad%89credentials" aria-label="這個段落的連結"></a></h2>
<p>如果把 Google Sheets 想像成一個藏寶庫，憑證(credentials)就像是一把鑰匙，只有擁有這把鑰匙的人才有打開門的資格；不然這個藏寶庫誰都能任意進出豈不是很危險？</p>
<h3 id="憑證credentials取得的步驟">憑證(credentials)取得的步驟<a class="h-anchor" href="#%e6%86%91%e8%ad%89credentials%e5%8f%96%e5%be%97%e7%9a%84%e6%ad%a5%e9%a9%9f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>請先確認<code>已經安裝過 Node.js &amp; npm</code>(跟著教學走的人都安裝過)，<code>有 Goolge 帳號</code></li>
<li>進入 Google Sheet Node.js 申請憑證(credentials)的<a href="https://developers.google.com/sheets/api/quickstart/nodejs">網頁</a>
<ol>
<li>
<p>按下紅框處按鈕 <strong>Enable theGoogle Sheets API</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-01.webp" width="1024" height="254" alt="憑證(credentials)取得的步驟（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>輸入專案的名稱，建議<strong>填寫符合專案功能的命名</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-02.webp" width="1024" height="297" alt="憑證(credentials)取得的步驟（圖 2）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>本轉案選擇 <strong>Desktop app</strong> 即可</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-03.webp" width="1024" height="528" alt="憑證(credentials)取得的步驟（圖 3）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>千萬<mark>不要忘記下載這個憑證</mark>，下面的Client ID、Client Secret不需特別複製，都存在憑證檔裡面了</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-04.webp" width="1024" height="528" alt="憑證(credentials)取得的步驟（圖 4）" loading="lazy" decoding="async"></p>
</li>
</ol>
</li>
<li>在專案的<code>tools 資料夾下新增一個 google_sheets 資料夾</code>，專門存放 Google Sheets 相關檔案</li>
<li>將<code>下載好的憑證(credentials.json)放進去 google_sheets 資料夾</code>，這樣的目錄節結構會方便我們日後管理</li>
</ol>
<hr>
<h2 class="h-nomark" id="2-在專案加入官方提供的範例程式做測試">2. 在專案加入官方提供的範例程式做測試<a class="h-anchor" href="#2-%e5%9c%a8%e5%b0%88%e6%a1%88%e5%8a%a0%e5%85%a5%e5%ae%98%e6%96%b9%e6%8f%90%e4%be%9b%e7%9a%84%e7%af%84%e4%be%8b%e7%a8%8b%e5%bc%8f%e5%81%9a%e6%b8%ac%e8%a9%a6" aria-label="這個段落的連結"></a></h2>
<p>在詳細的閱讀官方文件之前，我會先<mark>用官方的範例程式確認他能否在我的專案順利運作</mark>，如果遇到太多環境衝突才能儘早尋求替代方案</p>
<ol>
<li>
<p>下載 <code>googleapis</code> 套件</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 add googleapis@39
</span></span></code></pre></div></li>
<li>
<p>在 <code>tools/google_sheets 資料夾中新增 index.js</code> 檔案，並複製 Google 範例程式碼貼上</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> fs <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;fs&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> readline <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;readline&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { google } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;googleapis&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// If modifying these scopes, delete token.json.
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> SCOPES <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;https://www.googleapis.com/auth/spreadsheets.readonly&#39;</span>];
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// The file token.json stores the user&#39;s access and refresh tokens, and is
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// created automatically when the authorization flow completes for the first
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// time.
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> TOKEN_PATH <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;tools/google_sheets/token.json&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Load client secrets from a local file.
</span></span></span><span style="display:flex;"><span>fs.readFile(<span style="color:#a5d6ff">&#39;credentials.json&#39;</span>, (err, content) =&gt; {
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</span> console.log(<span style="color:#a5d6ff">&#39;Error loading client secret file:&#39;</span>, err);
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Authorize a client with credentials, then call the Google Sheets API.
</span></span></span><span style="display:flex;"><span>authorize(JSON.parse(content), listMajors);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</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:#8b949e;font-style:italic">* Create an OAuth2 client with the given credentials, and then execute the
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* given callback function.
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {Object} credentials The authorization client credentials.
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {function} callback The callback to call with the authorized client.
</span></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">function</span> authorize (credentials, callback) {
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { client_secret, client_id, redirect_uris } <span style="color:#ff7b72;font-weight:bold">=</span> credentials.installed;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> oAuth2Client <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> google.auth.OAuth2(
</span></span><span style="display:flex;"><span>    client_id, client_secret, redirect_uris[<span style="color:#a5d6ff">0</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Check if we have previously stored a token.
</span></span></span><span style="display:flex;"><span>fs.readFile(TOKEN_PATH, (err, token) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</span> getNewToken(oAuth2Client, callback);
</span></span><span style="display:flex;"><span>    oAuth2Client.setCredentials(JSON.parse(token));
</span></span><span style="display:flex;"><span>    callback(oAuth2Client);
</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:#8b949e;font-style:italic">/**
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* Get and store new token after prompting for user authorization, and then
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* execute the given callback with the authorized OAuth2 client.
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {getEventsCallback} callback The callback for the authorized client.
</span></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">function</span> getNewToken (oAuth2Client, callback) {
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> authUrl <span style="color:#ff7b72;font-weight:bold">=</span> oAuth2Client.generateAuthUrl({
</span></span><span style="display:flex;"><span>    access_type<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;offline&#39;</span>,
</span></span><span style="display:flex;"><span>    scope<span style="color:#ff7b72;font-weight:bold">:</span> SCOPES,
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#39;Authorize this app by visiting this url:&#39;</span>, authUrl);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> rl <span style="color:#ff7b72;font-weight:bold">=</span> readline.createInterface({
</span></span><span style="display:flex;"><span>    input<span style="color:#ff7b72;font-weight:bold">:</span> process.stdin,
</span></span><span style="display:flex;"><span>    output<span style="color:#ff7b72;font-weight:bold">:</span> process.stdout,
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>rl.question(<span style="color:#a5d6ff">&#39;Enter the code from that page here: &#39;</span>, (code) =&gt; {
</span></span><span style="display:flex;"><span>    rl.close();
</span></span><span style="display:flex;"><span>    oAuth2Client.getToken(code, (err, token) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</span> console.error(<span style="color:#a5d6ff">&#39;Error while trying to retrieve access token&#39;</span>, err);
</span></span><span style="display:flex;"><span>    oAuth2Client.setCredentials(token);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Store the token to disk for later program executions
</span></span></span><span style="display:flex;"><span>    fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) =&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</span> console.error(err);
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#39;Token stored to&#39;</span>, TOKEN_PATH);
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>    callback(oAuth2Client);
</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:#8b949e;font-style:italic">/**
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* Prints the names and majors of students in a sample spreadsheet:
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @see https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">* @param {google.auth.OAuth2} auth The authenticated Google OAuth client.
</span></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">function</span> listMajors (auth) {
</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>sheets.spreadsheets.values.get({
</span></span><span style="display:flex;"><span>    spreadsheetId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms&#39;</span>,
</span></span><span style="display:flex;"><span>    range<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Class Data!A2:E&#39;</span>,
</span></span><span style="display:flex;"><span>}, (err, res) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</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 style="color:#ff7b72">const</span> rows <span style="color:#ff7b72;font-weight:bold">=</span> res.data.values;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (rows.length) {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;Name, Major:&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Print columns A and E, which correspond to indices 0 and 4.
</span></span></span><span style="display:flex;"><span>    rows.map((row) =&gt; {
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>row[<span style="color:#a5d6ff">0</span>]<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">, </span><span style="color:#a5d6ff">${</span>row[<span style="color:#a5d6ff">4</span>]<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;No data found.&#39;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
<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>node tools/google_sheets/index.js
</span></span></code></pre></div><ol>
<li>
<p>你應該會先遇到找不到 credentials.json 的錯誤</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-05.webp" width="1024" height="125" alt="2. 在專案加入官方提供的範例程式做測試（圖 5）" loading="lazy" decoding="async"></p>
<p>請將範例程式中<code>讀取 'credentials.json' 路徑修改成專案的路徑 'tools/google_sheets/credentials.json'</code></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">//fs.readFile(&#39;credentials.json&#39;, (err, content) =&gt; {
</span></span></span><span style="display:flex;"><span>fs.readFile(<span style="color:#a5d6ff">&#39;tools/google_sheets/credentials.json&#39;</span>, (err, content) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (err) <span style="color:#ff7b72">return</span> console.log(<span style="color:#a5d6ff">&#39;Error loading client secret file:&#39;</span>, err);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Authorize a client with credentials, then call the Google Sheets API.
</span></span></span><span style="display:flex;"><span>    authorize(JSON.parse(content), listMajors);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
<li>
<p>再執行一次 <code>node tools/google_sheets/index.js</code> 時下面會有連結請你打開</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-06.webp" width="1024" height="57" alt="2. 在專案加入官方提供的範例程式做測試（圖 6）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>打開後的網頁會要你選擇登入的 Google 帳號，看到下面的網頁時別害怕，點擊 <code>進階</code> 然後再點擊 <code>前往「Quickstart」(不安全)</code></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-07.webp" width="1024" height="651" alt="2. 在專案加入官方提供的範例程式做測試（圖 7）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>接著所有的授權都必須按 <code>允許</code> ，最後你會看到一組授權碼，把他複製下來貼回終端機(Terminal)就完成惹</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-08.webp" width="1024" height="503" alt="2. 在專案加入官方提供的範例程式做測試（圖 8）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>如果終端機(Terminal)有輸出類似下面的資料時就代表你成功了</p>
<pre tabindex="0"><code>Name, Major:
Alexandra, English
Andrew, Math
Anna, English
Becky, Art
Benjamin, English
Carl, Art
Carrie, English
Dorothy, Math
...
</code></pre></li>
<li>
<p>聰明的你會發現 <code>專案根目錄下多了一個 token.json</code> 的檔案，如果說 credentials.json 是打開藏寶庫的鑰匙，token.json 就像是能夠把裡面寶藏搬進搬出的權杖，而這個權杖是有期限的(expiry_date)，期限過了就需要再申請</p>
<ol>
<li>
<p>為了方便管理我們把 <code>token.json 搬移到 tools/google_sheets</code> 的資料夾下</p>
</li>
<li>
<p><code>修改範例程式中讀取 token.json 的路徑</code></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">//const TOKEN_PATH = &#39;token.json&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> TOKEN_PATH <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;tools/google_sheets/token.json&#39;</span>;
</span></span></code></pre></div></li>
</ol>
</li>
</ol>
</li>
</ol>
<hr>
<h2 class="h-nomark" id="3-將-google-sheets-的憑證授權檔加入-gitignore">3. 將 Google Sheets 的憑證、授權檔加入 .gitignore<a class="h-anchor" href="#3-%e5%b0%87-google-sheets-%e7%9a%84%e6%86%91%e8%ad%89%e6%8e%88%e6%ac%8a%e6%aa%94%e5%8a%a0%e5%85%a5-gitignore" aria-label="這個段落的連結"></a></h2>
<p>我想應該不會有人想把藏寶庫的鑰匙跟權杖公開讓大家使用吧？如果你不小心把 <code>credentials.json、token.json</code> 兩個檔案加入版控就等於把你 Google Sheets 的權限送給別人了，所以你要調整 .gitignore 如下：</p>
<pre tabindex="0"><code>node_modules
.env
chromedriver.exe
debug.log
credentials.json
token.json
</code></pre><blockquote>
<p>不熟悉的朋友可以參考 <a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-07/">【Day7】gitignore-請勿上傳敏感、主程式以外的資料</a> 的文章</p>
</blockquote>
<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/day17">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day16_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天Google Sheets的起手式喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>要在tools/google_sheets資料夾放上自己的憑證，今天的程式才能執行喔</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/images/fb-ig-crawler-day-17-09.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 9）" 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>
    <item>
      <title>【Day1】技術只能陪你一陣子，學會解決問題才能過一輩子</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-01/</link>
      <pubDate>Wed, 16 Sep 2020 09:03:29 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-01/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>因朋友職業需求需要搜集與公司同類型品牌FB及IG粉專的資料，用每日追蹤人數的變化及貼文數量來做數據分析，像是：</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-01/images/cover_hu_70e2f929be0de39e.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>你不用很厲害才開始，你要開始了才會很厲害！</p>
</blockquote>
<h2 id="為什麼選擇這個題目">為什麼選擇這個題目？<a class="h-anchor" href="#%e7%82%ba%e4%bb%80%e9%ba%bc%e9%81%b8%e6%93%87%e9%80%99%e5%80%8b%e9%a1%8c%e7%9b%ae" aria-label="這個段落的連結"></a></h2>
<p>因朋友職業需求<mark>需要搜集與公司同類型品牌FB及IG粉專的資料，用每日追蹤人數的變化及貼文數量來做數據分析</mark>，像是：</p>
<ol>
<li>觀察發文頻率是否與紛絲成長數量成正比</li>
<li>某個粉專突然因為某篇貼文而紛絲爆量成長</li>
</ol>
<p>有這些數據後方便研擬各項策略，但需要追蹤的<mark>FB跟IG品牌頁面加起來超過 200 個</mark>(真的狂，這是現實案例)，如果靠手指一個一個點開，複製貼上到 excel，這個機械式的動作重複兩百多次我想想都很崩潰，於是他便向我詢問是否有辦法讓這些<mark>步驟自動化</mark></p>
<h2 id="這三十天我能學到什麼">這三十天我能學到什麼？<a class="h-anchor" href="#%e9%80%99%e4%b8%89%e5%8d%81%e5%a4%a9%e6%88%91%e8%83%bd%e5%ad%b8%e5%88%b0%e4%bb%80%e9%ba%bc" aria-label="這個段落的連結"></a></h2>
<p>說點喪志的話，無論我分享什麼技術，他遲早會被這個世界淘汰</p>
<p><strong>所以我認為與其透過文章讓你掌握技術，不如透過技術培養解決問題的邏輯</strong></p>
<p>這系列文章是透過一個<mark>真實外包案例</mark>，從零開始逐步分析如何完成它</p>
<h3 id="文章大綱">文章大綱<a class="h-anchor" href="#%e6%96%87%e7%ab%a0%e5%a4%a7%e7%b6%b1" aria-label="這個段落的連結"></a></h3>
<ol>
<li><strong>開發前環境設定</strong> 【<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-02/">day2</a>】
<ul>
<li>教你安裝本專案所需要的開發工具，確保能完成日後的實作</li>
</ul>
</li>
<li><strong>寫程式所需的基礎常識(Node.js)</strong> 【<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-03/">day3</a>~<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-07/">day7</a>】
<ul>
<li>如何撰寫不被同事嫌棄的程式</li>
<li>喚醒開發過程中被忽略的小技巧提升工作效率</li>
<li>結合經驗與專案實務，讓你了解沒常識有多可怕</li>
</ul>
</li>
<li><strong>爬蟲抓取網頁資訊(selenium)</strong> 【<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-08/">day8</a>~<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-16/">day16</a>】
<ul>
<li>理解為什麼挑選這個工具</li>
<li>網頁結構的分析技巧</li>
<li>完成需求後，程式重構與錯誤處理的重要性</li>
</ul>
</li>
<li><strong>將取得的資訊儲存(Google Sheets)</strong> 【<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/">day17</a>~<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-23/">day23</a>】
<ul>
<li>思考符合客戶需求的資訊載體</li>
<li>學會閱讀官方的文件</li>
<li>處理匯入爬蟲資料遇到的問題</li>
<li>在專案發生bug、有新功能需求時與客戶溝通的技巧</li>
</ul>
</li>
<li><strong>設定排程自動執行</strong> 【<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/">day24</a>~<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/">day26</a>】
<ul>
<li>有時一個工具不足以完成需求，培養將多種工具串連解決問題的能力</li>
</ul>
</li>
<li><strong>排程完成後透過LINE回報執行狀況</strong> 【<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/">day27</a>~<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-29/">day29</a>】
<ul>
<li>了解使用 api 前先用第三方程式測試的重要性</li>
<li>重構程式將專案爬蟲的訊息整合，透過 LINE 回傳當日爬蟲狀況</li>
</ul>
</li>
</ol>
<blockquote>
<p>文章的內容著重於<strong>思考</strong></p>
<p>我所提供的<strong>答案只是僅供參考</strong></p>
<p>一道題目解決的方法肯定不只一種，對於工程師而言最珍貴的是<strong>思考的過程</strong></p>
<p>直接看答案會限制你的思維</p>
<p>有了思考的過程你會給出一個更棒的解決方法</p>
</blockquote>
<h2 id="題目分析">題目分析<a class="h-anchor" href="#%e9%a1%8c%e7%9b%ae%e5%88%86%e6%9e%90" aria-label="這個段落的連結"></a></h2>
<ul>
<li>
<p><strong>專案目標：</strong> 透過爬蟲拯救一個沈淪在FB、IG分析報表的小編</p>
</li>
<li>
<p><strong>使用工具：</strong> 只要能把他從每天重複的動作中拯救出來就好，不管你用什麼工具</p>
</li>
<li>
<p><strong>外包預算：</strong> 友情無價，外包有價</p>
<blockquote>
<p>如果有朋友間的外包，<strong>我強烈建議一定要收錢</strong>，你的收費可以遠低於市場報價，但絕對要收錢，如果你不收錢，有超高機率鬧得雙方不愉快<br>並且你在<strong>做功能前要先報價，確定對方可以接受後再開始實做</strong><br>技術是有價值的，你有收錢，你會給自己時間壓力；對方有付錢，會認真地使用你做出來的產品並給予回饋<br>這樣才是正向循環<br>我看過太多朋友間免費的專案，到最後不是胎死腹中就是爛尾，所以請報價吧，對雙方都好</p>
</blockquote>
</li>
<li>
<p><strong>專案期限</strong>:<br>越快越好</p>
</li>
</ul>
<h2 id="需求規格">需求規格<a class="h-anchor" href="#%e9%9c%80%e6%b1%82%e8%a6%8f%e6%a0%bc" aria-label="這個段落的連結"></a></h2>
<p>我想很多工程師都遇過親朋好友委託專案，而他們給需求時通常都是很不具體的，你也不可能要他們寫需求規格書</p>
<p>所以我建議你要<mark>自己研擬一份簡易的需求規格書，並說明一些但書以免日後爭議</mark></p>
<ol>
<li><strong>FB</strong>
<ol>
<li>取得粉專追蹤人數</li>
<li>取得當日貼文數</li>
<li>頻率: 1次/天</li>
<li>報表出來時間: 22:00</li>
</ol>
</li>
<li><strong>IG</strong>
<ol>
<li>取得粉專追蹤人數</li>
<li>取得當日貼文數</li>
<li>頻率: 1次/天</li>
<li>報表出來時間: 22:00</li>
</ol>
</li>
<li><strong>報表呈現</strong>
<ol>
<li>excel格式</li>
<li>標題點進去可以直接連結粉專</li>
<li>FB與IG粉專各自獨立分頁顯示</li>
<li>爬蟲日期為欄，粉專標題為列</li>
<li>首欄與首列需要凍結窗格，方便使用者操作(新增需求)</li>
<li>插入日期為倒序插入，由近到遠(新增需求)</li>
</ol>
</li>
<li><strong>爬蟲完成通知</strong>(新增需求)
<ol>
<li>使用line進行通知(新增需求)</li>
<li>通知包含：總掃描粉專數量、共費時間、Google Sheets連結、無法正常爬蟲頁面列表(新增需求)</li>
</ol>
</li>
<li><strong>專案執行位置</strong>
<ol>
<li>自己電腦</li>
</ol>
</li>
<li><strong>預計交期</strong>
<ol>
<li>請研擬一個自己絕對能做出來的時間，只能多不能少</li>
</ol>
</li>
<li><strong>功能報價</strong>
<ol>
<li>這個部分關係到你對工具的熟悉程度、程式執行的效率、程式的穩定性、功能項目多寡&hellip;，光這個估計都能完成鐵人30篇惹</li>
</ol>
</li>
<li><strong>備註</strong>
<ol>
<li>如果FB或IG的網頁結構上的更動導致無法用原先程式運行時需與工程師討論維護報價</li>
</ol>
</li>
</ol>
<h2 id="實作的步驟">實作的步驟<a class="h-anchor" href="#%e5%af%a6%e4%bd%9c%e7%9a%84%e6%ad%a5%e9%a9%9f" aria-label="這個段落的連結"></a></h2>
<ol>
<li>使用什麼工具完成</li>
<li>列出功能項目報價</li>
<li>哪個功能項目需要最先研究</li>
<li>如何讓對方能夠使用及測試功能</li>
<li>結案與維護</li>
</ol>
<p>以上是我接案之前就會先思考的問題，但<strong>實際情況通常會比我列出這些步驟更為複雜</strong>，各位讀者也可以想想看自己在遇到這些需求時會怎麼設計呢～</p>
<p><em>PS.技術沒有善惡，人的使用方式才會有善惡，希望這份技術可以幫大家節省時間以及避免重工；如果你把它拿來盜取智慧財產、個人檔案及機敏資訊請自行負擔後續法律責任</em></p>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<p>感謝大家的閱讀，如果喜歡我的文章可以按Like支持一下喔，我們明天見</p>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-01/images/fb-ig-crawler-day-01-01.webp" width="1024" height="233" alt="實作的步驟（圖 1）" fetchpriority="high" 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>
    <item>
      <title>讓Line機器人變成你的小秘書</title>
      <link>https://deanlin.net/blog/202006/line-bot-personal-assistant/</link>
      <pubDate>Wed, 10 Jun 2020 12:13:46 +0800</pubDate>
      <guid>https://deanlin.net/blog/202006/line-bot-personal-assistant/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>機器人的秘書還不夠誘惑嗎ＸＤ</description>
      <media:content url="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-og_hu_2dae16f1f3dfcd3f.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>為什麼撰寫這篇文章</p>
</blockquote>
<p>在<a href="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/">上一篇</a>文章已經介紹了Google App Script如何串接Line Message Api的應用，在這裡我將延續之前的議題，讓Line機器人變成你的小秘書</p>
<blockquote>
<p>文章目標</p>
</blockquote>
<ol>
<li>使用Google Sheet裡面Script的功能</li>
<li>使用簡單的SQL語法抓出Google Sheet中你需要的資訊</li>
</ol>
<blockquote>
<p>本文章使用到的技術：</p>
</blockquote>
<ol>
<li>Google App Script : 這次著重在執行SQL語法</li>
<li>Google Sheet : 會把它當成讀取/寫入的資料庫</li>
</ol>
<h2 id="step-1-結合google-sheet的衍生應用">STEP 1: 結合Google Sheet的衍生應用<a class="h-anchor" href="#step-1-%e7%b5%90%e5%90%88google-sheet%e7%9a%84%e8%a1%8d%e7%94%9f%e6%87%89%e7%94%a8" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>Google App Script功能很多，但能做到什麼地步就看個人的想像能力，接下來我提供的只是一條思路，如果大家想到有趣的應用歡迎下方留言</p>
</blockquote>
<p>如果大家平常有使用Google sheet當成工作記錄習慣的話，你可以嘗試看看這個應用</p>
<p>這裡我先創建一個Google Sheet，裡面建立一些清單(<a href="https://docs.google.com/spreadsheets/d/1ZdmsEkS27VWR6HF42_aka5rrtgu31saZoUupOD8Xx6s/edit?usp=sharing">你可以直接複製我的</a>)：</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-01.webp" width="1400" height="591" alt="STEP 1: 結合Google Sheet的衍生應用（圖 1）" fetchpriority="high" decoding="async"></p>
<p>接著建立Google Sheet的Script</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-02.webp" width="1400" height="811" alt="STEP 1: 結合Google Sheet的衍生應用（圖 2）" loading="lazy" decoding="async"></p>
<p>將以下程式複製貼上，如果您願意可以自由更改，記得將<a href="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/">上一篇</a>取得的line token放上去喔</p>
<pre tabindex="0"><code>var ss = SpreadsheetApp.getActiveSpreadsheet()//抓取這張excel
function doPost (e) {
    var msg = JSON.parse(e.postData.contents);
    // 取出 replayToken 和發送的訊息文字
    var replyToken = msg.events[0].replyToken;
    var userMessage = msg.events[0].message.text;

    if (userMessage.indexOf(&#34;未完成&#34;) &gt; -1) {//分析傳遞字串確認是否有“未完成”
        // QUERY(目標文件及範圍,SQL語法)
        // 尋找sheet1範圍A2到C100裡面A跟B欄位的資料且C欄位為FALSE
        var formulaToUse = &#39;=QUERY(Sheet1!A2:C100,&#34;SELECT A,B WHERE C = FALSE&#34;)&#39;
        var result = query(formulaToUse)

        var userMessage = &#39;未完成文件如下：\n&#39;;//單純的文字組成
        result.forEach(row =&gt; {
            userMessage = userMessage + row[0] + &#39;:&#39; + row[1] + &#39;\n&#39;
        })
        return sendLineMsg(userMessage, replyToken);
    }
    if (userMessage.indexOf(&#34;已完成&#34;) &gt; -1) {//分析傳遞字串確認是否有“已完成”
        var formulaToUse = &#39;=QUERY(Sheet1!A2:C100,&#34;SELECT A,B WHERE C = TRUE&#34;)&#39;
        var result = query(formulaToUse)
        var userMessage = &#39;已完成文件如下：\n&#39;;
        result.forEach(row =&gt; {
            userMessage = userMessage + row[0] + &#39;:&#39; + row[1] + &#39;\n&#39;
        })
        return sendLineMsg(userMessage, replyToken);
    }
    if (userMessage.indexOf(&#34;功能&#34;) &gt; -1) {//這是列出你有提供功能的列表
        return sendLineMsg(&#39;朕不給的你不能搶ㄎㄎ\n• 未完成 ：列出excel未完成清單\n• 已完成 ：列出excel已完成清單&#39;, replyToken);
    }
}
function query (request) { //執行SQL語法的地方
    var sheet = ss.insertSheet(); //先插入一張新的sheet(也許有更好的做法，如果有請告訴我ＱＱ)
    sheet.getActiveRange().setFormula(request); //執行SQL
    var result = sheet.getDataRange().getValues(); //把整張表抓下來
    ss.deleteSheet(sheet); //把sheet刪除
    Logger.log(result);//可透過log來看輸出的資訊(上方的View Logs)
    return result; //回傳結果
}
function sendLineMsg (Message, replyToken) {
    var CHANNEL_ACCESS_TOKEN = &#39;你的 Channel access token&#39;;

    var url = &#39;https://api.line.me/v2/bot/message/reply&#39;;
    UrlFetchApp.fetch(url, {
        &#39;headers&#39;: {
            &#39;Content-Type&#39;: &#39;application/json; charset=UTF-8&#39;,
            &#39;Authorization&#39;: &#39;Bearer &#39; + CHANNEL_ACCESS_TOKEN,
        },
        &#39;method&#39;: &#39;post&#39;,
        &#39;payload&#39;: JSON.stringify({
            &#39;replyToken&#39;: replyToken,
            &#39;messages&#39;: [{
                &#39;type&#39;: &#39;text&#39;,
                &#39;text&#39;: Message,
            }],
        }),
    });
}
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/8f6d1e06b079ae3061d450d459cedc2c" rel="noopener">line_secretary.gs</a>（GitHub Gist）</p>
<p>接下來要將這個程式發布（如果有授權相關問題必須都接受才會通過）</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-03.webp" width="998" height="408" alt="STEP 1: 結合Google Sheet的衍生應用（圖 3）" loading="lazy" decoding="async"></p>
<p>Project version記得每次都要選擇New(否則不會更新)，並且要允許任何人可以存取這隻app，不然你的程式無法被line機器人執行</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-04.webp" width="892" height="804" alt="STEP 1: 結合Google Sheet的衍生應用（圖 4）" loading="lazy" decoding="async"></p>
<p>接下來複製你這串web app URL給LINE Message Api使用</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-05.webp" width="892" height="534" alt="STEP 1: 結合Google Sheet的衍生應用（圖 5）" loading="lazy" decoding="async"></p>
<p>貼到webhook URL上面後按下Save</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-06.webp" width="1400" height="698" alt="STEP 1: 結合Google Sheet的衍生應用（圖 6）" loading="lazy" decoding="async"></p>
<h2 id="step-2--驗證串接成功">STEP 2 : 驗證串接成功<a class="h-anchor" href="#step-2--%e9%a9%97%e8%ad%89%e4%b8%b2%e6%8e%a5%e6%88%90%e5%8a%9f" aria-label="這個段落的連結"></a></h2>
<p>打開你的LINE就可以輸入下面三種文字，機器人就會做相對應的回覆喔！</p>
<ol>
<li>功能</li>
<li>未完成</li>
<li>已完成</li>
</ol>
<p><img src="https://deanlin.net/blog/202006/line-bot-personal-assistant/images/line-bot-personal-assistant-07.webp" width="748" height="1140" alt="STEP 2 : 驗證串接成功（圖 7）" loading="lazy" decoding="async"></p>
<p>希望大家喜歡這篇文章的應用～</p>
]]></content:encoded>
    </item>
    <item>
      <title>用Google App Script打造打造自己的lINE機器人</title>
      <link>https://deanlin.net/blog/202006/line-bot-with-google-apps-script/</link>
      <pubDate>Wed, 10 Jun 2020 12:12:04 +0800</pubDate>
      <guid>https://deanlin.net/blog/202006/line-bot-with-google-apps-script/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>來打造一個回聲狗吧～</description>
      <media:content url="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-og_hu_5aca1942c5c55728.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>為什麼撰寫這篇文章</p>
</blockquote>
<p>Google App Script在以前我就非常的喜愛，也曾用他來當我專案後台登入的api來使用，搭配上Google Sheet，你簡直獲得了免費資料庫(不過他執行的速度就…反正就免費的速度XD)</p>
<p>但為了讓技術經過這段日子獲得更多提升，我想在原先資料庫的應用上加點料，比如說串接機器人，讓機器人對你定期提醒</p>
<p>本文章僅僅是示範最簡單的應用層面，實際上應該有非常多的應用等待大家去發掘</p>
<blockquote>
<p>文章目標</p>
</blockquote>
<ol>
<li>了解Google App Script，可撰寫簡單api並附帶參數</li>
<li>Google App Script串接Line機器人，讓你可以自由決定使用功能</li>
</ol>
<blockquote>
<p>本文章使用到的技術：</p>
</blockquote>
<ol>
<li>Google App Script : 撰寫你想要執行的邏輯</li>
<li>LINE developers : 打造自己的lINE機器人</li>
</ol>
<h2 id="setp-1-建立-line-messaging-api">SETP 1: 建立 LINE Messaging API<a class="h-anchor" href="#setp-1-%e5%bb%ba%e7%ab%8b-line-messaging-api" aria-label="這個段落的連結"></a></h2>
<p>我們要先在<a href="https://developers.line.biz/zh-hant/">這裡申請到line</a>發送訊息所需要的token</p>
<p>除了Channel type要寫Messaging API外其他就自由發揮，反正你填錯或是漏填LINE會逼著你填完</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-01.webp" width="1400" height="682" alt="SETP 1: 建立 LINE Messaging API（圖 1）" fetchpriority="high" decoding="async"></p>
<p>新增完成後在Messaging Api裡面的basic id就是這個機器人在line的id</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-02.webp" width="1400" height="905" alt="SETP 1: 建立 LINE Messaging API（圖 2）" loading="lazy" decoding="async"></p>
<p>你就能在line裡面透過id找到他惹</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-03.webp" width="624" height="810" alt="SETP 1: 建立 LINE Messaging API（圖 3）" loading="lazy" decoding="async"></p>
<p>除了id之外最重要的就是token，這個也是在Messaging Api裡面的Channel access token</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-04.webp" width="1400" height="668" alt="SETP 1: 建立 LINE Messaging API（圖 4）" loading="lazy" decoding="async"></p>
<p>另外我們不需要Greeting message、Auto response的功能</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-05.webp" width="1400" height="890" alt="SETP 1: 建立 LINE Messaging API（圖 5）" loading="lazy" decoding="async"></p>
<h2 id="step-2--建立-google-apps-script-接收-line-訊息">STEP 2 : 建立 Google Apps Script 接收 LINE 訊息<a class="h-anchor" href="#step-2--%e5%bb%ba%e7%ab%8b-google-apps-script-%e6%8e%a5%e6%94%b6-line-%e8%a8%8a%e6%81%af" aria-label="這個段落的連結"></a></h2>
<p>你可以在<a href="https://www.google.com/script/start/">這裡開始你的Google App Script</a>的故事 ，點擊Start Scripting，開啟一個Google App Script的專案吧</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-06.webp" width="1400" height="944" alt="STEP 2 : 建立 Google Apps Script 接收 LINE 訊息（圖 6）" loading="lazy" decoding="async"></p>
<p>將以下程式複製貼上，如果您願意可以自由更改，記得將剛剛取得的line token放上去喔</p>
<pre tabindex="0"><code>function doPost (e) { //凡事使用post method皆要通過這個function 

  var CHANNEL_ACCESS_TOKEN = &#39;你的 Channel access token&#39;;
  var msg = JSON.parse(e.postData.contents);

  // 取出 replayToken 和發送的訊息文字
  var replyToken = msg.events[0].replyToken;
  var userMessage = msg.events[0].message.text;

  if (typeof replyToken === &#39;undefined&#39;) {
    return;
  }

  var url = &#39;https://api.line.me/v2/bot/message/reply&#39;;
  UrlFetchApp.fetch(url, {
    &#39;headers&#39;: {
      &#39;Content-Type&#39;: &#39;application/json; charset=UTF-8&#39;,
      &#39;Authorization&#39;: &#39;Bearer &#39; + CHANNEL_ACCESS_TOKEN,
    },
    &#39;method&#39;: &#39;post&#39;,
    &#39;payload&#39;: JSON.stringify({
      &#39;replyToken&#39;: replyToken,
      &#39;messages&#39;: [{
        &#39;type&#39;: &#39;text&#39;, // 格式為文字，line可以傳送多種個格式(carousel、image)，有興趣的可以自己研究
        &#39;text&#39;: userMessage, //你目標傳遞的訊息
      }],
    }),
  });
}
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/7b90f26418375ed32aec1370a9673059" rel="noopener">code.gs</a>（GitHub Gist）</p>
<p>接下來要將這個程式發布（如果有授權相關問題必須都接受才會通過）</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-07.webp" width="1014" height="396" alt="STEP 2 : 建立 Google Apps Script 接收 LINE 訊息（圖 7）" loading="lazy" decoding="async"></p>
<p>Project version記得每次都要選擇New(否則不會更新)，並且要允許任何人可以存取這隻app，不然你的程式無法被line機器人執行</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-08.webp" width="892" height="804" alt="STEP 2 : 建立 Google Apps Script 接收 LINE 訊息（圖 8）" loading="lazy" decoding="async"></p>
<p>接下來複製你這串web app URL給LINE Message Api使用</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-09.webp" width="892" height="534" alt="STEP 2 : 建立 Google Apps Script 接收 LINE 訊息（圖 9）" loading="lazy" decoding="async"></p>
<p>貼到webhook URL上面後按下Save</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-10.webp" width="1400" height="698" alt="STEP 2 : 建立 Google Apps Script 接收 LINE 訊息（圖 10）" loading="lazy" decoding="async"></p>
<h2 id="step-3-驗證串接成功">STEP 3: 驗證串接成功<a class="h-anchor" href="#step-3-%e9%a9%97%e8%ad%89%e4%b8%b2%e6%8e%a5%e6%88%90%e5%8a%9f" aria-label="這個段落的連結"></a></h2>
<p>打開你的LINE就可以測試功能是否正常嚕～如果你是按照我的程式碼複製貼上，那他現在應該是回聲狗的功能XD</p>
<p><img src="https://deanlin.net/blog/202006/line-bot-with-google-apps-script/images/line-bot-with-google-apps-script-11.webp" width="748" height="886" alt="STEP 3: 驗證串接成功（圖 11）" loading="lazy" decoding="async"></p>
<p><strong>今天的教學就到這裡了～串接Google Sheet的部分我會放到<a href="https://deanlin.net/blog/202006/line-bot-personal-assistant/">下一篇文章</a>，歡迎有興趣的人來觀看喔！</strong></p>
<p>參考資源：<a href="https://www.oxxostudio.tw/articles/201805/backend-apps-script.html">簡易後端實作 ( Google Apps Script )</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>嫌Google Form太醜？那就把他當後台吧XD</title>
      <link>https://deanlin.net/blog/202006/google-form-as-backend/</link>
      <pubDate>Fri, 05 Jun 2020 16:03:36 +0800</pubDate>
      <guid>https://deanlin.net/blog/202006/google-form-as-backend/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>Google Form &   Google sheet & HTML & github 結合應用</description>
      <media:content url="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-og_hu_ccb8baea55aaf4e.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-01.webp" width="1400" height="714" alt="嫌Google Form太醜？那就把他當後台吧XD（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>為什麼撰寫這篇文章</p>
</blockquote>
<p>常常使用Google Form當問卷的你是否覺得Google Form有點太過制式化，或者說讓人覺得用Google Form有點誠意不足</p>
<p>但自己撰寫網頁問卷還要花錢買(租)一台Server放網頁跟建立資料庫，才能讓大家進來這個網頁填寫問卷，我覺得這樣一點都不划算，因為我只是想要讓這個問卷看起來有質感，不是Google Form而已XD</p>
<h2 id="於是我就誕生一個想法">於是我就誕生一個想法<a class="h-anchor" href="#%e6%96%bc%e6%98%af%e6%88%91%e5%b0%b1%e8%aa%95%e7%94%9f%e4%b8%80%e5%80%8b%e6%83%b3%e6%b3%95" aria-label="這個段落的連結"></a></h2>
<p>為什麼不要讓Google Form乖乖當後台就好，我放一個靜態網頁到Github上面就能省去過去在Server上面的花費</p>
<blockquote>
<p>文章目標</p>
</blockquote>
<ol>
<li>如何把Google Form的功能抽出來在一般靜態網頁使用</li>
<li>可全客製化自己心目中漂亮的表單(內文使用Bootstrap 4為範例)</li>
<li>讓你了解撰寫網頁會遇到的問題(謹以本範例為說明)</li>
</ol>
<blockquote>
<p><strong><em>本文章使用到的技術：</em></strong></p>
</blockquote>
<ol>
<li>前端:static web(Bootstrap 4)</li>
<li>後端:Google Form</li>
<li>資料庫:Google Sheets</li>
</ol>
<blockquote>
<p>Demo區讓你搶先體驗</p>
</blockquote>
<p><a href="https://dean9703111.github.io/customer_feedback_form/index.html">DEMO網頁</a> (看完文章後你可以自己客製化一個)</p>
<p><a href="https://docs.google.com/forms/d/e/1FAIpQLSeijRuLDQE62gbapDYgd_3pbmg7GyWY1CWPVeaEK-TiLDKzPw/viewform">Google Form</a> (他其實才是執行動作的本體)</p>
<p><a href="https://docs.google.com/spreadsheets/d/1aZyvHnajaVjdNBaUT1Md-Y9nYW8O_rdPnMmN5WFrdO4/edit?usp=sharing">Google Sheets</a> (資料最終儲存的地方)</p>
<blockquote>
<p>專案原始碼</p>
</blockquote>
<p><a href="https://github.com/dean9703111/customer_feedback_form">Github Source code</a> (如果實作上遇到問題可以參考)</p>
<h2 id="step-1--7-建立google-form表單">STEP 1 / 7: 建立Google Form表單<a class="h-anchor" href="#step-1--7-%e5%bb%ba%e7%ab%8bgoogle-form%e8%a1%a8%e5%96%ae" aria-label="這個段落的連結"></a></h2>
<p>先建立一個<a href="https://www.google.com.tw/intl/zh-TW/forms/about/">Google Form</a>，選取個人的即可：</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-02.webp" width="1400" height="741" alt="STEP 1 / 7: 建立Google Form表單（圖 2）" loading="lazy" decoding="async"></p>
<p>您可以按造自己的需求建立一個簡單的表單：</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-03.webp" width="1400" height="1132" alt="STEP 1 / 7: 建立Google Form表單（圖 3）" loading="lazy" decoding="async"></p>
<h2 id="step-2--7-抽出google-form表單裡面的元素">STEP 2 / 7: 抽出Google Form表單裡面的元素<a class="h-anchor" href="#step-2--7-%e6%8a%bd%e5%87%bagoogle-form%e8%a1%a8%e5%96%ae%e8%a3%a1%e9%9d%a2%e7%9a%84%e5%85%83%e7%b4%a0" aria-label="這個段落的連結"></a></h2>
<p>建立好表單後進入預覽模式</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-04.webp" width="1400" height="151" alt="STEP 2 / 7: 抽出Google Form表單裡面的元素（圖 4）" loading="lazy" decoding="async"></p>
<p>接下來進入網頁的開發者模式(按下F12)，選擇Elements檢視網頁元素，然後搜尋”entry”，這些name的標籤是之後會使用的參數欄位，請把他們複製起來</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-05.webp" width="1400" height="734" alt="STEP 2 / 7: 抽出Google Form表單裡面的元素（圖 5）" loading="lazy" decoding="async"></p>
<blockquote>
<p>2021/2/26 更新，朋友提醒新版的 ”entry” 已經轉移到了 &lt;input type=”hidden” name=”entry.xxx&quot; value=””&gt; 的位置，大家操作時注意一下</p>
</blockquote>
<figure class="figure"><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-06.webp" width="1400" height="739" alt="位置轉移惹" loading="lazy" decoding="async">
  <figcaption>位置轉移惹</figcaption>
</figure>

<p>輸入必填欄位資料按下送出(submit)按鈕後複製上面的網址，這個網址是之後網頁用來發送訊息的url</p>
<figure class="figure"><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-07.webp" width="1400" height="192" alt="網址會長的像:https://docs.google.com/forms/這些是你自己的網址/formResponse" loading="lazy" decoding="async">
  <figcaption>網址會長的像:<a href="https://docs.google.com/forms/u/0/d/e/1FAIpQLSeijRuLDQE62gbapDYgd_3pbmg7GyWY1CWPVeaEK-TiLDKzPw/formResponse">https://docs.google.com/forms/這些是你自己的網址/formResponse</a></figcaption>
</figure>

<h2 id="step-3--7-選一個你喜歡的表單模板">STEP 3 / 7: 選一個你喜歡的表單模板<a class="h-anchor" href="#step-3--7-%e9%81%b8%e4%b8%80%e5%80%8b%e4%bd%a0%e5%96%9c%e6%ad%a1%e7%9a%84%e8%a1%a8%e5%96%ae%e6%a8%a1%e6%9d%bf" aria-label="這個段落的連結"></a></h2>
<p>在這裡我提供一些大家可以直接使用的<a href="https://www.mockplus.com/blog/post/bootstrap-form-template">表單模板</a>，大家可以去找自己喜歡的，接下來我直接說明細節，完整的程式碼可以在<a href="https://github.com/dean9703111/customer_feedback_form">這裡下載</a></p>
<p>因為我設計的這份問卷裡面需要填寫：姓名、email、評分、您的建議，並且需要要以form表單傳送，所以會有html結構如下(您的結構可能跟我有些許不同)：</p>
<pre tabindex="0"><code>&lt;form id=&#34;google-form&#34; class=&#34;contact100-form validate-form&#34;&gt;
        &lt;span class=&#34;contact100-form-title&#34;&gt;
          餐後回饋
        &lt;/span&gt;

        &lt;label class=&#34;label-input100&#34; for=&#34;first-name&#34;&gt;姓名 *&lt;/label&gt;
        &lt;div class=&#34;wrap-input100 validate-input&#34; data-validate=&#34;姓名為必填&#34;&gt;
          &lt;input id=&#34;name&#34; class=&#34;input100&#34; type=&#34;text&#34; placeholder=&#34;王大明&#34;&gt;
          &lt;span class=&#34;focus-input100&#34;&gt;&lt;/span&gt;
        &lt;/div&gt;

        &lt;label class=&#34;label-input100&#34; for=&#34;email&#34;&gt;email&lt;/label&gt;
        &lt;div class=&#34;wrap-input100&#34;&gt;
          &lt;input id=&#34;email&#34; class=&#34;input100&#34; type=&#34;text&#34; placeholder=&#34;Eg. example@email.com&#34;&gt;
          &lt;span class=&#34;focus-input100&#34;&gt;&lt;/span&gt;
        &lt;/div&gt;

        &lt;label class=&#34;label-input100&#34; for=&#34;email&#34;&gt;評分(最高五分) *&lt;/label&gt;
        &lt;div class=&#34;wrap-input100 validate-input&#34; data-validate=&#34;煩請評分&#34;&gt;
          &lt;select class=&#34;form-control&#34; id=&#34;score&#34; style=&#34;border: 0px;&#34;&gt;
            &lt;option&gt;1&lt;/option&gt;
            &lt;option&gt;2&lt;/option&gt;
            &lt;option&gt;3&lt;/option&gt;
            &lt;option&gt;4&lt;/option&gt;
            &lt;option&gt;5&lt;/option&gt;
          &lt;/select&gt;
        &lt;/div&gt;

        &lt;label class=&#34;label-input100&#34; for=&#34;message&#34;&gt;您的建議&lt;/label&gt;
        &lt;div class=&#34;wrap-input100&#34; data-validate=&#34;Message is required&#34;&gt;
          &lt;textarea id=&#34;message&#34; class=&#34;input100&#34; placeholder=&#34;Write us a message&#34;&gt;&lt;/textarea&gt;
          &lt;span class=&#34;focus-input100&#34;&gt;&lt;/span&gt;
        &lt;/div&gt;

        &lt;div class=&#34;container-contact100-form-btn&#34;&gt;
          &lt;button class=&#34;contact100-form-btn&#34;&gt;
            送出
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/fe560b714307b9f4e52df4114ebd270c" rel="noopener">form_elements</a>（GitHub Gist）</p>
<p>但請記得在&lt;form&gt;、&lt;input&gt;、&lt;select&gt;這些tag裡面設定id，這樣後續在jquery裡面才能抓到相應的元素</p>
<h2 id="step-4--7-將google-form表單裡抽出的元素放進去html網頁">STEP 4 / 7: 將Google Form表單裡抽出的元素放進去HTML網頁<a class="h-anchor" href="#step-4--7-%e5%b0%87google-form%e8%a1%a8%e5%96%ae%e8%a3%a1%e6%8a%bd%e5%87%ba%e7%9a%84%e5%85%83%e7%b4%a0%e6%94%be%e9%80%b2%e5%8e%bbhtml%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h2>
<p>Google Form建立好了，網頁元素也都建立完成後就是將兩者bind在一起，以下是用jquery抓取html元素，並使用ajax將這個訊息送出，程式細節情看註解</p>
<blockquote>
<p>記得在要 把 jquery 引入&lt;script&gt;裡面，我朋友卡在這個問題很久 XD</p>
</blockquote>
<pre tabindex="0"><code>&lt;script&gt;
    $(&#39;#google-form&#39;).submit(function (e) {
      //在這裡我們要先擋掉form默認事件
      e.preventDefault();

      if ($(&#39;#name&#39;).val() &amp;&amp; $(&#39;#score&#39;).val()) {//需要先確認必填欄位是否填寫
        $.ajax({
          // url為Google Form按下submit的aciotn
          url: &#34;https://docs.google.com/forms/d/e/1FAIpQLSeijRuLDQE62gbapDYgd_3pbmg7GyWY1CWPVeaEK-TiLDKzPw/formResponse&#34;,
          crossDomain: true,//解決跨網域CORS的問題
          data: {// entry.xxxxx 這些需要填寫您表單裡面的值，與其相互對應
            &#34;entry.2040133429&#34;: $(&#39;#name&#39;).val(),
            &#34;entry.304089069&#34;: $(&#39;#email&#39;).val(),
            &#34;entry.207113062&#34;: $(&#39;#score&#39;).val(),
            &#34;entry.1310220291&#34;: $(&#39;#message&#39;).val()
          },
          type: &#34;POST&#34;, //因為是要進行insert的動作，故事用POST
          dataType: &#34;JSONP&#34;,
          complete: function () {
            //完成後把這些欄位清空
            $(&#39;#name&#39;).val(&#39;&#39;)
            $(&#39;#email&#39;).val(&#39;&#39;)
            $(&#39;#score&#39;).val(&#39;&#39;)
            $(&#39;#message&#39;).val(&#39;&#39;)
            //最後跳轉到感謝的頁面
            window.location.replace(&#34;thank.html&#34;);
          }
        });
      }
    });
  &lt;/script&gt;
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/726895bb8b6902f16b26e8254b954e03" rel="noopener">bind-html-googeForm</a>（GitHub Gist）</p>
<h2 id="step-5--7測試是否正常運作">STEP 5 / 7:測試是否正常運作<a class="h-anchor" href="#step-5--7%e6%b8%ac%e8%a9%a6%e6%98%af%e5%90%a6%e6%ad%a3%e5%b8%b8%e9%81%8b%e4%bd%9c" aria-label="這個段落的連結"></a></h2>
<p>你可以直接開啟html檔(點擊兩下就會跳出網頁了)，輸入數值後按下送出(如果你使用我的程式會看到如下成果)</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-08.webp" width="1400" height="1280" alt="STEP 5 / 7:測試是否正常運作（圖 8）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-09.webp" width="1400" height="734" alt="STEP 5 / 7:測試是否正常運作（圖 9）" loading="lazy" decoding="async"></p>
<p>接下來去Google Form的Responses區域看看是否資訊正常進入</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-10.webp" width="1400" height="1131" alt="STEP 5 / 7:測試是否正常運作（圖 10）" loading="lazy" decoding="async"></p>
<p>到這裡就能確認你的程式正常運作了(撒花～)</p>
<h2 id="step-6--7登入github帳戶並上傳你的專案">STEP 6 / 7:登入github帳戶，並上傳你的專案<a class="h-anchor" href="#step-6--7%e7%99%bb%e5%85%a5github%e5%b8%b3%e6%88%b6%e4%b8%a6%e4%b8%8a%e5%82%b3%e4%bd%a0%e7%9a%84%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h2>
<p>github帳戶的創建應該很多地方都有教學，這裡我就簡化這個步驟，說明最簡單上傳的方式</p>
<p>登入帳號後選擇Repositories，在按下New新增一個</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-11.webp" width="1400" height="227" alt="STEP 6 / 7:登入github帳戶，並上傳你的專案（圖 11）" loading="lazy" decoding="async"></p>
<p>填寫一些基礎資訊，這裡需要特別注意的是你必須要選擇Public，不然你這個專案是無法讓大家在網路上觀看的</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-12.webp" width="1400" height="1083" alt="STEP 6 / 7:登入github帳戶，並上傳你的專案（圖 12）" loading="lazy" decoding="async"></p>
<figure class="figure"><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-13.webp" width="1400" height="401" alt="選擇上傳存在的檔案" loading="lazy" decoding="async">
  <figcaption>選擇上傳存在的檔案</figcaption>
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-14.webp" width="1400" height="696" alt="直接把整個資料夾拖拉進去就好了" loading="lazy" decoding="async">
  <figcaption>直接把整個資料夾拖拉進去就好了</figcaption>
</figure>

<h2 id="step-7--7將專案開放給大家連線">STEP 7 / 7:將專案開放給大家連線<a class="h-anchor" href="#step-7--7%e5%b0%87%e5%b0%88%e6%a1%88%e9%96%8b%e6%94%be%e7%b5%a6%e5%a4%a7%e5%ae%b6%e9%80%a3%e7%b7%9a" aria-label="這個段落的連結"></a></h2>
<p>接下來要對這個專案做一些設定，先點擊最右側的Settings</p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-15.webp" width="1400" height="173" alt="STEP 7 / 7:將專案開放給大家連線（圖 15）" loading="lazy" decoding="async"></p>
<p>找到Source的位置，選擇master branch後，上方就會出現連結了</p>
<p>不過要注意這個連結是這個專案的根目錄，如果要導向到你的網頁需要設定路徑喔：</p>
<p>EX:</p>
<p>根目錄 https://dean9703111.github.io/test/</p>
<p>實體檔案位置 https://dean9703111.github.io/test/<strong><em>index.html</em></strong></p>
<p><img src="https://deanlin.net/blog/202006/google-form-as-backend/images/google-form-as-backend-16.webp" width="1400" height="739" alt="STEP 7 / 7:將專案開放給大家連線（圖 16）" loading="lazy" decoding="async"></p>
<p>做到這裡順利的話就能分享連結給朋友炫耀自己的表單啦～</p>
<p>如果在實作的過程中遇到任何問題歡迎在下方留言</p>
<p><strong>參考資源</strong>：<a href="https://colorlib.com/wp/template/contact-form-v17/">Bootstrap form templete</a>、<a href="https://codepen.io/JacobLett/pen/vyegPV">Bootstrap thank you page</a>、</p>
<p><a href="https://hackernoon.com/create-websites-using-google-forms-49a19cc8beb8">Create websites using Google Forms</a></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
