Python 爬蟲(詳細版)
===
---
## 今天要做什麼 ?
- 學習很多東東
----

---
## 你會學到的東西 ?
- 基礎的網頁知識
- 基礎的Html知識
- 基礎的python
- Python 爬蟲
---
## 啊這到底是可以幹嘛啦?
---
## 開始
----
### What is Html ?
在開始爬蟲的介紹之前
我們需要知道html是什麼
----
基本上就是你打開網頁看到的東東啦~
專業一點來說的話
Html的全名是**H**yper **T**ext **M**arkup **L**anguage
也就是規範一個網頁的架構所需要的語言
----
我們可以來看一下基礎的 html 長的樣子

----
### How Can I Get It ?
----
這裡我們先提一下網頁的基礎
當你在瀏覽器輸入一段網址
瀏覽器所幫你做的事情
是去向那個網址發送一個請求
就是所謂的GET
----

----

----
當請求完畢的時候
瀏覽器就會把拿到的html轉換成你看到的樣子啦
----
### Then ?
----
所以你就可以發現
如果我想爬取網頁上的東東
那我就GET他的html
然後再把我想要的部份取出來就好了啊~
----
沒錯 就是這麼簡單
---
## Learn Python
----
- Hello World
```python
print('Hello World')
```
----
- 宣告變數
一個變數就像是一個容器,可以將資料存放在裡面

----
- `=`賦值
把東西裝進箱子裡面!

----
把右邊的值給左邊的東西
```python
x = 1
print(x)
y = 3
print(y)
x = y
print(x)
```
----
不同的資料型態
- integer(整數)跟string(字串)
```python
x = 1 # 數字(integer) ##註解
print(x)
s = 'string' # 字串(string) ##就是文字
print(s)
I_have_a = "pen"
I_have_an = "apple"
print(I_have_an + I_have_a)
#string 的加法就是在前面一個string的尾巴加上後面一個string
```
----
- boolean(布林)
對或是錯
```python
a = True # 布林(boolean)
b = False
```
----
- list(列表)

list就像是個錢包,錢包裡面會裝零錢、鈔票、信用卡等
list可以塞很多資料進去,不管是整數(integer)或是字串(string)都可以!
----
```python
a = ['one', 1, 'two', 2.0] # 列表(list)
print(a[0])
print(a[1])
print(a)
```
特別要注意的是,從上面的例子可以看出,電腦儲存的資料是從 **0** 開始數的!
----
- dictionary(字典)

dictionary就像是一個上鎖的箱子,要拿到裡面的東西,我們必須要有那把獨特的鑰匙
dictionary的每個元素分成兩個部分,前面是`key`,中間隔一個冒號,後面是`value`。特別注意的是,`key`的資料型態一定是==string(字串)==
而dictionary的特色就是,我們要用獨一無二的`key`去找出與其相對應的`value`。例子就參考下面的範例code。
----
```python
pudding = {
'price': 50,
'flavor': 'milk'
} #字典(dictionary)
print(pudding)
print(pudding['price'])
```
----
- `==`
用來比較是否相等
```python
print(1 == 1)
x = 3
y = 4
print(x == y)
```
從上面的例子可以知道,`==`基本上就是你們平常在算數學時會看到的`=`
只是在程式語言的世界中 :
`=` 表示賦值
`==` 用來判斷左右兩邊的東西一不一樣,如果一樣的話這個敘述就是True,不一樣就是False
----
- `!=`
用來比較是否不相等。剛好跟上面介紹的`==`概念是相反的。
```python
x = 3
y = 4
print(x != y)
```
----
- 條件(if, else)
當我們想在某些條件成立的狀況下才做事,意即這個條件是`True`時我們才做事
這個概念就像英文的文法一樣(if....)
```python
i_am_handsome = True
# i_am_handsome = False
if i_am_handsome:
print("Thank You<3")
else:
print("So Sad:(")
icecream = 3
if icecream == 3:
print("I love icecream!")
print("Icecream is so yummmmy!")
```
大家應該也有注意到,在`if`跟`else`的下一行都會縮排。這個縮排的意義是要告訴電腦,接在`if`後面有哪幾行是這個`if`的範圍。亦即標示清楚如果進到這個`if`裡面,有哪幾行的程式碼是我們必須要執行的。例如icecream的那個例子,那兩個print就是進入`if`後必須執行的。
----
- 條件(if, else)
其實除了上述的寫法,還有另外一種是用在`string(字串)`上的
當我們要檢查某個特殊的字串是否出現在其他字串中,我們可以這樣寫
```python
if "a" in "aabbccc":
print("Ya!")
```
其中,`a`就是我們想要找的string。而我們想要判斷`a`有沒有出現在`aabbccc`這個string裡面,如果有的話,這個敘述就是一個對的(True)敘述。
同樣的,我們當然可以把這些string存在一個變數裡面,然後再拿這些變數去做判斷。
```python
zoo = "Tiger_Elephant_Monkey"
my_favorite_animal = "Monkey"
if my_favorite_animal in zoo:
print("Hurray!")
```
----
- 迴圈
當我們想做一件事情很多次,例如我們想買很多隻冰淇淋

