<?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>FB &amp; IG 爬蟲專案從零開始｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/series/fb-ig-crawler/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>FB &amp; IG 爬蟲專案從零開始｜林鼎淵 Dean Lin</title><link>https://deanlin.net/series/fb-ig-crawler/</link><width>144</width><height>144</height></image>
    <description>第 12 屆 iThome 鐵人賽 AI & Data 組佳作：用 Node.js 與 Selenium 從零打造 FB、IG 粉專爬蟲，一路做到 Google Sheets 存資料、排程與 LINE 通知；後來改寫成《JavaScript 爬蟲新思路！》。</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <copyright>© 2020–2026 林鼎淵 Dean Lin</copyright>
    <lastBuildDate>Thu, 15 Oct 2020 08:43:22 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/series/fb-ig-crawler/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>【Day30】時間只是考驗，心中信念絲毫未減</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-30/</link>
      <pubDate>Thu, 15 Oct 2020 08:43:22 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-30/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>因為在過去的鐵人賽中很少看到從專案角度來分享技術的文章，所以我選擇從這個角度出發，因為我認為了解專案中每個技術選擇的動機，與學習一個新的技術一樣重要；技術可能被淘汰，但經驗會留下來，希望大家會喜歡我這個系列的文章。</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-30/images/cover_hu_416f6ce1acb11831.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>當你停止拿著別人的地圖走在自己的人生道路上，那你的人生才算真正的開始。-Peter Su</p>
