---
tags: Python
---
# 爬蟲菜單
### 為何爬蟲
每當我們瀏覽網頁、查詢資料時,需要不斷更改請求內容、還得細細挑選從中擷取出關鍵內容,然而不斷重複這些步驟確實有點冗餘,所以為了自動化這些步驟,坐在辦公椅上好好喝咖啡,就得交給網路爬蟲(自動瀏覽網路機器人)來執行,其工作內容包含
1. 對伺服器發送請求(request),包含請求方式(method)、網址(url)、標頭(headers)、表單(form data)
2. 讀取回應檔案(response),常用的包含HTML、JSON、XML、圖片
3. 剖析檔案內容,如從HTML中的DOM(document object model)物件取出必要字串
4. 儲存擷取內容,如抓取網頁的表格儲存成excel、抓去圖片置於資料夾中
5. 設定排程,讓程式定時去執行抓取的動作
### 發送請求
首先在chrome中進入目標網頁,點選右鍵、檢視網頁原始碼,最上方有一排標籤,分別紀錄著
1. Elements當前網頁的HTML、CSS
2. Console網頁終端機
3. Sources來源伺服器的資料夾
4. Network資源取得的表現以及伺服器所回傳的資源
在網頁爬蟲中最常使用到的功能就是Elements與Network,Elements用來剖析HTML的DOM物件中包了哪些元素,可以幫助我們快速尋找到需要的內容,Network則紀載著詳細的請求內容與回傳的元件,像是請求內容、js檔、圖片等等,若要深入就得先補齊網頁前端的知識囉,這次來實作抓取英文單字的例句
```python
#首先從python原生的urlib模組的request類別匯入Request類別(對,就是類別的類別)
from urllib.request import Request
#定義要抓取的單字
vocabulary = 'crawler'
#使用文字樣板,將單字帶入網址中
url = f'https://sentence.yourdictionary.com/{vocabulary}'
#提交的表單,這次使用GET方法進行請求,不會有表單
form_data = None
#請求標頭,用來表明自己的身分,例子中為網頁版本Mozilla/5.0、作業系統windows 64位元
#有些網站會防止惡意請求,會檢查標頭來認證身分,通常加入User-Agent都可以通過
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'}
#製作請求的物件
request = Request(url,form_data,headers,method='GET')
```
(進階內容-可先跳過)很多電商網站都會有防爬機制,會使用cookie的暫存向伺服器通訊來檢查使用者身分,像是如果要進入蝦皮追蹤商品資訊,就要使用到比較進階的selenium(網頁操作自動化)來取得csrftoken(防止跨站攻擊的機制),首先下載chrome的webdriver軟體,要確定版本相符跟自己的一樣,點選chrome瀏覽器右上角的說明來查看當前版本,傳送門 https://sites.google.com/a/chromium.org/chromedriver/downloads
```python
from selenium import webdriver
driver = webdriver.Chrome('C:/Users/使用者名稱/Downloads/chromedriver.exe')
driver.get('https://shopee.tw/') #會彈出新的頁面
cookies = driver.get_cookies() #格式為list包dict
csrftoken = [item['value'] for item in cookies if item['name'] == 'csrftoken'][0]
```
### 讀取回應
製作好請求的內容後,就可以開始試水溫檢查回應的內容
```python
from urllib.request import urlopen, urlretrieve
#使用urlopen打開網頁,轉成urllib的response物件
response = urlopen(request)
#檢查回應的狀況,回應為2開頭的才能持續進行
print(response.status)
#轉換response物件成字串,read()之後為byte,decode()則將byte轉成字串
res_str = response.read().decode('utf-8')
#使用urlretrieve下載回傳的資源,不一定為html,也可以是png、json檔之類的
urlretrieve(request, '下載位置.html')
```
### 剖析物件
若回傳的是HTML檔,就得出動chrome開發人員工具、翻出目標資料的所在位置,右鍵開啟檢視網頁原始碼,在elements的功能下一個個將巢狀選單打開,直到找出包著例句的母元素是哪一個,以下是用chrome檢視原始碼攤開的畫面

