PTT 爬蟲範例教學

PTT Library 爬蟲教學

注意,此篇教學僅適用於 PTT Library 0.7 以下,已經是過時的教學。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

新版本開發指南: https://hackmd.io/@CodingMan/PyPttManual

↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑


如果你需要大量下載 PTT 文章並且使用 Python,這篇教學將會一步一步地引導你寫出你專屬的 PTT 爬蟲。

適用於 Python 初新者以上。

Step 0 安裝環境

你需要安裝 Python 3 最新版本,請至此處下載 https://www.python.org/

接下來安裝用來操作 PTT 的函式庫 PTT Library

pip install PTTLibrary

或者加個 --upgrade 來更新 PTT Library

pip install PTTLibrary --upgrade

 

Step 1 登入 PTT

首先這是一個最基本的 PTT 登入登出範例,接下來的步驟都會以這個範例慢慢演進。

import sys
from PTTLibrary import PTT

ID = '你的帳號'
Password = '你的密碼'

# 登入
ErrCode = PTTBot.login(ID, Password)
# 使用錯誤碼,判斷登入是否成功
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('登入失敗')
    sys.exit()

# 登出
PTTBot.logout()

如果你不希望機器人登入的時候,把其他登入一起踢掉,你可以加入一個參數 kickOtherLogin=False 像這樣

PTTBot = PTT.Library(kickOtherLogin=False)

如此一來,範例就變成

import sys
from PTTLibrary import PTT

ID = '你的帳號'
Password = '你的密碼'

# 如果不想在登入的時候踢掉其他登入,你可以這樣使用
PTTBot = PTT.Library(kickOtherLogin=False)
# 登入
ErrCode = PTTBot.login(ID, Password)
# 使用錯誤碼,判斷登入是否成功
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('登入失敗')
    sys.exit()

# 登出
PTTBot.logout()

如果你不想在程式碼裡寫死你的帳號密碼,你可以用以下程式碼從 Account.txt 讀入賬密,在 commit 時記得 ignore 或者刪除即可。而檔案內容則需要輸入。

{"ID":"YourID", "Password":"YourPW"}

如果檔案不存在或有錯誤,則會請你從螢幕輸入帳號密碼。

import json
import getpass

try:
    with open('Account.txt') as AccountFile:
        Account = json.load(AccountFile)
        ID = Account['ID']
        Password = Account['Password']
except FileNotFoundError:
    ID = input('請輸入帳號: ')
    Password = getpass.getpass('請輸入密碼: ')

如果把這段程式碼,結合了範例就會變成這樣。

import sys
import json
import getpass
from PTTLibrary import PTT

try:
    with open('Account.txt') as AccountFile:
        Account = json.load(AccountFile)
        ID = Account['ID']
        Password = Account['Password']
except FileNotFoundError:
    ID = input('請輸入帳號: ')
    Password = getpass.getpass('請輸入密碼: ')

# 如果不想在登入的時候踢掉其他登入,你可以這樣使用
PTTBot = PTT.Library(kickOtherLogin=False)
# 登入
ErrCode = PTTBot.login(ID, Password)
# 使用錯誤碼,判斷登入是否成功
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('登入失敗')
    sys.exit()

# 登出
PTTBot.logout()

到目前為止,我們就完成了有關登入的範例。

Step 2 爬文

我們在上一個步驟完成了,有關登入的教學,接下來開始爬文。

先介紹單一文章的擷取 API getPost,它的使用方法是長這樣的。

ErrCode, Post = PTTBot.getPost('某個板', PostIndex=文章編號)

我們可以看到他會回傳錯誤碼跟一篇文章回來,如果錯誤碼為 0 即為成功,接下來我們來解析一下如何使用回傳回來的文章資料結構。

API 說明
getBoard 文章所在的版 (Wanted)
getID 文章 ID ex: 1PCBfel1
getAuthor 作者
getDate 文章發布時間
getTitle 文章標題
getContent 文章內文
getMoney 文章 P 幣
getWebUrl 文章網址
getPushList 文章即時推文清單
getOriginalData 文章原始資料 (備份色碼用)

 

 

 

 

 

 

 

 

 

 

其中,getPushList 可以取得文章的推文清單,同樣地我們也來列出所有推文資訊。

getType 推文類別 推噓箭頭
getAuthor 推文 ID
getContent 推文內文
getIP 推文IP (如果存在)
getTime 推文時間

接下來我們可以把將文章資料拿出來的功能寫成一個函式方便使用。

