Build a Social Listening Tool Yourself

用 R 語言打造一個社群聆聽工具吧!

你也可以在我新建的medium publication上讀到這篇文章

1 Introduction

  我們都習慣在不同社群網站上晃來晃去,除了點讚或留言以外,有時候甚至還會花時間打一篇文章。對使用者來說這些寫文字沒什麼特別的,只是日常生活中講話以外,另外一種互動的形式而已,但是對於商人來說,這些數位足跡的價值無限!社群聆聽就是一個新商機的重要例子,它的意義很簡單,就是聆聽社群上的使用者都在談論什麼。

  對社群小編來說,掌握當下正熱的話題,寫文案或下標題時才能跟上時事;對品牌經營者來說,可以從社群端了解顧客的用戶輪廓,進而調整並優化行銷策略;對投放廣告的業主來說,若找來 KOL 宣傳產品,沒有追蹤宣傳成效的正確方法很難向老闆交代;對客服人員來說,若能夠找到對產品有疑慮或者不滿意服務的消費者,主動解決問題可以大幅提升顧客滿意度;對公關人員來說,實時監控社群並且在負評擴散前介入,有機會避免大型公關危機爆發。

QSearch 盤點2019上半年美妝 KOL 聲量 / 圖片來源: [QSearch官網](https://blog.qsearch.cc/2019/07/%E5%A6%82%E4%BD%95%E6%89%BE%E5%87%BA%E6%9C%80%E9%81%A9%E5%90%88%E7%9A%84%E7%A4%BE%E7%BE%A4%E7%BE%8E%E5%A6%9D%E4%BB%A3%E8%A8%80%E4%BA%BA%EF%BC%9F%E7%9B%A4%E9%BB%9E2019%E4%B8%8A%E5%8D%8A%E5%B9%B4)

Figure 1.1: QSearch 盤點2019上半年美妝 KOL 聲量 / 圖片來源: QSearch官網

  就如同上面舉的多樣例子,社群聆聽不只讓你可以知道現在社群上正在談論什麼,更能夠讓你在掌握趨勢後主動出擊,無論是政治宣傳的網軍,或是商業常見的寫手,都是口碑操作的具體呈現。

  以台灣本土廠商開發的社群聆聽工具而言,除了專精於 Facebook 的 QSearch 公司以外,有另外兩間比較有名的廠商,號稱可以監控全通路/頻道的網站,包含新聞類型以及社群網站類型,如 Facebook, Ptt, Youtube, mobile01, babyhome, 伊莉等。這兩間公司分別是意藍資訊 eLand 與大數聚,意藍開發了 OpView,而旗下擁有 dailyveiw 網路溫度計的大數聚則開發了 keypo。

大數聚旗下 DailyView 網站上對不同網站的追蹤 / 圖片來源: [DailyVeiw官網](https://dailyview.tw/HotArticle)

Figure 1.2: 大數聚旗下 DailyView 網站上對不同網站的追蹤 / 圖片來源: DailyVeiw官網

利用意藍產品 OpView 產出的洞察報告 / 圖片來源: [OpView官網](http://www.social-lab.cc/2019/03/social-insights/women/11142)

Figure 1.3: 利用意藍產品 OpView 產出的洞察報告 / 圖片來源: OpView官網

  這些社群聆聽工具會將各個通路/頻道的討論抓下,客戶再依照需求選擇適合的方案。最常見的方案是監控特定關鍵字,另外依照預算多寡,有些客戶會購買額外的加值功能,或者購買語意分析的相關服務。另外,因為台灣用戶習慣使用 Facebook 與 PTT ,所以國內廠商目前多半並未監控 twitter ,但 twitter 是國外的社群聆聽工具的重要社群資訊來源。

  雖然社群聆聽有很多好處,但是對新創公司或是中小企業來說,有可能因為預算問題難以負擔收費,如果自己的公司沒有錢,又或者苦於蒐集不到社群趨勢怎麼呢?如果你會R語言的話(當然 python 也可以),就算沒有錢錢購買專業產品,也可以做到社群聆聽。底下會從一個品牌端用戶的角度出發,試著用 R 寫出一個簡單的社群聆聽工具,這就是寫這篇文章希望達成的目標。

2 User Story

  在埋頭爆寫 code , 徒手刻出華麗的爬蟲之前(雖然會被來自IB的大神 Edward 電爆,希望 Edward 不會發現),還是要回到用戶的角度,想想對使用者來說,她們會重視什麼功能?使用情境會是什麼?希望能夠用這個產品滿足什麼需求?

  我們可以先遵循產品開發的步驟,從使用者的需求出發,列出 user story,設計對應的 feature 後再進行開發。想像一下,有一位經營潮流服飾品牌的老闆小翔,因為小翔的品牌鎖定年輕人客群,所以沒事的時候小翔就會打開 Dcard 跟 Ptt ,看有沒有人在討論自己賣的衣服。因為小翔的品牌不大,生意好的時候三四篇討論就是極限了,生意普通的時候甚是一天不到一篇。所以對小翔來說,她可能至少會想知道下列的事情:

  • 了解社群像是 Ptt 或 Dcard 上,大家怎麼討論自己的品牌
  • 若社群上出現針對店家服務或產品的負評,能夠及時上去回應
  • 競爭的品牌都怎麼經營社群,社群活動宣傳的成效又大概怎樣

如果小翔的品牌成長到 GU, ZARA 那種規模,她可能還會想知道:

  • 想知道跟服飾有關的熱門討論都在講些什麼
  • 因為擔心自己跟不上年輕人想法,想知道年輕人的話題

  雖然不是專業的工程師,但我們知道自己的精力有限,在開發功能時需要注重優先順序(prioritized)。小翔的需求也有輕重緩急,作為品牌經營者,小翔最迫切想要知道的就是社群上品牌討論,希望可以每天更新。如果時間充足的話,可以另外監控競品與服飾領域的關鍵字;想知道年輕人的特性還有話題,可以將觀察的對象放大到全站,不侷限在服飾版或購物板當中。

  上面的需求有些很基本,像是關鍵字監控,我們可以視開發時間調整規模;而有些需求則相對獨立,像是每日更新監控的流程。這些需求有些類似,可以視為任務相同規模不同,有些需求則屬於完全不同領域。在開發時若有隊友可以一起討論該怎麼排序,也可以詢問顧客的需求,但值得注意的是,有時候顧客說的不一定是他真正的需求,有功力的 product owner 或 researcher 常常能夠顧客挖出客戶沒說出口的願望。

最近training時講師學長介紹的產品開發寓言 / 圖片來源: [tree swing](http://www.projectcartoon.com/cartoon/1111)

Figure 2.1: 最近training時講師學長介紹的產品開發寓言 / 圖片來源: tree swing

  理解商業需求以後,接下來要把用戶的語言轉化成開發時的具體敘述。上面的需求可以拆解成下面三項: a.品牌相關詞監控 b.定時更新 c.提醒。

  確認要開發的功能後,接下來就要開始規劃任務並且排程。每個工程師團隊都會有不同的開發時程與分工方法。但因為每次期末報告時都沒有組員,現在當然也找不到人分工,為了加快效率,我把待開發的功能切成模組(module)後分工,也就是所謂模組化的概念。底下會先以 Dcard 為例子,進行簡單的任務拆分,其他網站像是 Ptt, mobile01 等皆可以比照辦理,我也會補充說明不同類型的社群網站/論壇要用什麼方法處理比較好。

具體拆分如下:

  1. 將品牌相關關鍵字(如 GU, Zara 等)丟到 Dcard 搜尋頁面
  2. 抓下搜尋頁面的文章資訊作為索引(index)
  3. 利用索引連結抓下對應到文章的內文與回應
  4. 分別整理索引,主文,回文的資料表,依照需求連結不同資料
  5. 將爬下的資料上傳到 google spreadsheet
  6. 寄信給老闆小翔通知有什麼新的品牌相關討論
  7. 每天都可以自動更新
開始寫程式前的流程筆記

Figure 2.2: 開始寫程式前的流程筆記

  其實在規劃的時候沒辦法一下就列好上面的步驟,因為使用者端的需求並不是線性的,要將用戶的需求映射(mapping)到開發的流程中。以每天自動更新而言,這是老闆小翔一開始最重視的功能,如果還要花時間執行爬蟲程式,小翔就不用顧店了,但是在寫程式的時候,定期排程(schedule)的任務(task)已經在很後面一步了,所以開發人員一定要花時間畫好流程圖!這是自己最大的體悟。

3 code

  底下就是將每一個小任務的程式了,平常使用R語言時,我主要使用的是tiydyverse系統下的套件,包含dplyr, tidyr, ggplot2, magrittr等,而爬蟲部分則是以 Hadley Wickam 開發的 rvest為主。

3.1 Choose keywords needed then search on Dcard

  以 Dcard 為例子,第一步使用rvest套件中的read_html,將品牌相關關鍵字丟到 Dcard 搜尋頁面後,讀取了 Dcard 搜尋頁面的網址資訊。

Dcard Search Bar 長相

Figure 3.1: Dcard Search Bar 長相

  

### Load needed package
library(tidyverse)
library(lubridate)
library(rvest)
library(httr)

### Choose keywords needed then search on Dcard
url_raw <- "https://www.dcard.tw/search/general?query=zara"

### using rvest::read_html() to parse the raw url
html_raw <- url_raw %>% read_html()

3.2 Parse raw html to create a index dataframe

  抓下搜尋頁面後,利用 css selector 確認要抓的節點位置,若對 html, css 有基本認識,也可以利用開發者工具直接檢視原始碼,看自己的目標節點有哪些。為了讓用戶可以快速跟上社群討論,我抓下了文章標題、連結、看板位置、日期、作者回應與愛心數量、摘要等,接下來再將上述欄位合併成資料表,並進行簡單的資料清理(data cleansing),最後呈現出整理過後的資料,作為後續進一步抓取文章內容與回應的索引。

### Use rvest:html_nodes() to parse different nodes of raw html
index_title <- html_raw %>% html_nodes(".PostEntry_unread_2U217-") %>% html_text()
index_link <- html_raw %>% html_nodes(".PostEntry_root_V6g0rd") %>% html_attr("href")
index_board <- html_raw %>% html_nodes(".PostEntry_forum_1m8nJA") %>% html_text()
index_date <- html_raw %>% html_nodes(".PostEntry_published_229om7") %>% html_text()
index_author <- html_raw %>% html_nodes(".hSAyoj") %>% html_nodes(".PostAuthor_root_3vAJfe") %>% html_text()
index_meta <- html_raw %>% html_nodes(".PostEntry_meta_1lGUFm") %>% html_text()
index_content <- html_raw %>% html_nodes(".PostEntry_content_g2afgv") %>% html_text()

### Bind columns then turn them to a dataframe 
df_index <- tibble(index_date,index_title,index_link,index_board,index_author,index_meta,index_content) %>%
  mutate(index_link = str_c("https://www.dcard.tw", index_link)) %>%
  mutate(index_title = str_remove_all(index_title,"\\(|\\)")) %>%
  mutate(index_title = str_remove_all(index_title,"\\(|\\)")) %>%
  mutate(index_title = str_remove_all(index_title,"\\?|\\?")) %>%
  mutate(index_title = str_remove_all(index_title,"\\[|\\〕")) %>%
  mutate(index_content = str_remove(index_content, index_title)) %>%
  mutate(index_content = str_remove(index_content, index_meta)) %>%
  mutate(index_content = str_remove_all(index_content, "\\\n")) %>%
  rename(index_excerpt = index_content) %>%
  mutate(index_date = str_replace_all(index_date, "月", "-")) %>%
  mutate(index_date = str_remove_all(index_date, "日")) %>%
  mutate(index_date = str_c(lubridate::year(as.POSIXct(Sys.time(), tz="Asia/Taipei")), index_date, sep = "-")) %>%
  mutate(index_date = ymd_hm(index_date)) %>%
  mutate(index_wday = lubridate::wday(index_date)) %>% 
  mutate(id = row_number()) %>%
  select(index_date, index_board, index_title, index_excerpt, index_author, index_meta, index_link, index_wday, id)

### Take a glimpse of the index dataframe
df_index %>% head(5)
索引資料表

Figure 3.2: 索引資料表

3.3 Use the index dataframe to crawl the articles and comments

  這個步驟視用戶需求而定,有時候上面的 index 資訊就夠了,但有些使用者沒時間點開討論串,想要一站式的服務,所以還是呈現了爬下整篇文章與回應的方法。我的主要架構是先進行爬蟲的前置作業,而後以迴圈重複解析資料,底下簡短說明一下概念:

  1. 因為爬蟲常常遇到連結死掉的問題,所以前置作業使用了purrr包的好朋友possibly(),把read_html()包在裡面,如果read_html()抓不到東西,就會變成 null ,後面解析資料的時候又使用了purrr包的另一個好朋友compact()清掉 null 的資料,這樣一來迴圈就不會被迫中斷了!傳統上都是使用 try catch 的方式應對 error 出現的情形,purrr的幾個好用函數讓我們可以更好的應對錯誤。另外前置作業也預先創建了空的 dataframe/list,等待爬蟲開始後便可以填入。

  2. 實際爬蟲主要是在迴圈裏面進行,這邊使用了purrr::map(),一次同時處理十篇文章。一開始針對可能出現的連結死掉情況做了預防措施,而後分別爬下主文與回應,最後在讓迴圈休息,因為本篇重點不是爬蟲 code 怎麼寫的效能問題,有興趣的朋友可以搜尋一下爬蟲相關資源,之前上課時老師也有提到其他爬蟲的做法譬如說使用 request/get 等方法,或是不處理 html ,著重於 JSON 等。都是可以參考的方向。特別注意一次不要爬太多文章,以免造成對方網站的負擔!

### Preceding operation: use purrr:possibly() to capture side effects
p_read_html <- possibly(read_html, otherwise = NULL)

### Preceding operation: create the dataframe/list
df_article <- tibble(article_link='', article_board='', article_title='', article_author='', article_author_school='', article_text='', article_love='', article_reply='', article_date="") %>% filter(row_number()<1)
df_comment <- list()

### Crawl: use for loop and purrr:map() to parse info
for(i in 1:2) {
  
  # Create counter variable
  j = (i*10) - 9
  k = j + 9
  
  # Use p_read_html() and map() to parse the raw url
  html_raw = df_index[j:k,] %>% pull(index_link) %>% 
    map(function(x){x %>% p_read_html()}) %>% 
    set_names(pull(df_index[j:k,"id"])) %>% compact()
  
  # Use is.na() to avoid error such that the content have been removed
  html_raw_index <- html_raw %>% 
    map(function(x){x %>% html_nodes(".Post_title_2O-1el") %>% html_text()}) %>%
    map_lgl(function(x){!is.na(x)})
   
  html_raw_f <- html_raw[html_raw_index]
  
  # Parse the article part
  article_id <- names(html_raw_f)
  article_title <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".Post_title_2O-1el") %>% html_text()})
  article_author <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".PostHeader_uid_3g_pzg") %>% html_text()}) %>%
    map(function(x){if(length(x)==0) x = NA_character_ else(x)})
  article_author_school <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".PostHeader_author_3AAMDh .PostAuthor_root_3vAJfe") %>% html_text()}) %>%
    map(function(x){if(length(x)==0) x = NA_character_ else(x)})
  article_board <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".Post_forum_1YYMfp") %>% html_text()})
  article_text <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".Post_root_23_VRn") %>% html_text()})
  article_love <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".jTOuHc") %>% html_text()})
  article_reply <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".DJrdA") %>% html_text()})
  article_date <- html_raw_f %>% 
    map(function(x){x %>% html_nodes(".Post_date_2ipeYS") %>% html_text()})
  
  df_article_tmp <- tibble(article_board=unlist(article_board),
                           article_title=unlist(article_title),
                           article_author=unlist(article_author),
                           article_author_school=unlist(article_author_school),
                           article_text=unlist(article_text),
                           article_love=unlist(article_love),
                           article_reply=unlist(article_reply),
                           article_date=unlist(article_date),
                           article_id = article_id)
  
  # Parse the comment part
  comment_floor <-  html_raw_f %>% 
    map(function(x){x %>% html_nodes(".CommentEntry_floor_VtDUyr") %>% html_text()})
  comment_author_school <-  html_raw_f %>% #.PostAuthorHeader_author_3O30xu span
    map(function(x){x %>% html_nodes(".CommentEntry_author_2BO0i1 .PostAuthorHeader_author_3O30xu span") %>% html_text()})
  comment_love <-  html_raw_f %>% 
    map(function(x){x %>% html_nodes(".jnzHNd") %>% html_text()})
  comment_text <-  html_raw_f %>% 
    map(function(x){x %>% html_nodes(".CommentEntry_content_1ATrw1") %>% html_text()})
  comment_time <-  html_raw_f %>% 
    map(function(x){x %>% html_nodes(".CommentEntry_date_2E4vYF") %>% html_text()})
  
  df_comment_tmp <- pmap(list(comment_floor,comment_author_school,comment_text,comment_time),function(a,b,c,d)
    {bind_cols(comment_floor=unlist(a),comment_author_school=unlist(b),comment_text=unlist(c),comment_time=unlist(d))})
  
  # Bind data from each for loop 
  df_comment <- c(df_comment, df_comment_tmp)
  df_article <- df_article %>% bind_rows(df_article_tmp)
  
  # Update status and let the crawler stop
  print(i)
  tmsleep <- sample(3:5,1)
  Sys.sleep(tmsleep)
}

