今天來分享一個可以從網路獲取資料的技術:叫「網路爬蟲」,英文稱做Web Crawler or Web Scapying,以下簡稱爬蟲,這篇文章將會分成「一、原理介紹」以及「二、程式實作」,如果有基本概念的朋友們,可以直接跳至「二、程式實作」開始,我將會以iPeen愛評網(以下簡稱iPeen)作為範例用Python來實作簡單的爬蟲,希望透過此篇入門介紹,能讓大家都會寫基本的爬蟲,抓到你想要的資料。

一、基本概念

爬蟲是一個將網路上的檔案下載下來然後做一些處理的程式,一般我們在網路上看到的網頁、圖片或影片等,都算是一個檔案,只是透過瀏覽器我們可以看到正確的結果,例如:網頁檔案本身是一個HTML檔案,瀏覽器幫我們轉成我們看得懂的網頁介面(如下圖所示,操作方式為在Chrome按右鍵的[檢視網頁原始碼])。

為何要講這個呢?
因為爬蟲所要處理的,是沒有透過瀏覽器處理的HTML原始碼 ,當我們看到網頁有我們要抓的資料之時(以iPeen為例,我們要抓店家的資訊,如下圖,操作方式為在網頁上游標移至店家名稱連結,然後按下右鍵按[檢查])

我們必須知道這些在網頁上我們所看到的店家名稱是在HTML原始碼所對應的位置(如下圖),必須看看這些這些程式碼有沒有什麼樣的規律或特徵,以剛剛所講述的店家網址名稱為例,我們可以看出店家連結是<a>標籤,擁有 data-label=”店名” 獨特可以找出店家連結的CSS屬性。

爬蟲兩大工作

  1. 下載檔案:在給定一個網址時,我們可以使用HTTP協定的方法去對於伺服器送出Request請求,並且取得Response回應,在我們爬蟲一般的情況就是對於伺服器送出GET Request來取得HTML檔案的Response,在取得HTML檔案後接著我們就可以進行第二步驟:分析內容。
  2. 分析內容:擁有HTML之後,我們就可以透過搜尋、正則、字串處理、切分、取代等的各種技巧來將HTML過濾抓到我們所要的資料,例如上述例子所提到的店家連結標籤,可以用正則找到<a>標籤,然後用搜尋過濾留下CSS包括data-label=”店名” 的標籤,然後再用正則去除標籤只留下連結和店家名稱。

二、程式實作

首先,我們先安裝必要的第三方程式套件,有了這些套件加上Python本身提供的套件可以加速我們的開發工作,這邊我們使用Python3來開發,上述爬蟲兩大工作「下載檔案」是使用Requests來實作,而「分析內容」是使用BeautifulSoup來實作,這兩個套件安裝方式可以參考官網,不再文章詳述,讓我們開始吧!

我們的目標是抓下iPeen的美食店家資訊,在我們開始寫程式之前,我們必須大致分析網頁結構,知道我們所要的資料是在哪一些頁面,爬蟲要從哪裡開始、有哪些網頁要爬、爬蟲要怎麼到那些網頁等,所以先在iPeen先找到可以抓店家列表的網頁作為爬蟲起始點,這邊我們找到「http://www.ipeen.com.tw/search/taiwan/000/1-0-0-0/」具有店家列表可以作為爬蟲起始點,從此頁面取得各店家的資訊頁面連結後,在個別進入每個店家頁面抓店家資訊。