def showPost(Post):
    PTTBot.Log('文章代碼: ' + Post.getID())
    PTTBot.Log('作者: ' + Post.getAuthor())
    PTTBot.Log('標題: ' + Post.getTitle())
    PTTBot.Log('時間: ' + Post.getDate())
    PTTBot.Log('價錢: ' + str(Post.getMoney()))
    PTTBot.Log('IP: ' + Post.getIP())
    PTTBot.Log('網址: ' + Post.getWebUrl())

    PTTBot.Log('內文:\n' + Post.getContent())

    PushCount = 0
    BooCount = 0
    ArrowCount = 0

    for Push in Post.getPushList():
        if Push.getType() == PTT.PushType.Push:
            PushCount += 1
        elif Push.getType() == PTT.PushType.Boo:
            BooCount += 1
        elif Push.getType() == PTT.PushType.Arrow:
            ArrowCount += 1
        
        Author = Push.getAuthor()
        Content = Push.getContent()
        # IP = Push.getIP()
        PTTBot.Log('推文: ' + Author + ': ' + Content)
        
    PTTBot.Log('共有 ' + str(PushCount) + ' 推 ' + str(BooCount) + ' 噓 ' + str(ArrowCount) + ' 箭頭')

現在是時候,將這些程式碼整到我們的範例試試看了,在下面的範例,我用 Wanted 版當作範例,並取得文章編號 6000 的文章。

import sys
import json
import getpass
from PTTLibrary import PTT

def showPost(Post):
    PTTBot.Log('文章代碼: ' + Post.getID())
    PTTBot.Log('作者: ' + Post.getAuthor())
    PTTBot.Log('標題: ' + Post.getTitle())
    PTTBot.Log('時間: ' + Post.getDate())
    PTTBot.Log('價錢: ' + str(Post.getMoney()))
    PTTBot.Log('IP: ' + Post.getIP())
    PTTBot.Log('網址: ' + Post.getWebUrl())

    PTTBot.Log('內文:\n' + Post.getContent())

    PushCount = 0
    BooCount = 0
    ArrowCount = 0

    for Push in Post.getPushList():
        PushType = Push.getType()

        if PushType == PTT.PushType.Push:
            PushCount += 1
        elif PushType == PTT.PushType.Boo:
            BooCount += 1
        elif PushType == PTT.PushType.Arrow:
            ArrowCount += 1
        
        Author = Push.getAuthor()
        Content = Push.getContent()
        # IP = Push.getIP()
        PTTBot.Log('推文: ' + Author + ': ' + Content)
        
    PTTBot.Log('共有 ' + str(PushCount) + ' 推 ' + str(BooCount) + ' 噓 ' + str(ArrowCount) + ' 箭頭')

try:
    with open('Account.txt') as AccountFile:
        Account = json.load(AccountFile)
        ID = Account['ID']
        Password = Account['Password']
except FileNotFoundError:
    ID = input('請輸入帳號: ')
    Password = getpass.getpass('請輸入密碼: ')

# 如果不想在登入的時候踢掉其他登入,你可以這樣使用
PTTBot = PTT.Library(kickOtherLogin=False)
# 登入
ErrCode = PTTBot.login(ID, Password)
# 使用錯誤碼,判斷登入是否成功
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('登入失敗')
    sys.exit()

ErrCode, Post = PTTBot.getPost('Wanted', PostIndex=6000)
if ErrCode == PTT.ErrorCode.PostDeleted:
    if Post.getDeleteStatus() == PTT.PostDeleteStatus.ByAuthor:
        PTTBot.Log('文章被原 PO 刪掉了')
    elif Post.getDeleteStatus() == PTT.PostDeleteStatus.ByModerator:
        PTTBot.Log('文章被版主刪掉了')
    PTTBot.Log('作者: ' + Post.getAuthor())
    sys.exit()
elif ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('取得文章詳細資訊失敗 錯誤碼: ' + str(ErrCode))
    sys.exit()

# 登出
PTTBot.logout()

這時候,你可能會想如果想要爬最新的一篇文章怎麼辦? 這時候我們則需要 getNewestIndex 這支API

ErrCode, NewestIndex = PTTBot.getNewestIndex(Board=Board)

NewestIndex 就會是該版的最新文章編號。

另外,如果需要搜尋條件,也是沒問題。PTT Library 支援所有搜尋形式。

Keyword 搜尋關鍵字
Author 搜尋作者
Push 搜尋推文數
Mark 搜尋標記 m or s
Money 搜尋稿酬

使用方法如下,要特別注意的是搜尋形式跟搜尋條件都要一起代給 getNewestIndex 與 getPost 喔,部分程式碼如下。

Board = 'Wanted'
# 搜尋類別
inputSearchType = PTT.PostSearchType.Keyword
# 搜尋條件
inputSearch = '公告'

ErrCode, NewestIndex = PTTBot.getNewestIndex(Board=Board, SearchType=inputSearchType, Search=inputSearch)
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('取得 ' + Board + ' 板最新文章編號失敗')
    sys.exit()