### Close unnecessary connections
#closeAllConnections()
#gc()

3.4 Join the article and comments by index ID

  上個步驟抓下了主文與回文,也就是文章內容本身與底下留言的樓層,這個步驟則是要合併(data joining)主文回文兩個資料表。因為回文而是由每篇文章所串成的的雙層list,並非data frame的形式,因此在資料合併前要先將回文list合併成單一的dataframe。我使用了dplyr::bind_rows(),將回文list轉換成以list name命名,帶有新欄位id的dataframe。
  

### Take a glimpse of the comments structure
df_comment %>% head(3) %>% str()

### Turn comments list into a dataframe and add a new column "id"
df_comment_bind <- df_comment %>% bind_rows(.id = "id")

  接下來就可以開始合併資料表了,主文的article_id 與回文的id都來自index資料表的id欄位,因此合併時我們使用了dplyr::left_join(),並指定by = c("article_id"="id")),對SQL熟悉的朋友一定會發現這就是 select * from a left join b on a.article_id = b.id;。最後可以看一下完整的資料表df_full與索引資料表df_index兩者的差別。
  

### Join the article dataframe and the comments dataframe
df_full <- df_article %>% left_join(df_comment_bind, by = c("article_id"="id"))

### Check the output
df_full %>% tail(5)

