爬蟲(web crawler) - 是一種自動化瀏覽網頁的機器人
HTML 就像網頁的骨架,決定網頁的組織架構以及呈現內容。
HTML 包含了一系列的元素(elements),而元素包含了標籤(tags)與內容(content)。
屬性可以為元素提供更多資訊,ex: 字型、顏色等等…。
格式為: 屬性名稱 = 屬性值
而class是將標籤分類(就想成放在同一個箱子裡),之後爬蟲經常會用到。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>my first html</title> </head> <body> <p>Hello, HTML!</p> </body> </html>
<head>
: 標頭,包含網頁的基本設置,不會顯示在畫面上,ex: 網頁標題、字元實體集…。<body>
: 包含了所有會顯示於網頁瀏覽者眼前的內容,ex: 段落、圖片…。<h1>~<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>
段落,通常用於文字內容。
<p> 超文本標記語言(英語:HyperText Markup Language, 簡稱:HTML)是一種用於建立網頁的標準標記語言。 HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站 用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。 網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。 HTML描述了一個網站的結構語意隨著線索的呈現, 使之成為一種標記語言而非程式語言。 </p>
超文本標記語言(英語:HyperText Markup Language, 簡稱:HTML)是一種用於建立網頁的標準標記語言。 HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站 用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。 網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。 HTML描述了一個網站的結構語意隨著線索的呈現, 使之成為一種標記語言而非程式語言。
<a>
超連結,用於連結其他網址,href屬性加上連結網址。
<a href="https://www.nchu.edu.tw/index1.php">中興大學首頁</a>
<div>
區塊,就是一個空的容器,可以在區塊中加入其他元素。
<div style="background-color:blue; color:white; width:400px; height:200px;"> <p>inside the div</p> </div>
inside the div
<span>
文字的區塊,包住文字並提供更多資訊。
<p>this color is <span style="color: yellow;">yellow</span></p>
this color is yellow
我們就是用戶端(Client),當我們點一個網頁時,就等於向存放該網頁的伺服器(Server)送出請求(Request),當伺服器接收到我們的請求後,就會回傳網頁的內容回來,這就是回應(Response)。
其實網路運作方式就跟去餐廳點餐的流程非常相似,我自己當作用戶端(Client),餐廳老闆當作伺服器(Server)。
當我們進到一間餐廳看著菜單,就像在瀏覽器上瀏覽眾多的網頁。
接著,到櫃檯向老闆點餐,就像是看到喜歡的網頁,發出請求(Request)給伺服端(Server)想看的網頁。
最後,等待老闆烹飪完成送到餐桌上,像是伺服端(Server)端將回應(Response)回傳給用戶端(Client)。
此次爬蟲所使用的程式語言為Python,是因為Python提供了許多好用的爬蟲套件,requests就是其中之一。
requests是一個幫助你抓下網頁的HTML原始碼的套件,抓下原始碼後才能對網頁進行分析。
// CMD
pip install requests
import requests
抓取網站使用到 requests.get() 函數,參數為我們想要爬蟲的網址(URL)
response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html")
抓下來的還只是最原始的HTML,需要透過BeautifulSoup解析。
BeautifulSoup是一個解析HTML原始碼並且提供搜尋節點功能的套件。
// CMD
pip install beautifulsoup4
from bs4 import BeautifulSoup
response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html") soup = BeautifulSoup(response.text, "html.parser") print(soup.prettify())
第一次進入PPT八卦板時,會先看到這個畫面:
點 "我同意" 之後才會看到文章列表。
但第二次再進去時,就不會再詢問一次是否滿18歲了,這是為什麼呢?
我們觀察 F12>>Application>>Cookies
然後當我按下 "我同意" 時,cookie會多出一個 over18=1,這就是PTT存在瀏覽器中,記錄我是否點過滿18的餅乾。
再來觀察 Network>>index.html>>Headers>>cookie,會發現PTT就是把cookie包裝在Headers中發送出去的。
也就是說,我們只要讓程式模擬使用者,發送一個一樣的cookie,就能順利抓到文章列表了!
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())
其實可以將cookie想成暫存在用戶端(client)這邊的便利貼,上面記錄了你之前的所做過的事情,之後在進相同的網頁時,就會連著cookie帶給伺服器,好讓伺服器知道你做過什麼事。
繼續做下去你可能會發現,當每次向伺服器送出請求(Request)時,都必須補上cookie,伺服器才會正確的傳回網頁,這是為什麼呢?
在網頁的世界裡,它是一個「無狀態」的協議。什麼意思呢?那就是每一個 request 都是一個「獨立的」request,彼此之間不會有任何關聯。所以 Server 那邊也不會保存任何狀態。每一個 request 都是一個新的 request。
你可以把伺服器想成是一個喪失記憶能力的人,每一次你去找他的時候,他都當作是第一次見到你,完全忘記你以前有去找他了。
就會發生…
session的英文意思是 持續一段時間的狀態,基本上和它的運作方式並沒有差太多,此機制確保了伺服器可以知道使用者的狀態。
想像伺服器留下一張便利貼,上面留者你的一切所作所為,並給你一個密碼,那當你下次拿著這個密碼來找伺服器時,伺服器就會對應到那張便條紙,就可以知道你之前做了哪些事了。
簡單來說,你可以把session當作是存在伺服器端(server)的cookie。
requests有內建session()函數,會暫存使用者的資料。
rs = requests.session()
前面我們是讓程式將cookie夾帶在headers中傳送給伺服器,但缺點是每次request都必須補上cookie,相當麻煩。
所以我們希望程式可以模擬的像是真人,回想一下,我們剛剛進八卦版做了哪些事情?
payload = { 'from': '/bbs/Gossiping/index.html', 'yes': 'yes' } # 用session紀錄此次使用的cookie rs = requests.session() response = rs.post("https://www.ptt.cc/ask/over18", data=payload)
查看 Network>>over18>>Headers>>Form Data
我們看到了這兩個參數,不難猜測代表的意思。
response = rs.get("https://www.ptt.cc/bbs/Gossiping/index.html") soup = BeautifulSoup(response.text, "html.parser") print(soup.prettify())
結果會和上一個一樣,不過你會發現如果想要再進入一次八卦版的話,就不需要再傳cookie了。
因為爬蟲會造成伺服器的負擔,所以大部分網站都不歡迎別人來爬他們的網站,因此多數網站都會設下許多障礙阻止爬蟲。
想當然,魔高一尺道高一丈,身為一個爬蟲工程師,就要想盡辦法讓你的程式看起來像是真人,以騙過伺服器的眼睛。
文章標題按右鍵>>檢查
會出現文章標題在HTML上的位置
多抓幾篇會發現,所有的標題都放在一個
class="title" 的 div 中
links = soup.find_all("div", class_="title") for link in links: print(link)
找到所有 class 名稱為 "title" 的 "div" ,
回傳一個列表。
因為我們想要文章的詳細資料,因此我們必須進到文章內才能抓到。
那麼,要怎麼抓出文章聯結呢?
<div class="title">
<a href="/bbs/Gossiping/M.1615705592.A.DEE.html">[問卦] 高雄最美的景點是什麼?</a>
</div>
會發現我們要的連結是在 <a>
中的"href"屬性。
links = soup.find_all("div", class_="title") for link in links: print(link.a["href"])
但這並不是完整的網址,所以我們要在前面加上:
"https://www.ptt.cc/"
還記得requests的get函數嗎?
接下來就用此函數進到文章內頁。
page_url = "https://www.ptt.cc/"+link.a["href"] # 進入文章頁面 response = rs.get(page_url) result = BeautifulSoup(response.text, "html.parser") print(result.prettify())
檢查文章作者、標題、發文時間的位置
右鍵>>檢查
發現都會在
class="main-content" 的 div 中
再往下找到
class="article-meta-value" 的 span 中
# 找出作者、標題、時間、留言 main_content = result.find("div", id="main-content") article_info = main_content.find_all("span", class_="article-meta-value")
因為會用list的方式裝下來,就用索引值取我們要的資料吧~
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
結果內容不存在任何類別內。只是單純的丟到 "main-content" 而已
那麼到底要怎麼把內容抓下來OAO?
檢查過每篇不同文章,就會發現文章都是以 "- -" 來做結尾的
把先前抓的 "main_content" 文字的部分(text),再用 split()
函數丟掉後面不需要的。
又因為文章內可能也含有 "- -" 符號而不小心被切割,再用 join()
函數恢復原樣。
# 將整段文字內容抓出來 all_text = main_content.text # 以--切割,抓最後一個--前的所有內容 pre_texts = all_text.split("--")[:-1] # 將前面的所有內容合併成一個 one_text = "--".join(pre_texts)
這樣子文章後面的部分搞定啦
試著先把目前抓到的print出來檢查
發現文章是從第一個換行後開始,所以就用 "\n" 來做分割~
跟上面一樣的方法,只是改成 "\n"。
# 以\n切割,第一行標題不要 texts = one_text.split("\n")[1:] # 將每一行合併 content = "\n".join(texts)
文章內容全部搞定w
所有留言都是放在class="push" 的 div 內
然後再細分出 "push-tag" "push-userid" "push-content" "push-ipdatetime"
跟前面一樣使用BeautifulSoup。
# 抓出所有留言 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,分別存放"推"、"→"、"噓"三種標籤。
push_dic = [] arrow_dic = [] shu_dic = []
以字典型式將所有資料塞好塞滿,然後再依照標籤分類囉~
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)
對上方導覽列的 "上頁" 按鈕,使用右鍵>>檢查。
我們要的連結在string="‹ 上頁" 的 <a>
中
把新的連結給url,再來就用for迴圈做到想要的頁數吧~
# 找到上頁的網址,並更新url url = "https://www.ptt.cc/"+ soup.find("a", string="‹ 上頁")["href"]
以易於讓人閱讀的文字為基礎,用來傳輸由屬性值或者序列性的值組成的資料交換語言
– 維基百科
python 與 json 的差別
首先,宣告一個list,名稱為data。
data = [] # 全部文章的資料
再來,宣告一個dict,名稱為article_data。
article_data = {} # 單篇文章的資料
以及另一個dict,名稱為comment_dic。
comment_dic = {} # 所有留言
dict的格式:{"key": "value"}
所有資料包起來
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)
Python內建有json套件,利用 dump() 將其轉成json然後匯出囉!
import json
# 輸出JSON檔案 with open('data.json', 'w', encoding='utf-8') as f: json.dump(data, f)
可以到網站json editor,可以很方便的確認最後匯出的json檔喔~
這堂課先從網頁的架構HTML開始,接著以PPT八卦版為例,代入cookie和session概念,以及使用Python的requests和BeautifulSoup套件篩選出我們要的資料,到最後匯出成JSON格式。
Q1: 所以我們這麼辛苦抓下了這些資料要做什麼?
A1: 在機器學習上,資料量決定了訓練出來的精準度,因此這些資料可是非常重要的喔!
Q2: 我不是上面講的那些人員,我也可以爬蟲嗎?
A2: 當然可以! 只要你是要從網路上取得大量資料,而且想要電腦幫你完成這件事的人,就可以利用爬蟲幫你省下大量時間!
Q3: 我想爬爬看別的網站,但我不知道從何開始下手?
A3: 一開始可以先從PTT的其他版開始,因為PTT應該算是最好爬的網站了,等到比較熟悉之後,就可以開始爬其他網站了,爬蟲的重點就是要多觀察,多利用右鍵>>檢查來了解原始碼的樣子,或到F12>>網路檢查傳了什麼request給伺服器,都能幫助你更了解這個網站的運作方式,並找出破綻來取得資料。
Q4: 我用這堂課教的方法在爬某個網站時,發現無論如何都抓不到資料,怎麼會這樣?
A4: BeautifulSoup和requests並不是萬用的,爬蟲還有很多的技巧可以用,不同的技巧對應到不同的情況,但這部分就要由你們自己去探索囉!
爬PTT政黑板,跟我做一樣的事情。
完成後將程式碼寄到我的信箱
chiang1051009@gmail.com。
信件標題格式為 學號-系級-姓名。
程式碼需要註解說明每一段的功用。