遜砲學習日記:ChatGPT 輔助的爬蟲實務

Photo by Michal Matlon on Unsplash

這個系列主要是嫩嫩 (Todler) 學習過程的簡單紀錄,把自己靈機一動、學習跟查找的歷程做一個基本的紀錄。

看完了 彭彭老師的爬蟲教學影片,迫不及待測試之前,決定試試看用 ptt 以外的一個網站。想來想去還是 ptt 網頁版最合適了!雖然 pttweb 沒有進版需要十八歲的警語,自然沒有 over18=1 的 cookie 需要,但也許也有其他需要注意的地方。在本次測試的過程中,我也看到了僅依賴 chatGPT 的一些侷限。本次以八卦版首頁為例,環境為 python3 + vscode。python 經驗為近乎於 0,JavaScript 新手等級,Html/CSS 還可以。

首先,以 urllib.request 套件好像無法達成效果。首先,如同範例,我抓取 Network (網路) 中 Gossip.html (相當於 ptt 的 index.html) 找尋 Header,並查詢 Request Header,將 cookie 與 user-agent 複製進傳送的爬蟲主程式 crawler 中。

Dynamic Cookies 難道要用 Session 處理?

跳出來了一些錯誤,主要是 403 forbidden。是不是 cookie 有一些問題呢?詢問 ChatGPT 4 (Web Browsing mode 可讓 AI 瀏覽網路),指我應該用 requests 這個套件,因為它更加優雅且現代,而且可以解決 dynamic cookie 的問題,因為它判斷該 cookie 是動態生成的,因此直接複製進網站的 cookie 使數據完全相同,就恰恰顯示了我是假人。因此我使用了 requests 搭配 session

url = "..."
import requests

headers= { ... }
session = req.Session()
response = session.get(url, headers=headers)

為求慎重起見,我在 Headers 中 Request Header 先複製了所有相應的資料。它提醒了我,偽元素是 HTTP2 的特性。在他的 AI 資料庫中 (~SEP 2021),應該是不支援的。由於該項資料研判應該跟爬蟲無關,我就先拿掉了(但這不是網站瀏覽模式嗎?)

:Authority:www.ptt.cc
:Method:GET
:Path:/bbs/Gossiping/index.html
:Scheme:https
Photo by American Heritage Chocolate on Unsplash

Decode 解碼插曲

如此一來,使用 print(response.text)資料是抓到了,但卻成為了天書,還出現許多的問號。奇怪,ChatGPT 不是跟我說 requests 套件會自動編碼嗎?不知怎的,我還是收到了一堆問號。�*�%?�w�C�a�Y���”!O��i��wm+��A��Q9����”?r[O��ͥ��1e��.���Xχ�\FE�7�8U*]�U����I)yz^��ʣ�0�
這種。我決定再次詢問 chatGPT。

response.encoding = 'utf-8'
print(response.text)

chatGPT 叫我印出 status_code 跟 headers 並觀察 Content-Encoding,我乖乖照作了。

response = session.get(url, headers=headers)
print(response.status_code)
print(response.headers)

⚠️ 在這裡我犯了一個錯誤。我錯看了欄位,將 Response Header 看成 Request Header ,並發現Content-Encoding : br 。竟然不是 html !我立即詢問 ChatGPT 如何處理 br 格式。他提示了我資料是被壓縮了,要我使用解壓縮 br 的套件 brotli,並且要我以 pip install 安裝他(pip 是 python 安裝時內建的套件安裝工具):

import brotli
if response.headers.get('Content-Encoding') == 'br':
decompressed_data = brotli.decompress(response.content)
text = decompressed_data.decode('utf-8')
print(text)
else:
print(response.text)

想當然爾,因為我錯看了欄位,資料並不是 br,所以問題並沒有解決。但在這裡我發現了 decode 的方式,在我腦中稍微記得。方法跟 urllib.request 小有不同。沒辦法,只好再問問 ChatGPT。它指出如果不是 br,則可以用 zlib 處理其他若干種演算法:

import zlib

decompressed_data = zlib.decompress(response.content)

絕望之下,他又給了我一個答案。印出

print(response.content[:10])

他說如果資料為 gzip 壓縮可被 zlib 解壓縮,則印出來會是 1f 8b,印出來10個位元(bytes) 必然是b’\x1f\x8b’。
結果出來是 b’<!doctype ‘。搞屁啊,根本就是 html 嘛!他提醒我,有可能資料根本就沒壓縮喔,直接印出來並用 utf-8 decode 看看吧

print(response.content.decode('utf-8'))

等一下,剛才用 response.text 現在卻用 response.content?為什麼啊?但跑得動我就先繼續做了。

觀察資料結構

Photo by Florian Olivo on Unsplash