### Compare with the index dataframe
df_index %>% tail(5)
文章與留言合併後資料表

Figure 3.3: 文章與留言合併後資料表

3.5 Upload data from local files to google spreadsheet

  將資料清理並合併後,下一步要將資料輸出。除了 google spreadsheet 以外,視需求也可輸出成 csv/excel/txt 檔案,這裡著重在容易共享並直接更改的 google drive。將資料從本地端上傳到雲端有三個常用的套件,分別是googlesheet, googlesheet4以及googledrivegooglesheet專門針對 google spreadsheet 所設計,有很多實用的函數,譬如說將資料表上傳到特定分頁,或者更改某個儲存格的內容,操作上可以非常精細,但因為開發套件的作者現在致力於開發新版更好用的套件googlesheet4,所以googlesheet就沒在維護了。

  也因為如此,我現在都使用googledrive,它的亮點是可以靈活的處理 google drive 裡面的資料,包含搜尋、 創建檔案、更動檔案位置等等。我會用到的功能是把輸出到本地硬碟的 csv 檔案上傳到 google spreadsheet 當中。

  因為小翔希望每天早上十點可以看到 Dcard 上的新內容,所以我們結合了dplyr::mutate()與常見的if_else(),把發文時間晚於前一天早上十點的文章都貼上”new”的標籤。貼標後再上傳至 google drive上,小翔若點進 google spreadsheet,可以優先查看標示為”new”的文章。在上傳前要先建立好空的資料夾與 google spreadsheet,接著利用googledrive::drive_get()讀取 google spreadsheet,再以googledrive::drive_get()更新。