</blockquote>
<p>因為在過去的鐵人賽中很少看到從專案角度來分享技術的文章，所以我選擇從這個角度出發，因為我認為<mark>了解專案中每個技術選擇的動機，與學習一個新的技術一樣重要</mark>；技術可能被淘汰，但經驗會留下來，希望大家會喜歡我這個系列的文章。</p>
<hr>
<h2 id="專案總結">專案總結<a class="h-anchor" href="#%e5%b0%88%e6%a1%88%e7%b8%bd%e7%b5%90" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="1-鐵人賽的收穫">1. 鐵人賽的收穫<a class="h-anchor" href="#1-%e9%90%b5%e4%ba%ba%e8%b3%bd%e7%9a%84%e6%94%b6%e7%a9%ab" aria-label="這個段落的連結"></a></h3>
<p>1.1 了解技術文章與技術文件的差異</p>
<p>1.2 深刻體會以前寫的程式有多爛</p>
<p>1.3 突破過去程式的盲點</p>
<h3 class="h-nomark" id="2-在鐵人賽過程中遇到的挑戰">2. 在鐵人賽過程中遇到的挑戰<a class="h-anchor" href="#2-%e5%9c%a8%e9%90%b5%e4%ba%ba%e8%b3%bd%e9%81%8e%e7%a8%8b%e4%b8%ad%e9%81%87%e5%88%b0%e7%9a%84%e6%8c%91%e6%88%b0" aria-label="這個段落的連結"></a></h3>
<p>2.1 在週末跟連假期發文</p>
<p>2.2 發現有專案有 bug，而且這個 bug 在好幾天前的文章就有了</p>
<p>2.3 瀏覽人數太低，沒人在你的文章留言跟按 LIKE</p>
<h3 class="h-nomark" id="3-完整專案程式碼--基礎設定說明">3. 完整專案程式碼 &amp; 基礎設定說明<a class="h-anchor" href="#3-%e5%ae%8c%e6%95%b4%e5%b0%88%e6%a1%88%e7%a8%8b%e5%bc%8f%e7%a2%bc--%e5%9f%ba%e7%a4%8e%e8%a8%ad%e5%ae%9a%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<hr>
<h2 class="h-nomark" id="1-鐵人賽的收穫-1">1. 鐵人賽的收穫<a class="h-anchor" href="#1-%e9%90%b5%e4%ba%ba%e8%b3%bd%e7%9a%84%e6%94%b6%e7%a9%ab-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-了解技術文章與技術文件的差異">1.1 了解技術文章與技術文件的差異<a class="h-anchor" href="#11-%e4%ba%86%e8%a7%a3%e6%8a%80%e8%a1%93%e6%96%87%e7%ab%a0%e8%88%87%e6%8a%80%e8%a1%93%e6%96%87%e4%bb%b6%e7%9a%84%e5%b7%ae%e7%95%b0" aria-label="這個段落的連結"></a></h3>
<p>在開賽前兩週我將完成的 26 天備稿放在 github 上請朋友幫忙給意見，除了技術上的漏洞外，<strong>下面這些回饋讓我一度思考到底要不要參加這個比賽</strong>：</p>
<ol>
<li>你分享的技術很有趣，但我有點消化不良</li>
<li>我現在還在上班嗎？你這根本是技術文件吧？</li>
<li>排版好亂，閱讀起來有障礙</li>
<li>你這個是草稿沒錯吧？</li>
</ol>
<p>感謝身旁願意說真話的朋友們，是你們讓我意識到：<mark>過去自己寫的是技術文件，能讓合作夥伴看懂就好了；但是分享到一個平台上你除了要讓人看懂外，你還要讓人看得下去</mark>，與朋友深聊之後對文章做了以下改進：</p>
<ol>
<li>加入個人經驗分享讓文章多一點人味</li>
<li>在實作的段落加上一些與讀者的對話</li>
<li>多看別人的文章吸收經驗，調整排版讓閱讀變得舒適</li>
</ol>
<blockquote>
<p>關於這方面的技巧大家可以參考 <a href="https://tw.alphacamp.co/blog/2018-06-14-18352">技術寫作六步驟 讓工程師撰寫流暢的技術部落格</a></p>
</blockquote>
<hr>
<h3 class="h-nomark" id="12-深刻體會以前寫的程式有多爛">1.2 深刻體會以前寫的程式有多爛<a class="h-anchor" href="#12-%e6%b7%b1%e5%88%bb%e9%ab%94%e6%9c%83%e4%bb%a5%e5%89%8d%e5%af%ab%e7%9a%84%e7%a8%8b%e5%bc%8f%e6%9c%89%e5%a4%9a%e7%88%9b" aria-label="這個段落的連結"></a></h3>
<p>原本以為這次鐵人賽會非常輕鬆，因為我是把以前寫的專案拿出來分享；但開始寫文章後就發現自己錯的很離譜，因為<mark>寫文章需要反覆思考每一行程式的意義</mark>，這時會發現下面的問題隨處可見：</p>
<ol>
<li>變數都在亂宣告</li>
<li>程式中有許多不必要的判斷式</li>
<li>超冗長與意義不明的函式</li>
<li>註解亂七八糟，導致現在的自己看不懂以前為什麼這樣寫</li>
</ol>
<blockquote>
<p>針對這些問題，我有統整成一篇 <a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-03/">Day3 努力寫出更好的程式碼，有時你不是一個人在戰鬥</a> 給大家參考</p>
</blockquote>
<p>因為是以專案的架構來寫這系列鐵人文章，所以我需要在每天都進行各種測試，並且深入的了解官方文件，在這個過程中我深深感受到以前專案的缺點：</p>
<ol>
<li>程式穩定性不夠，遇到錯誤時容易崩潰</li>
<li>對官方文件理解不足，導致執行程式時多了很多不必要的步驟</li>
</ol>
<blockquote>
<p>錯誤處理的部分可以參考 <a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/">Day14 爬蟲執行時又又又崩潰了嗎？來點 try-catch 吧</a> 這篇喔</p>
</blockquote>
<hr>
<h3 class="h-nomark" id="13-突破過去程式的盲點">1.3 突破過去程式的盲點<a class="h-anchor" href="#13-%e7%aa%81%e7%a0%b4%e9%81%8e%e5%8e%bb%e7%a8%8b%e5%bc%8f%e7%9a%84%e7%9b%b2%e9%bb%9e" aria-label="這個段落的連結"></a></h3>
<p>過去的我認為學習新技術、多寫一些專案可以讓自己的程式進步，但經過這 30 天的鐵人賽磨練後我發現<mark>精煉自己的程式可以學到更多東西</mark>：</p>
<ol>
<li>當你<strong>每天都在看一樣的程式時你才會去思考要如何讓它變得更好</strong>，而你這些思考與嘗試的過程會讓你日後撰寫程式時多一點 sense</li>
<li>因為不停的思考與嘗試，你可能<strong>會發現有些工具打從一開始就選錯了</strong>；像是 Node.js 發出 request 這個功能我原本是用 <a href="https://www.npmjs.com/package/xmlhttprequest">xmlhttprequest</a> 來完成，但撰寫文章時才驚覺當時選了一個古老的套件，<strong>只為了快速完成功能容易造成決策上的錯誤</strong></li>
</ol>
<hr>
<h2 class="h-nomark" id="2-在鐵人賽過程中遇到的挑戰-1">2. 在鐵人賽過程中遇到的挑戰<a class="h-anchor" href="#2-%e5%9c%a8%e9%90%b5%e4%ba%ba%e8%b3%bd%e9%81%8e%e7%a8%8b%e4%b8%ad%e9%81%87%e5%88%b0%e7%9a%84%e6%8c%91%e6%88%b0-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-在週末跟連假期發文">2.1 在週末跟連假期發文<a class="h-anchor" href="#21-%e5%9c%a8%e9%80%b1%e6%9c%ab%e8%b7%9f%e9%80%a3%e5%81%87%e6%9c%9f%e7%99%bc%e6%96%87" aria-label="這個段落的連結"></a></h3>
<p>當身旁所有人都在吃喝玩樂的時候把專注力放在撰寫文章上真的很違反人性，在這種內心鬥爭的階段我會選擇聽一些勵志的歌曲來提神：</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-30/images/fb-ig-crawler-day-30-01.webp" width="480" height="360" alt="Yes" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>每次聽到 <mark>“我不怕千萬人阻擋，只怕自己投降”</mark> 這段，我寫文的速度跟意志力就爆發惹！</p>
</blockquote>
<hr>
<h3 class="h-nomark" id="22-發現有專案有-bug而且這個-bug-在好幾天前的文章就有了">2.2 發現有專案有 bug，而且這個 bug 在好幾天前的文章就有了<a class="h-anchor" href="#22-%e7%99%bc%e7%8f%be%e6%9c%89%e5%b0%88%e6%a1%88%e6%9c%89-bug%e8%80%8c%e4%b8%94%e9%80%99%e5%80%8b-bug-%e5%9c%a8%e5%a5%bd%e5%b9%be%e5%a4%a9%e5%89%8d%e7%9a%84%e6%96%87%e7%ab%a0%e5%b0%b1%e6%9c%89%e4%ba%86" aria-label="這個段落的連結"></a></h3>
<p>說出來不知道該是驕傲還是怕被人笑，我每篇文章撰寫時間大約落在 4 ~ 20 小時，之所以會花這麼長的時間是為了處理之前的歷史業障，我是一個有點偏執狂的人，所以發現 bug 後會把每天的文章跟 sample code 全部看一遍重新校訂，並且全部測試一遍確保都能正常運行</p>
<p><mark>專案的架構使得每篇文章關聯性太大，遇到這種 bug 時真的萬分想讓人棄賽&hellip;</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-30/images/fb-ig-crawler-day-30-02.webp" width="1024" height="92" alt="2.2 發現有專案有 bug，而且這個 bug 在好幾天前的文章就有了（圖 2）" loading="lazy" decoding="async"></p>
<blockquote>
<p>截至今天完賽，我的 github 已經有 400 多次的 commit，這系列的文章真的是我心血之作</p>
</blockquote>
<hr>
<h3 class="h-nomark" id="23-瀏覽人數太低沒人在你的文章留言跟按-like">2.3 瀏覽人數太低，沒人在你的文章留言跟按 LIKE<a class="h-anchor" href="#23-%e7%80%8f%e8%a6%bd%e4%ba%ba%e6%95%b8%e5%a4%aa%e4%bd%8e%e6%b2%92%e4%ba%ba%e5%9c%a8%e4%bd%a0%e7%9a%84%e6%96%87%e7%ab%a0%e7%95%99%e8%a8%80%e8%b7%9f%e6%8c%89-like" aria-label="這個段落的連結"></a></h3>
<p>我認為這是心理層面的挑戰，<mark>當你用盡全力寫文章而無人關注時你會開始自我懷疑</mark>：懷疑是不是自己寫的不好、思考難道自己選錯題目了嗎？</p>
<p>我想這種負面想法應該在不少參賽者心中出現過，玻璃心的我也曾經自我懷疑了好一段時間，但最後我選擇<mark>把自我懷疑的情緒轉化成改善文章的動力</mark>，如果文章夠好一定會有人來看並樂於分享，如果沒人關注那一定是有什麼不足的地方</p>
<blockquote>
<p>花若盛開蝴蝶自來，人若精彩天自安排</p>
</blockquote>
<hr>
<h2 class="h-nomark" id="3-完整專案程式碼--基礎設定說明-1">3. 完整專案程式碼 &amp; 基礎設定說明<a class="h-anchor" href="#3-%e5%ae%8c%e6%95%b4%e5%b0%88%e6%a1%88%e7%a8%8b%e5%bc%8f%e7%a2%bc--%e5%9f%ba%e7%a4%8e%e8%a8%ad%e5%ae%9a%e8%aa%aa%e6%98%8e-1" aria-label="這個段落的連結"></a></h2>
<p>在文章的最後放上這次專案的完整程式碼，你只要下載這個<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day29_sample_code.zip">壓縮檔</a>並依照下方的提示完成設定就能體驗爬蟲的樂趣嚕</p>
<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 資料夾放上 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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
<li>填上 LINE Notify 申請的權杖(LINE_TOKEN)，申請流程請參考 <a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/">Day27</a></li>
</ul>
</li>
<li>在終端機下指令 <strong>yarn start</strong> 執行專案(windows為 <strong>yarn win_start</strong>)</li>
</ul>
<blockquote>
<ol>
<li>
<p>如果完全無法執行可能是開發環境沒設定好，請參考 <a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-02/">Day2</a> 中筆者安裝的版本</p>
</li>
<li>
<p>如果執行過程中失敗請將 <code>tools/initDrive.js</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>options.addArguments(<span style="color:#a5d6ff">&#39;--headless&#39;</span>)
</span></span></code></pre></div></li>
<li>
<p>如果想幫專案加上排程請閱讀 <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> 的文章喔</p>
</li>
</ol>
</blockquote>
<hr>
<h2 id="致謝">致謝<a class="h-anchor" href="#%e8%87%b4%e8%ac%9d" aria-label="這個段落的連結"></a></h2>
<p>首先要感謝的是願意讓我分享這份專案的老朋友：<strong>Ray</strong>，沒有他就不會有這 30 天的系列文章，更無法讓我重新審視自己程式的缺點</p>
<p>接著感謝願意看我那不堪入目的備稿並給予意見的工程師們：<strong>永遠給予支持的凱霖學長、讀書會的超強筆記男、愛用好想兔貼圖的前同事&hellip;</strong>，你們的建議讓我撰寫技術文章的能力上升了一個台階，至少現在讀起來不會反胃了吧XD</p>
<p>特別感謝在撰寫文章過程給我排版建議的朋友：<strong>部落格男</strong>，他是我心中真正的鐵人，已經堅持每天寫一篇部落格超過 400 天</p>
<p>其實我原本是打算一個人參賽的，但在報名後看到團體組列表中有<a href="https://ithelp.ithome.com.tw/2020-12th-ironman/signup/team/96">北科大計算機</a>，就向<a href="https://ithelp.ithome.com.tw/users/20110850">團長Peter學程式</a>詢問我能不能+1，感謝團長的收留，自從大學畢業後就沒有這種組隊參賽的熱血了</p>
<p>最後感謝比賽期間陪伴在我身旁的<strong>可愛女友</strong>，擁有行銷專業的她給我許多下標題的建議</p>
<p>雖然鐵人賽在這裡告了一段落，但對我來說：<mark>技術分享的旅途才剛開始呢</mark></p>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-30/images/fb-ig-crawler-day-30-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>【Day29】為爬蟲加上通知 - 成功收到 LINE 通知爬蟲摘要訊息，專案大功告成！</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-29/</link>
      <pubDate>Wed, 14 Oct 2020 09:00:25 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-29/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>這份專案所用到的各種技術都不難，難的是將這些技術整合成一個能讓客戶買單的專案；當時在跟工程師朋友分享這個專案的技術組成時他們都覺得很是獵奇，沒想到爬蟲專案還能用如此低成本的方式完成，而且真的能幫客戶解決問題</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-29/images/cover_hu_66dd8bb7e683172a.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%ad%86%e8%80%85%e6%9c%89%e8%a9%b1%e5%85%88%e8%aa%aa" aria-label="這個段落的連結"></a></h2>
<p>這份專案所用到的各種技術都不難，難的是<mark>將這些技術整合成一個能讓客戶買單的專案</mark>；當時在跟工程師朋友分享這個專案的技術組成時他們都覺得很是獵奇，沒想到爬蟲專案還能用如此<code>低成本</code>的方式完成，而且真的能<mark>幫客戶解決問題</mark></p>
<p>今天這篇文章是實作的最後一篇，我們要將爬蟲執行時的資訊做成一份摘要，透過 LINE 發送給使用者</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>分析爬蟲摘要訊息來源</li>
<li>調整爬蟲函式回傳的資料結構</li>
<li>調整主程式傳遞 lineNotify 所需的參數</li>
<li>在 <code>lineNotify</code> 函式中整合摘要訊息</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-分析爬蟲摘要訊息來源">1. 分析爬蟲摘要訊息來源<a class="h-anchor" href="#1-%e5%88%86%e6%9e%90%e7%88%ac%e8%9f%b2%e6%91%98%e8%a6%81%e8%a8%8a%e6%81%af%e4%be%86%e6%ba%90" aria-label="這個段落的連結"></a></h2>
<p>根據需求規格書，在摘要中需包含以下訊息：</p>
<ol>
<li><strong>爬蟲總費時</strong>：在主程式計算從爬蟲開始到更新完 Google sheet 的時間</li>
<li><strong>總計掃描FB粉專、IG帳號數量</strong>：因為爬蟲函式 <code>crawlerIG、crawlerFB</code> 回傳的結果 <code>result_array</code> 是陣列的形式，所以數量的部分使用陣列的長度即可</li>
<li><strong>Google Sheets 連結</strong>：依照 .env 環境檔中的 <code>SPREADSHEET_ID</code> 組成連結</li>
<li><strong>無法爬蟲的FB粉專、IG帳號名稱</strong>：在爬蟲函式 <code>crawlerIG、crawlerFB</code> 中紀錄無法爬蟲的標題</li>
</ol>
<hr>
<h2 class="h-nomark" id="2-調整爬蟲函式回傳的資料結構">2. 調整爬蟲函式回傳的資料結構<a class="h-anchor" href="#2-%e8%aa%bf%e6%95%b4%e7%88%ac%e8%9f%b2%e5%87%bd%e5%bc%8f%e5%9b%9e%e5%82%b3%e7%9a%84%e8%b3%87%e6%96%99%e7%b5%90%e6%a7%8b" aria-label="這個段落的連結"></a></h2>
<p>下面用 <strong>crawlerFB</strong> 來舉例，crawlerIG 可以自己嘗試修改看看喔～</p>
<ul>
<li>使用 <code>error_title_array</code> 來紀錄無法爬蟲的標題</li>
<li>完成爬蟲後回傳 <strong>爬蟲結果（result_array)、無法爬蟲的粉專標題（error_title_array)</strong> 這兩個參數，讓後續 Google Sheet 更新、LINE Notify 傳送通知時使用</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> [], error_title_array <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:#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 style="color:#8b949e;font-style:italic">// 將無法爬蟲的標題放入陣列
</span></span></span><span style="display:flex;"><span>                    error_title_array.push(fanpage.title)
</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></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></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">// 回傳爬蟲結果、無法爬蟲的粉專標題
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> { <span style="color:#a5d6ff">&#34;result_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> result_array, <span style="color:#a5d6ff">&#34;error_title_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> error_title_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="3-調整主程式傳遞-linenotify-所需的參數">3. 調整主程式傳遞 lineNotify 所需的參數<a class="h-anchor" href="#3-%e8%aa%bf%e6%95%b4%e4%b8%bb%e7%a8%8b%e5%bc%8f%e5%82%b3%e9%81%9e-linenotify-%e6%89%80%e9%9c%80%e7%9a%84%e5%8f%83%e6%95%b8" aria-label="這個段落的連結"></a></h2>
<ul>
<li>
<p><strong>爬蟲總費時</strong>：使用 <code>spend_time</code> 函式把時間差<mark>轉換成 時 分 秒</mark>的格式</p>
<blockquote>
<p>因為朋友數百個品牌爬蟲總費時快兩小時，如果用秒數呈現實在太不人性化了</p>
</blockquote>
</li>
<li>
<p>整合 <code>lineNotify</code> 所需的參數： 爬蟲總費時（<strong>spend_time</strong>）、總計掃描FB粉專、IG帳號數量（<strong>result_array.length</strong>）、無法爬蟲的FB粉專、IG帳號名稱（<strong>error_title_array</strong>）</p>
</li>
</ul>
<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></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 style="color:#ff7b72">const</span> { lineNotify } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./tools/lineNotify.js&#34;</span>);
</span></span><span style="display:flex;"><span>exports.crawler <span style="color:#ff7b72;font-weight:bold">=</span> crawler;
</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:#ff7b72">const</span> start_time <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Date(); <span style="color:#8b949e;font-style:italic">// 取得開始時間
</span></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> { <span style="color:#a5d6ff">&#34;result_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> ig_result_array, <span style="color:#a5d6ff">&#34;error_title_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> ig_error_title_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> { <span style="color:#a5d6ff">&#34;result_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> fb_result_array, <span style="color:#a5d6ff">&#34;error_title_array&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> fb_error_title_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></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 style="color:#ff7b72">const</span> end_time <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Date(); <span style="color:#8b949e;font-style:italic">// 取得結束時間
</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> spend_time <span style="color:#ff7b72;font-weight:bold">=</span> spendTime(start_time, end_time)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 執行完畢後用 lineNotify 回報爬蟲狀況
</span></span></span><span style="display:flex;"><span>    lineNotify(spend_time, ig_result_array.length, fb_result_array.length, ig_error_title_array, fb_error_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">function</span> spendTime (start_time, end_time) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> milisecond <span style="color:#ff7b72;font-weight:bold">=</span> end_time.getTime() <span style="color:#ff7b72;font-weight:bold">-</span> start_time.getTime()  <span style="color:#8b949e;font-style:italic">//時間差的毫秒數  
</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> days <span style="color:#ff7b72;font-weight:bold">=</span> Math.floor(milisecond <span style="color:#ff7b72;font-weight:bold">/</span> (<span style="color:#a5d6ff">24</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">3600</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</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> leave1 <span style="color:#ff7b72;font-weight:bold">=</span> milisecond <span style="color:#ff7b72;font-weight:bold">%</span> (<span style="color:#a5d6ff">24</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">3600</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)<span style="color:#8b949e;font-style:italic">// 計算天數後剩余的毫秒數  
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> hours <span style="color:#ff7b72;font-weight:bold">=</span> Math.floor(leave1 <span style="color:#ff7b72;font-weight:bold">/</span> (<span style="color:#a5d6ff">3600</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</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> leave2 <span style="color:#ff7b72;font-weight:bold">=</span> leave1 <span style="color:#ff7b72;font-weight:bold">%</span> (<span style="color:#a5d6ff">3600</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)<span style="color:#8b949e;font-style:italic">// 計算小時數後剩余的毫秒數  
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> minutes <span style="color:#ff7b72;font-weight:bold">=</span> Math.floor(leave2 <span style="color:#ff7b72;font-weight:bold">/</span> (<span style="color:#a5d6ff">60</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</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> leave3 <span style="color:#ff7b72;font-weight:bold">=</span> leave2 <span style="color:#ff7b72;font-weight:bold">%</span> (<span style="color:#a5d6ff">60</span> <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)<span style="color:#8b949e;font-style:italic">// 計算分鐘數後剩余的毫秒數  
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> seconds <span style="color:#ff7b72;font-weight:bold">=</span> Math.round(leave3 <span style="color:#ff7b72;font-weight:bold">/</span> <span style="color:#a5d6ff">1000</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> time_msg <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (days <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>        time_msg <span style="color:#ff7b72;font-weight:bold">=</span> time_msg <span style="color:#ff7b72;font-weight:bold">+</span> days <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;天&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (hours <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>        time_msg <span style="color:#ff7b72;font-weight:bold">=</span> time_msg <span style="color:#ff7b72;font-weight:bold">+</span> hours <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;小時&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (minutes <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>        time_msg <span style="color:#ff7b72;font-weight:bold">=</span> time_msg <span style="color:#ff7b72;font-weight:bold">+</span> minutes <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;分&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (seconds <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>        time_msg <span style="color:#ff7b72;font-weight:bold">=</span> time_msg <span style="color:#ff7b72;font-weight:bold">+</span> seconds <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;秒&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> time_msg
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="4-在-linenotify-中整合摘要訊息">4. 在 <code>lineNotify</code> 中整合摘要訊息<a class="h-anchor" href="#4-%e5%9c%a8-linenotify-%e4%b8%ad%e6%95%b4%e5%90%88%e6%91%98%e8%a6%81%e8%a8%8a%e6%81%af" aria-label="這個段落的連結"></a></h2>
<ul>
<li>使用 <code>combineErrMsg</code> 將無法爬蟲的FB粉專、IG帳號標題整合</li>
<li>將最終要傳送的訊息組合在 <code>message</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">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">var</span> FormData <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;form-data&#39;</span>);
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;dotenv&#39;</span>).config();
</span></span><span style="display:flex;"><span>module.exports.lineNotify <span style="color:#ff7b72;font-weight:bold">=</span> lineNotify;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> combineErrMsg (error_title_array, type) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> error_msg <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> error_title <span style="color:#ff7b72">of</span> error_title_array) {
</span></span><span style="display:flex;"><span>        error_msg <span style="color:#ff7b72;font-weight:bold">=</span> error_msg <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#39;\n&#39;</span> <span style="color:#ff7b72;font-weight:bold">+</span> error_title
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (error_msg <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">&#34;&#34;</span>) {
</span></span><span style="display:flex;"><span>        error_msg <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`\n\n下列</span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">標題無法正常爬蟲:`</span> <span style="color:#ff7b72;font-weight:bold">+</span> error_msg
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> error_msg
</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> lineNotify (time, ig_total_page, fb_total_page, ig_error_title_array, fb_error_title_array) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> token <span style="color:#ff7b72;font-weight:bold">=</span> process.env.LINE_TOKEN;
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 無法爬蟲的FB粉專、IG帳號名稱整合
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fb_error_msg <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> combineErrMsg(fb_error_title_array, <span style="color:#a5d6ff">&#34;FB&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> ig_error_msg <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> combineErrMsg(ig_error_title_array, <span style="color:#a5d6ff">&#34;IG&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> error_msg <span style="color:#ff7b72;font-weight:bold">=</span> fb_error_msg <span style="color:#ff7b72;font-weight:bold">+</span> ig_error_msg
</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> message <span style="color:#ff7b72;font-weight:bold">=</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`\n\n已完成今日爬蟲排程作業`</span> <span style="color:#ff7b72;font-weight:bold">+</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`\n共費時:</span><span style="color:#a5d6ff">${</span>time<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span> <span style="color:#ff7b72;font-weight:bold">+</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`\n總計掃描FB粉專: </span><span style="color:#a5d6ff">${</span>fb_total_page<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> 、IG帳號: </span><span style="color:#a5d6ff">${</span>ig_total_page<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span> <span style="color:#ff7b72;font-weight:bold">+</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">`\nGoogle Sheet: 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 style="color:#ff7b72;font-weight:bold">+</span>
</span></span><span style="display:flex;"><span>        error_msg;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> form_data <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> FormData();
</span></span><span style="display:flex;"><span>    form_data.append(<span style="color:#a5d6ff">&#34;message&#34;</span>, message);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> headers <span style="color:#ff7b72;font-weight:bold">=</span> Object.assign({
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;Authorization&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`Bearer </span><span style="color:#a5d6ff">${</span>token<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>
</span></span><span style="display:flex;"><span>    }, form_data.getHeaders());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    axios({
</span></span><span style="display:flex;"><span>        method<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;post&#39;</span>,
</span></span><span style="display:flex;"><span>        url<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;https://notify-api.line.me/api/notify&#39;</span>,
</span></span><span style="display:flex;"><span>        data<span style="color:#ff7b72;font-weight:bold">:</span> form_data,
</span></span><span style="display:flex;"><span>        headers<span style="color:#ff7b72;font-weight:bold">:</span> headers
</span></span><span style="display:flex;"><span>    }).then(<span style="color:#ff7b72">function</span> (response) {
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// HTTP狀態碼 200 代表成功
</span></span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> response.status);
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// 觀察回傳的資料是否與 POSTMAN 測試一致
</span></span></span><span style="display:flex;"><span>        console.log(response.data);
</span></span><span style="display:flex;"><span>    }).<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#34;LINE通知發送失敗&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (error.response) { <span style="color:#8b949e;font-style:italic">// 顯示錯誤原因            
</span></span></span><span style="display:flex;"><span>            console.error(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> error.response.status);
</span></span><span style="display:flex;"><span>            console.error(error.response.data);
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>            console.error(error);
</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>
<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>為了確認我們爬蟲摘要能紀錄無法爬蟲的頁面，<code>我在 ig.json、fb.json 裡面各新增了兩個無效的粉專網址</code></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>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-29/images/fb-ig-crawler-day-29-01.webp" width="1024" height="301" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>收到 LINE 通知爬蟲摘要訊息就代表大功告成啦！！！</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-29/images/fb-ig-crawler-day-29-02.webp" width="708" height="609" alt="執行程式（圖 2）" 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/day29">這裡</a>找到喔</li>
<li>我昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day28_sample_code.zip">壓縮檔</a>，你可以在乾淨的環境完成專案的最後一步：發出LINE通知讓使用者知道這次爬蟲的情況
<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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
<li>填上LINE Notify申請的權杖(LINE_TOKEN)，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/">Day27</a></li>
</ul>
</li>
<li>在終端機下指令 <strong>npm install forever -g</strong> ，讓你在終端機的任何位置都能管控排程</li>
<li>在sh資料夾中設定執行排程的shell script</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-29/images/fb-ig-crawler-day-29-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>【Day28】為爬蟲加上通知 - 用 axios 發出 LINE 通知</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-28/</link>
      <pubDate>Tue, 13 Oct 2020 08:53:06 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-28/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>雖然眾多的套件都能實現這個目標，但還是把套件做個簡單的區分：</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/cover_hu_8b8e2b164043efaf.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>成功要選人少的路走，套件要選人多的來用</p>
</blockquote>
<h2 id="如何選擇-nodejs-中發出-request-的套件">如何選擇 Node.js 中發出 Request 的套件？<a class="h-anchor" href="#%e5%a6%82%e4%bd%95%e9%81%b8%e6%93%87-nodejs-%e4%b8%ad%e7%99%bc%e5%87%ba-request-%e7%9a%84%e5%a5%97%e4%bb%b6" aria-label="這個段落的連結"></a></h2>
<p>雖然眾多的套件都能實現這個目標，但還是把套件做個簡單的區分：</p>
<ol>
<li><strong>古老且沒再更新的套件</strong>：<a href="https://www.npmjs.com/package/https">https</a>、<a href="https://www.npmjs.com/package/xmlhttprequest">xmlhttprequest</a>、<a href="https://github.com/request/request">request</a>(這個套件在今年二月宣告廢棄)</li>
<li><strong>使用人數眾多且持續更新的套件</strong>：<a href="https://www.npmjs.com/package/superagent">superagent</a>、<a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>、<a href="https://www.npmjs.com/package/got">got</a>、<a href="https://www.npmjs.com/package/axios">axios</a></li>
</ol>
<p>原則上選擇<mark>使用人數眾多且持續更新的套件最安全</mark>，因為古老的套件通常文件不齊全且無人維護，如果某一天宣告廢棄你的專案就要大量改寫了</p>
<blockquote>
<p>本專案採用 <code>axios</code> 來實現這個需求，如果你有興趣你也能嘗試不同的套件來完成今天的功能</p>
</blockquote>
<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-axios-基礎介紹">1. axios 基礎介紹<a class="h-anchor" href="#1-axios-%e5%9f%ba%e7%a4%8e%e4%bb%8b%e7%b4%b9" aria-label="這個段落的連結"></a></h3>
<p>1.1 安裝 / GET / POST 基礎用法</p>
<p>1.2 請求配置說明</p>
<h3 class="h-nomark" id="2-用-axios-發出-line-通知">2. 用 axios 發出 LINE 通知<a class="h-anchor" href="#2-%e7%94%a8-axios-%e7%99%bc%e5%87%ba-line-%e9%80%9a%e7%9f%a5" aria-label="這個段落的連結"></a></h3>
<hr>
<h2 class="h-nomark" id="1-axios-基礎介紹-1">1. axios 基礎介紹<a class="h-anchor" href="#1-axios-%e5%9f%ba%e7%a4%8e%e4%bb%8b%e7%b4%b9-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-安裝--get--post-基礎用法">1.1 安裝 / GET / POST 基礎用法<a class="h-anchor" href="#11-%e5%ae%89%e8%a3%9d--get--post-%e5%9f%ba%e7%a4%8e%e7%94%a8%e6%b3%95" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p><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-bash" data-lang="bash"><span style="display:flex;"><span>npm install axios
</span></span><span style="display:flex;"><span>yarn add axios
</span></span></code></pre></div></li>
<li>
<p><strong>GET 基礎用法</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> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>); <span style="color:#8b949e;font-style:italic">// 引入套件
</span></span></span><span style="display:flex;"><span>axios.get(<span style="color:#a5d6ff">&#39;url/users&#39;</span>, { <span style="color:#8b949e;font-style:italic">// 填入 api 網址
</span></span></span><span style="display:flex;"><span>    params<span style="color:#ff7b72;font-weight:bold">:</span> { <span style="color:#8b949e;font-style:italic">// 傳入 params 物件
</span></span></span><span style="display:flex;"><span>        ID<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">12345</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) { 
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 處理成功後要做的事
</span></span></span><span style="display:flex;"><span>    console.log(response);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 發生意外地處理
</span></span></span><span style="display:flex;"><span>    console.log(error);
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div></li>
<li>
<p><strong>POST 基礎用法</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>axios.post(<span style="color:#a5d6ff">&#39;url/users&#39;</span>, { <span style="color:#8b949e;font-style:italic">// 要傳送的資料由後方物件帶入
</span></span></span><span style="display:flex;"><span>    firstName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Fred&#39;</span>,
</span></span><span style="display:flex;"><span>    lastName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Flintstone&#39;</span>
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) {
</span></span><span style="display:flex;"><span>    console.log(response);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>    console.log(error);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 class="h-nomark" id="12-請求配置說明">1.2 請求配置說明<a class="h-anchor" href="#12-%e8%ab%8b%e6%b1%82%e9%85%8d%e7%bd%ae%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p><strong>axios 的組成結構</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>axios.(config 物件)
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) {})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {});
</span></span></code></pre></div></li>
<li>
<p><code>config 物件</code>：發出 Request 的配置選項，其中只有 url 為必填</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></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// url 為必填
</span></span></span><span style="display:flex;"><span>    url<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;/users&#39;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// method 是發出 Request 時使用的方法
</span></span></span><span style="display:flex;"><span>    method<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;get&#39;</span>, <span style="color:#8b949e;font-style:italic">// 默認為 get
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// baseURL 如果填寫就會被添加到 url 前面，除非 url 為絕對路徑
</span></span></span><span style="display:flex;"><span>    baseURL<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;https://some-domain.com/api/&#39;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 你可以在這個地方定義自己的 headers
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 通常會拿來設定 Authorization、Content-Type
</span></span></span><span style="display:flex;"><span>    headers<span style="color:#ff7b72;font-weight:bold">:</span> {<span style="color:#a5d6ff">&#39;Content-Type&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;application/json&#39;</span>},
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// params 通常跟著 GET method 一起使用
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 這裡填寫的參數會帶入 url 後面，ex : .../user?ID=123
</span></span></span><span style="display:flex;"><span>    params<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        ID<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">123</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">// data 只適用於 POST、PUT、PATCH 這幾個 method
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 主要作為 Request 傳送的資料
</span></span></span><span style="display:flex;"><span>    data<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        firstName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Fred&#39;</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">// Request 時間超過 1000毫秒(1秒)後會被中止
</span></span></span><span style="display:flex;"><span>    timeout<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1000</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 選項: &#39;arraybuffer&#39;, &#39;document&#39;, &#39;json&#39;, &#39;text&#39;, &#39;stream&#39;
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 瀏覽器才有 &#39;blob&#39;
</span></span></span><span style="display:flex;"><span>    responseType<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;json&#39;</span>, <span style="color:#8b949e;font-style:italic">// 默認為 json
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>     <span style="color:#8b949e;font-style:italic">// 限制 http 回應時的內容大小
</span></span></span><span style="display:flex;"><span>    maxContentLength<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">2000</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><blockquote>
<p>這裡只列出常用的配置選項，如果你有興趣深入研究請參考<a href="https://www.npmjs.com/package/axios#request-config">官方文件</a></p>
</blockquote>
</li>
<li>
<p><strong>下面列出使用 then 時常用的回應參數</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>axios.get(<span style="color:#a5d6ff">&#39;/users/123&#39;</span>)
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span>(response) {
</span></span><span style="display:flex;"><span>    console.log(response.data); <span style="color:#8b949e;font-style:italic">// api 回傳的資料會在這裡
</span></span></span><span style="display:flex;"><span>    console.log(response.status); <span style="color:#8b949e;font-style:italic">// 這個是 HTTP狀態碼
</span></span></span><span style="display:flex;"><span>    console.log(response.config); <span style="color:#8b949e;font-style:italic">// 這個 Request 的 config
</span></span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 class="h-nomark" id="2-用-axios-發出-line-通知-1">2. 用 axios 發出 LINE 通知<a class="h-anchor" href="#2-%e7%94%a8-axios-%e7%99%bc%e5%87%ba-line-%e9%80%9a%e7%9f%a5-1" aria-label="這個段落的連結"></a></h2>
<p>在對 axios 有基礎了解後我們就把昨日在 POSTMAN 設定 Request 的參數搬移到專案程式吧。<code>在專案 tools 資料夾內新增 lineNotify.js</code>，今天我們要用這隻程式發出 LINE 通知</p>
<ul>
<li>因為傳送 data 的資料格式為 form-data ，所以請先下指令 <code>yarn add form-data</code> 安裝套件喔</li>
</ul>
<h3 id="toolslinenotifyjs">tools/lineNotify.js<a class="h-anchor" href="#toolslinenotifyjs" 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">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">var</span> FormData <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;form-data&#39;</span>);
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;dotenv&#39;</span>).config();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> lineNotify () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> token <span style="color:#ff7b72;font-weight:bold">=</span> process.env.LINE_TOKEN;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 使用 form-data 傳遞資料
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> form_data <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> FormData();
</span></span><span style="display:flex;"><span>    form_data.append(<span style="color:#a5d6ff">&#34;message&#34;</span>, <span style="color:#a5d6ff">&#39;測試 lineNotify&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 設定 LINE Notify 的 權杖 &amp; form-data
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> headers <span style="color:#ff7b72;font-weight:bold">=</span> Object.assign({
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;Authorization&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`Bearer </span><span style="color:#a5d6ff">${</span>token<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>
</span></span><span style="display:flex;"><span>    }, form_data.getHeaders());
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    axios({
</span></span><span style="display:flex;"><span>        method<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;post&#39;</span>,
</span></span><span style="display:flex;"><span>        url<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;https://notify-api.line.me/api/notify&#39;</span>,
</span></span><span style="display:flex;"><span>        data<span style="color:#ff7b72;font-weight:bold">:</span> form_data,
</span></span><span style="display:flex;"><span>        headers<span style="color:#ff7b72;font-weight:bold">:</span> headers
</span></span><span style="display:flex;"><span>    }).then(<span style="color:#ff7b72">function</span> (response) {
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// HTTP狀態碼 200 代表成功
</span></span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> response.status);
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// 觀察回傳的資料是否與 POSTMAN 測試一致
</span></span></span><span style="display:flex;"><span>        console.log(response.data);
</span></span><span style="display:flex;"><span>    }).<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#34;LINE通知發送失敗&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (error.response) { <span style="color:#8b949e;font-style:italic">// 顯示錯誤原因            
</span></span></span><span style="display:flex;"><span>            console.error(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> error.response.status);
</span></span><span style="display:flex;"><span>            console.error(error.response.data);
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>            console.error(error);
</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>lineNotify()
</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>node tools/lineNotify.js
</span></span></code></pre></div></li>
<li>
<p>確認印出的 HTTP狀態碼 &amp; 回傳的資料與 POSTMAN 測試結果一致</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-01.webp" width="1024" height="70" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>如果你的 LINE 收到發出的通知就代表你成功嚕！</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-02.webp" width="716" height="444" alt="執行程式（圖 2）" 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/day28">這裡</a>找到喔</li>
<li>我昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day27_sample_code.zip">壓縮檔</a>，你可以在乾淨的環境為專案加入發出LINE通知的功能
<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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
<li>填上LINE Notify申請的權杖(LINE_TOKEN)，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/">Day27</a></li>
</ul>
</li>
<li>在終端機下指令 <strong>npm install forever -g</strong> ，讓你在終端機的任何位置都能管控排程</li>
<li>在sh資料夾中設定執行排程的shell script</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>
<ol>
<li><a href="https://medium.com/dot-js/%E7%94%B1%E5%89%8D%E7%AB%AFrequest-%E7%9A%84%E5%B9%BE%E7%A8%AE%E6%96%B9%E6%B3%95-fbf8a0b4023a">由前端request 的幾種方法</a></li>
<li><a href="https://www.twilio.com/blog/2017/08/http-requests-in-node-js.html">5 Ways to Make HTTP Requests in Node.js</a></li>
<li><a href="https://ithelp.ithome.com.tw/articles/10212120">axios 基本使用 &amp; Config</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-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>【Day27】為爬蟲加上通知 - 透過 POSTMAN 了解 LINE Notify 如何使用</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-27/</link>
      <pubDate>Mon, 12 Oct 2020 08:33:53 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-27/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>在通知這個部分我選擇使用 LINE Notify 作為範例，你在了解邏輯後也可以自己改為用 Slack、Wechat、Messager... 等通訊軟體來通知喔</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/cover_hu_50d30f9d953f2c0.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%80%9a%e7%9f%a5%e5%be%88%e9%87%8d%e8%a6%81" aria-label="這個段落的連結"></a></h2>
<ol>
<li>在爬蟲的運作完全自動化的狀態下你需要<mark>透過接收通知，知道爬蟲執行完畢</mark></li>
<li>通知的內容要簡述這次爬蟲的狀況，因為實際上使用者只有在要報告或是寫文章的時候才會去 Google Sheets 看爬蟲的詳細數據，平常他只需要<mark>知道爬蟲有沒有穩定執行</mark>就好</li>
</ol>
<p>在通知這個部分我<mark>選擇使用 LINE Notify</mark> 作為範例，你在了解邏輯後也可以自己改為用 Slack、Wechat、Messager&hellip; 等通訊軟體來通知喔</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-取得-line-notify-權杖">1. 取得 LINE Notify 權杖<a class="h-anchor" href="#1-%e5%8f%96%e5%be%97-line-notify-%e6%ac%8a%e6%9d%96" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="2-使用-postman-測試-line-notify">2. 使用 POSTMAN 測試 LINE Notify<a class="h-anchor" href="#2-%e4%bd%bf%e7%94%a8-postman-%e6%b8%ac%e8%a9%a6-line-notify" aria-label="這個段落的連結"></a></h3>
<p>2.1 為什麼要先用 POSTMAN 測試 LINE Notify 呢？</p>
<p>2.2 用 POSTMAN 發出 LINE 的訊息</p>
<p>2.3 透過 POSTMAN 傳送錯誤參數來了解 HTTP狀態碼的意思</p>
<hr>
<h2 class="h-nomark" id="1-取得-line-notify-權杖-1">1. 取得 LINE Notify 權杖<a class="h-anchor" href="#1-%e5%8f%96%e5%be%97-line-notify-%e6%ac%8a%e6%9d%96-1" aria-label="這個段落的連結"></a></h2>
<ol>
<li>
<p>請以 LINE 帳號<a href="https://notify-bot.line.me/zh_TW/">登入 LINE Notify</a>，登入後選擇 <mark>個人頁面</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-01.webp" width="1024" height="425" alt="1. 取得 LINE Notify 權杖（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>為了要獲得控制 LINE Notify 發訊息的權限，我們要 <mark>發行權杖</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-02.webp" width="1024" height="729" alt="1. 取得 LINE Notify 權杖（圖 2）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在發行權杖的彈窗中，<strong>權杖名稱</strong>請<mark>填寫與功能相關的名稱</mark>，<strong>接收通知的聊天室</strong>可選擇 <mark>1對1、任意聊天室</mark> 這兩種，確認無誤後按 <code>發行</code></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-03.webp" width="1024" height="728" alt="1. 取得 LINE Notify 權杖（圖 3）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>接著我們要將這組權杖(Token) <code>複製</code> 下來</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-04.webp" width="1024" height="566" alt="1. 取得 LINE Notify 權杖（圖 4）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>關閉彈窗後能看到 <strong>已連動的服務中</strong> 有我們剛剛新增的服務</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-05.webp" width="1024" height="566" alt="1. 取得 LINE Notify 權杖（圖 5）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>同時你收到 LINE Notify 帳號啟用權杖的訊息</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-06.webp" width="1024" height="603" alt="1. 取得 LINE Notify 權杖（圖 6）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>將發行的權杖(Token)加入 <strong>.env</strong> 環境檔，在明天的教學會使用到</p>
<h3 id="envexmaple">.env.exmaple<a class="h-anchor" href="#envexmaple" aria-label="這個段落的連結"></a></h3>
<pre tabindex="0"><code>#放LINE Notify申請的權杖
LINE_TOKEN=&#39;XXXXXX&#39;
</code></pre></li>
</ol>
<hr>
<h2 class="h-nomark" id="2-使用-postman-測試-line-notify-1">2. 使用 POSTMAN 測試 LINE Notify<a class="h-anchor" href="#2-%e4%bd%bf%e7%94%a8-postman-%e6%b8%ac%e8%a9%a6-line-notify-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-為什麼要先用-postman-測試-line-notify-呢">2.1 為什麼要先用 POSTMAN 測試 LINE Notify 呢？<a class="h-anchor" href="#21-%e7%82%ba%e4%bb%80%e9%ba%bc%e8%a6%81%e5%85%88%e7%94%a8-postman-%e6%b8%ac%e8%a9%a6-line-notify-%e5%91%a2" aria-label="這個段落的連結"></a></h3>
<p>說明這個問題之前，我們先來探討為什麼 POSTMAN 會這麼被工程師推崇，在這我歸納成以下幾點：</p>
<ol>
<li>對 <strong>後端工程師</strong> 而言，可以向合作夥伴證明自己寫的 api 沒有在唬爛，真的可以 work</li>
<li>對 <strong>前端工程師、App工程師、有使用 api 需求的人</strong> 來說，能在串接 api 前先用 POSTMAN 確認這些 api 真的可以 work</li>
<li>有了這個工具作輔助，我們就能<mark>將前後端責任歸屬的很清楚</mark>。ex：如果 POSTMAN 上面可以正常 work，那前端就要去思考自己 api 哪裡沒串好</li>
<li><mark>工具操作難度極低</mark>，只需要對網路的世界有基礎的認知就能快速上手</li>
</ol>
<p>所以我們先用 POSTMAN 測試 LINE Notify 的理由是：</p>
<ol>
<li>確保 LINE Notify api <mark>執行所需的 Request 參數、Respone 的結果與說明書一致</mark>（這點非常重要，因為我看過很多 api 回傳的結構跟說明書完全不一樣）</li>
<li><mark>在一行程式都不用寫</mark>的狀態下測試所有的可能性</li>
<li>在 POSTMAN 驗證後就能專心寫程式，因為<mark>如果程式無法發出 LINE 的通知一定是自己的問題</mark></li>
</ol>
<blockquote>
<h4 id="補充">補充：<a class="h-anchor" href="#%e8%a3%9c%e5%85%85" aria-label="這個段落的連結"></a></h4>
<ul>
<li><strong>POSTMAN</strong> 在工程師的世界中素有 <mark>API測試神器</mark>之稱，如果你對 POSTMAN 一無所知的話你可以先透過<a href="https://tw.alphacamp.co/blog/postman-api-tutorial-for-beginners">這篇文章</a>了解一下他是做什麼的</li>
<li>他可以直接用 Gmail帳號登入在<a href="https://www.postman.com/">網頁使用</a>，也能<a href="https://www.postman.com/downloads/">下載桌面版 App</a>，相同帳號的操作記錄在兩邊都會同步</li>
</ul>
</blockquote>
<hr>
<h3 class="h-nomark" id="22-用-postman-發出-line-的訊息">2.2 用 POSTMAN 發出 LINE 的訊息<a class="h-anchor" href="#22-%e7%94%a8-postman-%e7%99%bc%e5%87%ba-line-%e7%9a%84%e8%a8%8a%e6%81%af" aria-label="這個段落的連結"></a></h3>
<ol>
<li>設定 Request 的參數
<ul>
<li>
<p><strong>URL</strong>：https://notify-api.line.me/api/notify</p>
</li>
<li>
<p><strong>Method</strong>：POST</p>
</li>
<li>
<p><strong>Authorization</strong>：</p>
<ul>
<li><strong>TYPE</strong>：Bearer Token</li>
<li><strong>Token</strong>：填入你剛剛在 LINE Notify 申請的權杖</li>
</ul>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-07.webp" width="912" height="419" alt="2.2 用 POSTMAN 發出 LINE 的訊息（圖 7）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p><strong>Body</strong>：</p>
<ul>
<li><strong>form-data</strong>：
<table>
  <thead>
      <tr>
          <th>key</th>
          <th>value</th>
          <th>DESCRIPTION</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>message</td>
          <td>來自寶寶的爬蟲通知</td>
          <td>純文字(必填)</td>
      </tr>
      <tr>
          <td>stickerPackageId</td>
          <td>2</td>
          <td>貼圖的PackageId</td>
      </tr>
      <tr>
          <td>stickerId</td>
          <td>144</td>
          <td>貼圖的Id</td>
      </tr>
      <tr>
          <td>imageFile</td>
          <td>icon.png</td>
          <td>電腦上的圖片檔</td>
      </tr>
  </tbody>
</table>
</li>
</ul>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-08.webp" width="896" height="420" alt="2.2 用 POSTMAN 發出 LINE 的訊息（圖 8）" loading="lazy" decoding="async"></p>
<p>DESCRIPTION 是我對參數描述，實作上可以不避填<br>如果你想深入的了解可以參考<a href="https://notify-bot.line.me/doc/en/">官方文件</a></p>
</blockquote>
</li>
</ul>
</li>
<li>完成 Request 參數填寫後按下 <code>Send</code> 發出 LINE 訊息
<ul>
<li>
<p>看到 Response 的結果，status 為 200 代表傳送成功</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-09.webp" width="836" height="189" alt="2.2 用 POSTMAN 發出 LINE 的訊息（圖 9）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在發行權杖的聊天室中，看到 POSTMAN 傳送的 <strong>訊息、貼圖、圖片</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-10.webp" width="760" height="880" alt="2.2 用 POSTMAN 發出 LINE 的訊息（圖 10）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
</ol>
<hr>
<h3 class="h-nomark" id="23-透過-postman-傳送錯誤參數來了解-http狀態碼的意思">2.3 透過 POSTMAN 傳送錯誤參數來了解 HTTP狀態碼的意思<a class="h-anchor" href="#23-%e9%80%8f%e9%81%8e-postman-%e5%82%b3%e9%80%81%e9%8c%af%e8%aa%a4%e5%8f%83%e6%95%b8%e4%be%86%e4%ba%86%e8%a7%a3-http%e7%8b%80%e6%85%8b%e7%a2%bc%e7%9a%84%e6%84%8f%e6%80%9d" aria-label="這個段落的連結"></a></h3>
<p>我們剛剛在 POSTMAN 的 Response 看到 HTTP狀態碼為 200，接下來我們故意做一些錯誤測試來看看會有什麼樣的 Response</p>
<blockquote>
<p><mark>請注意並不是每隻 api Response data 中的 status 都等於 HTTP狀態碼</mark>，只是 LINE Notify 剛好是依照這個邏輯顯示，我們可以用它來幫助我們對 HTTP狀態碼的了解</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-11.webp" width="829" height="189" alt="2.3 透過 POSTMAN 傳送錯誤參數來了解 HTTP狀態碼的意思（圖 11）" loading="lazy" decoding="async"></p>
</blockquote>
<ol>
<li><strong>不填寫 LINE Notify 的權杖</strong>
<ul>
<li>
<p>HTTP狀態碼為 401，代表需要授權才能使用</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-12.webp" width="919" height="506" alt="2.3 透過 POSTMAN 傳送錯誤參數來了解 HTTP狀態碼的意思（圖 12）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
<li><strong>在 Body 中不填寫 message 的參數</strong>
<ul>
<li>
<p>HTTP狀態碼為 400，意味伺服器收到無效的語法</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-13.webp" width="896" height="643" alt="2.3 透過 POSTMAN 傳送錯誤參數來了解 HTTP狀態碼的意思（圖 13）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
</ol>
<p>透過上面的範例回傳的資訊，我們可以了解到不同 HTTP狀態碼的意思：<mark>200 代表成功、400可能是參數錯誤、401是沒有授權</mark>，當然 HTTP狀態碼不只這些，如果你想更深入的了解可以參考<a href="https://blog.poychang.net/http-status-code/">這篇文章</a></p>
<hr>
<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://dotblogs.com.tw/TingI/2019/02/12/182723">[API連線測試]使用Postman測試LINE Notify的訊息推送</a></li>
<li><a href="https://notify-bot.line.me/doc/en/">LINE Notify API Document</a></li>
<li><a href="https://tw.alphacamp.co/blog/postman-api-tutorial-for-beginners">Postman 新手教學</a></li>
<li><a href="https://blog.poychang.net/http-status-code/">HTTP 狀態碼一覽表</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/images/fb-ig-crawler-day-27-14.webp" width="1024" height="233" alt="參考資源（圖 14）" 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>【Day26】排程 - 重開機後排程不見惹？簡單幾個步驟，從此以後完全自動</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-26/</link>
      <pubDate>Sun, 11 Oct 2020 10:00:01 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-26/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>昨天的教學讓你的排程穩定的在背景執行，但是你重開機在終端機(Terminal)輸入 forever list 的指令你會發現 forever 根本沒有 forever</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/cover_hu_cb8cc53065be1517.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>昨天的教學讓你的排程穩定的在背景執行，但是你重開機在終端機(Terminal)輸入 <strong>forever list</strong> 的指令你會發現 <mark>forever 根本沒有 forever</mark></p>
<p>為了決這個問題，今天會教你如何在<mark>電腦啟動時執行 script 讓 forever 跑起來</mark>的方法，這塊我會分成 windows 跟 mac 來說明，請大家安心服用</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-設定-mac-排程">1. 設定 mac 排程<a class="h-anchor" href="#1-%e8%a8%ad%e5%ae%9a-mac-%e6%8e%92%e7%a8%8b" aria-label="這個段落的連結"></a></h3>
<p>1.1 撰寫執行排程的 shell script ：<code>crawler_cron.sh</code></p>
<p>1.2 將 crawler_cron.sh 設定為可以被執行</p>
<p>1.3 設定 crawler_cron.sh 在使用者登入後自動執行</p>
<p>1.4 重新登入確認 forever 是否運行</p>
<h3 class="h-nomark" id="2-設定-windows-排程">2. 設定 windows 排程<a class="h-anchor" href="#2-%e8%a8%ad%e5%ae%9a-windows-%e6%8e%92%e7%a8%8b" aria-label="這個段落的連結"></a></h3>
<p>2.1 進入排程設定頁面</p>
<p>2.2 設定觸發條件</p>
<p>2.3 打開 windows 歷程紀錄</p>
<p>2.4 重開機確認 forever 是否運行</p>
<hr>
<h2 class="h-nomark" id="1-設定-mac-排程-1">1. 設定 mac 排程<a class="h-anchor" href="#1-%e8%a8%ad%e5%ae%9a-mac-%e6%8e%92%e7%a8%8b-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-撰寫執行排程的-shell-script-crawler_cronsh">1.1 撰寫執行排程的 shell script ：<code>crawler_cron.sh</code><a class="h-anchor" href="#11-%e6%92%b0%e5%af%ab%e5%9f%b7%e8%a1%8c%e6%8e%92%e7%a8%8b%e7%9a%84-shell-script-crawler_cronsh" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>在專案目錄下建立存放 shell script 的 <code>sh 資料夾</code>，並<code>新增 crawler_cron.sh</code></p>
<ul>
<li>
<p>shell script 寫法很簡單，平常你怎麼在終端機下指令就怎麼寫</p>
</li>
<li>
<p><strong>專案資料夾路徑</strong>可用 <code>pwd</code> 指令取得</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-01.webp" width="1024" height="59" alt="1.1 撰寫執行排程的 shell script ：crawler_cron.sh（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
</ul>
<h4 id="crawler_cronsh">crawler_cron.sh<a class="h-anchor" href="#crawler_cronsh" 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"># 進入專案資料夾</span>
</span></span><span style="display:flex;"><span>cd /專案資料夾路徑
</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>yarn forever
</span></span></code></pre></div></li>
<li>
<p>執行 crawler_cron.sh 確認運作邏輯跟你想的一樣</p>
<pre tabindex="0"><code>sh crawler_cron.sh
</code></pre><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-02.webp" width="1024" height="141" alt="crawler_cron.sh（圖 2）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ol>
<h3 class="h-nomark" id="12-將-crawler_cronsh-設定為可以被執行">1.2 將 crawler_cron.sh 設定為可以被執行<a class="h-anchor" href="#12-%e5%b0%87-crawler_cronsh-%e8%a8%ad%e5%ae%9a%e7%82%ba%e5%8f%af%e4%bb%a5%e8%a2%ab%e5%9f%b7%e8%a1%8c" aria-label="這個段落的連結"></a></h3>
<p>建立的檔案<mark>一開始只有讀寫功能</mark>，我們要將 crawler_cron.sh <mark>設定為可以被執行</mark>，才不會在自動化時因權限不足執行失敗</p>
<ol>
<li>
<p>我們先觀察 <strong>crawler_cron.sh</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 進入資料夾</span>
</span></span><span style="display:flex;"><span>cd sh
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 列出資料夾地下檔案權限</span>
</span></span><span style="display:flex;"><span>ll
</span></span></code></pre></div><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-03.webp" width="1024" height="163" alt="1.2 將 crawler_cron.sh 設定為可以被執行（圖 3）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>目前 crawler_cron.sh 的權限是 <code>-rw-r--r-- </code></p>
<ul>
<li>
<p>權限要把他切分成 3 段來看：owner/group/others，所以代表檔案的 owner 有讀(r)寫(w)的權限，而 group/others 只有讀(r)的權限</p>
</li>
<li>
<p>而我們要讓這個檔案可以被執行(x)需要下指令更改權限</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>chmod <span style="color:#a5d6ff">755</span> crawler_cron.sh
</span></span></code></pre></div><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-04.webp" width="1024" height="155" alt="1.2 將 crawler_cron.sh 設定為可以被執行（圖 4）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ul>
<blockquote>
<p>想要深入了解 chmod 檔案權限概念的請參考<a href="https://shian420.pixnet.net/blog/post/344938711-%5Blinux%5D-chmod-%E6%AA%94%E6%A1%88%E6%AC%8A%E9%99%90%E5%A4%A7%E7%B5%B1%E6%95%B4!">這篇文章</a></p>
</blockquote>
</li>
</ol>
<h3 class="h-nomark" id="13-設定-crawler_cronsh-在使用者登入後自動執行">1.3 設定 crawler_cron.sh 在使用者登入後自動執行<a class="h-anchor" href="#13-%e8%a8%ad%e5%ae%9a-crawler_cronsh-%e5%9c%a8%e4%bd%bf%e7%94%a8%e8%80%85%e7%99%bb%e5%85%a5%e5%be%8c%e8%87%aa%e5%8b%95%e5%9f%b7%e8%a1%8c" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>我們要先確認 crawler_cron.sh 的檔案資訊</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-05.webp" width="1024" height="587" alt="1.3 設定 crawler_cron.sh 在使用者登入後自動執行（圖 5）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>將打開檔案的應用程式設定成 <strong>iTerm</strong> (如果設定成 mac 原生的 <strong>終端機</strong> 也可以)</p>
<ul>
<li>
<p><mark>shell script 必須在終端機相關應用程式下執行</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-06.webp" width="364" height="640" alt="Imgur" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
<li>
<p>設定 crawler_cron.sh 在使用者登入後自動執行</p>
<blockquote>
<ol>
<li>
<p>進入 <strong>系統偏好設定</strong> 後，選擇 <strong>使用者與群組</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-07.webp" width="1024" height="1020" alt="1.3 設定 crawler_cron.sh 在使用者登入後自動執行（圖 7）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>選擇要觸法這個 shell script 的使用者，在 <strong>登入項目</strong> 下按 <strong>+</strong> 來新增觸發的項目</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-08.webp" width="1024" height="785" alt="1.3 設定 crawler_cron.sh 在使用者登入後自動執行（圖 8）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>請選擇剛剛設定為可以 <strong>被執行的 crawler_cron.sh</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-09.webp" width="1024" height="725" alt="1.3 設定 crawler_cron.sh 在使用者登入後自動執行（圖 9）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>加入項目後把 <strong>隱藏</strong> 打勾</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-10.webp" width="1024" height="785" alt="1.3 設定 crawler_cron.sh 在使用者登入後自動執行（圖 10）" loading="lazy" decoding="async"></p>
</li>
</ol>
</blockquote>
</li>
</ol>
<h3 class="h-nomark" id="14-重新登入確認-forever-是否運行">1.4 重新登入確認 forever 是否運行<a class="h-anchor" href="#14-%e9%87%8d%e6%96%b0%e7%99%bb%e5%85%a5%e7%a2%ba%e8%aa%8d-forever-%e6%98%af%e5%90%a6%e9%81%8b%e8%a1%8c" aria-label="這個段落的連結"></a></h3>
<p>請你重新登入後在終端機(Trerminal)輸入 <strong>forever list</strong> 的指令確認是否正常運行</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-11.webp" width="1024" height="77" alt="1.4 重新登入確認 forever 是否運行（圖 11）" loading="lazy" decoding="async"></p>
<hr>
<h2 class="h-nomark" id="2-設定-windows-排程-1">2. 設定 windows 排程<a class="h-anchor" href="#2-%e8%a8%ad%e5%ae%9a-windows-%e6%8e%92%e7%a8%8b-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-進入排程設定頁面">2.1 進入排程設定頁面<a class="h-anchor" href="#21-%e9%80%b2%e5%85%a5%e6%8e%92%e7%a8%8b%e8%a8%ad%e5%ae%9a%e9%a0%81%e9%9d%a2" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>在鍵盤同時按下 : WIN + R</p>
</li>
<li>
<p>輸入 : taskschd.msc</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-12.webp" width="507" height="255" alt="2.1 進入排程設定頁面（圖 12）" loading="lazy" decoding="async"></p>
</li>
</ul>
<h3 class="h-nomark" id="22-設定觸發條件">2.2 設定觸發條件<a class="h-anchor" href="#22-%e8%a8%ad%e5%ae%9a%e8%a7%b8%e7%99%bc%e6%a2%9d%e4%bb%b6" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>建立工作</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-13.webp" width="984" height="705" alt="2.2 設定觸發條件（圖 13）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>選擇以<mark>最高權限執行</mark>，這裡我選擇 <strong>不論使用者登入與否均執行</strong> 是因為我要在開機時就觸發，而剛開機的時候使用者是還沒登入的；這塊可以依照自己的實際需求去做調整</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-14.webp" width="788" height="639" alt="2.2 設定觸發條件（圖 14）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在這裡設定為<code>啟動時</code>會開始工作</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-15.webp" width="737" height="647" alt="2.2 設定觸發條件（圖 15）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>編輯動作的部分有幾個需要注意</p>
<ul>
<li>
<p>程式或指令碼 : 你要找到當初安裝 yarn 的路徑，及 yarn.cmd 的檔案</p>
</li>
<li>
<p>新增引數 : 填上 <strong>forever</strong> ，因為我們是用 yarn forever 啟動程式的</p>
</li>
<li>
<p>開始位置 : 把專案的路徑複製上去即可</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-16.webp" width="568" height="630" alt="2.2 設定觸發條件（圖 16）" loading="lazy" decoding="async"></p>
</li>
</ul>
</li>
</ol>
<h3 class="h-nomark" id="23-打開-windows-歷程紀錄">2.3 打開 windows 歷程紀錄<a class="h-anchor" href="#23-%e6%89%93%e9%96%8b-windows-%e6%ad%b7%e7%a8%8b%e7%b4%80%e9%8c%84" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>windows 需打開歷程紀錄否則不會執行該排程</p>
<blockquote>
<ol>
<li>
<p>我們看到建立的工作在歷程紀錄旁邊顯示 <strong>(已停用)</strong>，我們需要把他開啟</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-17.webp" width="1024" height="332" alt="2.3 打開 windows 歷程紀錄（圖 17）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在左上角 <strong>工作排程器(本機)</strong> 按右鍵，選擇 <strong>啟用所有工作歷程紀錄</strong></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-18.webp" width="1024" height="334" alt="2.3 打開 windows 歷程紀錄（圖 18）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>確認你要執行工作的歷程紀錄旁邊 <strong>沒有(已停用)</strong>，就代表設定完成嚕</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-19.webp" width="1024" height="276" alt="2.3 打開 windows 歷程紀錄（圖 19）" loading="lazy" decoding="async"></p>
</li>
</ol>
</blockquote>
</li>
</ul>
<h3 class="h-nomark" id="24-重開機確認-forever-是否運行">2.4 重開機確認 forever 是否運行<a class="h-anchor" href="#24-%e9%87%8d%e9%96%8b%e6%a9%9f%e7%a2%ba%e8%aa%8d-forever-%e6%98%af%e5%90%a6%e9%81%8b%e8%a1%8c" aria-label="這個段落的連結"></a></h3>
<p>請注意因為使用的是最高權限執行 <strong>yarn forever</strong>，所以你在 cmd 裡面用 <strong>forever list</strong> 是看不到的</p>
<ol>
<li>
<p>必須要用<mark>系統管理員身分執行 cmd</mark></p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-20.webp" width="981" height="800" alt="2.4 重開機確認 forever 是否運行（圖 20）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>在這個權限下輸入指令 <strong>forever list</strong> 才能確認是否正常運行</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-21.webp" width="1024" height="164" alt="2.4 重開機確認 forever 是否運行（圖 21）" loading="lazy" decoding="async"></p>
</li>
</ol>
<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/day26">這裡</a>找到喔</li>
<li>我昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day25_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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
</ul>
</li>
<li>在終端機下指令 <strong>npm install forever -g</strong> ，讓你在終端機的任何位置都能管控排程</li>
<li>在sh資料夾中設定執行排程的shell script</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://shian420.pixnet.net/blog/post/344938711-%5Blinux%5D-chmod-%E6%AA%94%E6%A1%88%E6%AC%8A%E9%99%90%E5%A4%A7%E7%B5%B1%E6%95%B4!">[LINUX] chmod 檔案權限大統整</a></li>
<li><a href="https://cjy998.pixnet.net/blog/post/63190869-%E5%B7%A5%E4%BD%9C%E6%8E%92%E7%A8%8B%E5%99%A8-%E5%95%9F%E7%94%A8%E6%AD%B7%E7%A8%8B%E8%A8%98%E9%8C%84-%E5%8E%9F%E9%A1%AF%E7%A4%BA%28%E5%B7%B2%E5%81%9C%E7%94%A8%29--win">工作排程器 啟用歷程記錄 原顯示(已停用) windows 2012</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-26/images/fb-ig-crawler-day-26-22.webp" width="1024" height="233" alt="參考資源（圖 22）" 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>【Day25】排程 - 用forever套件來控管排程，背景執行才是王道！</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-25/</link>
      <pubDate>Sat, 10 Oct 2020 10:44:44 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-25/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>我想很多人跟我一樣不小心順手把終端機(Terminal)給關了 (或是很乾脆地把 VScode 關了)，但視窗關閉後排程其實還在背景持續運行，下面提供一個快速的解決方案：</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/cover_hu_78c86479e941f53f.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>如果一個套件無法解決問題，那就用兩個吧！</p>
</blockquote>
<h2 id="思考一下昨天的排程有什麼缺點">思考一下昨天的排程有什麼缺點<a class="h-anchor" href="#%e6%80%9d%e8%80%83%e4%b8%80%e4%b8%8b%e6%98%a8%e5%a4%a9%e7%9a%84%e6%8e%92%e7%a8%8b%e6%9c%89%e4%bb%80%e9%ba%bc%e7%bc%ba%e9%bb%9e" aria-label="這個段落的連結"></a></h2>
<p><strong>昨天完成的排程其實有幾個缺點：</strong></p>
<ol>
<li>在終端機(Terminal)執行排程後你<mark>必須保持開啟</mark>的狀態你才知道他有沒有正確執行</li>
<li>如果不小心把執行排程的終端機(Terminal)關掉你會很困擾，因為你關掉的是終端機不是排程，<mark>你想關掉排程還要另外下幾個指令</mark></li>
<li>管理困難，<mark>啟動、停止、重啟你都要在專案資料夾</mark>下處理</li>
<li>如果運行過程中程式<mark>意外關閉需要手動重開</mark></li>
<li><mark>在排程運行中你修改爬蟲json裡面的粉專列表是無效的</mark>，實驗看看就會發現觸發爬蟲時跑的是舊版的粉專列表</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>
<ol>
<li>砍掉執行中的排程</li>
<li>使用套件 <code>forever</code> 來管理排程</li>
<li>在 package.json 中加入驅動 forever 的 scripts</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-砍掉執行中的排程">1. 砍掉執行中的排程<a class="h-anchor" href="#1-%e7%a0%8d%e6%8e%89%e5%9f%b7%e8%a1%8c%e4%b8%ad%e7%9a%84%e6%8e%92%e7%a8%8b" aria-label="這個段落的連結"></a></h2>
<p>我想很多人跟我一樣不小心順手把終端機(Terminal)給關了 (或是很乾脆地把 VScode 關了)，但<mark>視窗關閉後排程其實還在背景持續運行</mark>，下面提供一個快速的解決方案：</p>
<pre tabindex="0"><code>ps aux | grep [搜尋關鍵字]
kill [程式的PID]
</code></pre><h3 id="ps-process-status---顯示進程狀態的指令">ps (process status) - 顯示進程狀態的指令<a class="h-anchor" href="#ps-process-status---%e9%a1%af%e7%a4%ba%e9%80%b2%e7%a8%8b%e7%8b%80%e6%85%8b%e7%9a%84%e6%8c%87%e4%bb%a4" aria-label="這個段落的連結"></a></h3>
<ul>
<li>參數說明
<ul>
<li>a : 列出所有使用者與 terminal 無關的所有 process</li>
<li>u : 以使用者名稱顯示的格式</li>
<li>x : 列出與 terminal 有關的所有 process (通常與 a 搭配使用)</li>
</ul>
</li>
<li>顯示欄位 : USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND</li>
</ul>
<h3 id="kill---刪除執行中的-process">kill - 刪除執行中的 process<a class="h-anchor" href="#kill---%e5%88%aa%e9%99%a4%e5%9f%b7%e8%a1%8c%e4%b8%ad%e7%9a%84-process" aria-label="這個段落的連結"></a></h3>
<p>我們需要找到執行程式的 process PID 才能將其刪除 <code>(PID 就在 ps 進程狀態的第二欄)</code></p>
<h3 id="實際操作範例">實際操作範例<a class="h-anchor" href="#%e5%af%a6%e9%9a%9b%e6%93%8d%e4%bd%9c%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>找出正在運行的排程</p>
<pre tabindex="0"><code>ps aux | grep cron.js
</code></pre><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-01.webp" width="1024" height="112" alt="實際操作範例（圖 1）" fetchpriority="high" decoding="async"></p>
</blockquote>
</li>
<li>
<p>你可以看到執行排程的 PID 是 23888，把他複製起來用 kill 指令刪除</p>
<pre tabindex="0"><code>kill 23888
</code></pre><p>刪除後查詢進程，確認是否刪除成功</p>
<pre tabindex="0"><code>ps aux | grep cron.js
</code></pre><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-02.webp" width="1024" height="87" alt="實際操作範例（圖 2）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ol>
<hr>
<h2 class="h-nomark" id="2-使用套件-forever-來管理排程">2. 使用套件 <code>forever</code> 來管理排程<a class="h-anchor" href="#2-%e4%bd%bf%e7%94%a8%e5%a5%97%e4%bb%b6-forever-%e4%be%86%e7%ae%a1%e7%90%86%e6%8e%92%e7%a8%8b" aria-label="這個段落的連結"></a></h2>
<p>單純使用 cron 套件會讓你遇到開頭所說的各種問題，所以遇到過這個問題的高手們也寫了一個<code>讓 Node.js 程式運行永不停止的套件：forever</code>，這個套件操作極其簡單，不僅能讓排程在背景穩定執行，還能在終端機的任何位置做管理</p>
<ul>
<li>
<p>首先要請你<mark>全域安裝</mark>這個套件，這樣才能在任何一個位置執行它</p>
<pre tabindex="0"><code>[sudo] npm install forever -g
</code></pre></li>
<li>
<p>基礎指令（以操作專案的 cron.js 為範例）</p>
<ul>
<li>
<p><strong>啟動程式</strong></p>
<pre tabindex="0"><code>forever --minUptime=1000 --spinSleepTime=1000 start tools/cron.js
</code></pre><blockquote>
<p>如果你沒加上 <code>--minUptime=1000 --spinSleepTime=1000</code> 的參數 會有警告如下圖</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-03.webp" width="1024" height="89" alt="2. 使用套件 forever 來管理排程（圖 3）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p><strong>監聽檔案更動並自動重啟</strong></p>
<pre tabindex="0"><code>forever --minUptime=1000 --spinSleepTime=1000 -w start tools/cron.js
</code></pre><blockquote>
<ul>
<li>
<p>這個指令可以<mark>監控檔案的變更</mark>，解決過去爬蟲json資料變更但是執行時跑舊資料的問題</p>
</li>
<li>
<p>要在專案資料夾下<mark>新增 .foreverignore 的檔案</mark>，被列入的檔案/資料夾在偵測變更時會被忽略</p>
<h3 id="foreverignore">.foreverignore<a class="h-anchor" href="#foreverignore" aria-label="這個段落的連結"></a></h3>
<pre tabindex="0"><code>node_modules
.env
chromedriver.exe
debug.log
</code></pre></li>
</ul>
</blockquote>
</li>
<li>
<p><strong>顯示所有運行的狀態</strong></p>
<pre tabindex="0"><code>forever list
</code></pre><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-04.webp" width="1024" height="85" alt=".foreverignore（圖 4）" loading="lazy" decoding="async"></p>
<ul>
<li>可以在 <code>logfile</code> 裡面看到排程執行的狀況</li>
</ul>
</blockquote>
</li>
<li>
<p><strong>關閉所有運行的程式</strong></p>
<pre tabindex="0"><code>forever stopall
</code></pre><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-05.webp" width="1024" height="75" alt=".foreverignore（圖 5）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p><strong>重新啟動程式</strong></p>
<pre tabindex="0"><code>forever restart tools/cron.js
</code></pre><blockquote>
<ul>
<li>請注意如果你 <mark>修改了 .env 裡面的資料，則必須要重啟才會生效</mark></li>
</ul>
</blockquote>
</li>
</ul>
</li>
<li>
<p>執行 forever 時你可能會遇到警告如下</p>
<pre tabindex="0"><code>(node:30512) Warning: Accessing non-existent property &#39;padLevels&#39; of module exports inside circular dependency
</code></pre><ul>
<li>這個警告是因為 forever 有相依套件在 node v14 之後不再支援，<mark>但這些警告不影響套件運行</mark></li>
<li>Node.js 的版本更新的非常快速，許多套件都可能在更新後不支援，<strong>所以更新版本後請切記要運行一次確保正常</strong></li>
<li>目前 <code>Node.js 的 LTS 版本為 12.19.0，如果你使用這個穩定的版本就能避免這個問題</code></li>
</ul>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="3-在-packagejson-中加入驅動-forever-的-scripts">3. 在 package.json 中加入驅動 forever 的 scripts<a class="h-anchor" href="#3-%e5%9c%a8-packagejson-%e4%b8%ad%e5%8a%a0%e5%85%a5%e9%a9%85%e5%8b%95-forever-%e7%9a%84-scripts" aria-label="這個段落的連結"></a></h2>
<p>將昨天 cron 的 scripts 改為 forever 的版本</p>
<ul>
<li>為了避免重複啟動，這個 scipts 的執行邏輯為：<mark>先暫停所有運行的 forever 程式，然後再啟動程式</mark></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-json" data-lang="json"><span style="display:flex;"><span><span style="color:#a5d6ff">&#34;forever&#34;</span><span style="color:#f85149">:</span><span style="color:#a5d6ff">&#34;forever stopall &amp;&amp; forever --minUptime=1000 --spinSleepTime=1000 -w start tools/cron.js&#34;</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 forever
</span></span></code></pre></div><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-06.webp" width="1024" height="145" alt="執行程式（圖 6）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>然後再執行指令 <strong>forever list</strong> 確認排程正在背景運行</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-07.webp" width="1024" height="67" alt="執行程式（圖 7）" 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/day25">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day24_sample_code.zip">壓縮檔</a>，你可以在乾淨的環境嘗試用forever來控管排程吧
<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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
</ul>
</li>
<li>在終端機下指令 <strong>npm install forever -g</strong> ，讓你在終端機的任何位置都能管控排程</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://medium.com/mess-up/%E5%A6%82%E6%9E%9Cctrl-c%E6%B2%92%E6%B3%95%E7%B5%82%E6%AD%A2%E6%80%8E%E9%BA%BC%E8%BE%A6-5e720fd66e32">如果ctrl + c沒法終止怎麼辦?</a></li>
<li><a href="http://puremonkey2010.blogspot.com/2011/02/linux-linux-ps-l-ps-aux-ps-axjf.html">Linux 程序管理 ( ps -l / ps aux / ps axjf )</a></li>
<li><a href="https://andy6804tw.github.io/2018/01/17/api-forever/">[Node.js打造API] 使用forever運行API永遠不停止</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-25/images/fb-ig-crawler-day-25-08.webp" width="1024" height="233" alt="參考資源（圖 8）" 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>【Day24】排程 - Cron套件超詳解，讓爬蟲自己動起來</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-24/</link>
      <pubDate>Fri, 09 Oct 2020 10:33:01 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-24/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>在客戶要求這個功能前其實我就把他的雛型完成了，因為我是一個懶人，同時也不是一個記性很好的人，如果每天都要在某個時間打開專案執行 yarn start 然後等數據跑完，這對我來說實在是一件很容易忘記且沒有效率的事情，我認為電腦可以做到事情那就交給電腦去做，所以接下來幾天就來跟大家分…</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/images/cover_hu_b6a0d4828cd4dd66.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%e8%a6%81%e5%ad%b8%e7%bf%92%e6%8e%92%e7%a8%8b" aria-label="這個段落的連結"></a></h2>
<p>在客戶要求這個功能前其實我就把他的雛型完成了，<mark>因為我是一個懶人，同時也不是一個記性很好的人</mark>，如果每天都要在某個時間打開專案執行 <strong>yarn start</strong> 然後等數據跑完，這對我來說實在是一件很容易忘記且沒有效率的事情，我認為<mark>電腦可以做到事情那就交給電腦去做</mark>，所以接下來幾天就來跟大家分享我是如何<del>偷懶</del>使用排程的</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-排程套件---cron">1. 排程套件 - Cron<a class="h-anchor" href="#1-%e6%8e%92%e7%a8%8b%e5%a5%97%e4%bb%b6---cron" aria-label="這個段落的連結"></a></h3>
<p>1.1 測試官方程式</p>
<p>1.2 分析 api 架構</p>
<p>1.3 了解 cronTime 語法</p>
<p>1.4 範例</p>
<h3 class="h-nomark" id="2-把排程加入爬蟲">2. 把排程加入爬蟲<a class="h-anchor" href="#2-%e6%8a%8a%e6%8e%92%e7%a8%8b%e5%8a%a0%e5%85%a5%e7%88%ac%e8%9f%b2" aria-label="這個段落的連結"></a></h3>
<p>2.1 將主程式的函式<code>crawler</code>設定成外部函式，供排程呼叫</p>
<p>2.2 建立專門管控排程的程式：<code>cron.js</code></p>
<p>2.3 調整<code>package.json</code>，建立專門跑排程的 scripts</p>
<hr>
<h2 class="h-nomark" id="1-排程套件---cron-1">1. 排程套件 - Cron<a class="h-anchor" href="#1-%e6%8e%92%e7%a8%8b%e5%a5%97%e4%bb%b6---cron-1" aria-label="這個段落的連結"></a></h2>
<hr>
<p>這裡我們需要安裝一個套件 <strong>cron</strong> ，他可以在你指定的時間執行你希望他幫你做的事情，這是他的<a href="https://www.npmjs.com/package/cron">官方文檔</a>，下面我會詳細介紹他的功能(因為我自己也很常使用XD)</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 cron
</span></span></code></pre></div><h3 class="h-nomark" id="11-測試官方程式">1.1 測試官方程式<a class="h-anchor" href="#11-%e6%b8%ac%e8%a9%a6%e5%ae%98%e6%96%b9%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>先在 tools 的資料夾裡面新增一個 cron.js 的檔案</p>
</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:#ff7b72">var</span> CronJob <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;cron&#39;</span>).CronJob;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">var</span> job <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> CronJob(<span style="color:#a5d6ff">&#39;* * * * * *&#39;</span>, <span style="color:#ff7b72">function</span>() {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;You will see this message every second&#39;</span>);
</span></span><span style="display:flex;"><span>}, <span style="color:#79c0ff">null</span>, <span style="color:#79c0ff">true</span>, <span style="color:#a5d6ff">&#39;America/Los_Angeles&#39;</span>);
</span></span><span style="display:flex;"><span>job.start();
</span></span></code></pre></div></li>
<li>
<p>在專案資料夾執行 <strong>node tools/cron.js</strong></p>
<ul>
<li>如果終端機(Terminal)每秒鐘都印出 <strong>You will see this message every second</strong> 就代表套件運行成功</li>
</ul>
</li>
</ol>
<hr>
<h3 class="h-nomark" id="12-分析-api-架構">1.2 分析 api 架構<a class="h-anchor" href="#12-%e5%88%86%e6%9e%90-api-%e6%9e%b6%e6%a7%8b" 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>constructor(cronTime, onTick, onComplete, start, timezone, context, runOnInit, unrefTimeout)
</span></span></code></pre></div><ul>
<li>cronTime [必填] 設定定時任務時間</li>
<li>onTick [必填] 定時任務要執行的函式</li>
<li>onComplete [選填] 完成定時任務後要執行的函式</li>
<li>Start [選填] 是否自動啟動job，默認為false</li>
<li>timeZone [選填] - 指定執行的時區，莫認為當前時區，關於時區代碼可參考<a href="https://www.zeitverschiebung.net/en/">網站</a></li>
</ul>
<hr>
<h3 class="h-nomark" id="13-了解-crontime-語法">1.3 了解 cronTime 語法<a class="h-anchor" href="#13-%e4%ba%86%e8%a7%a3-crontime-%e8%aa%9e%e6%b3%95" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>總共分為六個區塊：秒 分鐘 小時 天 月份 星期幾</p>
<pre tabindex="0"><code>秒：0-59
分鐘：0-59
小時：0-23
天：1-31
月份：0-11（1~12月，特別注意月份是從0開始）
星期幾：0-6（星期日~星期六，Sun~Sat）
</code></pre></li>
<li>
<p>語法範例</p>
<ul>
<li><code>*</code> 全部
<ul>
<li>
<p>每秒都執行(官方範例)</p>
<pre tabindex="0"><code>* * * * * *
</code></pre></li>
<li>
<p>每分鐘的第 10 秒執行</p>
<pre tabindex="0"><code>10 * * * * *
</code></pre></li>
<li>
<p>每天晚上 10點30分10秒時執行</p>
<pre tabindex="0"><code>10 30 22 * * *
</code></pre></li>
</ul>
</li>
<li><code>-</code> 時間區間
<ul>
<li>
<p>每天早上 9點到12點的整點執行</p>
<pre tabindex="0"><code>0 0 9-12 * * *
</code></pre></li>
</ul>
</li>
<li><code>,</code> 分隔符號，可以輸入多個數值
<ul>
<li>
<p>每分鐘的第 5,20,30 秒執行</p>
<pre tabindex="0"><code>5,20,30 * * * * *
</code></pre></li>
</ul>
</li>
<li><code>/</code> 間隔多少時間執行
<ul>
<li>
<p>每 3 分鐘執行一次</p>
<pre tabindex="0"><code>* */3 * * * *
</code></pre></li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="14-範例">1.4 範例<a class="h-anchor" href="#14-%e7%af%84%e4%be%8b" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>每分鐘的第 10 秒執行</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">var</span> CronJob <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;cron&#39;</span>).CronJob;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> CronJob(<span style="color:#a5d6ff">&#39;10 * * * * *&#39;</span>, <span style="color:#ff7b72">function</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>    console.log(datetime);
</span></span><span style="display:flex;"><span>}, <span style="color:#79c0ff">null</span>, <span style="color:#79c0ff">true</span>);
</span></span></code></pre></div><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/images/fb-ig-crawler-day-24-01.webp" width="1024" height="125" alt="1.4 範例（圖 1）" fetchpriority="high" decoding="async"></p>
</blockquote>
</li>
<li>
<p>每分鐘的第 5,20,30 秒執行</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">var</span> CronJob <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;cron&#39;</span>).CronJob;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> CronJob(<span style="color:#a5d6ff">&#39;5,20,30 * * * * *&#39;</span>, <span style="color:#ff7b72">function</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>    console.log(datetime);
</span></span><span style="display:flex;"><span>}, <span style="color:#79c0ff">null</span>, <span style="color:#79c0ff">true</span>);
</span></span></code></pre></div><blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/images/fb-ig-crawler-day-24-02.webp" width="1024" height="197" alt="1.4 範例（圖 2）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-把排程加入爬蟲-1">2. 把排程加入爬蟲<a class="h-anchor" href="#2-%e6%8a%8a%e6%8e%92%e7%a8%8b%e5%8a%a0%e5%85%a5%e7%88%ac%e8%9f%b2-1" aria-label="這個段落的連結"></a></h2>
<p>我們要調整三個檔案讓排程可以順利觸發爬蟲</p>
<h3 class="h-nomark" id="21-將主程式的函式crawler設定成外部函式供排程呼叫">2.1 將主程式的函式<code>crawler</code>設定成外部函式，供排程呼叫<a class="h-anchor" href="#21-%e5%b0%87%e4%b8%bb%e7%a8%8b%e5%bc%8f%e7%9a%84%e5%87%bd%e5%bc%8fcrawler%e8%a8%ad%e5%ae%9a%e6%88%90%e5%a4%96%e9%83%a8%e5%87%bd%e5%bc%8f%e4%be%9b%e6%8e%92%e7%a8%8b%e5%91%bc%e5%8f%ab" aria-label="這個段落的連結"></a></h3>
<ul>
<li>因避免被引用時自動觸發 <strong>crawler</strong> 函式，故將下方觸發用的 crawler() 移除</li>
</ul>
<h4 id="indexjs">index.js<a class="h-anchor" href="#indexjs" 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-javascript" data-lang="javascript"><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;dotenv&#39;</span>).config(); 
</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>exports.crawler <span style="color:#ff7b72;font-weight:bold">=</span> crawler; <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:#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:#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:#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-建立專門管控排程的程式cronjs">2.2 建立專門管控排程的程式：<code>cron.js</code><a class="h-anchor" href="#22-%e5%bb%ba%e7%ab%8b%e5%b0%88%e9%96%80%e7%ae%a1%e6%8e%a7%e6%8e%92%e7%a8%8b%e7%9a%84%e7%a8%8b%e5%bc%8fcronjs" aria-label="這個段落的連結"></a></h3>
<ul>
<li>我將 CronJob 的參數都帶出來，這樣在<mark>日後維護時比較了解每個參數的意義</mark></li>
<li>請編輯 .env檔填上排程觸發爬蟲的時間點(CRONJOB_TIME)</li>
</ul>
<h4 id="cronjs">cron.js<a class="h-anchor" href="#cronjs" 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-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> CronJob <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;cron&#39;</span>).CronJob;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { crawler } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;../index.js&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> CronJob({
</span></span><span style="display:flex;"><span>    cronTime<span style="color:#ff7b72;font-weight:bold">:</span> process.env.CRONJOB_TIME,<span style="color:#8b949e;font-style:italic">//請編輯.env檔填上自己的爬蟲時段喔
</span></span></span><span style="display:flex;"><span>    onTick<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">`開始執行爬蟲排程作業： </span><span style="color:#a5d6ff">${</span><span style="color:#ff7b72">new</span> Date()<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">await</span> crawler()
</span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#39;排程作業執行完畢！&#39;</span>);
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    start<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span>,
</span></span><span style="display:flex;"><span>    timeZone<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Asia/Taipei&#39;</span>
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="23-調整-packagejson建立專門跑排程的-scripts">2.3 調整 <code>package.json</code>，建立專門跑排程的 scripts<a class="h-anchor" href="#23-%e8%aa%bf%e6%95%b4-packagejson%e5%bb%ba%e7%ab%8b%e5%b0%88%e9%96%80%e8%b7%91%e6%8e%92%e7%a8%8b%e7%9a%84-scripts" aria-label="這個段落的連結"></a></h3>
<p>針對 node 如何執行 js 指定 function 的指令寫法可以參考這篇<a href="https://stackoverflow.com/questions/30782693/run-function-in-script-from-command-line-node-js">文章</a></p>
<ul>
<li><strong>start</strong> : mac 執行單次爬蟲時使用的</li>
<li><strong>win_start</strong> : windows 執行單次爬蟲時使用的(需要將單引號與雙引號反過來，因為 windows 只會將單引號內的資訊印除來而不會去執行)</li>
<li><strong>cron</strong> : mac、windows 排程執行爬蟲時使用的</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></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;name&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;crawler&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;version&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;0.0.1&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;description&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;FB &amp; IG 爬蟲30天鐵人文章&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;author&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;dean lin&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;dependencies&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;cron&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;^1.8.2&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;dateformat&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;^3.0.3&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;dotenv&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;^8.2.0&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;googleapis&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;59&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;selenium-webdriver&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;^4.0.0-alpha.7&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;devDependencies&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {},
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;scripts&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;start&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;node  -e &#39;require(\&#34;./index\&#34;).crawler()&#39;&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;win_start&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;node  -e \&#34;require(&#39;./index&#39;).crawler()\&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#34;cron&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">&#34;node tools/cron.js&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;main&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;index.js&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#34;license&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;MIT&#34;</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>建議將 .env 檔中 CRONJOB_TIME 的時間調整成 3 分鐘後的時間比較好測試</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>確認爬蟲程式是否依照你設定的時間開始執行</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/images/fb-ig-crawler-day-24-03.webp" width="1024" height="634" alt="執行程式（圖 3）" loading="lazy" decoding="async"></p>
</li>
<li>
<p>因為<mark>排程的程式會一直執行，所以你不會看到以往 Done in xxs 的訊息</mark>，如果想要中斷終端機(Terminal)執行的程式，可以用下面按鍵組合:</p>
<ul>
<li>Windows: Ctrl + c</li>
<li>Mac: control + c</li>
</ul>
</li>
</ol>
<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/day24">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day23_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>
<li>填上爬蟲執行時間(CRONJOB_TIME)</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://stackoverflow.com/questions/30782693/run-function-in-script-from-command-line-node-js">Run function in script from command line (Node JS)</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-24/images/fb-ig-crawler-day-24-04.webp" width="1024" height="233" alt="參考資源（圖 4）" 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>【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>【Day16】不藏私，加上5行程式就能優化爬蟲的小技巧 ＆ 學會爬蟲，之後呢？</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-16/</link>
      <pubDate>Thu, 01 Oct 2020 09:42:52 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-16/</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-16/images/cover_hu_1e98c29409e2512e.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>
<ol>
<li>如果你跟著這幾天的教學走還是遇到一些莫名其妙的問題，希望在今天的文章有得到你想要的解答</li>
<li>這幾天所教的技巧足以讓你爬下絕大多數的網頁資訊了，<mark>學習爬蟲只是一個過程，我們的目標是希望能靠這些數據給我們帶來便利</mark>，所以今天也分享一些思路給大家</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-優化爬蟲">1. 優化爬蟲<a class="h-anchor" href="#1-%e5%84%aa%e5%8c%96%e7%88%ac%e8%9f%b2" aria-label="這個段落的連結"></a></h3>
<p>1.1 優化分成哪些面向?</p>
<p>1.2 優化執行效率及穩定度</p>
<p>1.3 背景執行並減少記憶體的消耗</p>
<h3 class="h-nomark" id="2-學會爬蟲之後呢">2. 學會爬蟲，之後呢？<a class="h-anchor" href="#2-%e5%ad%b8%e6%9c%83%e7%88%ac%e8%9f%b2%e4%b9%8b%e5%be%8c%e5%91%a2" aria-label="這個段落的連結"></a></h3>
<hr>
<h2 class="h-nomark" id="1-優化爬蟲-1">1. 優化爬蟲<a class="h-anchor" href="#1-%e5%84%aa%e5%8c%96%e7%88%ac%e8%9f%b2-1" aria-label="這個段落的連結"></a></h2>
<p>昨天我們已經完成爬蟲所需要的功能了，下一步要思考的是如何優化</p>
<h3 class="h-nomark" id="11-優化分成哪些面向">1.1 優化分成哪些面向?<a class="h-anchor" href="#11-%e5%84%aa%e5%8c%96%e5%88%86%e6%88%90%e5%93%aa%e4%ba%9b%e9%9d%a2%e5%90%91" aria-label="這個段落的連結"></a></h3>
<p>前面<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/">【Day13】refactor-重構程式碼，讓合作夥伴對你比讚</a>在討論如何讓你在開發上更有效率，原則上不影響輸出的結果；而今天討論的優化是一個多面向的問題，我把他區分成<mark>顯性及隱性</mark>兩種：</p>
<ul>
<li><strong>顯性</strong>：使用者<mark>肉眼可見</mark>
<ul>
<li>改善使用者操作流程、介面</li>
<li>程式執行過程的畫面優化</li>
</ul>
</li>
<li><strong>隱性</strong>：使用者覺得自己的<mark>電腦變順</mark>了(不過他們通常不會聯想到是程式優化了)
<ul>
<li>更好地執行效率</li>
<li>使用更少的記憶體</li>
<li>執行的穩定性</li>
</ul>
</li>
</ul>
<p>優化是一條沒有盡頭的路，上面我只是提了幾個常見要注意的點而已，接下來我們就來實際優化現在的爬蟲吧～</p>
<hr>
<h3 class="h-nomark" id="12-優化執行效率及穩定度">1.2 優化執行效率及穩定度<a class="h-anchor" href="#12-%e5%84%aa%e5%8c%96%e5%9f%b7%e8%a1%8c%e6%95%88%e7%8e%87%e5%8f%8a%e7%a9%a9%e5%ae%9a%e5%ba%a6" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p><strong>遇到的問題</strong></p>
<ol>
<li>在 driver.wait 加上等待時間後偶爾還是會出現登入失敗或是抓不到追蹤人數的問題</li>
<li>在爬蟲的時候會影響到我看 youtube 的流暢程度</li>
</ol>
</li>
<li>
<p><strong>判斷造成的原因</strong></p>
<ol>
<li>本身的<mark>網路速度太慢</mark>，造成網頁過了 driver.wait 的等待時間後依舊還沒完成載入</li>
<li>FB、IG 粉專都是<mark>充滿圖片的頁面</mark>，載入這些資源時消耗太多流量</li>
</ol>
</li>
<li>
<p><strong>優化的方式</strong><br>因為本專案<mark>爬蟲所分析的內容都跟圖片無關，所以只要不載入這些資源</mark>就有以下好處：</p>
<ol>
<li>減少瀏覽器使用流量</li>
<li>減少瀏覽器載入時間</li>
</ol>
<p>想要知道如何不載入圖片，你可以在 Google 搜尋 <strong>selenium chrome參數</strong> ，有很多熱心的大神已經幫你整理好常用的參數了</p>
<ul>
<li>
<p>打開 <strong>initDrive.js</strong> 在 chrome 的 option 加入<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>options.addArguments(<span style="color:#a5d6ff">&#39;blink-settings=imagesEnabled=false&#39;</span>)<span style="color:#8b949e;font-style:italic">//不加載圖片提高效率
</span></span></span></code></pre></div><p>加上這段程式後，在專案資料夾的終端機(Terminal)執行指令 <strong>yarn start</strong> ，便可以很明顯的感受到<mark>網頁載入速度變快很多，並且所有圖片都沒有載入</mark>(下圖)</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-16/images/fb-ig-crawler-day-16-01.webp" width="1024" height="671" alt="1.2 優化執行效率及穩定度（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="13-背景執行並減少記憶體的消耗">1.3 背景執行並減少記憶體的消耗<a class="h-anchor" href="#13-%e8%83%8c%e6%99%af%e5%9f%b7%e8%a1%8c%e4%b8%a6%e6%b8%9b%e5%b0%91%e8%a8%98%e6%86%b6%e9%ab%94%e7%9a%84%e6%b6%88%e8%80%97" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>遇到的問題</strong>
<ol>
<li>有時爬蟲執行到一半手賤點一下爬蟲瀏覽器導致程式拋出例外</li>
<li>爬蟲執行時候的畫面一直閃好煩</li>
<li>在記憶體 8G 的電腦執行爬蟲時覺得電腦變得很頓</li>
</ol>
</li>
<li><strong>判斷造成的原因</strong>
<ol>
<li>人有時控制不住自己的手</li>
<li>執行爬蟲時<mark>干擾到使用者的日常工作</mark></li>
<li><mark>chrome 吃太多記憶體</mark></li>
</ol>
</li>
<li><strong>優化的方式</strong><br>在爬蟲每個步驟及功能都穩定後，其實不需跳出瀏覽器視窗來觀察他的動作，<mark>讓瀏覽器在背景執行不但不會干擾使用者工作更能減少記憶體的浪費</mark>
<ul>
<li>
<p>打開 <strong>initDrive.js</strong> 並在 chrome 的 option 加入<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>options.addArguments(<span style="color:#a5d6ff">&#39;--headless&#39;</span>)<span style="color:#8b949e;font-style:italic">//瀏覽器不提供頁面觀看，linux下如果系統是純文字介面不加這條會啓動失敗
</span></span></span><span style="display:flex;"><span>options.addArguments(<span style="color:#a5d6ff">&#39;--log-level=3&#39;</span>)<span style="color:#8b949e;font-style:italic">//這個option可以讓你跟headless時網頁端的console.log說掰掰
</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>options.addArguments(<span style="color:#a5d6ff">&#39;--disable-dev-shm-usage&#39;</span>)<span style="color:#8b949e;font-style:italic">//使用共享內存RAM
</span></span></span><span style="display:flex;"><span>options.addArguments(<span style="color:#a5d6ff">&#39;--disable-gpu&#39;</span>)<span style="color:#8b949e;font-style:italic">//規避部分chrome gpu bug
</span></span></span></code></pre></div><p>設定完這些後，在專案資料夾的終端機(Terminal)執行指令 <strong>yarn start</strong> ，就會發現<mark>爬蟲變成背景執行，並且電腦順暢許多</mark></p>
</li>
</ul>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-學會爬蟲之後呢-1">2. 學會爬蟲，之後呢？<a class="h-anchor" href="#2-%e5%ad%b8%e6%9c%83%e7%88%ac%e8%9f%b2%e4%b9%8b%e5%be%8c%e5%91%a2-1" aria-label="這個段落的連結"></a></h2>
<p><mark>原本我打算用兩天的文章來教取得當日貼文數量的方法，但文章寫完後就刪掉了，因為讀者完全有能力用這幾天所教的技術自行實踐這項功能；我希望用這30天分享更多不同的技術及有幫助的觀念</mark></p>
<blockquote>
<p><strong>授人以魚不如授人以漁</strong></p>
<p>使用模擬瀏覽器來爬蟲我認為是最符合初學者的，你不用去分析複雜的 api，去猜裡面的參數是甚麼意義</p>
<p>你需要做的只是把你平常操作的流程用程式的方式來替你做一遍，然後抓取你所需要的資訊</p>
<p>我寫的文章只是爬蟲的技巧的冰山一角，但只要有了開頭以及研究的方向我相信你是有能力獨立完成其他網頁的爬蟲</p>
</blockquote>
<p>現在人工智慧流行，大數據當道，爬蟲是取得大數據的方法之一，你能透過這些數據分析的東西太多了，我在這裡簡單舉例：</p>
<ul>
<li><strong>追蹤人數爆減/爆增</strong>
<ul>
<li>是因為某一篇貼文引起的嗎?</li>
<li>當天是否有媒體新聞的報導?</li>
<li>粉專經營者是否有特殊決策?</li>
</ul>
</li>
<li><strong>發文頻率與追蹤人數的關係</strong>
<ul>
<li>發文頻率是否與追蹤人數成長正相關</li>
<li>怎麼樣的發文頻率是最恰當的</li>
<li>不同行業的發文頻率比較</li>
</ul>
</li>
<li><strong>貼文按讚與回應</strong>
<ul>
<li>什麼樣的貼文會獲得最多迴響</li>
<li>獲得最多讚的貼文有什麼共同特徵</li>
</ul>
</li>
</ul>
<p>隨便舉的例子都能獨立出來都能成為一篇論文或是商品，<mark>爬蟲是一個很強的武器，但這把武器的強度要看使用者如何發揮他</mark>，我想會看這系列教學文章的讀者心中都有想要爬蟲的目標，開始動手實作吧！</p>
<p><em>再次提醒，技術沒有善惡，人的使用方式才會有善惡，希望這份技術可以幫大家節省時間以及避免重工；如果你把它拿來盜取智慧財產、個人檔案及機敏資訊請自行負擔後續法律責任</em></p>
<hr>
<blockquote>
<p><strong>筆者碎碎念</strong></p>
<p>我希望看完文章的讀者能夠吸收到問題思考的方式，不要只是單純的 copy &amp; paste ，這樣對技術長期來講是在累積負債</p>
<p>如果你是程式的新手，你可以透過修改一些參數來看看結果會有什麼樣的變化，少加哪些參數是不是真的會有錯誤</p>
<p>不要害怕犯錯，因為學習程式的路上幾乎不存在一條完美的道路</p>
<p>你現在滿意的程式往往在幾個月後你會覺得當時怎麼寫的這麼爛</p>
<p>當你誕生出這個想法的時候就是成長了</p>
</blockquote>
<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>關於使用網頁爬蟲的技術文章就分享到今天，接下來我們要來研究 <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/day16">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day15_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天json改寫的部分喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>windows需下載與你chrome版本相同的<a href="http://chromedriver.storage.googleapis.com/index.html">chrome driver</a>放在專案根目錄</li>
<li>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
</ul>
</li>
<li>調整fanspages資料夾內目標爬蟲的粉專網址</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>
<ol>
<li><a href="https://stackoverflow.max-everyday.com/2019/12/selenium-chrome-options/">selenium啓動Chrome的進階配置參數</a></li>
</ol>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-16/images/fb-ig-crawler-day-16-02.webp" width="1024" height="233" alt="參考資源（圖 2）" 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>【Day15】json x 爬蟲 = 瑣事自動化，生命應該浪費在美好的事情上</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-15/</link>
      <pubDate>Wed, 30 Sep 2020 08:51:22 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-15/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>專案在需求規格中有一條是在自己的電腦執行，在分析完各種檔案格式的優缺點後我決定使用json格式：</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-15/images/cover_hu_2b5138667ef2ac5c.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>即使客戶不懂程式，你也可以讓他學習一點工程師的知識</p>
</blockquote>
<h2 id="我們要請客戶將爬蟲的網址列表填寫到什麼檔案呢">我們要請客戶將爬蟲的網址列表填寫到什麼檔案呢?<a class="h-anchor" href="#%e6%88%91%e5%80%91%e8%a6%81%e8%ab%8b%e5%ae%a2%e6%88%b6%e5%b0%87%e7%88%ac%e8%9f%b2%e7%9a%84%e7%b6%b2%e5%9d%80%e5%88%97%e8%a1%a8%e5%a1%ab%e5%af%ab%e5%88%b0%e4%bb%80%e9%ba%bc%e6%aa%94%e6%a1%88%e5%91%a2" aria-label="這個段落的連結"></a></h2>
<p>專案在需求規格中有一條是<mark>在自己的電腦執行</mark>，在分析完各種檔案格式的優缺點後我決定使用json格式：</p>
<ul>
<li><strong>txt</strong>
<ul>
<li>優點：在所有作業系統中都能夠打開做編輯</li>
<li>缺點：<mark>格式難以律定</mark>，只要使用者多個空格、表點符號&hellip;都容易造成程式的誤判</li>
</ul>
</li>
<li><strong>excel</strong>
<ul>
<li>優點：這是對使用者而言最友善的一種編輯器了，視覺化良好、操作友善</li>
<li>缺點：<mark>不是所有人電腦都有安裝 excel</mark> 編輯器(像我朋友就沒有)</li>
</ul>
</li>
<li><strong>json</strong>
<ul>
<li>優點：在所有作業系統中都能夠打開做編輯，<mark>key-value 的格式易於理解、程式可直接讀取</mark></li>
<li>缺點：使用者需要花幾分鐘時間理解 json 格式的規則</li>
</ul>
</li>
</ul>
<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-了解-json-格式與基本規則並建立-json-格式的粉專清單">1. 了解 json 格式與基本規則，並建立 json 格式的粉專清單<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3-json-%e6%a0%bc%e5%bc%8f%e8%88%87%e5%9f%ba%e6%9c%ac%e8%a6%8f%e5%89%87%e4%b8%a6%e5%bb%ba%e7%ab%8b-json-%e6%a0%bc%e5%bc%8f%e7%9a%84%e7%b2%89%e5%b0%88%e6%b8%85%e5%96%ae" aria-label="這個段落的連結"></a></h3>
<p>1.1 json 格式與基本規則</p>
<p>1.2 在專案中建立 json 格式的粉專清單</p>
<h3 class="h-nomark" id="2-讓程式讀取-json-裡面的資料進行爬蟲">2. 讓程式讀取 json 裡面的資料進行爬蟲<a class="h-anchor" href="#2-%e8%ae%93%e7%a8%8b%e5%bc%8f%e8%ae%80%e5%8f%96-json-%e8%a3%a1%e9%9d%a2%e7%9a%84%e8%b3%87%e6%96%99%e9%80%b2%e8%a1%8c%e7%88%ac%e8%9f%b2" aria-label="這個段落的連結"></a></h3>
<p>2.1 在專案引入 json 檔案</p>
<p>2.2 批量爬蟲程式撰寫</p>
<hr>
<h2 class="h-nomark" id="1-了解-json-格式與基本規則並建立一份-json-格式的粉專清單">1. 了解 json 格式與基本規則，並建立一份 json 格式的粉專清單<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3-json-%e6%a0%bc%e5%bc%8f%e8%88%87%e5%9f%ba%e6%9c%ac%e8%a6%8f%e5%89%87%e4%b8%a6%e5%bb%ba%e7%ab%8b%e4%b8%80%e4%bb%bd-json-%e6%a0%bc%e5%bc%8f%e7%9a%84%e7%b2%89%e5%b0%88%e6%b8%85%e5%96%ae" aria-label="這個段落的連結"></a></h2>
<p>今天說的這個部分應該是大部分小編最關心的，要每天手動瀏覽那麼多的粉專想想都很崩潰</p>
<p>所以要來跟大家介紹的一個很棒的文本格式：<strong>json</strong> (JavaScript Object Notation) ，<mark>他能幫助我們批量爬蟲，同時也是網頁 api 最常見的一種回傳格式</mark></p>
<h3 class="h-nomark" id="11-json格式與基本規則">1.1 json格式與基本規則<a class="h-anchor" href="#11-json%e6%a0%bc%e5%bc%8f%e8%88%87%e5%9f%ba%e6%9c%ac%e8%a6%8f%e5%89%87" aria-label="這個段落的連結"></a></h3>
<p>json 可以包含 <code>object (物件)</code> 與 <code>array (陣列)</code></p>
<ul>
<li><strong>物件</strong>
<ul>
<li>
<p>為 key-value 的格式，用<code>{...}</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:#a5d6ff">&#34;key&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;value&#34;</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{ <span style="color:#a5d6ff">&#34;姓名&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;寶寶不說&#34;</span> } <span style="color:#8b949e;font-style:italic">//通常用於描寫一項屬性的數值
</span></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:#a5d6ff">&#34;key&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;value&#34;</span> , <span style="color:#a5d6ff">&#34;key&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;value&#34;</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{ <span style="color:#a5d6ff">&#34;姓名&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;寶寶不說&#34;</span> , <span style="color:#a5d6ff">&#34;性別&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;不明&#34;</span> } <span style="color:#8b949e;font-style:italic">//多個物件可表達一個東西的多種屬性
</span></span></span></code></pre></div></li>
</ul>
</li>
<li><strong>陣列</strong>
<ul>
<li>
<p>多個 value 之間使用 , 分隔，用<code>[...]</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>[ value, value ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 陣列與物件裡面value可以使用多種資料型態如下
</span></span></span><span style="display:flex;"><span>[ <span style="color:#a5d6ff">0</span> , { <span style="color:#a5d6ff">&#34;傳奇寶寶&#34;</span> <span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">null</span> } , <span style="color:#a5d6ff">&#34;string&#34;</span> , <span style="color:#79c0ff">false</span> , [<span style="color:#a5d6ff">&#39;test&#39;</span>] ]
</span></span></code></pre></div></li>
</ul>
</li>
</ul>
<h3 class="h-nomark" id="12-在專案中建立json格式的粉專清單">1.2 在專案中建立json格式的粉專清單<a class="h-anchor" href="#12-%e5%9c%a8%e5%b0%88%e6%a1%88%e4%b8%ad%e5%bb%ba%e7%ab%8bjson%e6%a0%bc%e5%bc%8f%e7%9a%84%e7%b2%89%e5%b0%88%e6%b8%85%e5%96%ae" aria-label="這個段落的連結"></a></h3>
<p><strong>建構邏輯：</strong></p>
<ol>
<li>
<p>我們有<mark>許多粉專需要去追蹤</mark>，所以最外層會用 <code>陣列(array)</code></p>
</li>
<li>
<p>而<mark>每個粉專都有自己的屬性</mark>(title為粉專名稱、url為粉專網址)，所以陣列裡面的 value 採用 <code>物件(object)</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-json" data-lang="json"><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;粉專A&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;粉專A連結&#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;粉專B&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;url&#34;</span>: <span style="color:#a5d6ff">&#34;粉專B連結&#34;</span>
</span></span><span style="display:flex;"><span>    }  
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div></li>
</ol>
<p><strong>在專案建立的步驟：</strong></p>
<ol>
<li>在專案資料夾中<mark>建立 fanspages 的資料夾</mark>，專門存放粉專頁面爬蟲要使用的json檔案</li>
<li>在 <code>fanspages 資料夾中新增 ig.json、fb.json</code> 這兩個檔案，並依照上述json結構填上你要爬蟲的資訊</li>
<li>你可以先複製下方範例做測試(筆者是以下貼圖的狂熱愛好者，歡迎一起追蹤按愛心XD)</li>
</ol>
<h4 id="igjson">ig.json<a class="h-anchor" href="#igjson" 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-json" data-lang="json"><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.instagram.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.instagram.com/sweethouse.sl/&#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.instagram.com/mochi_dad/&#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.instagram.com/chien_chien0608/&#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.instagram.com/machiko324/&#34;</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="2-讓爬蟲程式讀取-json-裡面的資料">2. 讓爬蟲程式讀取 json 裡面的資料<a class="h-anchor" href="#2-%e8%ae%93%e7%88%ac%e8%9f%b2%e7%a8%8b%e5%bc%8f%e8%ae%80%e5%8f%96-json-%e8%a3%a1%e9%9d%a2%e7%9a%84%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-在專案引入-json-檔案">2.1 在專案引入 json 檔案<a class="h-anchor" href="#21-%e5%9c%a8%e5%b0%88%e6%a1%88%e5%bc%95%e5%85%a5-json-%e6%aa%94%e6%a1%88" aria-label="這個段落的連結"></a></h3>
<ul>
<li>引入 json 檔案時請注意相對路徑，建議先用 <code>console.log 確認是否有正確引入</code></li>
<li>因為這個檔案是人工撰寫，可能會不小心複製到重複粉專，所以使用 <code>filter 來過濾</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">let</span> fanpage_array <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;../fanspages/ig.json&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">//過濾掉重複的粉專頁面，減少資源浪費
</span></span></span><span style="display:flex;"><span>fanpage_array <span style="color:#ff7b72;font-weight:bold">=</span> fanpage_array.filter((fanpage, index, self) =&gt;
</span></span><span style="display:flex;"><span>    index <span style="color:#ff7b72;font-weight:bold">===</span> self.findIndex(f =&gt; f.url <span style="color:#ff7b72;font-weight:bold">===</span> fanpage.url)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>console.log(fanpage_array)
</span></span></code></pre></div><h3 class="h-nomark" id="22-批量爬蟲程式撰寫">2.2 批量爬蟲程式撰寫<a class="h-anchor" href="#22-%e6%89%b9%e9%87%8f%e7%88%ac%e8%9f%b2%e7%a8%8b%e5%bc%8f%e6%92%b0%e5%af%ab" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>將前往粉專頁面與爬蟲的部分用 <code>for/of迴圈</code> 包起來(因迴圈內用到 <strong>async/await</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">for</span> (fanpage <span style="color:#ff7b72">of</span> fanpage_array) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 撰寫你要對fanpage做的事       
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
<li>
<p>我們要當個好爬蟲，每個頁面設定爬蟲停留 3-6 秒，<mark>不要因為自己的爬蟲造成別人伺服器的負擔</mark></p>
<blockquote>
<p>這裡我們使用Math.random()取得區間亂數</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">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></code></pre></div></li>
<li>
<p>統整後完整程式如下</p>
<h4 id="crawlerigjs">crawlerIG.js<a class="h-anchor" href="#crawlerigjs" 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-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">let</span> fanpage_array <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;../fanspages/ig.json&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">//過濾掉重複的粉專頁面，減少資源浪費
</span></span></span><span style="display:flex;"><span>fanpage_array <span style="color:#ff7b72;font-weight:bold">=</span> fanpage_array.filter((fanpage, index, self) =&gt;
</span></span><span style="display:flex;"><span>    index <span style="color:#ff7b72;font-weight:bold">===</span> self.findIndex(f =&gt; f.url <span style="color:#ff7b72;font-weight:bold">===</span> fanpage.url)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_username <span style="color:#ff7b72;font-weight:bold">=</span> process.env.IG_USERNAME
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_userpass <span style="color:#ff7b72;font-weight:bold">=</span> process.env.IG_PASSWORD
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { By, until } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver&#39;</span>)
</span></span><span style="display:flex;"><span>exports.crawlerIG <span style="color:#ff7b72;font-weight:bold">=</span> crawlerIG;
</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> crawlerIG (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> loginInstagram(driver, By, until)
</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">`IG開始爬蟲`</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">for</span> (fanpage <span style="color:#ff7b72">of</span> fanpage_array) { <span style="color:#8b949e;font-style:italic">// 用迴圈爬蟲每個粉專
</span></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">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:#8b949e;font-style:italic">//每個頁面爬蟲停留3~6秒，不要造成別人的伺服器負擔
</span></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></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></span><span style="display:flex;"><span>.
</span></span></code></pre></div></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>在專案資料夾的終端機(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>確認畫面執行順序如你預期：Instagram自動登入 → 依序跳轉到指定帳號 → Facebook自動登入 → 依序跳轉到粉絲頁 → 關閉</p>
</li>
<li>
<p>大概裝完一壺水的時間後，你就能看到 FB &amp; IG 的追蹤人數嚕～</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-15/images/fb-ig-crawler-day-15-01.webp" width="1024" height="422" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
</ol>
<p>搭配上json後有沒有覺得自己功力大增XD，上面的是IG的範例，大家可以自己嘗試看看FB部分的如何改寫喔～</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/day15">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day14_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天json改寫的部分喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>windows需下載與你chrome版本相同的<a href="http://chromedriver.storage.googleapis.com/index.html">chrome driver</a>放在專案根目錄</li>
<li>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
</ul>
</li>
<li>調整fanspages資料夾內目標爬蟲的粉專網址</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>
<ol>
<li><a href="https://www.footmark.info/programming-language/javascript/json-format-and-javascript/">JSON 格式與 JavaScript 解析教學範例</a></li>
<li><a href="https://ithelp.ithome.com.tw/articles/10197904">用Math.random()取得亂數的技巧</a></li>
</ol>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-15/images/fb-ig-crawler-day-15-02.webp" width="1024" height="233" alt="參考資源（圖 2）" 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>【Day14】爬蟲執行時又又又崩潰了嗎？來點 try-catch 吧</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-14/</link>
      <pubDate>Tue, 29 Sep 2020 10:23:54 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-14/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>在 Day8 selenium-webdriver：爬蟲起手式 有使用到 try-catch 來解決抓不到 chrome driver 的例外事件，今天我們會更清楚的說明如何讓 try-catch 幫助你更高效的 debug 以及增加程式穩定性</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/cover_hu_1a0b83fb1b7e8e9b.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>你的絕望我懂！</p>
</blockquote>
<h2 id="回憶一下專案之前的應用場景">回憶一下專案之前的應用場景<a class="h-anchor" href="#%e5%9b%9e%e6%86%b6%e4%b8%80%e4%b8%8b%e5%b0%88%e6%a1%88%e4%b9%8b%e5%89%8d%e7%9a%84%e6%87%89%e7%94%a8%e5%a0%b4%e6%99%af" aria-label="這個段落的連結"></a></h2>
<p>在 <a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-08/">Day8 selenium-webdriver：爬蟲起手式</a> 有使用到 try-catch 來<strong>解決抓不到 chrome driver 的例外事件</strong>，今天我們會更清楚的說明如何<strong>讓 try-catch 幫助你更高效的 debug 以及增加程式穩定性</strong></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-了解什麼情境下需要用到-try-catch">1. 了解什麼情境下需要用到 try-catch<a class="h-anchor" href="#1-%e4%ba%86%e8%a7%a3%e4%bb%80%e9%ba%bc%e6%83%85%e5%a2%83%e4%b8%8b%e9%9c%80%e8%a6%81%e7%94%a8%e5%88%b0-try-catch" aria-label="這個段落的連結"></a></h3>
<p>1.1 try-catch-finally 基礎語法說明</p>
<p>1.2 專案如果少了 try-catch 會發生哪些悲劇</p>
<h3 class="h-nomark" id="2-在專案中應用-try-catch-實作">2. 在專案中應用 try-catch 實作<a class="h-anchor" href="#2-%e5%9c%a8%e5%b0%88%e6%a1%88%e4%b8%ad%e6%87%89%e7%94%a8-try-catch-%e5%af%a6%e4%bd%9c" aria-label="這個段落的連結"></a></h3>
<p>2.1 <code>loginInstagram</code>：確認登入的步驟是否順利</p>
<p>2.2 <code>crawlerIG</code>：利用 <strong>loginInstagram函式</strong> 回傳值判斷是否繼續執行</p>
<p>2.3 <code>goFansPage</code>：確認傳入的參數符合網址規則</p>
<p>2.4 <code>getTrace</code>：確認網頁能抓到追蹤人數的元件</p>
<hr>
<h2 class="h-nomark" id="1-什麼情境下需要用到-try-catch">1. 什麼情境下需要用到 try-catch?<a class="h-anchor" href="#1-%e4%bb%80%e9%ba%bc%e6%83%85%e5%a2%83%e4%b8%8b%e9%9c%80%e8%a6%81%e7%94%a8%e5%88%b0-try-catch" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-try-catch-finally-基礎語法說明">1.1 try-catch-finally 基礎語法說明<a class="h-anchor" href="#11-try-catch-finally-%e5%9f%ba%e7%a4%8e%e8%aa%9e%e6%b3%95%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<p>在 javascript <mark>執行過程發成錯誤時會拋出例外</mark>，接著尋找有沒有能處理這個例外的程式，如果找不到就會從拋出例外的函式跳出，尋找上一層的函式能否處理這個例外，這個過程直到觸及最外層的函式後終止</p>
<p>如果我們<mark>沒有做錯誤處理，程式執行過程中發生意外就會直接在那裡crash</mark>，不過我們可以用<code>try-catch-finally</code>的語句來避免程式crash</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">try</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">// 如果這裡執行發生錯誤就會立即進入 catch
</span></span></span><span style="display:flex;"><span>} <span style="color:#ff7b72">catch</span> (exception) {
</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">// 通常都會用console.log印出錯誤訊息
</span></span></span><span style="display:flex;"><span>} <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 無論try-catch怎麼跑，最後這塊都一定會執行到
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="12-專案如果少了try-catch會發生哪些悲劇">1.2 專案如果少了try-catch會發生哪些悲劇<a class="h-anchor" href="#12-%e5%b0%88%e6%a1%88%e5%a6%82%e6%9e%9c%e5%b0%91%e4%ba%86try-catch%e6%9c%83%e7%99%bc%e7%94%9f%e5%93%aa%e4%ba%9b%e6%82%b2%e5%8a%87" aria-label="這個段落的連結"></a></h3>
<ul>
<li>程式在<mark>缺乏 try-catch 的機制下是非常脆落的，只要發生例外事件就容易崩潰</mark>，下面舉例幾個讓程式崩潰或是卡住的方式：
<ol>
<li><strong>把 Facebook 跟 Instagram 登入網址改成不存在的網址</strong> → 會因為網頁不存在而卡在那個畫面</li>
<li><strong>把粉專的網址改成不存在的網址</strong> → 會因為無法抓到要讀取的元件而崩潰</li>
<li><strong>把粉專的網址改成不存在的粉專</strong>(或是移除的粉專) → 會因為無法抓到要讀取的元件而崩潰</li>
</ol>
</li>
<li>為了避免這些悲劇的發生，<strong>try-catch</strong> 就是我們的好朋友，以下是應用上要注意的地方：
<ol>
<li><strong>一個try-catch區塊<mark>只判斷一件事有沒有做好</mark></strong>
<ul>
<li>與<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/">上一篇重構</a>提到的概念類似，一個函式專注做好一件事，try-catch 也是專注在確認這一件事執行時有沒有發生意外</li>
</ul>
</li>
<li><strong>除了系統自行吐出的 exception 以外，建議你也要<code>加上console.error</code>(&lsquo;我在哪個步驟錯了&rsquo;)</strong>
<ul>
<li>在程式結構龐大且長時間執行的狀態下，系統吐出的 exception 有時多到你眼花，這時你撰寫的 console.error 可以幫助你對發生錯誤的位置快速定位</li>
</ul>
</li>
<li><strong>程式需要設計<mark>中斷邏輯</mark>避免進入無窮的等待</strong>
<ul>
<li>我們在抓網頁元件時用了 wait&hellip;until 的結構，如果我們沒有設定最多等待幾秒，你等到天荒地老，因為他永遠停在 try 的區塊出不去，所以 try-catch 也幫不了你</li>
</ul>
</li>
<li><strong>透過例外處理減少多餘的步驟</strong>
<ul>
<li>像是 Instagram 我們一定要登入後才能爬蟲，所以我們就可以設定當登入失敗時(讓函式return false)不會執行後續步驟</li>
</ul>
</li>
</ol>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-在專案中應用-try-catch-實作-1">2. 在專案中應用 try-catch 實作<a class="h-anchor" href="#2-%e5%9c%a8%e5%b0%88%e6%a1%88%e4%b8%ad%e6%87%89%e7%94%a8-try-catch-%e5%af%a6%e4%bd%9c-1" aria-label="這個段落的連結"></a></h2>
<p>實踐出真知，下面以 <code>crawlerIG.js</code> 裡面的函式為範例</p>
<h3 class="h-nomark" id="21-logininstagram">2.1 <code>loginInstagram</code><a class="h-anchor" href="#21-logininstagram" aria-label="這個段落的連結"></a></h3>
<h4 id="調整參數讓程式gg">調整參數讓程式GG<a class="h-anchor" href="#%e8%aa%bf%e6%95%b4%e5%8f%83%e6%95%b8%e8%ae%93%e7%a8%8b%e5%bc%8fgg" aria-label="這個段落的連結"></a></h4>
<ol>
<li>把 IG 的登入網址改成非網址格式的字串(如：&rsquo;error&rsquo;)
<ul>
<li>程式會crash</li>
</ul>
</li>
<li>把 IG 的登入網址改成其他網址
<ul>
<li>找不到網頁上帳號密碼的文字輸入框、點擊登入的按鈕元件，程式會無限期等待</li>
</ul>
</li>
<li>輸入錯誤的登入帳密
<ul>
<li>因為登入失敗網頁右上角永遠不會出現頭像元件，程式會無限期等待</li>
</ul>
</li>
</ol>
<h4 id="解決方式">解決方式<a class="h-anchor" href="#%e8%a7%a3%e6%b1%ba%e6%96%b9%e5%bc%8f" aria-label="這個段落的連結"></a></h4>
<ol>
<li>
<p>把這段登入的邏輯用 try-catch 包起來</p>
</li>
<li>
<p>執行成功時回傳 true，如果執行上發生錯誤則回傳 false，終止後續爬蟲動作</p>
</li>
<li>
<p>為 driver.wait 加上合理的等待時間(3~5秒鐘)</p>
<blockquote>
<p>PS.在程式的世界中通常時間都是以毫秒為單位，填上 3000 就是 3 秒鐘的意思</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> loginInstagram (driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;https://www.instagram.com/accounts/login&#39;</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> driver.get(web)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//為 driver.wait 加上合理的等待時間(3~5秒鐘)
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> ig_username_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;username&#39;]&#34;</span>)), <span style="color:#a5d6ff">3000</span>);
</span></span><span style="display:flex;"><span>        ig_username_ele.sendKeys(ig_username)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">let</span> ig_password_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;password&#39;]&#34;</span>)), <span style="color:#a5d6ff">3000</span>);
</span></span><span style="display:flex;"><span>        ig_password_ele.sendKeys(ig_userpass)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> login_elem <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;button[type=&#39;submit&#39;]&#34;</span>)), <span style="color:#a5d6ff">3000</span>)
</span></span><span style="display:flex;"><span>        login_elem.click()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//為 driver.wait 加上合理的等待時間(3~5秒鐘)
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]//*[contains(@class,&#34;_47KiJ&#34;)]`</span>)), <span style="color:#a5d6ff">5000</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#39;IG登入失敗&#39;</span>)
</span></span><span style="display:flex;"><span>        console.error(e)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
</ol>
<h4 id="gotcha捕獲野生錯誤">GOTCHA！捕獲野生錯誤<a class="h-anchor" href="#gotcha%e6%8d%95%e7%8d%b2%e9%87%8e%e7%94%9f%e9%8c%af%e8%aa%a4" aria-label="這個段落的連結"></a></h4>
<ol>
<li>
<p>將 IG 登入網址改為錯誤字串</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 web = &#39;https://www.instagram.com/accounts/login&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;error&#39;</span>;
</span></span></code></pre></div><blockquote>
<p>會因不符合網址格式跳錯誤訊息</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-01.webp" width="1024" height="103" alt="GOTCHA！捕獲野生錯誤（圖 1）" fetchpriority="high" decoding="async"></p>
</blockquote>
</li>
<li>
<p>將 IG 登入網址改為其他無關網址</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 web = &#39;https://www.instagram.com/accounts/login&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;https://www.google.com&#39;</span>;
</span></span></code></pre></div><blockquote>
<p>會因找不到使用者輸入框元件超時而跳錯誤訊息</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-02.webp" width="1024" height="130" alt="GOTCHA！捕獲野生錯誤（圖 2）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
<li>
<p>把 .env環境檔裡面的登入帳號密碼改成錯誤的</p>
<pre tabindex="0"><code>IG_USERNAME=&#39;error&#39;
IG_PASSWORD=&#39;error&#39;
</code></pre><blockquote>
<p>會因找不到頭像元件超時而跳錯誤訊息</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-03.webp" width="1024" height="105" alt="GOTCHA！捕獲野生錯誤（圖 3）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ol>
<hr>
<h3 class="h-nomark" id="22-crawlerig">2.2 <code>crawlerIG</code><a class="h-anchor" href="#22-crawlerig" aria-label="這個段落的連結"></a></h3>
<ul>
<li>如果 <mark>loginInstagram函式回傳 false 代表 IG 登入失敗</mark>，後續爬蟲的動作就無需執行</li>
<li>如果 <mark>goFansPage函式回傳 false 代表該 IG帳號網址無效</mark>，就不需要去取得追蹤人數了</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> crawlerIG (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> loginInstagram(driver, By, until)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (isLogin) {<span style="color:#8b949e;font-style:italic">//如果登入成功才執行下面的動作
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.instagram.com/baobaonevertell/&#34;</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)
</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(<span style="color:#a5d6ff">3000</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">const</span> trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> getTrace(driver)
</span></span><span style="display:flex;"><span>            console.log(<span style="color:#a5d6ff">`IG追蹤人數：</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></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="23-gofanspage">2.3 <code>goFansPage</code><a class="h-anchor" href="#23-gofanspage" aria-label="這個段落的連結"></a></h3>
<h4 id="調整參數讓程式gg-1">調整參數讓程式GG<a class="h-anchor" href="#%e8%aa%bf%e6%95%b4%e5%8f%83%e6%95%b8%e8%ae%93%e7%a8%8b%e5%bc%8fgg-1" aria-label="這個段落的連結"></a></h4>
<ul>
<li>將傳入的 <strong>web_url</strong> 改成非網址格式的字串(如：&rsquo;error_page')
<ul>
<li>程式會crash</li>
</ul>
</li>
</ul>
<h4 id="解決方式-1">解決方式<a class="h-anchor" href="#%e8%a7%a3%e6%b1%ba%e6%96%b9%e5%bc%8f-1" aria-label="這個段落的連結"></a></h4>
<ul>
<li>
<p>把這段登入的邏輯用 try-catch 包起來</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> goFansPage (driver, web_url) {
</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">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">await</span> driver.get(web_url)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#39;無效的網址&#39;</span>)
</span></span><span style="display:flex;"><span>        console.error(e)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
</ul>
<h4 id="gotcha捕獲野生錯誤-1">GOTCHA！捕獲野生錯誤<a class="h-anchor" href="#gotcha%e6%8d%95%e7%8d%b2%e9%87%8e%e7%94%9f%e9%8c%af%e8%aa%a4-1" aria-label="這個段落的連結"></a></h4>
<ul>
<li>
<p>將 IG 帳號網址改為錯誤字串</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 fanpage = &#34;https://www.instagram.com/baobaonevertell/&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;error_page&#34;</span>
</span></span></code></pre></div><blockquote>
<p>會因不符合網址格式跳錯誤訊息</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-04.webp" width="1024" height="100" alt="GOTCHA！捕獲野生錯誤（圖 4）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="24-gettrace">2.4 <code>getTrace</code><a class="h-anchor" href="#24-gettrace" aria-label="這個段落的連結"></a></h3>
<h4 id="調整參數讓程式gg-2">調整參數讓程式GG<a class="h-anchor" href="#%e8%aa%bf%e6%95%b4%e5%8f%83%e6%95%b8%e8%ae%93%e7%a8%8b%e5%bc%8fgg-2" aria-label="這個段落的連結"></a></h4>
<ul>
<li>
<p>上一步 goFansPage 如果導向的並非 IG帳號網址，或者該IG帳號不存在時</p>
<blockquote>
<p>會因為找不到網頁上追蹤人數的元件，使程式陷入無限期等待</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-05.webp" width="1024" height="213" alt="調整參數讓程式GG（圖 5）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ul>
<h4 id="解決方式-2">解決方式<a class="h-anchor" href="#%e8%a7%a3%e6%b1%ba%e6%96%b9%e5%bc%8f-2" aria-label="這個段落的連結"></a></h4>
<ol>
<li>
<p>把這段登入的邏輯用 try-catch 包起來</p>
</li>
<li>
<p>為 driver.wait 加上合理的等待時間(5秒鐘)</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> getTrace (driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;<span style="color:#8b949e;font-style:italic">//這是紀錄IG追蹤人數
</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> ig_trace_xpath <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]/section/main/div/header/section/ul/li[2]/a/span`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//我們採取5秒內如果抓不到該元件就跳出的條件
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> ig_trace_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(ig_trace_xpath)), <span style="color:#a5d6ff">5000</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// ig因為當人數破萬時文字不會顯示，所以改抓title
</span></span></span><span style="display:flex;"><span>        ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> ig_trace_ele.getAttribute(<span style="color:#a5d6ff">&#39;title&#39;</span>)
</span></span><span style="display:flex;"><span>        ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> ig_trace.replace(<span style="color:#79c0ff">/\D/g</span>, <span style="color:#a5d6ff">&#39;&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> ig_trace
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#39;無法抓取IG追蹤人數&#39;</span>)
</span></span><span style="display:flex;"><span>        console.error(e)
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">null</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
</ol>
<h4 id="gotcha捕獲野生錯誤-2">GOTCHA！捕獲野生錯誤<a class="h-anchor" href="#gotcha%e6%8d%95%e7%8d%b2%e9%87%8e%e7%94%9f%e9%8c%af%e8%aa%a4-2" aria-label="這個段落的連結"></a></h4>
<ul>
<li>
<p>將 IG 帳號網址改為不存在帳號的網址(或是無關網址)</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 fanpage = &#34;https://www.instagram.com/baobaonevertell/&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.instagram.com/error_page_ex/&#34;</span>
</span></span></code></pre></div><blockquote>
<p>會因找不到追蹤人數的元件而跳錯誤訊息</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-06.webp" width="1024" height="94" alt="GOTCHA！捕獲野生錯誤（圖 6）" loading="lazy" decoding="async"></p>
</blockquote>
</li>
</ul>
<hr>
<h4 id="筆者碎碎念">筆者碎碎念<a class="h-anchor" href="#%e7%ad%86%e8%80%85%e7%a2%8e%e7%a2%8e%e5%bf%b5" aria-label="這個段落的連結"></a></h4>
<p>程式越龐大 try-catch 的機制越重要，因為隨著開發的時間軸拉長，你對過去撰寫的程式掌握度會越來越低，甚至會忘記自己曾經寫了這一段程式碼；萬一在遙遠的某一天運轉好好的程式突然崩潰了，沒有撰寫 try-catch 的人在 debug 會浪費非常多的時間，因為他無法掌握是哪裡出錯了，所以建議大家培養撰寫 try-catch 的好習慣</p>
<p>如果有時麼解釋不夠清楚的歡迎在下方留言討論喔</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/day14">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day13_sample_code.zip">壓縮檔</a>，你可以用裡面乾淨的環境來實作今天try-catch的部分喔
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>windows需下載與你chrome版本相同的<a href="http://chromedriver.storage.googleapis.com/index.html">chrome driver</a>放在專案根目錄</li>
<li>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/images/fb-ig-crawler-day-14-07.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 7）" 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>【Day13】重構程式碼，減少歷史業障</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-13/</link>
      <pubDate>Mon, 28 Sep 2020 08:45:12 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-13/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>昨天我們完成了一個跑得動的程式，但很明顯這不是一個好的程式 大家應該有感受到隨著程式越來越長，我們對他的掌握力逐步下降 根據 Clean Code 的原則而言就是說這程式寫得很爛XD 很多人為了盡快完成功能而埋頭狂寫，但這樣容易導致你日後維護以及交接的困難性，你所負責的專案越大這…</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/images/cover_hu_5d125fe20b2659b6.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>數據顯示，工程師完成最後 20% 的工作時間跟之前的 80% 一樣多</p>
</blockquote>
<h2 id="完成功能後重構是另一個開始">完成功能後，重構是另一個開始<a class="h-anchor" href="#%e5%ae%8c%e6%88%90%e5%8a%9f%e8%83%bd%e5%be%8c%e9%87%8d%e6%a7%8b%e6%98%af%e5%8f%a6%e4%b8%80%e5%80%8b%e9%96%8b%e5%a7%8b" aria-label="這個段落的連結"></a></h2>
<p>昨天我們完成了一個<mark>跑得動的程式</mark>，但很明顯這<mark>不是一個好的程式</mark></p>
<p>大家應該有感受到隨著程式越來越長，我們對他的掌握力逐步下降</p>
<p>根據 Clean Code 的原則而言就是說這<strong>程式寫得很爛XD</strong></p>
<p>很多人為了盡快完成功能而埋頭狂寫，但這樣容易導致你日後維護以及交接的困難性，你所負責的專案越大這個感受會更明顯</p>
<p>所以在完成功能後<mark>重構是必要的任務，他能提升你日後解決 bug、需求變更的工作效率</mark></p>
<p>接下來就以昨天程式為範例來重構吧！</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>
<p>了解 &amp; 修正目前專案所犯下的錯誤</p>
<ol>
<li>主程式做了太多事情</li>
<li>一個函式做了太多事情</li>
<li>物件宣告的方式太複雜且多次被使用</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-主程式做了太多事情">1. 主程式做了太多事情<a class="h-anchor" href="#1-%e4%b8%bb%e7%a8%8b%e5%bc%8f%e5%81%9a%e4%ba%86%e5%a4%aa%e5%a4%9a%e4%ba%8b%e6%83%85" aria-label="這個段落的連結"></a></h2>
<p>主程式就像是專案的總經理，他只需要知道大方向的架構；而架構裡面的細節就交給各部門處理，他只是溝通資料的橋樑</p>
<p>根據上述的邏輯，我們應該把在主程式中的函式轉移到另外的資料夾(ex:tools)，依據功能去命名檔名，範例如下:</p>
<ol>
<li>主程式 - index.js</li>
<li>初始化瀏覽器 - tools/initDrive.js</li>
<li>Facebook爬蟲 - tools/crawlerFB.js</li>
<li>Instagram爬蟲 - tools/crawlerIG.js</li>
</ol>
<p>在這樣的分工後我們的主程式是不是變得很乾淨呢？</p>
<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></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></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 style="color:#8b949e;font-style:italic">//driver不存在就結束程式
</span></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">//因為有些人是用FB帳號登入IG，為了避免增加FB登出的動作，所以採取先對IG進行爬蟲
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> crawlerIG(driver)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> crawlerFB(driver)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    driver.quit();
</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>
<h2 class="h-nomark" id="2-一個函式做了太多事情">2. 一個函式做了太多事情<a class="h-anchor" href="#2-%e4%b8%80%e5%80%8b%e5%87%bd%e5%bc%8f%e5%81%9a%e4%ba%86%e5%a4%aa%e5%a4%9a%e4%ba%8b%e6%83%85" aria-label="這個段落的連結"></a></h2>
<p><mark>記住一個大原則：一個函式只需要做一件事情！</mark></p>
<p>現在我們把 FB 登入並取得追蹤人數寫在同一隻函式，這樣會增加你日後維護的困難度，因為<code>函式越長，你越難除錯(debug)</code>；以 <code>loginFacebookGetTrace</code> 這隻函式舉例，它實際上可以解構成好幾個功能：</p>
<ol>
<li>取得 Facebook(經典版/新版) 的爬蟲路徑 - <strong>getCrawlerPath</strong></li>
<li>Facebook爬蟲 - <strong>crawlerFB</strong></li>
<li>登入Facebook - <strong>loginFacebook</strong></li>
<li>前往粉絲頁 - <strong>goFansPage</strong></li>
<li>取得追蹤人數 - <strong>getTrace</strong></li>
</ol>
<p>把每個功能獨立成函式，你就能輕鬆除錯(debug)。下面程式以 FB 爬蟲作為解構範例，你可以自己練習改寫 IG 的部分喔</p>
<h3 id="crawlerfbjs">crawlerFB.js<a class="h-anchor" href="#crawlerfbjs" 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">const</span> fb_username <span style="color:#ff7b72;font-weight:bold">=</span> process.env.FB_USERNAME
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fb_userpass <span style="color:#ff7b72;font-weight:bold">=</span> process.env.FB_PASSWORD
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { By, until } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver&#39;</span>) <span style="color:#8b949e;font-style:italic">// 從套件中取出需要用到的功能
</span></span></span><span style="display:flex;"><span>exports.crawlerFB <span style="color:#ff7b72;font-weight:bold">=</span> crawlerFB;<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:#8b949e;font-style:italic">// FB有經典版以及新版的區分，兩者的爬蟲路徑不同，我們藉由函式取得各自的路徑
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { fb_head_path, fb_trace_path } <span style="color:#ff7b72;font-weight:bold">=</span> getCrawlerPath();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getCrawlerPath () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (process.env.FB_VERSION <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#39;new&#39;</span>) {<span style="color:#8b949e;font-style:italic">//如果是新版FB
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_head_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;fzdkajry&#34;)]`</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_trace_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;knvmm38d&#34;)]`</span>
</span></span><span style="display:flex;"><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>        <span style="color:#ff7b72">return</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_head_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;_1vp5&#34;)]`</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_trace_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[@id=&#34;PagesProfileHomeSecondaryColumnPagelet&#34;]//*[contains(@class,&#34;_4bl9&#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">async</span> <span style="color:#ff7b72">function</span> crawlerFB(driver) {    
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> loginFacebook(driver)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.facebook.com/baobaonevertell/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> goFansPage(driver, fanpage)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.sleep(<span style="color:#a5d6ff">3000</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> getTrace(driver)
</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">async</span> <span style="color:#ff7b72">function</span> loginFacebook(driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;https://www.facebook.com/login&#39;</span>;<span style="color:#8b949e;font-style:italic">//我們要前往FB
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.get(web)<span style="color:#8b949e;font-style:italic">//在這裡要用await確保打開完網頁後才能繼續動作
</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">const</span> fb_email_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;email&#34;]`</span>)));
</span></span><span style="display:flex;"><span>    fb_email_ele.sendKeys(fb_username)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fb_pass_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;pass&#34;]`</span>)));
</span></span><span style="display:flex;"><span>    fb_pass_ele.sendKeys(fb_userpass)
</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> login_elem <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;loginbutton&#34;]`</span>)))
</span></span><span style="display:flex;"><span>    login_elem.click()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//因為登入這件事情要等server回應，你直接跳轉粉絲專頁會導致登入失敗
</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">await</span> driver.wait(until.elementLocated(By.xpath(fb_head_path)))
</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">async</span> <span style="color:#ff7b72">function</span> goFansPage(driver, web_url) {
</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> driver.get(web_url)
</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">async</span> <span style="color:#ff7b72">function</span> getTrace(driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> fb_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;<span style="color:#8b949e;font-style:italic">//這是紀錄FB追蹤人數
</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> fb_trace_eles <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.
</span></span><span style="display:flex;"><span>        wait(until.elementsLocated(By.xpath(fb_trace_path)))
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> fb_trace_ele <span style="color:#ff7b72">of</span> fb_trace_eles) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> fb_text <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fb_trace_ele.getText()
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (fb_text.includes(<span style="color:#a5d6ff">&#39;人在追蹤&#39;</span>)) {
</span></span><span style="display:flex;"><span>            fb_trace <span style="color:#ff7b72;font-weight:bold">=</span> fb_text.replace(<span style="color:#79c0ff">/\D/g</span>, <span style="color:#a5d6ff">&#39;&#39;</span>)<span style="color:#8b949e;font-style:italic">//只取數字
</span></span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`FB追蹤人數：</span><span style="color:#a5d6ff">${</span>fb_trace<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 class="h-nomark" id="3-物件宣告的方式太複雜且多次被使用">3. 物件宣告的方式太複雜且多次被使用<a class="h-anchor" href="#3-%e7%89%a9%e4%bb%b6%e5%ae%a3%e5%91%8a%e7%9a%84%e6%96%b9%e5%bc%8f%e5%a4%aa%e8%a4%87%e9%9b%9c%e4%b8%94%e5%a4%9a%e6%ac%a1%e8%a2%ab%e4%bd%bf%e7%94%a8" aria-label="這個段落的連結"></a></h2>
<p>我們要將<mark>建立瀏覽器的部分獨立成一隻js</mark>，原因如下</p>
<ul>
<li>建立瀏覽器前需要做事前確認及複雜的設定</li>
<li>這個瀏覽器作為物件會被多次使用</li>
<li>如果瀏覽器的參數需要變更可以在一個檔案集中管理</li>
</ul>
<p>基於上述原因，我們<strong>應該把他獨立出來</strong>，這樣你只需要<code>在一個檔案管理你宣告的物件(瀏覽器)設定，而主程式很單純的使用回傳的物件(瀏覽器)即可</code></p>
<h3 id="initdrivejs">initDrive.js<a class="h-anchor" href="#initdrivejs" 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>exports.initDrive <span style="color:#ff7b72;font-weight:bold">=</span> initDrive;<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:#ff7b72">const</span> webdriver <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver&#39;</span>) <span style="color:#8b949e;font-style:italic">// 加入虛擬網頁套件
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> chrome <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver/chrome&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> options <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> chrome.Options();
</span></span><span style="display:flex;"><span>options.setUserPreferences({ <span style="color:#a5d6ff">&#39;profile.default_content_setting_values.notifications&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1</span> });<span style="color:#8b949e;font-style:italic">//因為FB會有notifications干擾到爬蟲，所以要先把它關閉
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> path <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;path&#39;</span>);<span style="color:#8b949e;font-style:italic">//用於處理文件路徑的小工具
</span></span></span><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">&#34;fs&#34;</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:#ff7b72">async</span> <span style="color:#ff7b72">function</span> initDrive () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>checkDriver()) {<span style="color:#8b949e;font-style:italic">// 檢查Driver是否是設定，如果無法設定就結束程式
</span></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></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 建立這個broswer的類型
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> driver <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> <span style="color:#ff7b72">new</span> webdriver.Builder().forBrowser(<span style="color:#a5d6ff">&#34;chrome&#34;</span>).withCapabilities(options).build();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//考慮到ig在不同螢幕寬度時的Xpath不一樣，所以我們要在這裡設定統一的視窗大小
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.manage().window().setRect({ width<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1280</span>, height<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">800</span>, x<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">0</span>, y<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">0</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> driver
</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> checkDriver() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        chrome.getDefaultService()<span style="color:#8b949e;font-style:italic">//確認是否有預設
</span></span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> {
</span></span><span style="display:flex;"><span>        console.warn(<span style="color:#a5d6ff">&#39;找不到預設driver!&#39;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//&#39;../chromedriver.exe&#39;記得調整成自己的路徑
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> file_path <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;../chromedriver.exe&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//請確認印出來日誌中的位置是否與你路徑相同
</span></span></span><span style="display:flex;"><span>        console.log(path.join(__dirname, file_path));
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//確認路徑下chromedriver.exe是否存在            
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (fs.existsSync(path.join(__dirname, file_path))) {
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">const</span> service <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> chrome.ServiceBuilder(path.join(__dirname, file_path)).build();<span style="color:#8b949e;font-style:italic">//設定driver路徑
</span></span></span><span style="display:flex;"><span>            chrome.setDefaultService(service);
</span></span><span style="display:flex;"><span>            console.log(<span style="color:#a5d6ff">&#39;設定driver路徑&#39;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>            console.error(<span style="color:#a5d6ff">&#39;無法設定driver路徑&#39;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">false</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> <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>這篇文章是以自己的程式作為範例講解，如果你想更深入了解重構請閱讀<a href="https://medium.com/%E5%BE%8C%E7%AB%AF%E6%96%B0%E6%89%8B%E6%9D%91/%E7%AD%86%E8%A8%98-%E9%87%8D%E6%A7%8B-chapter-1-2-%E7%AC%AC%E4%B8%80%E5%80%8B%E7%AF%84%E4%BE%8B-%E9%87%8D%E6%A7%8B%E5%8E%9F%E5%89%87-ca57a6d40f42">重構—改善既有的程式設計</a>，他深入淺出說明重構的原則讓我受益良多</p>
<blockquote>
<p>程式碼往往沒有所謂最好的寫法，只有當下最合理的選擇。而需求永遠在變動，就算當下重構到很滿意，可能下個月再回來看又覺得不夠了。 - YH Yu</p>
</blockquote>
<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>觀察執行順序：Instagram 自動登入 → 跳轉到指定帳號 → Facebook自動登入 → 跳轉到粉絲頁 → 關閉</p>
</li>
<li>
<p>確定與程式重構後能順利取得FB &amp; IG的追蹤人數</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/images/fb-ig-crawler-day-13-01.webp" width="1024" height="159" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
</ol>
<p>如果你還有什麼問題或是覺得有可以改善的地方歡迎在下方留言討論</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/day13">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day12_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>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
</ul>
</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>
<ol>
<li><a href="https://blog.camel2243.com/2017/06/24/nodejs-module-exports-%E8%88%87-exports-%E7%9A%84%E5%B7%AE%E5%88%A5/">[Nodejs] module.exports 與 exports 的差別</a></li>
<li><a href="https://medium.com/%E5%BE%8C%E7%AB%AF%E6%96%B0%E6%89%8B%E6%9D%91/%E7%AD%86%E8%A8%98-%E9%87%8D%E6%A7%8B-chapter-1-2-%E7%AC%AC%E4%B8%80%E5%80%8B%E7%AF%84%E4%BE%8B-%E9%87%8D%E6%A7%8B%E5%8E%9F%E5%89%87-ca57a6d40f42">重構—改善既有的程式設計</a></li>
</ol>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/images/fb-ig-crawler-day-13-02.webp" width="1024" height="233" alt="參考資源（圖 2）" 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>【Day12】合體吧！用一隻程式搞定FB、IG爬蟲</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-12/</link>
      <pubDate>Sun, 27 Sep 2020 07:59:57 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-12/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>1.1 瀏覽器關閉導致無法讀取網頁元件1.2 跨網域(CORS)錯誤</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-12/images/cover_hu_f487751a291cc221.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-%e5%88%86%e6%9e%90%e5%af%a6%e4%bd%9c%e9%81%8e%e7%a8%8b%e4%b8%ad%e5%8f%af%e8%83%bd%e6%9c%83%e9%81%87%e5%88%b0%e7%9a%84%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h3>
<p>1.1 瀏覽器關閉導致無法讀取網頁元件</p>
<p>1.2 跨網域(CORS)錯誤</p>
<h3 class="h-nomark" id="2-一隻程式爬完-fb--ig-粉專">2. 一隻程式爬完 FB &amp; IG 粉專<a class="h-anchor" href="#2-%e4%b8%80%e9%9a%bb%e7%a8%8b%e5%bc%8f%e7%88%ac%e5%ae%8c-fb--ig-%e7%b2%89%e5%b0%88" aria-label="這個段落的連結"></a></h3>
<p>2.1 分析程式結構邏輯</p>
<p>2.2 思考程式上有什麼地方需要改進</p>
<hr>
<h2 class="h-nomark" id="1-實作過程中可能會遇到的問題">1. 實作過程中可能會遇到的問題<a class="h-anchor" href="#1-%e5%af%a6%e4%bd%9c%e9%81%8e%e7%a8%8b%e4%b8%ad%e5%8f%af%e8%83%bd%e6%9c%83%e9%81%87%e5%88%b0%e7%9a%84%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-瀏覽器關閉導致無法讀取網頁元件">1.1 瀏覽器關閉導致無法讀取網頁元件<a class="h-anchor" href="#11-%e7%80%8f%e8%a6%bd%e5%99%a8%e9%97%9c%e9%96%89%e5%b0%8e%e8%87%b4%e7%84%a1%e6%b3%95%e8%ae%80%e5%8f%96%e7%b6%b2%e9%a0%81%e5%85%83%e4%bb%b6" aria-label="這個段落的連結"></a></h3>
<p>如果你直接把兩天文章複製貼上爬的話一定會看到這個錯誤訊息</p>
<pre tabindex="0"><code>WebDriverError: element not interactable
</code></pre><ul>
<li>這是因為<strong>一次跳出兩個瀏覽器</strong>在執行，只要其中一個瀏覽器完成爬蟲任務觸發 <strong>driver.quit()</strong> 就會導致另一個還沒爬蟲完畢的<strong>瀏覽器因無法讀取元件而掛掉</strong></li>
<li>面對這個問題，我建議<strong>開一個瀏覽器</strong>來處理：
<ol>
<li>因為<mark>跑爬蟲會消耗電腦的記憶體以及網路流量</mark>，我希望電腦跑爬蟲的時候不要影響我做其他事情的效率</li>
<li>使用一個瀏覽器在跑<mark>遇到問題時方便除錯</mark></li>
</ol>
</li>
</ul>
<h3 class="h-nomark" id="12-跨網域cors錯誤">1.2 跨網域(CORS)錯誤<a class="h-anchor" href="#12-%e8%b7%a8%e7%b6%b2%e5%9f%9fcors%e9%8c%af%e8%aa%a4" aria-label="這個段落的連結"></a></h3>
<p>如果你把他<strong>改成一個瀏覽器</strong>執行，跑完 FB 粉專再跑 IG 粉專應該又會出現<strong>跨網域(CORS)問題</strong>的錯誤(目前只有 windows 作業系統才會遇到)</p>
<ul>
<li>
<p>這是<mark>網域切換時因為一些安全性的疑慮而拒絕跳轉</mark>，像是從 <a href="https://www.facebook.com/">https://www.facebook.com/</a> 跳轉到 <a href="https://www.instagram.com/">https://www.instagram.com</a> 就會遇到這個問題</p>
</li>
<li>
<p>這個問題在建立瀏覽器時加上設定 <code>{ acceptSslCerts: true, acceptInsecureCerts: true }</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">let</span> driver <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> webdriver.Builder().forBrowser(<span style="color:#a5d6ff">&#34;chrome&#34;</span>).withCapabilities(options,
</span></span><span style="display:flex;"><span>    { acceptSslCerts<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span>, acceptInsecureCerts<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span> }<span style="color:#8b949e;font-style:italic">//這是為了解決跨網域問題
</span></span></span><span style="display:flex;"><span>).build();
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 class="h-nomark" id="2-一隻程式爬完-fb--ig-粉專-1">2. 一隻程式爬完 FB &amp; IG 粉專<a class="h-anchor" href="#2-%e4%b8%80%e9%9a%bb%e7%a8%8b%e5%bc%8f%e7%88%ac%e5%ae%8c-fb--ig-%e7%b2%89%e5%b0%88-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%e7%a8%8b%e5%bc%8f%e7%b5%90%e6%a7%8b%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h3>
<p>為了幫助讀者能較快掌握程式的邏輯，我根據每個函式所做的功能以及執行步驟列出結構如下：</p>
<ul>
<li><code>crawler</code> : 觸發爬蟲的函式
<ol>
<li>
<p>檢查 Driver 是否是設定</p>
</li>
<li>
<p>建立這個 browser 的類型</p>
</li>
<li>
<p>設定 browser 的視窗大小</p>
</li>
<li>
<p><code>loginInstagramGetTrace</code> : 登入IG並取得指定帳號的追蹤人數</p>
<blockquote>
<p>先執行 IG 爬蟲是因為有些人登入 IG 的方式是綁定 FB 帳號，這會導致我們程式需要判斷更多狀況；而讓IG先爬蟲就能避免這個問題嚕～</p>
</blockquote>
<ol>
<li>前往 IG 登入頁，填入登入資訊</li>
<li>點擊登入按紐</li>
<li>判斷是否登入成功</li>
<li>登入成功後跳轉指定帳號頁面</li>
<li>獲取指定帳號追蹤人數</li>
</ol>
</li>
<li>
<p><code>loginFacebookGetTrace</code> : 登入 FB 並取得粉絲專業的追蹤人數</p>
<ol>
<li>前往 FB 登入頁，填入登入資訊</li>
<li>點擊登入按紐</li>
<li>判斷是否登入成功</li>
<li>登入成功後跳轉指定粉專頁面</li>
<li>獲取指定粉專追蹤人數</li>
</ol>
</li>
<li>
<p>關閉 browser</p>
</li>
</ol>
</li>
</ul>
<blockquote>
<p>寫程式如同拼積木，隨著功能的增加程式的體積會越來越大，這裡我建議你寫一份程式架構圖的文件來輔助，未來遇到需求變更時看架構圖就能迅速修改</p>
</blockquote>
<h4 id="indexjs">index.js<a class="h-anchor" href="#indexjs" 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-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> path <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;path&#39;</span>);<span style="color:#8b949e;font-style:italic">//用於處理文件路徑的小工具
</span></span></span><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">&#34;fs&#34;</span>);<span style="color:#8b949e;font-style:italic">//讀取檔案用
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">//取出.env檔案的FB、IG資訊
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_username <span style="color:#ff7b72;font-weight:bold">=</span> process.env.IG_USERNAME
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_userpass <span style="color:#ff7b72;font-weight:bold">=</span> process.env.IG_PASSWORD
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fb_username <span style="color:#ff7b72;font-weight:bold">=</span> process.env.FB_USERNAME
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fb_userpass <span style="color:#ff7b72;font-weight:bold">=</span> process.env.FB_PASSWORD
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> webdriver <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver&#39;</span>), <span style="color:#8b949e;font-style:italic">// 加入虛擬網頁套件
</span></span></span><span style="display:flex;"><span>    By <span style="color:#ff7b72;font-weight:bold">=</span> webdriver.By,<span style="color:#8b949e;font-style:italic">//你想要透過什麼方式來抓取元件，通常使用xpath、css
</span></span></span><span style="display:flex;"><span>    until <span style="color:#ff7b72;font-weight:bold">=</span> webdriver.until;<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:#ff7b72">const</span> chrome <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;selenium-webdriver/chrome&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> options <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> chrome.Options();
</span></span><span style="display:flex;"><span>options.setUserPreferences({ <span style="color:#a5d6ff">&#39;profile.default_content_setting_values.notifications&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1</span> });<span style="color:#8b949e;font-style:italic">//因為FB會有notifications干擾到爬蟲，所以要先把它關閉
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getCrawlerPath () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (process.env.FB_VERSION <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#39;new&#39;</span>) {<span style="color:#8b949e;font-style:italic">//如果是新版FB
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_head_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;fzdkajry&#34;)]`</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_trace_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;knvmm38d&#34;)]`</span>
</span></span><span style="display:flex;"><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>        <span style="color:#ff7b72">return</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_head_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[contains(@class,&#34;_1vp5&#34;)]`</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;fb_trace_path&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`//*[@id=&#34;PagesProfileHomeSecondaryColumnPagelet&#34;]//*[contains(@class,&#34;_4bl9&#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">async</span> <span style="color:#ff7b72">function</span> loginFacebookGetTrace (driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;https://www.facebook.com/login&#39;</span>;<span style="color:#8b949e;font-style:italic">//我們要前往FB
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.get(web)<span style="color:#8b949e;font-style:italic">//在這裡要用await確保打開完網頁後才能繼續動作
</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">const</span> fb_email_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;email&#34;]`</span>)));
</span></span><span style="display:flex;"><span>    fb_email_ele.sendKeys(fb_username)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fb_pass_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;pass&#34;]`</span>)));
</span></span><span style="display:flex;"><span>    fb_pass_ele.sendKeys(fb_userpass)
</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> login_elem <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;loginbutton&#34;]`</span>)))
</span></span><span style="display:flex;"><span>    login_elem.click()
</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">const</span> { fb_head_path, fb_trace_path } <span style="color:#ff7b72;font-weight:bold">=</span> getCrawlerPath();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//因為登入這件事情要等server回應，你直接跳轉粉絲專頁會導致登入失敗
</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">await</span> driver.wait(until.elementLocated(By.xpath(fb_head_path)))
</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> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.facebook.com/baobaonevertell/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.get(fanpage)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> fb_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;<span style="color:#8b949e;font-style:italic">//這是紀錄FB追蹤人數
</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> fb_trace_eles <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementsLocated(By.xpath(fb_trace_path)))
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> fb_trace_ele <span style="color:#ff7b72">of</span> fb_trace_eles) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> fb_text <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fb_trace_ele.getText()
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (fb_text.includes(<span style="color:#a5d6ff">&#39;人在追蹤&#39;</span>)) {
</span></span><span style="display:flex;"><span>            fb_trace <span style="color:#ff7b72;font-weight:bold">=</span> fb_text
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`FB追蹤人數：</span><span style="color:#a5d6ff">${</span>fb_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">async</span> <span style="color:#ff7b72">function</span> loginInstagramGetTrace (driver) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> web <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;https://www.instagram.com/accounts/login&#39;</span>;<span style="color:#8b949e;font-style:italic">//前往IG登入頁面
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.get(web)<span style="color:#8b949e;font-style:italic">//在這裡要用await確保打開完網頁後才能繼續動作
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//填入ig登入資訊
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> ig_username_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;username&#39;]&#34;</span>)));
</span></span><span style="display:flex;"><span>    ig_username_ele.sendKeys(ig_username)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> ig_password_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;password&#39;]&#34;</span>)));
</span></span><span style="display:flex;"><span>    ig_password_ele.sendKeys(ig_userpass)
</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> login_elem <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;button[type=&#39;submit&#39;]&#34;</span>)))
</span></span><span style="display:flex;"><span>    login_elem.click()
</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">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]//*[contains(@class,&#34;_47KiJ&#34;)]`</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> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.instagram.com/baobaonevertell/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.get(fanpage)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;<span style="color:#8b949e;font-style:italic">//這是紀錄IG追蹤人數
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> ig_trace_xpath <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]/section/main/div/header/section/ul/li[2]/a/span`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> ig_trace_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(ig_trace_xpath)))
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// ig因為當人數破萬時文字不會顯示，所以改抓title
</span></span></span><span style="display:flex;"><span>    ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> ig_trace_ele.getAttribute(<span style="color:#a5d6ff">&#39;title&#39;</span>)
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`IG追蹤人數：</span><span style="color:#a5d6ff">${</span>ig_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></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> checkDriver() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>        chrome.getDefaultService()<span style="color:#8b949e;font-style:italic">//確認是否有預設
</span></span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> {
</span></span><span style="display:flex;"><span>        console.warn(<span style="color:#a5d6ff">&#39;找不到預設driver!&#39;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//&#39;../chromedriver.exe&#39;記得調整成自己的路徑
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> file_path <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;../chromedriver.exe&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//請確認印出來日誌中的位置是否與你路徑相同
</span></span></span><span style="display:flex;"><span>        console.log(path.join(__dirname, file_path));
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">//確認路徑下chromedriver.exe是否存在            
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (fs.existsSync(path.join(__dirname, file_path))) {
</span></span><span style="display:flex;"><span>            <span style="color:#8b949e;font-style:italic">//設定driver路徑
</span></span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">const</span> service <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> chrome.ServiceBuilder(path.join(__dirname, file_path)).build();
</span></span><span style="display:flex;"><span>            chrome.setDefaultService(service);
</span></span><span style="display:flex;"><span>            console.log(<span style="color:#a5d6ff">&#39;設定driver路徑&#39;</span>);
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>            console.error(<span style="color:#a5d6ff">&#39;無法設定driver路徑&#39;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">return</span> <span style="color:#79c0ff">false</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> <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 style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> crawler () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>checkDriver()) {<span style="color:#8b949e;font-style:italic">// 檢查Driver是否是設定，如果無法設定就結束程式
</span></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">// 建立這個browser的類型
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> driver <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> <span style="color:#ff7b72">new</span> webdriver.Builder().forBrowser(<span style="color:#a5d6ff">&#34;chrome&#34;</span>).withCapabilities(options).build();
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">//考慮到ig在不同螢幕寬度時的Xpath不一樣，所以我們要在這裡設定統一的視窗大小
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> driver.manage().window().setRect({ width<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1280</span>, height<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">800</span>, x<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">0</span>, y<span style="color:#ff7b72;font-weight:bold">:</span> <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">//因為有些人是用FB帳號登入IG，為了避免增加FB登出的動作，所以採取先對IG進行爬蟲
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> loginInstagramGetTrace(driver)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> loginFacebookGetTrace(driver)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    driver.quit();
</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 class="h-nomark" id="22-思考程式上有什麼地方需要改進">2.2 思考程式上有什麼地方需要改進<a class="h-anchor" href="#22-%e6%80%9d%e8%80%83%e7%a8%8b%e5%bc%8f%e4%b8%8a%e6%9c%89%e4%bb%80%e9%ba%bc%e5%9c%b0%e6%96%b9%e9%9c%80%e8%a6%81%e6%94%b9%e9%80%b2" aria-label="這個段落的連結"></a></h3>
<p>看完上面的程式後，我想大部分的人都有點暈了XD，如果你有認真看程式應該有以下體會：</p>
<ol>
<li>閱讀起來有點吃力</li>
<li>遇到錯誤時不好找問題</li>
<li>很難掌握每個函式的功能</li>
<li>幾個月後回來看這隻程式估計一臉茫然</li>
</ol>
<p>如果還有更多的體會歡迎大家在下方留言(請鞭小力一點QQ)</p>
<p>明天會講<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-13/">程式碼的重構</a>，透過重構我們可以更有效率的掌握程式；後天會講<a href="https://deanlin.net/blog/202009/fb-ig-crawler-day-14/">try-catch在本專案的應用</a>，方便日後的除錯</p>
<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>觀察執行順序是否為：Instagram自動登入 → 跳轉到指定帳號 → Facebook自動登入 → 跳轉到粉絲頁 → 關閉</p>
<blockquote>
<p>如果想要中斷終端機(Terminal)執行的程式，可以用下面按鍵組合:</p>
<ul>
<li>Windows: Ctrl + c</li>
<li>Mac: control + c</li>
</ul>
</blockquote>
</li>
<li>
<p>順利的話就能看到FB &amp; IG的追蹤人數嚕～</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-12/images/fb-ig-crawler-day-12-01.webp" width="1024" height="155" alt="執行程式（圖 1）" fetchpriority="high" 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/day12">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day11_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>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-12/images/fb-ig-crawler-day-12-02.webp" width="1024" height="233" alt="ℹ️ 專案原始碼（圖 2）" 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>【Day11】舉一反三，帶你了解IG爬蟲不可忽略的細節</title>
      <link>https://deanlin.net/blog/202009/fb-ig-crawler-day-11/</link>
      <pubDate>Sat, 26 Sep 2020 08:44:52 +0800</pubDate>
      <guid>https://deanlin.net/blog/202009/fb-ig-crawler-day-11/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>有了 Facebook 爬蟲的經驗後，我相信這篇大家是有能力獨自完成的，建議大家先用自己的方式來完成今日目標，這篇文章適合實作遇到問題或是你實作完成後再來參考</description>
      <media:content url="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/cover_hu_30d62b86535ec880.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%ad%86%e8%80%85%e6%9c%89%e8%a9%b1%e5%85%88%e8%aa%aa" aria-label="這個段落的連結"></a></h2>
<p>有了 Facebook 爬蟲的經驗後，我相信這篇大家是有能力獨自完成的，<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-完成-ig-自動登入">1. 完成 IG 自動登入<a class="h-anchor" href="#1-%e5%ae%8c%e6%88%90-ig-%e8%87%aa%e5%8b%95%e7%99%bb%e5%85%a5" aria-label="這個段落的連結"></a></h3>
<p>1.1 分析 IG 登入頁面的元件</p>
<p>1.2 改用 css 抓取元件，完成 IG 自動登入</p>
<h3 class="h-nomark" id="2-從畫面判斷使用者是否登入成功">2. 從畫面判斷使用者是否登入成功<a class="h-anchor" href="#2-%e5%be%9e%e7%95%ab%e9%9d%a2%e5%88%a4%e6%96%b7%e4%bd%bf%e7%94%a8%e8%80%85%e6%98%af%e5%90%a6%e7%99%bb%e5%85%a5%e6%88%90%e5%8a%9f" aria-label="這個段落的連結"></a></h3>
<h3 class="h-nomark" id="3-前往-ig-帳號並取得追蹤人數">3. 前往 IG 帳號並取得追蹤人數<a class="h-anchor" href="#3-%e5%89%8d%e5%be%80-ig-%e5%b8%b3%e8%99%9f%e4%b8%a6%e5%8f%96%e5%be%97%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8" aria-label="這個段落的連結"></a></h3>
<p>3.1 了解響應式網頁對爬蟲的影響</p>
<p>3.2 設定瀏覽器開啟時的視窗大小</p>
<p>3.3 完成前往 IG 帳號並取得追蹤人數的程式</p>
<hr>
<h2 class="h-nomark" id="1-ig-自動登入">1. IG 自動登入<a class="h-anchor" href="#1-ig-%e8%87%aa%e5%8b%95%e7%99%bb%e5%85%a5" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-分析-ig-登入頁面的元件">1.1 分析 IG 登入頁面的元件<a class="h-anchor" href="#11-%e5%88%86%e6%9e%90-ig-%e7%99%bb%e5%85%a5%e9%a0%81%e9%9d%a2%e7%9a%84%e5%85%83%e4%bb%b6" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>我在反覆使用 <a href="https://www.instagram.com/accounts/login">IG 登入</a> 時發現<mark>登入的畫面偶爾會長不一樣</mark>(如下圖)導致Xpath路徑錯誤</p>
<table>
  <thead>
      <tr>
          <th>版本1</th>
          <th>版本2</th>
      </tr>
  </thead>
  <tbody>
  </tbody>
</table>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-01.webp" width="1400" height="1267" alt="Imgur" fetchpriority="high" decoding="async"></p>
<p>|</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-02.webp" width="1400" height="1267" alt="Imgur" loading="lazy" decoding="async"></p>
</li>
<li>
<p>先<mark>進入開發者模式</mark>確認紅框內元件的特徵</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-03.webp" width="1400" height="1267" alt="Imgur" loading="lazy" decoding="async"></p>
<ul>
<li>在下圖我們觀察到 <strong>電話號碼、用戶名稱或電子郵件</strong> 以及 <strong>密碼</strong> 這兩個元件的 <strong>共通點</strong>：
<ol>
<li>
<p>都是 <code>input 的 html 標籤</code></p>
</li>
<li>
<p>用 <code>name 這個 attribute</code> 來表示他們的作用</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-04.webp" width="968" height="142" alt="1.1 分析 IG 登入頁面的元件（圖 4）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-05.webp" width="946" height="142" alt="1.1 分析 IG 登入頁面的元件（圖 5）" loading="lazy" decoding="async"></p>
</li>
</ol>
</li>
<li><strong>登入按鈕</strong> 則用以下條件來取出：
<ol>
<li>
<p>使用 <code>button 的 html 標籤</code></p>
</li>
<li>
<p>用 <code>type 這個 attribute</code> 來表示他要執行的動作</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-06.webp" width="1024" height="302" alt="1.1 分析 IG 登入頁面的元件（圖 6）" loading="lazy" decoding="async"></p>
</li>
</ol>
</li>
</ul>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="12-改用-css-抓取元件完成-ig-自動登入">1.2 改用 css 抓取元件，完成 IG 自動登入<a class="h-anchor" href="#12-%e6%94%b9%e7%94%a8-css-%e6%8a%93%e5%8f%96%e5%85%83%e4%bb%b6%e5%ae%8c%e6%88%90-ig-%e8%87%aa%e5%8b%95%e7%99%bb%e5%85%a5" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>根據上面的分析後我們<mark>改用 css 抓取 IG 登入的元件</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">//填入ig登入資訊
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> ig_username_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;username&#39;]&#34;</span>)));
</span></span><span style="display:flex;"><span>ig_username_ele.sendKeys(ig_username)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> ig_password_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;input[name=&#39;password&#39;]&#34;</span>)));
</span></span><span style="display:flex;"><span>ig_password_ele.sendKeys(ig_userpass)
</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> login_elem <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.css(<span style="color:#a5d6ff">&#34;button[type=&#39;submit&#39;]&#34;</span>)))
</span></span><span style="display:flex;"><span>login_elem.click()
</span></span></code></pre></div><blockquote>
<ul>
<li>
<p>別忘記在 .env檔填寫 IG登入的帳號密碼</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><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></code></pre></div></li>
</ul>
</blockquote>
</li>
</ul>
<hr>
<h2 class="h-nomark" id="2-判斷使用者是否登入成功">2. 判斷使用者是否登入成功<a class="h-anchor" href="#2-%e5%88%a4%e6%96%b7%e4%bd%bf%e7%94%a8%e8%80%85%e6%98%af%e5%90%a6%e7%99%bb%e5%85%a5%e6%88%90%e5%8a%9f" aria-label="這個段落的連結"></a></h2>
<ul>
<li>
<p>與 FB 類似，<mark>IG 要登入之後才會有右上角功能列</mark>，這裡我使用他作為判斷登入與否</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-07.webp" width="1024" height="1013" alt="2. 判斷使用者是否登入成功（圖 7）" 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><span style="color:#8b949e;font-style:italic">//登入後才會有右上角功能列，我們以這個來判斷是否登入
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(<span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]//*[contains(@class,&#34;_47KiJ&#34;)]`</span>)))
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 class="h-nomark" id="3-抓出-ig-追蹤人數">3. 抓出 IG 追蹤人數<a class="h-anchor" href="#3-%e6%8a%93%e5%87%ba-ig-%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="31-了解響應式網頁對爬蟲的影響">3.1 了解響應式網頁對爬蟲的影響<a class="h-anchor" href="#31-%e4%ba%86%e8%a7%a3%e9%9f%bf%e6%87%89%e5%bc%8f%e7%b6%b2%e9%a0%81%e5%b0%8d%e7%88%ac%e8%9f%b2%e7%9a%84%e5%bd%b1%e9%9f%bf" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>響應式網頁設計(RWD)是什麼？</strong>：網頁會根據瀏覽器解析度的不同重新排版，同一個網頁你在手機、平板、電腦看到的畫面配置都是不一樣的</li>
<li>如果你跟我一樣充滿實驗精神，你會發現 <mark>IG 的頁面會隨著螢幕寬度更改而重新排版</mark>，Xpath 的路徑也會跟著改變
<ul>
<li>
<p><strong>寬螢幕</strong> Xpath</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-08.webp" width="640" height="94" alt="Imgur" loading="lazy" decoding="async"></p>
<pre tabindex="0"><code>//*[@id=&#34;react-root&#34;]/section/main/div/header/section/ul/li[2]/a/span
</code></pre></li>
<li>
<p><strong>窄螢幕</strong> Xpath</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-09.webp" width="640" height="71" alt="Imgur" loading="lazy" decoding="async"></p>
<pre tabindex="0"><code>//*[@id=&#34;react-root&#34;]/section/main/div/ul/li[2]/a/span
</code></pre></li>
</ul>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="32-設定瀏覽器開啟時的視窗大小">3.2 設定瀏覽器開啟時的視窗大小<a class="h-anchor" href="#32-%e8%a8%ad%e5%ae%9a%e7%80%8f%e8%a6%bd%e5%99%a8%e9%96%8b%e5%95%9f%e6%99%82%e7%9a%84%e8%a6%96%e7%aa%97%e5%a4%a7%e5%b0%8f" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>為了避免因為螢幕寬度不一樣造成 Xpath 不同，所以我們要<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">// 建立browser的類型
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> driver <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> <span style="color:#ff7b72">new</span> webdriver.Builder().forBrowser(<span style="color:#a5d6ff">&#34;chrome&#34;</span>).withCapabilities(options).build();
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">//考慮到ig在不同螢幕寬度時的Xpath不一樣，所以我們要在這裡設定統一的視窗大小
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> driver.manage().window().setRect({ width<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1280</span>, height<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">800</span>, x<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">0</span>, y<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">0</span> });
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 class="h-nomark" id="33-完成前往-ig-帳號並取得追蹤人數的程式">3.3 完成前往 IG 帳號並取得追蹤人數的程式<a class="h-anchor" href="#33-%e5%ae%8c%e6%88%90%e5%89%8d%e5%be%80-ig-%e5%b8%b3%e8%99%9f%e4%b8%a6%e5%8f%96%e5%be%97%e8%bf%bd%e8%b9%a4%e4%ba%ba%e6%95%b8%e7%9a%84%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p>在固定螢幕寬度後，使用 Xpath 就能穩定的抓出帳號追蹤人數</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">const</span> fanpage <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;https://www.instagram.com/baobaonevertell/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> driver.get(fanpage)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> driver.sleep(<span style="color:#a5d6ff">3000</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;<span style="color:#8b949e;font-style:italic">//這是紀錄IG追蹤人數
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_trace_xpath <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`//*[@id=&#34;react-root&#34;]/section/main/div/header/section/ul/li[2]/a/span`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> ig_trace_ele <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> driver.wait(until.elementLocated(By.xpath(ig_trace_xpath)))
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// ig因為當人數破萬時文字不會顯示，所以改抓title
</span></span></span><span style="display:flex;"><span>ig_trace <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> ig_trace_ele.getAttribute(<span style="color:#a5d6ff">&#39;title&#39;</span>)
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">`追蹤人數：</span><span style="color:#a5d6ff">${</span>ig_trace<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>driver.quit();
</span></span></code></pre></div></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>在專案資料夾的終端機(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>你會看到 Instagram 自動登入 → 跳轉到指定帳號 → 關閉</p>
</li>
<li>
<p>確認輸出正確的粉專追蹤人數你就成功嚕～</p>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-10.webp" width="1024" height="133" alt="執行程式（圖 10）" loading="lazy" decoding="async"></p>
</li>
</ol>
<p>到目前為止我們已經可以抓出 FB &amp; IG 粉專的追蹤人數了，充滿好奇心的讀者可以先試著看看利用爬蟲爬完 FB 粉專後繼續爬 IG</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/day11">這裡</a>找到喔</li>
<li>我也貼心地把昨天的把昨天FB爬蟲的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day10_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>調整你.env檔填上IG登入資訊</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>
<ol>
<li><a href="https://stackoverflow.com/a/49940401">Filling in login forms in Instagram using selenium and webdriver (chrome) python OSX</a></li>
</ol>
<blockquote>
<p><em>免責聲明:文章技術僅抓取公開數據作爲研究，任何組織和個人不得以此技術盜取他人智慧財產、造成網站損害，否則一切后果由該組織或個人承擔。作者不承擔任何法律及連帶責任！</em></p>
</blockquote>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202009/fb-ig-crawler-day-11/images/fb-ig-crawler-day-11-11.webp" width="1024" height="233" alt="參考資源（圖 11）" loading="lazy" decoding="async"></p>
<p><strong>在許多人的幫助下，本系列文章已出版成書，並添加了新的篇章與細節補充：</strong></p>
<ul>
<li>加入更多實務經驗，用完整的開發流程讓讀者了解專案每個階段要注意的事項</li>
<li>將爬蟲的步驟與技巧做更詳細的說明，讓讀者可以輕鬆入門</li>
<li>調整專案架構
<ul>
<li>優化爬蟲程式，以更廣的視角來擷取網頁資訊</li>
<li>增加資料驗證、錯誤通知等功能，讓爬蟲執行遇到問題時可以第一時間通知使用者</li>
<li>排程部分改用 node-schedule &amp; pm2 的組合，讓讀者可以輕鬆管理專案程序並獲得更精確的 log 資訊</li>
</ul>
</li>
</ul>
<p><strong>有興趣的朋友可以到天瓏書局選購，感謝大家的支持。</strong></p>
<p><strong>購書連結</strong>：<a href="https://www.tenlong.com.tw/products/9789864348008">https://www.tenlong.com.tw/products/9789864348008</a></p>
</blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
