# 社群網路爬蟲實作課 --- #### 講者:資工三 江尚軒 --- ## What is 爬蟲? ![](https://i.imgur.com/9LFv0kB.png =40%x) ![](https://i.imgur.com/IMGKK6l.jpg =25%x) > 爬蟲(web crawler) - 是一種自動化瀏覽網頁的機器人 --- ## Why 爬蟲? ![](https://i.imgur.com/CNk6LGS.png =45%x) <!-- 有時資料量可是幾百萬筆起跳的,手動抓會抓到發瘋 --> --- ## Who 爬蟲? * 訓練機器學習之人員 * 股票分析人員 * 市場分析人員 --- ## How 爬蟲? --- ## 1. 首先,我們還要知道網頁長什麼樣子。 --- #### HTML簡介 ---- ### HTML(Hypertext Markup Language) HTML 就像網頁的骨架,決定網頁的組織架構以及呈現內容。 HTML 包含了一系列的元素(elements),而元素包含了標籤(tags)與內容(content)。 ---- ### 元素組成 ![](https://i.imgur.com/x7X1TRF.png) 1. Opening tag: 起始標籤,<>內放入標籤名稱。 2. Closing tag: 結尾標籤,<>內放入和起始標籤一致的標籤名稱,而且要在名稱前加上/符號。 3. Content: 標籤內容。 ---- ### 屬性 ![](https://i.imgur.com/9IBJo4j.png) 屬性可以為元素提供更多資訊,ex: 字型、顏色等等...。 格式為: `屬性名稱 = 屬性值` 而class是將標籤分類(就想成放在同一個箱子裡),之後爬蟲經常會用到。 ---- ### HTML架構 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>my first html</title> </head> <body> <p>Hello, HTML!</p> </body> </html> ``` 1. `<head>`: 標頭,包含網頁的基本設置,不會顯示在畫面上,ex: 網頁標題、字元實體集...。 2. `<body>`: 包含了所有會顯示於網頁瀏覽者眼前的內容,ex: 段落、圖片...。 ---- ### 看起來... ![](https://i.imgur.com/W5JKock.jpg) ---- ### 基本元素介紹 #### `<h1>~<h6>` 標題,一共有六層,字體從大到小,通常用於文章標題與副標題。 ```htmlmixed= <h1>This is heading1</h1> <h2>This is heading2</h2> <h3>This is heading3</h3> <h4>This is heading4</h4> <h5>This is heading5</h5> <h6>This is heading6</h6> ``` ---- <h1>This is heading1</h1> <h2>This is heading2</h2> <h3>This is heading3</h3> <h4>This is heading4</h4> <h5>This is heading5</h5> <h6>This is heading6</h6> ---- #### `<p>` 段落,通常用於文字內容。 ```htmlmixed= <p> 超文本標記語言(英語:HyperText Markup Language, 簡稱:HTML)是一種用於建立網頁的標準標記語言。 HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站 用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。 網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。 HTML描述了一個網站的結構語意隨著線索的呈現, 使之成為一種標記語言而非程式語言。 </p> ``` ---- <p> 超文本標記語言(英語:HyperText Markup Language, 簡稱:HTML)是一種用於建立網頁的標準標記語言。 HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站 用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。 網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。 HTML描述了一個網站的結構語意隨著線索的呈現, 使之成為一種標記語言而非程式語言。 </p> ---- #### `<a>` 超連結,用於連結其他網址,href屬性加上連結網址。 ```htmlmixed= <a href="https://www.nchu.edu.tw/index1.php">中興大學首頁</a> ``` ---- <a href="https://www.nchu.edu.tw/index1.php">中興大學首頁</a> ---- #### `<div>` 區塊,就是一個空的容器,可以在區塊中加入其他元素。 ```htmlembedded= <div style="background-color:blue; color:white; width:400px; height:200px;"> <p>inside the div</p> </div> ``` ---- <div style="background-color:blue; color:white; width:400px; height:200px;"> <p>inside the div</p> </div> ---- #### `<span>` 文字的區塊,包住文字並提供更多資訊。 ```htmlembedded= <p>this color is <span style="color: yellow;">yellow</span></p> ``` ---- <p>this color is <span style="color: yellow;">yellow</span></p> <!-- last page of ch1 --> --- ## 2. 再來,我們得先知道網頁的運作方式。 --- #### request和response的機制 ---- ![](https://i.imgur.com/KbjaM94.png =80%x) 我們就是用戶端(Client),當我們點一個網頁時,就等於向存放該網頁的伺服器(Server)送出請求(Request),當伺服器接收到我們的請求後,就會回傳網頁的內容回來,這就是回應(Response)。 ---- ### 感覺很複雜嗎? 其實網路運作方式就跟去餐廳點餐的流程非常相似,我自己當作用戶端(Client),餐廳老闆當作伺服器(Server)。 ![](https://i.imgur.com/jsvqtfX.png) ---- 當我們進到一間餐廳看著菜單,就像在瀏覽器上瀏覽眾多的網頁。 接著,到櫃檯向老闆點餐,就像是看到喜歡的網頁,發出請求(Request)給伺服端(Server)想看的網頁。 最後,等待老闆烹飪完成送到餐桌上,像是伺服端(Server)端將回應(Response)回傳給用戶端(Client)。 <!-- last page of ch2 --> --- ## 3. OK,那可以開始爬蟲了吧? --- #### requests和BeautifulSoup套件介紹 ---- #### 本文以[PTT八卦版](https://www.ptt.cc/bbs/Gossiping/index.html)為例,教會你如何爬蟲。 ---- ### Python - requests 此次爬蟲所使用的程式語言為Python,是因為Python提供了許多好用的爬蟲套件,requests就是其中之一。 ---- ### requests是蝦米?? requests是一個幫助你抓下網頁的HTML原始碼的套件,抓下原始碼後才能對網頁進行分析。 ---- ### 下載requests ``` // CMD pip install requests ``` ### 引用requests ```python= import requests ``` ---- ### requests抓取網頁HTML原始碼 抓取網站使用到 requests.get() 函數,參數為我們想要爬蟲的網址(URL) ```python= response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html") ``` ---- ### 你以為抓下來就可以直接用了嗎,並沒有! 抓下來的還只是最原始的HTML,需要透過BeautifulSoup解析。 ---- ### BeautifulSoup又是蝦米?? BeautifulSoup是一個解析HTML原始碼並且提供搜尋節點功能的套件。 ![](https://i.imgur.com/eNL70dh.png =30%x) ---- ### 下載BeautifulSoup ``` // CMD pip install beautifulsoup4 ``` ### 引用BeautifulSoup ```python= from bs4 import BeautifulSoup ``` ---- ### BeautifulSoup解析網頁HTML原始碼 ```python= response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html") soup = BeautifulSoup(response.text, "html.parser") print(soup.prettify()) ``` ---- ### 結果... ![](https://i.imgur.com/ZhUlHqz.png) <!-- last page of ch3 --> --- ## 4. 你騙我! 你不是說這樣就可以抓到HTML了嗎? --- #### cookie介紹 ---- ### 我們來找找看原因 第一次進入PPT八卦板時,會先看到這個畫面: ![](https://i.imgur.com/8HOiGFG.png) 點 "我同意" 之後才會看到文章列表。 但第二次再進去時,就不會再詢問一次是否滿18歲了,這是為什麼呢? ---- 我們觀察 **F12>>Application>>Cookies** ![](https://i.imgur.com/LiVKv1b.jpg) ---- 然後當我按下 "我同意" 時,cookie會多出一個 over18=1,這就是PTT存在瀏覽器中,記錄我是否點過滿18的餅乾。 ![](https://i.imgur.com/Dz53JA8.jpg) ---- 再來觀察 **Network>>index.html>>Headers>>cookie**,會發現PTT就是把cookie包裝在Headers中發送出去的。 ![](https://i.imgur.com/Fx7fEhw.jpg =90%x) ---- 也就是說,我們只要讓程式模擬使用者,發送一個一樣的cookie,就能順利抓到文章列表了! ```python= my_headers = {"cookie": "over18=1"} url = "https://www.ptt.cc/bbs/Gossiping/index.html" response = requests.get(url, headers=my_headers) soup = BeautifulSoup(response.text, "html.parser") print(soup.prettify()) ``` ---- ### 結果... ![](https://i.imgur.com/b3rZimq.png) ![](https://i.imgur.com/9elgmgr.png) ![](https://i.imgur.com/dtS8Mfr.png) ![](https://i.imgur.com/mHBXwxx.png) ---- 其實可以將cookie想成暫存在用戶端(client)這邊的便利貼,上面記錄了你之前的所做過的事情,之後在進相同的網頁時,就會連著cookie帶給伺服器,好讓伺服器知道你做過什麼事。 <!-- last page of ch4 --> --- ## 5. 看起來不錯,但好像少了些什麼? --- #### session介紹 ---- 繼續做下去你可能會發現,當每次向伺服器送出請求(Request)時,都必須補上cookie,伺服器才會正確的傳回網頁,這是為什麼呢? ---- 在網頁的世界裡,它是一個「無狀態」的協議。什麼意思呢?那就是每一個 request 都是一個「獨立的」request,彼此之間不會有任何關聯。所以 Server 那邊也不會保存任何狀態。每一個 request 都是一個新的 request。 ---- 你可以把伺服器想成是一個喪失記憶能力的人,每一次你去找他的時候,他都當作是第一次見到你,完全忘記你以前有去找他了。 就會發生... ---- ![](https://i.imgur.com/LUhdhfA.png =40%x)![](https://i.imgur.com/4FGfF0U.png =40%x) ---- ### 我懂了,但每次都要傳cookie我覺得好麻煩,有沒有更好的做法? ---- ### 有的,那就是session~ ---- ### session是什麼? session的英文意思是 *持續一段時間的狀態*,基本上和它的運作方式並沒有差太多,此機制確保了伺服器可以知道使用者的狀態。 ---- 想像伺服器留下一張便利貼,上面留者你的一切所作所為,並給你一個密碼,那當你下次拿著這個密碼來找伺服器時,伺服器就會對應到那張便條紙,就可以知道你之前做了哪些事了。 簡單來說,你可以把session當作是存在伺服器端(server)的cookie。 ---- ### session該怎麼用? requests有內建session()函數,會暫存使用者的資料。 ```python= rs = requests.session() ``` ---- ### 跟cookie有點不一樣 前面我們是讓程式將cookie夾帶在headers中傳送給伺服器,但缺點是每次request都必須補上cookie,相當麻煩。 所以我們希望==程式可以模擬的像是真人==,回想一下,我們剛剛進八卦版做了哪些事情? ---- 1. 先進入 "詢問你是否滿18歲" 的畫面 2. 然後你按了 "我同意" 3. 瀏覽器存下 "over18的cookie" 4. 進入文章列表 ---- ### 那麼,我們怎麼讓程式做一樣的事呢? ```python= payload = { 'from': '/bbs/Gossiping/index.html', 'yes': 'yes' } # 用session紀錄此次使用的cookie rs = requests.session() response = rs.post("https://www.ptt.cc/ask/over18", data=payload) ``` ---- ### post vs. get * get: 將資料全部寫在header上,就像你寫明信片一樣,傳遞上較不安全。 * post: 將資料寫在內部,就像你寫信然後裝進信封袋一樣,傳遞上比較安全且傳遞的資訊可以比較多。 ---- ### post 使用 data 參數傳遞資料 查看 **Network>>over18>>Headers>>Form Data** 我們看到了這兩個參數,不難猜測代表的意思。 * from: 代表我們進入的是八卦版。 * yes: 代表按了同意。 ![](https://i.imgur.com/WhaxWRR.png =100%x) ---- ### 看看是不是成功了 ```python= response = rs.get("https://www.ptt.cc/bbs/Gossiping/index.html") soup = BeautifulSoup(response.text, "html.parser") print(soup.prettify()) ``` 結果會和上一個一樣,不過你會發現如果想要再進入一次八卦版的話,就不需要再傳cookie了。 ---- ### 好session,不用嗎? ![](https://i.imgur.com/EnQ9D9k.png =60%x) ---- ### 爬蟲重要觀念 ### ==讓程式模擬真人的行為== 因為爬蟲會造成伺服器的負擔,所以大部分網站都不歡迎別人來爬他們的網站,因此多數網站都會設下許多障礙阻止爬蟲。 想當然,魔高一尺道高一丈,身為一個爬蟲工程師,就要想盡辦法==讓你的程式看起來像是真人==,以騙過伺服器的眼睛。 <!-- last page of ch5 --> --- ## 6. 抓到了沒錯,可是,我們要的東西在哪裡? --- #### BeautifulSoup分析HTML+搜尋節點 ---- ### 先別急,我們先來觀察一下八卦版 ---- 文章標題按右鍵>>檢查 ![](https://i.imgur.com/bXeeuK6.png) ---- 會出現文章標題在HTML上的位置 ![](https://i.imgur.com/9ezbUoD.png) ---- 多抓幾篇會發現,所有的標題都放在一個 ==class="title" 的 div 中== ![](https://i.imgur.com/5em1ZQ5.png) ![](https://i.imgur.com/n11aRUl.png) ---- ### BeautifulSoup搜尋節點 * find_all("標籤名稱", 屬性="值"): ```python= links = soup.find_all("div", class_="title") for link in links: print(link) ``` 找到所有 class 名稱為 "title" 的 "div" , 回傳一個列表。 ---- ### 結果... ![](https://i.imgur.com/YpT2ak2.png) <!-- last page of ch6 --> --- ## 7. 我們要什麼? 我們要連結! --- #### 抓出每篇文章的連結 ---- 因為我們想要文章的詳細資料,因此我們必須進到文章內才能抓到。 **那麼,要怎麼抓出文章聯結呢?** ---- ### 我們先看一下剛剛抓到的東西 ``` <div class="title"> <a href="/bbs/Gossiping/M.1615705592.A.DEE.html">[問卦] 高雄最美的景點是什麼?</a> </div> ``` 會發現我們要的連結是在 ==`<a>`中的"href"屬性==。 ```python= links = soup.find_all("div", class_="title") for link in links: print(link.a["href"]) ``` ---- ### 結果 ![](https://i.imgur.com/OzRqsWs.png =80%x) ---- 但這並不是完整的網址,所以我們要在前面加上: "https://www.ptt.cc/" ---- ### 結果 ![](https://i.imgur.com/RgHtTQ3.png =80%x) ---- 還記得requests的get函數嗎? 接下來就用此函數進到文章內頁。 ```python= page_url = "https://www.ptt.cc/"+link.a["href"] # 進入文章頁面 response = rs.get(page_url) result = BeautifulSoup(response.text, "html.parser") print(result.prettify()) ``` ---- ### 結果 ![](https://i.imgur.com/cHBl3rd.png =45%x) <!-- last page of ch7 --> --- ## 8. 一切都準備好啦! --- #### 抓出文章的作者、標題、發文時間 ---- 檢查文章作者、標題、發文時間的位置 **右鍵>>檢查** ![](https://i.imgur.com/6Hw1wii.png) ---- 發現都會在 ==class="main-content" 的 div 中== ![](https://i.imgur.com/l5oB6is.png) ---- 再往下找到 ==class="article-meta-value" 的 span 中== ```python= # 找出作者、標題、時間、留言 main_content = result.find("div", id="main-content") article_info = main_content.find_all("span", class_="article-meta-value") ``` ---- 因為會用list的方式裝下來,就用索引值取我們要的資料吧~ ```python= if len(article_info) != 0: author = article_info[0].string # 作者 title = article_info[2].string # 標題 time = article_info[3].string # 時間 else: author = "無" # 作者 title = "無" # 標題 time = "無" # 時間 ``` 很小機率會出現沒有 "article-metaline" 等資訊還是要特殊處理 QAQ --- ## 9. 都是一樣抓東西,應該很簡單吧~(Flag --- #### 抓出文章內容 ---- ### 到文章內容>>右鍵>>檢查 ---- 結果內容不存在任何類別內。只是單純的丟到 "main-content" 而已 ![](https://i.imgur.com/h6LvRUJ.png) **那麼到底要怎麼把內容抓下來OAO?** ---- ### 也只能再仔細觀察文章內容在HTML內的形式 ---- 檢查過每篇不同文章,就會發現文章都是以 "- -" 來做結尾的 ![](https://i.imgur.com/FaDdQcB.png) ---- 把先前抓的 "main_content" 文字的部分(text),再用 `split()` 函數丟掉後面不需要的。 又因為文章內可能也含有 "- -" 符號而不小心被切割,再用 `join()` 函數恢復原樣。 ```python= # 將整段文字內容抓出來 all_text = main_content.text # 以--切割,抓最後一個--前的所有內容 pre_texts = all_text.split("--")[:-1] # 將前面的所有內容合併成一個 one_text = "--".join(pre_texts) ``` **這樣子文章後面的部分搞定啦** ---- 試著先把目前抓到的print出來檢查 ![](https://i.imgur.com/Oljgk4L.png) 發現文章是從第一個換行後開始,所以就用 "\n" 來做分割~ ---- 跟上面一樣的方法,只是改成 "\n"。 ```python= # 以\n切割,第一行標題不要 texts = one_text.split("\n")[1:] # 將每一行合併 content = "\n".join(texts) ``` **文章內容全部搞定w** --- ## 10. 蛤?管你是誰發的,我全都要! --- #### 抓出留言並分類 ---- ### 到流言處>>右鍵>>檢查 ---- 所有留言都是放在==class="push" 的 div 內== 然後再細分出 "push-tag" "push-userid" "push-content" "push-ipdatetime" ![](https://i.imgur.com/BHUvTxe.png) ---- 跟前面一樣使用BeautifulSoup。 ```python= # 抓出所有留言 comments = main_content.find_all("div", class_="push") for comment in comments: push_tag = comment.find( "span", class_="push-tag").string # 分類標籤 push_userid = comment.find( "span", class_="push-userid").string # 使用者ID push_content = comment.find( "span", class_="push-content").string # 留言內容 push_time = comment.find( "span", class_="push-ipdatetime").string # 留言時間 ``` ---- 為了分類,我建立三個list,分別存放"推"、"→"、"噓"三種標籤。 ```python= push_dic = [] arrow_dic = [] shu_dic = [] ``` ---- 以字典型式將所有資料塞好塞滿,然後再依照標籤分類囉~ ```python= dict1 = {"push_userid": push_userid, "push_content": push_content, "push_time": push_time} if push_tag == "推 ": push_dic.append(dict1) if push_tag == "→ ": arrow_dic.append(dict1) if push_tag == "噓 ": shu_dic.append(dict1) ``` --- ## 11. 抓一頁不夠,那就抓兩頁阿! --- #### 自動換頁 ---- #### 現在只能取得一頁的文章,但如果想要取得其他頁的文章該怎麼做? ---- #### 我們想要看上一頁時,會做什麼事? ---- 對上方導覽列的 "上頁" 按鈕,使用**右鍵>>檢查**。 我們要的連結在==string="‹ 上頁" 的 `<a>` 中== ![](https://i.imgur.com/5cyNOug.png) ---- 把新的連結給url,再來就用for迴圈做到想要的頁數吧~ ```python= # 找到上頁的網址,並更新url url = "https://www.ptt.cc/"+ soup.find("a", string="‹ 上頁")["href"] ``` --- ## 12. JSON是什麼挖糕? --- #### JSON格式介紹 ---- 以易於讓人閱讀的文字為基礎,用來傳輸由屬性值或者序列性的值組成的資料交換語言 -- 維基百科 ![](https://i.imgur.com/o4ALd1u.png =300x) ---- python 與 json 的差別 ![](https://i.imgur.com/TBjpKm9.png) ---- #### 簡單來說,json就是單純由list、dict型態所組成的複合資料型態。 ---- #### 所以,我們要做的前置作業就是把資料整理好,以利將其轉成json檔。 ---- 首先,宣告一個list,名稱為data。 ```python= data = [] # 全部文章的資料 ``` 再來,宣告一個dict,名稱為article_data。 ```python= article_data = {} # 單篇文章的資料 ``` 以及另一個dict,名稱為comment_dic。 ```python= comment_dic = {} # 所有留言 ``` dict的格式:`{"key": "value"}` ---- 所有資料包起來 ```python= article_data["author"] = author article_data["title"] = title article_data["time"] = time article_data["content"] = content comment_dic["推"] = push_dic comment_dic["→"] = arrow_dic comment_dic["噓"] = shu_dic article_data["comment"] = comment_dic data.append(article_data) ``` ---- #### 最後轉成json格式後,如果只是單純顯示在terminal上會很難辨識,也很難讓其他程式使用。 ---- #### 那就乾脆寫檔匯出吧! Python內建有json套件,利用 *dump()* 將其轉成json然後匯出囉! ```python= import json ``` ```python= # 輸出JSON檔案 with open('data.json', 'w', encoding='utf-8') as f: json.dump(data, f) ``` ---- 可以到網站[json editor](https://jsoneditoronline.org/),可以很方便的確認最後匯出的json檔喔~ ![](https://i.imgur.com/yTNhBCq.png) ---- #### 有了這些資料後,就可以用於其他的應用了~ --- ## 總結 --- 這堂課先從網頁的架構HTML開始,接著以PPT八卦版為例,代入cookie和session概念,以及使用Python的requests和BeautifulSoup套件篩選出我們要的資料,到最後匯出成JSON格式。 --- ## Q & A ---- Q1: 所以我們這麼辛苦抓下了這些資料要做什麼? A1: 在機器學習上,資料量決定了訓練出來的精準度,因此這些資料可是非常重要的喔! ---- Q2: 我不是上面講的那些人員,我也可以爬蟲嗎? A2: 當然可以! 只要你是要從網路上取得大量資料,而且想要電腦幫你完成這件事的人,就可以利用爬蟲幫你省下大量時間! ---- Q3: 我想爬爬看別的網站,但我不知道從何開始下手? A3: 一開始可以先從PTT的其他版開始,因為PTT應該算是最好爬的網站了,等到比較熟悉之後,就可以開始爬其他網站了,爬蟲的重點就是要**多觀察**,多利用**右鍵>>檢查**來了解原始碼的樣子,或到**F12>>網路**檢查傳了什麼request給伺服器,都能幫助你更了解這個網站的運作方式,並找出破綻來取得資料。 ---- Q4: 我用這堂課教的方法在爬某個網站時,發現無論如何都抓不到資料,怎麼會這樣? A4: BeautifulSoup和requests並不是萬用的,爬蟲還有很多的技巧可以用,不同的技巧對應到不同的情況,但這部分就要由你們自己去探索囉! --- [完整程式碼GitHub網址](https://github.com/AndyChiangSH/NCHU_topic_homework/tree/master/hw01_PTT%20crawler) --- ## Homework --- 爬[PTT政黑板](https://www.ptt.cc/bbs/HatePolitics/index.html),跟我做一樣的事情。 完成後將程式碼寄到我的信箱 **chiang1051009@gmail.com**。 信件標題格式為 **學號-系級-姓名**。 程式碼需要註解說明每一段的功用。 --- ## END ![](https://i.imgur.com/rTUnT06.png =50%x)
{"metaMigratedAt":"2023-06-16T07:16:09.857Z","metaMigratedFrom":"YAML","title":"This is heading1","breaks":true,"slideOptions":"{\"transition\":\"slide\"}","contributors":"[{\"id\":\"368f247e-5311-4476-bb9f-5b2db977e314\",\"add\":15625,\"del\":1329}]"}
    1645 views