library(googledrive)

### Label "new" if the articles were posted later than 22:00 yesterday
df_index_newtag <- df_index %>%
  mutate(new_tag = if_else(index_date >= as.POSIXct(ymd_hms(str_c(ymd(Sys.Date()-1), "10:00:00")), tz="Asia/Taipei"), "new", "old")) %>%
  mutate(hyperlink = str_c('=HYPERLINK("',index_link,'","',index_title,'")')) %>%
  select(-id)

### Export data to the local disk
df_index_newtag %>%
  add_row(index_date = ymd_hms(str_c(ymd(Sys.Date()), "10:00:01")),index_board = "=now()", index_title ='=IMPORTRANGE("https://docs.google.com/spreadsheets/OOO/","dcard_new!A2")') %>%
  arrange(desc(index_date)) %>%
  select(-index_wday) %>%
  write_csv("df_article.csv")

### Pre create a file "cralwer" and a empty gsheet "crawler_dcard"
### Get gsheet from gdrive and update new csv
# gd_crawler_dcard <- drive_get("~/crawler/crawler_dcard")
# gd_crawler_dcard %>% drive_update(media = "df_article.csv")

3.6 Write E-mail to the user as a notification for new articles

  這邊我使用的是gmailr套件,具體操作可以參考這篇教學。在R裡面寫信不只是為了潮而已,gmailr厲害之處在於可以一次發送大量客製化信件給不同人。想像你是一位常常需要批改大量作業並告訴同學分數的老師,或者是需要寄送大量客製化邀請的 EDM 給不同廠商的公關專員,這些需求都可以在 R 中做到!      實際寄信前需要參考教學,先在 Google Developers Console 申請 API 權限後才能得到信箱的存取權,但過程並不會很難,建議可以將 chrome 語言設定成英文版本,方便對照。