開頭我們先寫一個方法從起始點抓到所有的店家資訊頁面連結,並且儲存到列表中,程式碼如下(完整版在 commit (6741fd)[https://github.com/enginebai/iPeenCrawler/commit/6741fde67ea829544c01712d9f8aa76625789135]):

def get_shop_link_list():
    list_req = requests.get(LIST_URL)
    if list_req.status_code == requests.codes.ok:
        soup = BeautifulSoup(list_req.content, HTML_PARSER)
        shop_links_a_tags = soup.find_all('a', attrs={'data-label': '店名'})

        shop_links = []
        for link in shop_links_a_tags:
            shop_link = ROOT_URL + link['href']
            print(shop_link)
            shop_links.append(shop_link)
            parse_shop_information(shop_link)

我們先使用 requests.get() 來下載HTML程式碼 list_req.content 並且傳入 BeautifulSoup() 作為分析來源,然後我們可以用BeautifulSoup.find_all(‘a’, attrs={‘data-label’: ‘店名’})
找到所有CSS為 ‘data-label’: ‘店名’<a> 標籤。

接著,我們針對每個店家頁面去分析出詳細資訊,程式碼如下(完整版在 commit fd6435a):

def parse_shop_information(shop_link):
    shop_id = re.sub(re.compile(r'^.*/' + SHOP_PATH), '', shop_link).split('-')[0]
    print(shop_id)

    req = requests.get(shop_link)
    if req.status_code == requests.codes.ok:
        soup = BeautifulSoup(req.content, HTML_PARSER)
        shop_header_tag = soup.find('div', id='shop-header')
        name_tag = shop_header_tag.find('span', attrs={'itemprop': 'name'})
        print(re.sub(SPACE_RE, '', name_tag.text))
        category_tag = shop_header_tag.find("p", class_={'cate i'})
        print(re.sub(SPACE_RE, '', category_tag.a.text))
        address_tag = shop_header_tag.find('a', attrs={'data-label': '上方地址'})
        print(re.sub(SPACE_RE, '', address_tag.text))

        gps_str = address_tag['href']
        # print(gps_str)

        gps_str = re.search('/c=(\d+.\d*),(\d+.\d*)/', gps_str).group().replace('/', '')
        # print(gps_str)

        lat = gps_str.split(',')[0]
        lng = gps_str.split(',')[1]
        print(lat.split('=')[1], lng)

這邊我們可以看到用 BeautifulSoup.find() 再搭配正則 re.search() / re.sub() 和字串操作 string.split() / string.replace() 就可以取得店家的名稱、分類、地址、GPS(分析取得字串方法不只一種,你可以寫的跟我不同沒關係,重點在於可以有效且正確分析出字串即可)。

大功告成,所有完整程式碼在我的GitHub上,歡迎下載使用,也多用星星來支持。

FAQ

  1. 寫爬蟲有一個問題很多人會來問我「奇怪,我的網頁程式碼有看到,為何爬蟲卻無法抓到?」,這個原因很簡單,因為你看到的是「假的」(無誤),是因為網頁內容是動態產生的(以用Javascript產生的內容為例),你所看到的是經過瀏覽器已經將Javascript執行後產生的結果,而一般爬蟲預設是沒有執行這些Javascript,所以當然抓不到,我們要如何判別呢?在Chrome當中如果是右鍵按下[檢視網頁原始碼],則你所看到的HTML是沒有動態產生內容的,也就是我們一般爬蟲預設抓到的資料;在網頁上游標移動到其中一個元素按右鍵點[檢查]所看到的HTML內容則是經過動態產生的結果,一般爬蟲沒有特別處理是無法得到這些內容的。 這問題有沒有解?答案是有的,需要用其他方式例如讓爬蟲先模擬瀏覽器取得動態產生內容後,再去取得分析HTML的方式解決。
  2. 爬蟲是否能抓到一個需要登入或驗證後的網頁資訊?答案是可以的,但前提是你要知道該網頁怎麼登入、怎麼驗證身份,接著像瀏覽器一樣用 Session / Cookie 將登入或驗證後的資料儲存一起帶入到須登入驗證的網頁中去爬。
  3. 有時候網頁會回傳 HTTP 429 Too Many Requests的錯誤,我該如何處理?會出現這錯誤表示你存取得太頻繁,每一個爬蟲都是一個Request,對於伺服器都是一個資源損耗,有些伺服器會擋甚至你存取太快會視為攻擊的一種(類DDOS),正確的解法是調整爬蟲抓網頁的速度,可以設定延遲機制來預防。

後記

附上我在Android讀書會所分享「Android x 網路爬蟲」的完整簡報內容:SlideShare

我是大白,Co-founder & Developer @DualCores Studio / Developer @17 Media],歡迎對於Bot開發、人工智慧、語意分析、機器學習有興趣的朋友多跟我一起交流學習,接下來會更專注分享相關的題目。

最近聊天機器人Bot這股趨勢才慢慢要崛起,之前做了一點觀察紀錄,這次進入實作階段,首先就直接使用Facebook Message Platform做為開始。

其實Facebook官方文件寫的也蠻清楚的,只是有一些技巧要注意。

前置作業

  1. 先有一個粉絲團:粉絲團就是用來當作Bot的身份,只支援粉絲團對一般使用者聊天而已,先確定你有一個粉絲團是用來讓Bot綁定的,這邊可以選擇你既有的粉絲團或是新建立一個,如果是新建立的,粉絲團新建立完後可以先不發佈,但是如果要開始測試Bot的話,就要發佈了。
  2. 準備開發環境和專案:新建立一個Python專案,並且將我們會用到的套件:FlaskRequests安裝好。
  3. 申請一個FB應用程式:去facebook for developer建立或選擇一個應用程式,如果是新建立的話,應用程式類型選擇「網站」即可,建立完之後在主控台左側功能表找到「Messager」點下。

4.為自己的網域名稱申請SSL:Facebook要求所有Messager呼叫的API都是要走https,所以這步驟是必要的(我也覺得比較麻煩的地方),這邊我用的是Let's Encrypt來申請,依照「Installing Client Software」的步驟來進行,先用git clone把專案複製下來,然後在終端機使用certbot-auto工具來產生憑證,我是用Apache HTTP Server,所以執行

sudo ./certbot-auto --apache -d YOUR_DOMAIN_NAME.COM

YOURDOMAINNAME.COM 要換成你自己的網域名稱,不用加http://開頭,接著依照它的指示做即可。

憑證產生好後,我們需要將憑證複製到我們的API專案內,我這邊是在專案內建立一個 ssl 的資料夾,並且使用終端機切換目錄到 /etc/letsencrypt/live/YOURDOMAINNAME.COM/ (這邊 YOURDOMAINNAME.COM 記得代換成自己的網域名稱)裡面,將 fullchain.pemprivkey.pem 複製到剛剛建立的 ssl 資料夾,這等等會用到,千萬別複製錯檔案,我一開始就是複製到 cert.pem / privkey.pem,然後後面就出錯了(看下圖)。

OK,到目前為止,前置步驟算完成,我們可以開始寫程式了。


程式撰寫

程式的部分主要分成三個主要步驟:

  1. 設定Webhook做驗證。
  2. 從Facebook接收使用者的訊息。
  3. 傳遞訊息到Facebook。

1. 設定Webhook

新增一個可以接收驗證碼的HTTP GET方法,裡面的 I_AM_VERIFICATION_CODE 要換成自己的,其他 hub.* 開頭的都是Facebook既定好的,要照著寫。

@app.route(API_ROOT + FB_WEBHOOK, methods=["GET"])
def fb_webhook():
    verification_code = 'I_AM_VERIFICIATION_CODE'
    verify_token = request.args.get('hub.verify_token')
    if verification_code == verify_token:
        return request.args.get('hub.challenge')

再回到Facebook應用程式主控台,按下Setup Webhooks會出現下圖的視窗,要將你的API網址 (API_ROOT + FB_WEBHOOK) 和上面的I_AM_VERIFICATION_CODE分別輸入進去,然後勾選你要的訊息功能:

勾選訂閱項目如下:

  • message_deliveries: 訊息傳遞的相關報表。
  • messages: 最基本的訊息接收和傳遞。(最基本一定要勾選的)
  • messaging_optins: 允許使用者使用在你網站上的Send-to-Messager按鈕,並且可以授權傳遞訊息到這。
  • messaging_postbacks: 允許你傳遞除了文字之外的訊息種類,例如:按鈕和按鈕按下後的事件。

2. 接收訊息。

從Facebook傳遞過來的文字訊息JSON格式如下:

{
    "object": "page",
    "entry": [
        {
            "id": 1719080561637302,
            "time": 1463187309603,
            "messaging": [
                {
                    "sender": {
                        "id": 1176454272400076
                    },
                    "recipient": {
                        "id": 1719080561637302
                    },
                    "timestamp": 1463187309521,
                    "message": {
                        "mid": "mid.1463187309223:ae45fc642d87298297",
                        "seq": 23,
                        "text": "嗨,你好。"
                    }
                }
            ]
        }
    ]
}

可以從 entrymessaging 取得每一則訊息傳送者和內容,取得訊息程式如下,使用HTTP POST,然後去解JSON即可:

@app.route(API_ROOT + FB_WEBHOOK, methods=['POST'])
def fb_handle_message():
    message_entries = json.loads(request.data.decode('utf8'))['entry']
    for entry in message_entries:
        messagings = entry['messaging']
        for message in messagings:
            sender = message['sender']['id']
            if message.get('message'):
                text = message['message']['text']
                print("{} says {}".format(sender, text))
    return "Hi"

3. 傳遞訊息。

傳遞訊息也很簡單,只需要呼叫Graph Message API即可,程式碼如下:

def send_fb_message(to, message):
    post_message_url = 'https://graph.facebook.com/v2.6/me/messages?access_token={token}'.format(token=config.FB_TOKEN)
    response_message = json.dumps({"recipient":{"id": to}, 
                                   "message":{"text":message}})
    req = requests.post(post_message_url, 
                        headers={"Content-Type": "application/json"}, 
                        data=response_message)
    print("[{}] Reply to {}: {}", req.status_code, to, message)

裡面的 config.FB_TOKEN 是我們在前置作業新增的FB應用程式所產生的,要去應用程式的主控台去找。(config模組要另外新增喔!)

再來要綁定這個FB應用程式和你Bot的粉絲團,開啟終端機執行

curl -ik -X POST "https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=ACCESS_TOKEN"

上面的 ACCESS_TOKEN 要用剛剛的粉絲專頁存取權杖複製代換,執行完成功後會出現 {“success”: true}

最後一步,我們要啟動我們的API,加入執行API的程式:

if __name__ == '__main__':
    context = ('ssl/fullchain.pem', 'ssl/privkey.pem')
    app.run(host='0.0.0.0', debug=True, ssl_context=context)

這邊的context所用到是我們前置作業所複製出來的兩個憑證,為了是讓我們的API是用https存取,這也是Facebook Message Platform所要求的。

最後執行API來讓我們跟Bot聊天吧。

完成程式碼如GitHub連結https://github.com/enginebai/Facebook-Message-Bot 請給星星吧,我需要你們的支持~

常見問題可能的解法

1.Callback網址有問題?先用工具測試你的Webhook GET API是否正常運作,可以呼叫Webhook API然後給 hub.verify_tokenhub.challege 這兩個參數,照我們寫的程式正確會回傳 hub.challege 的值。

2.訊息沒有收到或傳遞失敗?別忘了要綁定Facebook應用程式和粉絲團(執行 curl 那一段)
如果你還有其他疑問,歡迎留言來詢問。


此文章同步分享於我的Medium

我是大白,Co-founder & Developer @DualCores Studio,歡迎對於Bot開發、人工智慧、語意分析、機器學習有興趣的朋友多跟我一起交流學習,接下來會更專注分享相關的題目。

近期幾個科技巨頭不斷釋出聊天機器人(以下簡稱Bot)相關的開發套件:

以及慢慢浮現的Bot Store:BotlistBotpages…等等,不難發現這股趨勢或許會開始影響一般使用者、設計師以及開發者,或者,根本完全不會?根本只是新聞炒作下的熱品也不一定(就像「大數據」這個詞彙一樣,大數據講白了就是統計帶來的資料效益而已),但是可以確定的是,巨頭帶領的這股趨勢是否會改變整體市場需要一點時間發酵,近期做了一些觀察整理出的心得分享,希望能引起更多人共鳴:

Bot帶來的衝擊?

以下針對設計師、開發者、使用者以及一般服務提供者來個別說明,衝擊是大是小留給各位去評斷,也歡迎留言跟我們討論。

1. 對於設計師

Bot帶來的是最基本介面上、體驗上的改變,將從傳統的App介面(Graphic UI,以下簡稱GUI)轉到聊天介面(Conversational UI,以下簡稱CUI),對於現在介面設計師來說,設計師不需要再設計那麼多的介面,會從聚焦於圖像介面設計轉變到互動、流程設計,這將會讓使用者體驗設計更加的抬頭,如果你想知道更多聊天介面設計上的一些原則和心得,可以參考我的夥伴Evonne Wu所寫的「設計筆記:淺談聊天介面與人機互動設計」,裡面會有更詳盡的解說。

對於設計師,多了一個CUI,可以依照產品服務和操作體驗選擇不同的呈現方式。

2. 對於開發者

簡單一句話,開發工作總量沒有減少,因為一項服務只有換了介面、體驗和流程,但整體產品目標是一樣的,介面刻畫上可能會相對簡單許多,比較需要客製化的介面數量應該會減少一些,不過,這都只是相對而言,端看你的產品服務適合用什麼樣的介面來呈現,這就需要跟設計師多多討論。

Bot仰賴語意分析和人工智慧(關於人工智慧,可以參考我之前寫的「AI人工智慧是? 為何要有它? 對我們有何影響?」一文介紹),這也是開發者在Bot需要琢磨的部分,目的在於讓電腦可以讀懂使用者輸入的話語以及自動完成所需要進行的動作,以上面電影資訊Bot來說,使用者問:「請告訴我相關的劇情」,Bot要可以語意分析出 →

  1. 下文Context:目前使用者問的是電影「星際大戰7:原力覺醒」,討論基礎為此電影的相關資訊。
  2. 使用者的意圖Intent:需要查詢該電影的劇情,Bot需要呼叫相關的動作或程式。

人工智慧和語意分析會是Bot發展的重要推手,如果Bot少了語意分析,使用者只能依照設計好的輸入選項來跟Bot互動,例如:Facebook Assist要輸入編號來執行動作,[Slack Statsbot]需要依照它的指令輸入才能作動,這樣一來,使用者還需要記指令,雖然可以輸入「help」來看有哪些指令可以用,但是久了沒用都要查一次,這樣的體驗未必會是好的,這樣CUI反而沒有比GUI來得方便。

在Bot開發上也分幾個層次整理如下圖,層次越底下對於開發者來說越簡單、明確,越高層難度越高、越無法做到很精確,底下的幾個層次,可以用電腦科學解決,越高的層次越需要跨領域的知識結合,包含語言、心理、認知、人類學、社會學…等:

  1. 既定事實、主題:要Bot可以回答既定的事實或相關主題,我們可以把相關的資料轉為結構性的資料,例如:問天氣、找商品…等,這些應用做成Bot的背後都是跟一般現有資訊平台雷同。
  2. 學習和反應:透過機器學習Bot可以更瞭解使用者,這邊可以做一些相關的推薦或客製化功能,也可以修正語意分析上的問題,包括讀不懂、會錯意、同一句話有許多解讀意思(例如:三句話總結男人一生 XD),就像我們人一樣,一開始都會有上述情況,透過學習後可以知道我們在說什麼並且做出相對應的回答或反應。
  3. 情緒分析:「哇,這口感十分棒,入口即化」「講都講不聽,到底在幹嘛!」「阿不就好棒棒!」Bot要如何知道第一句話是褒、第二句話是生氣罵人、最後一句話是說好棒卻是反諷?這有賴文法分析、修辭學以及大量的字詞語句資料讓電腦學習和分類。
  4. 情境和記憶:我們跟人溝通的時候,一連串的對話下來,可能包含很多討論主題,我們可以很清楚地知道我們目前談論的是什麼,但是對於Bot來說,需要記憶不同的情境,依照不同情境去做回應這件事情是困難的,難在情境的儲存方式、結構、要如何連結情境和相對應的回應…等,電腦擅長記憶,但是加上情境(我們稱做上下文、Context)後,情況就會變得很複雜。
  5. 個性和情感:Bot知道愛是什麼?如何表現溫柔?Bot會不會有脾氣?等等,開發者如何讓Bot知道愛這個動作、怎麼呈現愛這回事、該如何表現愛(產生什麼舉動之類),這都是值得探討的議題,會不會哪天Skynet真的出現,出現自我意識和啟動自我保護。

對於開發者,就是要致力讓Bot可以像真人一樣和使用者互動,可以賦予他情感、讓它像人一樣說話。

3. 對於使用者

一個產品服務將會從既定的介面操作轉變到人和人最基本的互動方式:「交談」來進行,如果未來介面都改用CUI來呈現,面對不同的產品服務,使用者不需要再去適應各種不同的介面,不用再去抱怨什麼介面或流程設計的那麼難用,因為全部都變成談話,很多服務都在與Bot談話之中完成。

使用者:「幫我叫車,15分鐘後在台北101等我。」
Bot:「OK,沒問題,等等車號1234-AB將會在台北101等你上車。」

使用者只需要下載一個Bot的家,這邊我暫時找不到合適的字詞描述,就是讓Bot可以依附的平台,使用者只需要這平台,需要什麼服務只需要呼叫對應的Bot即可或是拉Bot加入,叫車Bot、天氣Bot、新聞Bot、購物Bot…等等,不用再去載琳琅滿目的App,有些下載使用幾次後也就忘了,要完成不同事情也不需要再App之間切換。

如果一項產品服務選擇好的呈現方式(GUI / CUI相輔相成),對於使用者Bot都是一項加分的體驗。

4. 一般業者

業者的不同服務可以做更有效地整合,例如:Yahoo有推出新聞、氣象和拍賣數個獨立App,如果是使用傳統GUI整合,可能會讓App變得肥大、流程混雜、介面難用、程式難寫…等問題,而如今是使用Bot來提供服務,可以將這些東西整合到CUI呈現,全部通通在對話中進行服務,當使用者想知道新聞時只要說「告訴我新鮮事!」、想知道明天氣象只要問「明天天氣如何?」、想要買東西時只要說「幫我找xyz,規格好一點」,業者可以將所提供的產品整合成由Bot來服務,使用者只需要這個Bot平台就可以同時擁有不同的產品服務。

另外,服務可以依照使用者來客製化,傳統App一旦推出後,如果要加新功能或是修正錯誤,只能透過更新來發佈,有了Bot之後不需要再更新App,功能或修正可以無縫換上,這也將會改變App生態系的重要一環,App Store要轉變為Bot Store。

整體生態系從套裝軟體 → 網頁 → App → Bot/CUI

App還有戲唱嗎?

短時間內答案是肯定的,App依然會存在主宰,Bot的發展需要時間發酵,在Bot還無法很聰明地知道使用者想說什麼、不夠智慧還無法讓使用者盡情使用一般語言和Bot對話之時,App依舊仍會是主流,也並非所有產品服務都適合用Bot來做,一項工作如果可以拆解成數個繁雜的步驟才可以完成、或者整項工作需要來回溝通多次才可以完成的,例如:安排會議時間,都比較適合利用Bot來完成,至於GUI或是傳統App就給其他按鈕很快按一按就完成,比打一串話告訴Bot來做更快、更適合的產品服務來做。

下圖我用本文所提到電影資訊App → Bot,沒有考慮相關的設計原則,單純把原本App轉變為Bot,如果現在你要訂票,你會覺得哪種互動體驗比較好?你喜歡哪一種?原因為何?

結論

面對這波趨勢,我個人是蠻看好跟期待,看好的原因是因為使用者體驗在某些產品服務當中,使用Bot能比純GUI提供更好的體驗,加上語意分析與人工智慧的發展,會使得Bot能讓我們用簡單、自然的互動方式完成複雜的工作。 最後,期待我們的新作…

我是大白,Co-founder & Developer @DualCores Studio,歡迎對於Bot開發、人工智慧、語意分析、機器學習有興趣的朋友多跟我一起交流學習,接下來會更專注分享相關的題目。

同步發表於Medium,歡迎按讚以及關注。