接下來就是很直觀的觀察資料結構,找到我想抓的八卦版標題。在該網站,資料通常處在 <span> 標籤中,但又有 <span data-xs> 這樣的屬性在內。這不在範例之中,我也請 chatGPT 提點了我一下。

titles = root.find_all("span", attrs={"data-v-005654ec": True})
for title in titles:
print(title.text.strip())

如此一來,除了得知 attrs 要放在中括弧裡頭表明為 true(通常印出來格式為 data-xs="" 而已),也發現以 strip() 可以去除前後空格!
不過後來觀察資料結構,titles ㄧ樣抓某 class 的 span 更是我要的。雖然我其實不了解為何要在 class 後面加上 underline,我以為是 python 的某種 snakecase 固定寫法,畢竟有 find_all 這樣的語法嘛!

由於在範例中是使用 string,這邊也提醒了我使用 text,我立刻請 chatGPT 提點了他們的差異:

  • .text:會回傳標籤內所有子孫標籤的文字內容。(也可作.get_text()
  • .string:只有在標籤只有一個 NavigableString 類型子節點時才會回傳內容,否則會回傳 None(包含<b>這類標籤)。

大體上就是裡面沒有包其他標籤的狀態下,才能使用 string。所以範例時的 a.string 才可以,但 text 會把裡頭所有東西都印出來。因為該網站有做某種 RWD 響應設定,目測應該是如果是手機版 (xs),會將 [問卦] 這個標頭拿掉,但我想要這個標頭,所以我就直接去抓那個 class 了:雖然裡頭有兩個子標籤,但其中一個是空白的,因此還蠻符合我所需的。

titles = root.find_all("span", class_="e7-show-if-device-is-not-xs")

除此與接下來的一切則都蠻類似範例的,不管是引用 beautifulsoup4 (bs4)來解析 html (parse),或者是後面才會提到的捲動頁面 Selenium(先擱置)。 算是一個有趣的小練習,但也發現了幾件事:

Photo by Jonathan Kemper on Unsplash
  1. 因為疏忽混淆 Request Header 跟 Response Header ,導致沒有發現其實 request header 的資料格式妥妥是 text/html。之所以出現亂碼有很多原因,但有一個原因 (詢問 ChatGPT) 是因為中文是一種非 ASCII 字符,變成了特殊符號的 /xexex。如\xe6\x96\x87\xe7\xab\xa0\xe5\x88\x97\xe8\xa1\xa8 這就是預設的 Unicode 編碼。可以用 decode(‘unicode-escape’) 使其變成可讀的中文, encode() 則是把它變回去。

但我當初看到的是一堆問號(應該是非 ASCII 字符的特殊符號)是很像,但在終端機 (terminal) 無法重現,最近測試都變成了 \xe6 這樣的格式。那是因為我後來使用了 response.content。如果是用 response.text 則又會變成那樣。除非我加上

response.encoding = response.apparent_encoding  
  1. 詢問 Sr.,class_ 是因為 python 裡保留字的設定,就像 JavaScript 裡頭已經有 class 保留字,因此 React 中模擬 html 的 JSX不能重複 class 這個名字,而要改用 className 一般。相對於 JS 通常是 lowerCamelCase (小駝峰表示法),Python 則是🐍 Snake Case —— 就如他的名稱「蟒蛇」ㄧ樣,代表常用底線隔開。
  2. ChatGPT 一如往常會提醒我小心觸法,最少會被鎖 IP 之類的,十分貼心。其中要我去看 user policy 以外 document (文件) 的 .robots 我覺得最實用。
  3. ⚠️ 沒有被提醒可能根本就沒 cookie 擋我。畢竟 pttweb 可能也是爬別人的,其實 headers 裡根本不用放 cookies 驗證,連 user-agent 都不用!看來應該有更好的練習。Sr. 提醒我,爬蟲時可以直接複製 request header 再一個個刪除,看看哪個是必要的,看來這類實戰心法還是寶物啊。繞了蠻多遠路的
Photo by Mimi Thian on Unsplash

至於捲動 Selenium 之類的,就下次再說啦!

Photo by ilgmyzin on Unsplash

本次問的問題,我當時使用了非常短的 prompt。
https://chat.openai.com/share/5c76927c-ade0-44a4-a3a3-7cb9ead71a02

https://chat.openai.com/share/87a4fc12-8f59-455b-8b04-04ba54ec97ed

--

--

Murmurline Spirit (Steward Tsou) 星際特調
星際廢話線

你療癒、硬頸的文字好夥伴:星際廢話線、星際區塊線 @murmurline 🚀 FB fanpage 行銷、設計、網站開發基礎與區塊鏈 Writer / Designer / Marketing / Web3&Blockchain / NFT ✨FB🚀@murmursteward