library(gmailr)
library(glue)
library(tableHTML)

### Below is a demo code since it needs credentials to run

### Read the credentials
use_secret_file("gmailr-zara.json")

### Filter "new" articles
df_index_email <- df_index_newtag %>%
  filter(new_tag == "new") %>%
  select(-c(new_tag, index_wday)) %>%
  mutate(hyperlink = str_sub(index_link, 1, str_locate(index_link, "-")[,1]-1))

### Turn the articles dataframe into a html table
msg = tableHTML(df_index_email)
html_bod <- str_c("<p> Good Morning! There are ",dim(df_index_email)[1], " articles total. </p>", msg)

### Send the email
mime() %>%
  to("MKT_HYHsiang@gmail.com") %>%
  from("BOSS_HYHsiang@gmail.com") %>%
  subject(str_c("ZARA_morning_monitor_", ymd(Sys.Date()))) %>% 
  html_body(html_bod) %>% 
  send_message()
收到 R 自動寄出的信,這邊呈現了以黃底標註的uqiqlo關鍵字監控

Figure 3.4: 收到 R 自動寄出的信,這邊呈現了以黃底標註的uqiqlo關鍵字監控

3.7 Update the results on a daily basis

  因為工作原因,現在暫時使用蘋果電腦,所以我使用的排程套件是cronR,可以參考套件官方網站cronR的好處是提供 Rtudio 當中的 add in,不用把函數背起來。