----
你以為這樣就沒了嗎?
不,故事還沒說完
----

----
`icecreams`在這邊是一個list,裡面有編號1~5的冰淇淋。而我們另外立了一個`icecream`,它會從 1 開始被裝進各個編號的冰淇淋,然後去做for迴圈裡所寫的事情。
```python
icecreams = [1, 2, 3, 4, 5]
for icecream in icecreams:
print(icecream)
```
----
- 函式

函式就像裁縫店一樣,它會產出款式相同的衣服,但是會為了每個人不同的身材量身打造。
舉`len()`這個函式來說,顧名思義它就是負責找出長度(length)的函式。但不同的東西長度當然不同,因此它會接收**參數**,最後才回傳結果。
```python
my_string = "hello"
x = len(my_string)
#把len()所回傳的結果存在x裡面
print(x)
```
最後要提醒的就是,不論參數給什麼,函式本身所做的事是不會變的,如上面提到的例子,`len()`的功能就是找出我們所丟給它的東西的長度。
----
- 引入函式庫
Python 有很多的函式庫

剛剛我們把函式看作裁縫店,現在我們將函式看成一項工具。因為函式接受到的參數雖然不盡相同,但函式所要做的事情是一樣的。
而既然函式是一項工具,那麼函式庫便是裝有各式各樣工具的工具箱了!
----
想要安裝或是引入他們的時候怎麼辦?

----
例如 math 函式庫裡面有 sqrt(開根號), gcd(最大公因數) 等等
那我們可以
```python
import math
print(math.sqrt(520))
```
----
如果你連 math. 都懶得打
你也可以
```python
from math import gcd
print(gcd(520, 1314))
```
---
## Learn Python Scrapping
----
### Lesson 1
全部連結!

----
進入 http://iogames.fun

----
目標: 列出所有的遊戲連結
----
首先 將我們要用的函式庫引入
```python=
import requests
from bs4 import BeautifulSoup
import lxml
```
----
再來 我們用 **requests** 的 **get** 函數
來 get 到我們想要的網址 並存到 res 這個變數裡面
然後我們把 **res.text** 放到 **BeautifulSoup** 這個函數裡面
並加上**字串型態**的 **lxml** 這個參數
最後可以 print(soup.prettify())
來檢查結果是否正確的拿到想要的 html
```python
import requests
from bs4 import BeautifulSoup
import lxml
url = "http://iogames.fun"
res = requests.get(url)
soup = BeautifulSoup(res.text, "lxml")
print(soup.prettify())
```
----
這時候會看到這個網頁的 html
在你看來可能是一堆亂碼 不過其實他是有意義的
檢查正確之後就可以把 print **註解**掉 =>
```python
# print(soup.prettify())
```
----
那你接下來一定會有個疑問
html code 這麼大一串
啊我怎麼知道要去哪一段找我要的東東啊QQ

----
**F12 是你的好朋友**
打開你的網頁

----
按下F12,點選**左上角**「**選取頁面中的元素**」

----
選取包含遊戲的區塊

----
可以看到我們這裡選的是
```htmlmixed
<div class='tiles'>
</div>
```
這個**標籤**的名稱就叫作 **div**
然後他有一個**屬性(attribute)** 叫做class
----
這裡我們稍微帶過 html 的標籤與屬性
標籤就像是人們會有不同的職業,而不同的職業會有不同的工作內容
因此網頁中不同的區塊會有不同的標籤,就表示他們有著不同的特性
(可以參考下面所列一些常見的標籤)