ErrCode, Post = PTTBot.getPost(Board, PostIndex=NewestIndex, SearchType=inputSearchType, Search=inputSearch)
if ErrCode == PTT.ErrorCode.PostDeleted:
    if Post.getDeleteStatus() == PTT.PostDeleteStatus.ByAuthor:
        PTTBot.Log('文章被原 PO 刪掉了')
    elif Post.getDeleteStatus() == PTT.PostDeleteStatus.ByModerator:
        PTTBot.Log('文章被版主刪掉了')
    PTTBot.Log('作者: ' + Post.getAuthor())
    sys.exit()
elif ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('取得文章詳細資訊失敗 錯誤碼: ' + str(ErrCode))
    sys.exit()

最後的最後,終於要進入最厲害的 API 了,crawlBoard。

此 API 適合一次爬行大量文章,並可自訂處理方式,其中 PostHandler 就是你可以自訂處理文章的地方,每爬到一篇文章,PostHandler 就會被呼叫一次,你可以根據 showPost 取出任何你需要的文章資訊,做存檔或者顯示。

import sys
import json
import getpass
import codecs
from PTTLibrary import PTT

def showPost(Post):
    PTTBot.Log('文章代碼: ' + Post.getID())
    PTTBot.Log('作者: ' + Post.getAuthor())
    PTTBot.Log('標題: ' + Post.getTitle())
    PTTBot.Log('時間: ' + Post.getDate())
    PTTBot.Log('價錢: ' + str(Post.getMoney()))
    PTTBot.Log('IP: ' + Post.getIP())
    PTTBot.Log('網址: ' + Post.getWebUrl())

    PTTBot.Log('內文:\n' + Post.getContent())

    PushCount = 0
    BooCount = 0
    ArrowCount = 0

    for Push in Post.getPushList():
        PushType = Push.getType()

        if PushType == PTT.PushType.Push:
            PushCount += 1
        elif PushType == PTT.PushType.Boo:
            BooCount += 1
        elif PushType == PTT.PushType.Arrow:
            ArrowCount += 1
        
        Author = Push.getAuthor()
        Content = Push.getContent()
        # IP = Push.getIP()
        PTTBot.Log('推文: ' + Author + ': ' + Content)
        
    PTTBot.Log('共有 ' + str(PushCount) + ' 推 ' + str(BooCount) + ' 噓 ' + str(ArrowCount) + ' 箭頭')

def PostHandler(Post):
    
    with codecs.open("CrawlBoardResult.txt", "a", "utf-8") as ResultFile:
        ResultFile.write('P幣: ' + str(Post.getMoney()) + ' ' + Post.getTitle() + '\n')

try:
    with open('Account.txt') as AccountFile:
        Account = json.load(AccountFile)
        ID = Account['ID']
        Password = Account['Password']
except FileNotFoundError:
    ID = input('請輸入帳號: ')
    Password = getpass.getpass('請輸入密碼: ')

# 如果不想在登入的時候踢掉其他登入,你可以這樣使用
PTTBot = PTT.Library(kickOtherLogin=False)
# 登入
ErrCode = PTTBot.login(ID, Password)
# 使用錯誤碼,判斷登入是否成功
if ErrCode != PTT.ErrorCode.Success:
    PTTBot.Log('登入失敗')
    sys.exit()

CrawPost = 100

EnableSearchCondition = False
inputSearchType = PTT.PostSearchType.Keyword
inputSearch = '[公告]'

if EnableSearchCondition:
    ErrCode, NewestIndex = PTTBot.getNewestIndex(Board='Wanted', SearchType=inputSearchType, Search=inputSearch)
else:
    ErrCode, NewestIndex = PTTBot.getNewestIndex(Board='Wanted')

if ErrCode == PTT.ErrorCode.Success:
    PTTBot.Log('取得 ' + 'Wanted' + ' 板最新文章編號成功: ' + str(NewestIndex))
else:
    PTTBot.Log('取得 ' + 'Wanted' + ' 板最新文章編號失敗')
    sys.exit()

# MaxMultiLogin             多重登入數量
# SearchType                搜尋種類
# Search                    搜尋條件

if EnableSearchCondition:
    ErrCode, SuccessCount, DeleteCount = PTTBot.crawlBoard('Wanted', PostHandler, StartIndex=NewestIndex - CrawPost + 1, EndIndex=NewestIndex, SearchType=inputSearchType, Search=inputSearch)
else:
    ErrCode, SuccessCount, DeleteCount = PTTBot.crawlBoard('Wanted', PostHandler, StartIndex=NewestIndex - CrawPost + 1, EndIndex=NewestIndex)

if ErrCode == PTT.ErrorCode.Success:
    PTTBot.Log('爬行成功共 ' + str(SuccessCount) + ' 篇文章 共有 ' + str(DeleteCount) + ' 篇文章被刪除')

# 登出
PTTBot.logout()

如果有任何使用上的問題歡迎留言或者在 giiter 上留言,我會用最快的速度幫助你。

如果想了解其他功能,可以參考其他文章,或者直接到 PTT Library 頁面的 Test.py 有非常詳細的範例。

 


CodingMan,Bug maker

Github: https://github.com/PttCodingMan