cronR add in 在 Rstudio 中的位置

Figure 3.5: cronR add in 在 Rstudio 中的位置

  如下圖所示,cronR的圖形化介面中分為兩部分,一部分是創建新的任務,一部分是管理現存的任務。使用者在創建新任務時需要填入待執行的 R script 檔案位置、開始時間、排程週期等,其中排程週期最為重要,可以設定一次性的工作,或是每分鐘/每小時/每日執行,若要以更複雜的方式排程,譬如說每周一三五定期執行程式碼,可以參考cronR::cron_add(),這個函數可以提升使用者排程的自由度。

cronR 的 menu

Figure 3.6: cronR 的 menu

  有另一個值得一提的案例,因為公關業的 agency 要幫 in-house 做每日新聞監測,可能會有早報與午報的需求,譬如說早上十點跑一次下午五點又跑一次,這時候要做的不是研究函數要怎麼實現這麼複雜的需求,最簡單的做法就是設定兩個任務,分別在對應的時間執行就好。

  在使用cronR之前,需要先安裝下列幾個套件才能順利運作: shiny, miniUI, shinyFiles,如果擔心沒辦法上手套件用法的話,可以參考這個影片。另外我自己在蘋果電腦上執行的時候,遇到了一段錯誤碼:“crontab: tmp/tmp.X: Operation not permitted”,後來在這裡找到了答案,基本上就是要開權限給 terminal,如果還是不行的話可以再開權限給 R 以及 Rstudio。若是 windows 系統,則是使用 task scheduler,可以參考教學