----
- **\<a\>**: anchor 連結,通常會有 **href** 這個屬性,代表要連向的網址
- **\<div\>**: division 區塊,html 中重要的標籤,區分一個個不同的區塊,通常也是爬蟲的時候優先找的對象
- **\<img\>**: image 顧名思義,通常有 **src** 這個屬性,代表圖片的網址
- **\<h1\>**: 有 **h1** ~ **h6**,代表不一樣大小的標題
----
還有一些重要的屬性

----
- **class**: 代表一種類別,通常同一種**class**的標籤會有同樣的性質,所以也是爬蟲中重要的屬性
- **id**: 像你的身份證號碼,通常一個**id**只屬於一個標籤
----
我們發現所有的遊戲連結都會在一個 **a** 標籤裡面
而這些 **a** 標籤共同的特性是我們想要關注的
我們發現他們都長這樣
```htmlmixed
<a class="tiles_item" href="..."></a>
```
前面有提過連結都會在 a 標籤的 **href** 裡面
所以我們先想辦法把 a 標籤拿到手
----
這裡介紹 BeautifulSoup 的第一個函數 **find_all**
```python
# find_all("標籤名稱", 屬性=...)
```
回傳一個 **list** 包含符合的標籤們
----
我們接序前面的 Code
```python
import requests
from bs4 import BeautifulSoup
import lxml
url = "http://iogames.fun"
res = requests.get(url)
soup = BeautifulSoup(res.text, "lxml")
links = soup.find_all("a", class_="tiles-item")
for link in links:
print(link["href"])
```
----
好像成功的印出東西了
不過似乎不太像是一個正常的網址
所以我們加上伺服器的名字
```python
for link in links:
# (x) print(link["href"])
print(url + link["href"])
```
---
### Practice 1
瘋狂購物
----
**目標:**
在蝦皮拍賣中搜尋任意物品
整理出搜尋到的物品的連結、名稱、價格