找到了!例句在ul下的li元素中,li的class為voting_li、id是序列碼、裡面有包著div和b,直接轉成字串一個一個處理好像有點吐血,還好有解析HTML的套件bs4,可以用select函數如前端的document.querySelector般選取DOM元素,還附有get_text()函數,就算摻雜了其他標籤,照樣把文字通通乾淨的抽出來
```python
from bs4 import BeautifulSoup
#將上面回應的物件轉成soup的html剖析物件
soup = BeautifulSoup(response,'html.parser')
#選取所有tag為li且class為voting_li的元素
#在querySelector中,class用「.」表示、id用「#」
print(soup.select('li.voting_li').prettify()) #使用prettify函數來排版
#使用get_text()把xml標牽內的文字通通抽出來
sentences = [li.get_text() for li in soup.select('li.voting_li')]
```
當然熟正規表達式是件好事,以下是小抄與簡單範例
| 位置鎖定類 | 字元選擇類 | 匹配次數類 |
| -------- | -------- | -------- |
|^開頭|.任何字元|*匹配零至多次|
|$結尾|[]字元候選組合|+匹配一至多次|
|()目標字串組|\d數字,等同於[0-9],\D非數字|?匹配零或一次|
||\w英文字母,等同於[A-Za-z],\W非英文字母|{n}匹配n次|
||\s任何空白字元,等同於[\f\n\r\t\v],\S非空白字元|{n,m}匹配n到m次|
```python
import re
string = '<div>Some <b>Text</b> Here</div>'
#搜尋包在<b></b>的所有內容(.*),group(0)回傳整段內容
re.search(r'<b>(.*)</b>', string).group(0) #輸出<b>Text</b>
#改成group(1),回傳小括號內所有符合的字元
re.search(r'<b>(.*)</b>', string).group(1) #輸出Text
```
如果只是單存要擷取HTML裡面的table表格,用pandas套件最簡單
```python
import pandas as pd
dataframe_list = pd.read_html('網址')
dataframe_list[0].to_excel('超簡單的.xlsx',index=0)
```
### 儲存內容
將抓取英文單字例句的實作改成函式來跑迴圈,並且輸出成json檔,可做為存檔點供下次使用
```python
from urllib.request import Request, urlopen
from bs4 import BeautifulSoup
def get_sentences(vocabulary):
url = f'https://sentence.yourdictionary.com/{vocabulary}'
req = Request(url)
res = urlopen(req)
soup = BeautifulSoup(res,'html.parser')
sentences = [li.get_text() for li in soup.select('li.voting_li')]
return sentences
import time
results = dict()
for vocabulary in ['better','faster','stronger']:
results[vocabulary] = get_sentences(vocabulary)
time.sleep(1) #有些網站會偵測IP發送頻率
import json
with open('例句.json','w') as file:
json.dump(results, file) #把w改成r、dump改成load(file)就變成讀取了
```
### 設定排程
> windows使用者
1. 搜尋並開啟工作排程器(應該每個人都有吧)
2. 右方動作列點選建立工作
3. (一般)設定獨一無二的名稱,將安全性選項改為「不論使用者登入與否均執行」
4. (觸發程序)新增執行程式的模式,要確定左下角的已啟用是勾選的
5. (動作)程式或指令碼處輸入python.exe的位置,新增引數放入.py檔的位置
6. 按下確定後會要求輸入登入密碼
> mac、linux使用者(終端機cron)
|分鐘|小時|日|月|星期|使用者|指令|
|-|-|-|-|-|-|-|-|
|0-59|0-23|1-31|1-12|0日-6六|root|引數|
|符號|用意|
|-|-|
|/5|每隔五單位|
|1-5|在單位1到5之間|
|*|不指定|
```shell=
進入crontab設定檔
$ nano /etc/crontab
每天早上六點整執行一次
0 6 * * * usr/bin/python /home/root/crawler.py
每隔15分鐘執行一次
/15 * * * * usr/bin/python /home/root/crawler.py
星期一到五每小時執行一次
*/1 * * * 1-5 usr/bin/python /home/root/crawler.py
啟用cron服務
$ service cron start
```
## selenium
> webdriver
* [Chrome](https://sites.google.com/a/chromium.org/chromedriver/downloads)
* [Edge](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/)
* [Firefox](https://github.com/mozilla/geckodriver/releases)
* [Safari](https://webkit.org/blog/6900/webdriver-support-in-safari-10/)
```python=
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
driver = webdriver.Chrome()
driver.get('http://www.google.com')
driver.close()
el = driver.find_element_by_class_name('gLFyf.gsfi')
el.send_keys('hi')
el.clear()
btn = driver.find_element_by_class_name('gNO89b')
btn.click()
el.send_keys(Keys.CONTROL, 'c')
driver.forward()
driver.back()
```
## puppeteer + cheerio
```javascript=
const puppeteer = require('puppeteer');
const fs = require('fs');
async function printPDF() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('C:/Users/Bill/Desktop/範例.html', { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
fs.writeFileSync('範例.pdf', pdf, 'binary');
return pdf;
}
printPDF().then(pdf=>{})
```