4 Conclusion

4.1 Additional work for future

  前面有提到,對老闆小翔來說,還有許多待解決的需求,我在底下列出未來可以改進的方向,以及動手前確定可以實踐的具體方法。

  • 有些品牌不只有一個關鍵詞,以服飾品牌 UQ 為例就有 uniqlo, UQ, 優衣褲等
  • 現在只有看一個品牌,但其他競爭的品牌討論也很重要
  • 除了 Dcard 以外,還想看 Ptt, mobile01 等網站的相關討論
  • 不想限於服飾,想看到宏觀趨勢

  針對上述需求,底下提出對應的解方。

  • 不只一個關鍵詞:以 Dcard 為例,可以在搜尋頁面打不同關鍵詞,抓下來後再將資料表合併,若擔心重複可以活用 dplyr::distinct()
  • 多看幾個品牌:這個也很簡單,多設其他品牌關鍵字,合併時可以增加品牌的欄位
  • 多看幾個網站:mobile01 本身也有提供站內搜尋的功能,所以可以用跟 Dcard 一樣的方法爬下資料。Ptt 比較麻煩,沒有全站通用的搜尋,這時候可以考慮抓取有搜尋功能的 Ptt 備份網站,或者是先鎖定特定幾個相關版面再寫爬蟲。
  • 宏觀趨勢:這時候不需要使用關鍵字,而是改抓每個版面或泉站的熱門文章,只有目標改變但方法不變。

4.2 A short review

  在這篇文章當中,我依照品牌端使用者小翔的需求,使用 R 語言打造了一個能夠做到社群聆聽資料自動上傳每天更新的小工具,若有什麼建議或是希望增加的功能都可以留言跟我說,希望你會喜歡!特別感謝 Samuel 和 jilung 告訴我排程的相關資訊,以及 WB 給我的靈感。

Dennis Tseng
Dennis Tseng
An analyst & R lover

Use data to solve problems, tell stories, and change the world