----
連結:
[https://shopee.tw](https://shopee.tw)
---
### Lesson 2
可愛貓貓

----
**目標:**
貓貓很可愛
所以我們要蒐集貓貓的圖片
dcard 有個寵物版
我們就從裡面找出貓貓的圖片並且下載下來吧
----
連結:
[https://www.dcard.tw/f/pet](https://www.dcard.tw/f/pet)
----
那我們跟第一課一樣
先觀察 dcard 寵物版的網頁

----
我們會想要把每個文章的連結擷取出來
再分別從中找出裡面的圖片
並利用 python 來下載他們
跟 lesson 1 一樣
我們要的超連結都會在
**a** 標籤的 **href**
所以我們要想辦法收集這些標籤
----
跟 lesson 1 一樣
我們按下 F12 點選左上角的「選取頁面中的元素」按鈕

移動鼠標到文章的連結
可以看到我們選到一個 a 標籤
```htmlmixed
<a class="PostEntry_root_V6g0rd">...</a>
```
----
首先 一樣先引入需要的函式庫
```python
import requests
from bs4 import BeautifulSoup
import lxml
```
----
再來跟 lesson 1 一樣
我們用 **requests** 的 **get** 函數以及 **BeautifulSoup** 來解析
```python
import requests
from bs4 import BeautifulSoup
import lxml
res = requests.get("https://www.dcard.tw/f/pet")
soup = BeautifulSoup(res.text, "lxml")
```
----
再來我們利用 BeautifulSoup 的 **find_all** 函數
找到所有文章的 **a** 標籤
```python
for a in soup.find_all("a", class_="PostEntry_root_V6g0rd"):
print(a["href"])
```
----
我們拿到一個個文章的標籤之後
我們就利用可以利用 **a** 標籤的 **href** 屬性
加上 **requests** 的 **get** 函數跟 **BeautifulSoup** 函數
請求一個個單獨的文章了
```python
for a in soup.find_all("a", class_="PostEntry_root_V6g0rd"):
res = requests.get("https://www.dcard.tw" + a["href"])
soup = BeautifulSoup(res.text, "lxml")
```
----
做到這步其實可以開始抓出圖片了
不過我們要先來判斷這篇文章跟貓貓有沒有關係
那我們要如何判斷呢

----
我們可以看到每個文章的底下都有一些tag
所以我們只要抓出所有的 tag
判斷有沒有「貓」在裡面
我們先用**選取頁面中的元素**
看一下 tag 的 html 標籤是什麼
我們可以發現是
```htmlmixed
<a class="TopicList_topic_1XGOjs">...</a>
```
----
所以我們可以再用一次 find_all 函數
然後判斷**貓**這個字有沒有出現在 a 的文字裡面
```python
for a in soup.find_all("a", class_="PostEntry_root_V6g0rd"):
res = requests.get("https://www.dcard.tw" + a["href"])
soup = BeautifulSoup(res.text, "lxml")
tags = soup.find_all("a", class_="TopicList_topic_1XGOjs")
if "貓" in [tag.text for tag in tags]:
print(tags)
```
----
找出所有跟貓有關的文章之後
我們可以再用一次 F12 的**選取頁面中的元素**找出內文的部份

可以看到我們選到
```htmlmixed
<article class="Post_root_23_VRn"></article>
```
----
那這裡我們要學一個新的 BeautifulSoup 函數
跟 find_all 很像的 find
前面我們說過 find_all 會回傳一個 list
因為可能會找到很多個
而 find 只會找到一個而已
所以會回傳單一的元素
而其他的用法基本上跟 find_all 一樣
```python
if "貓" in [tag.text for tag in tags]:
article = soup.find("article", class_="Post_root_23_VRn")
```
----
最後 找到所有內文的圖片

----
我們可以看到是 img 的標籤
而圖片的網址我們可以看到是存在 src 這個屬性裡面
```python
if "貓" in [tag.text for tag in tags]:
article = soup.find("article", class_="Post_root_23_VRn")
for img in article.find_all("img"):
print(img["src"])
```
----
圖片怎麼下載的部分
我們提供了一個函數來幫助你
所以我們可以直接利用 downloader 函數
```python
from download import downloader
if "貓" in [tag.text for tag in tags]:
article = soup.find("article", class_="Post_root_23_VRn")
for img in article.find_all("img"):
downloader(img["src"])
```
----
大功告成
---
Downloader 的介紹(參考用)
----
downloader 的一個參數 src
src 指的是要下載的地址
```python=
# download.py
import random
import string
import requests
def downloader(src):
print(f"Downloading {src} ...")
with requests.get(src, stream=True) as res:
res.raise_for_status()
fn = "./images/" + ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) + ".jpg"
with open(fn, "wb") as f:
for chunk in res.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print("Done.")
```
我們把這段 code 慢慢分開來講解
----
```python
requests.get(src, stream=True)
```
讓讀進來的資料不會一次就被寫入
當你的 requests 可能記憶體會很大(ex. 影片、圖片)
你就應該用 stream=True
----
```python
res.raise_for_status()
```
當錯誤發生的時候
等待下一個正確的回應
----
```python
res.iter_content(chunk_size=8192)
```
跑過所有 request get 那到的 response(回應資料)
----
```python
with open(fn, "wb") as f:
f.write(chunk)
```
python 寫入檔案的方式
要先 open 一個 file
檔案的位置名稱就是 dst
並且告訴他你打開這個檔案的作用是要做什麼
我們是要寫入一個二進位檔案
所以用 wb
----
chunk
https://www.byvoid.com/zht/blog/http-keep-alive-header
----
隨機的字串產生
```python
import random
import string
random_string = ''.join(random.choices(string.ascii_uppercase + string.digits, k=size))
```
----
str.join(array) 就是把陣列裡的字串都連接再一起
並且中間用 str 來分隔
```python
print(",".join(["a", "b", "c", "d"])) # a,b,c,d
```
random.choices(字串集合, 長度)
就是從字串集合裡面做出某長度的隨機字串
string.acsii_uppercase 大寫字母
string.digits 數字
---
### Extra Practice
****** **hard** ******
----
眼尖的同學們應該會發現
其實我們並沒有把整個 dcard pet 的圖片全部抓下來
我們抓的只有一部份的文章而已
這是因為當你做
```python
requests.get("https://www.dcard.tw/f/pet")
```
的時候
dcard 只會回傳部分的文章
但是當你將頁面往下滾
就會有更多文章載入了
----
目標: 爬到更多的文章
可以設定爬到 100 篇就停下來
{"metaMigratedAt":"2023-06-14T21:11:55.219Z","metaMigratedFrom":"Content","title":"Python 爬蟲(詳細版)","breaks":true,"contributors":"[{\"id\":\"c94b5949-af35-40c0-b13c-36bc311fc68c\",\"add\":17600,\"del\":8587},{\"id\":\"596938fa-db11-427a-9336-7b937648a7a4\",\"add\":9851,\"del\":7731},{\"id\":\"3adbc7c8-0f40-4e05-b033-9162a6e709b1\",\"add\":725,\"del\":240}]"}