[爬蟲系列]Python爬蟲入門實作(二)—PTT八卦版貼文

nana ba
20 min readMar 13, 2021

--

在上一篇[爬蟲入門實作(一)]中,我用字典進行對爬蟲進行擴展,但是現實中的網頁存在著很多眉眉角角,不一定都是那麼規律,可能連簡單的請求伺服器訪問都有限制(像是進入網站前詢問是否滿18歲),PTT八卦版就是一個很好的例子,因此本次實作要來做PTT八卦的貼文爬蟲。

我先列出這次爬蟲程式要達到的需求,以便寫出函式框架

  • 要有貼文標題,作者,發佈時間
  • 能直接打印出來
  • 能轉成CSV表格(我也不知道PTT貼文轉表格要幹嘛,但是上篇有做這篇也做一下)
  • 能用關鍵字篩選標題
  • 可以選擇要爬幾頁

條件列出後,我寫先出五個函式框架,分別是

#用requests回傳 url.text給其他函式熬美麗湯
def getHtml(url):
pass
#獲取資訊的主要函式,要能傳入頁數,是否要轉表格,關鍵字篩選
def getmsg(url, page, data_cvs=True, keyword=''):
pass
#從當前的頁面找到下一頁的連結並回傳
def nextpage(url):
pass
#回傳貼文連結,要有能關鍵字篩選功能
def get_article_href(url, keyword):
pass
#轉成csv表格的函式
def csvmsg():
pass

這邊要補充說明的函式是get_article_herf()這個函式,因為在PTT八卦版的頁面,原始碼中是找不到文章作者的資訊跟時間的

這邊並沒有作者跟時間

點進一篇文章查看原始碼,會看到文章標題作者跟時間都被放在<span class=”article-meta-value”>這個tag裡面

點進文章查看原始碼,59元買洨真的太貴
在<span class=”article-meta-value”>中有我們所需要的所有資訊

因此我寫了這個抓取文章連結的函式get_article_herf(),讓我們能獲取資訊,這個函式也要加上關鍵字篩選功能,不是我們要的貼文就不用麻煩回傳了

第一步,先import所有需要的libs

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
import os
import datetime

再來,我們先看第一個函式getHtml(url),用try和except功能增加程式穩定度,接下來按照標準程序走,利用raise_for_status()檢查伺服器請求情況

import requestsdef getHtml(url):
try:
resp = requests.get(url)
resp.encoding = 'utf-8'
resp.raise_for_status()

return resp.text
except: return '獲取Html訊息失敗'

這時候卻發現,沒辦法順利進入,用過ptt的人都知道,八卦版有個小朋友守門員,一進去會先問你有沒有滿18,所以直接訪問的話會訪問到這個畫面

我已經滿18了,放我進去

這時候我們來看一下這個限制頁面的運作,點開原始碼會看到這邊有個很可疑的over18,想必跟我們能不能訪問到內容有大的關係。

當我點下,”我同意”以後,在網頁的原始碼終會看到over18是cookie的回應,如果滿18歲的話over18會返回1的值

所以在這裡我們將函式改一下,讓他帶有cookie值的去訪問,伺服器看到over18=1就沒問題了

import requestsdef getHtml(url):
try:
headers = {'cookie': 'over18=1'}
resp = requests.get(url, headers=headers)
resp.encoding = 'utf-8'
resp.raise_for_status()

return resp.text
except:return '獲取Html訊息失敗'

再來我們來看翻頁的函式nextpage,在原始碼中,PTT的翻頁連結放在<a class “btn wide”>的href裡面,(PTT是從新頁往回翻,所以翻頁是上頁)

因此我將連結回傳,熬湯,利用find_all將所有a tag class=’btn wide’的內容找出來,會回“最舊”,“上頁”,“最新”的tag,我們需要的是第二個“上頁”tag的href,因此用[1][‘href’]將其slide出來,再加上 ‘https://www.ptt.cc’回傳即可。

def nextpage(url):
soup = BeautifulSoup(getHtml(url), 'lxml')
link = soup.find_all('a', class_='btn wide')[1]['href']
link = 'https://www.ptt.cc' + link

return link

統整一下目前的code

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
import os
import datetime
#用requests回傳 url.text給其他函式熬美麗湯
def getHtml(url):
try:
resp = requests.get(url)
resp.encoding = 'utf-8'
resp.raise_for_status()

return resp.text
except:return '獲取Html訊息失敗'#獲取資訊的主要函式,要能傳入頁數,是否要轉表格,關鍵字篩選
def getmsg(url, page, data_cvs=True, keyword=''):
pass
#從當前的頁面找到下一頁的連結並回傳
def nextpage(url):
soup = BeautifulSoup(getHtml(url), 'lxml')
link = soup.find_all('a', class_='btn wide')[1]['href']
link = 'https://www.ptt.cc' + link

return link
#回傳貼文連結,要有能關鍵字篩選功能
def get_article_href(url, keyword):
pass
#轉成csv表格的函式
def csvmsg():
pass

這part要看的是get_article_href()函式,這個函式接收到的是已經熬好的湯,作用是把<div class=”title”>裡的a tag的連結提取出來回傳,因此我們只需要find(‘a’)就可以。

後面的text的功能,beautifulsoup中很好用的一個參數,能夠靠tag裡的string去做搜尋,下圖的例子就可以用 find(‘a’, text=’前面’)的方式來找到,這邊搭配re正則器來使用,就可以回傳含有關鍵的標題,最後一樣提取出href加上’https://www.ptt.cc'便可回傳。

def get_article_href(row, keyword=''):
wordcheck = row.find('a', text= re.compile(keyword))
#如果有找到再做提取href
if wordcheck:
address = wordcheck['href']
article_url = 'https://www.ptt.cc' + address
return article_url

接著是csvmsg函數,跟上篇一樣,先將資訊轉成DataFrame後加上時間戳,再轉csv檔即可

def csvmsg(content):
columns = ['作者', '標題', '發佈時間']
df = pd.DataFrame(content, columns=columns)
#時間戳
timestamp = datetime.datetime.now().strftime('%Y%m%d')
timestamp = timestamp[:4] + '-' + timestamp[4:6] + '-' + timestamp[6:9]

filename = 'ptt gossip ' + timestamp+ '.csv'
#轉成csv檔
df.to_csv(filename, index=False)
print(f'文件{filename}已成功保存至{os.getcwd()}')

統整一下目前的code

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
import os
import datetime
#用requests回傳 url.text給其他函式熬美麗湯
def getHtml(url):
try:
resp = requests.get(url)
resp.encoding = 'utf-8'
resp.raise_for_status()

return resp.text
except:return '獲取Html訊息失敗'#獲取資訊的主要函式,要能傳入頁數,是否要轉表格,關鍵字篩選
def getmsg(url, page, data_cvs=True, keyword=''):
pass
#從當前的頁面找到下一頁的連結並回傳
def nextpage(url):
soup = BeautifulSoup(getHtml(url), 'lxml')
link = soup.find_all('a', class_='btn wide')[1]['href']
link = 'https://www.ptt.cc' + link

return link
#回傳貼文連結,要有能關鍵字篩選功能
def get_article_href(row, keyword=''):
wordcheck = row.find('a', text= re.compile(keyword))
#如果有找到再做提取href
if wordcheck:
address = wordcheck['href']
article_url = 'https://www.ptt.cc' + address
return article_url
#轉成csv表格的函式
def csvmsg(content):
columns = ['作者', '標題', '發佈時間']
df = pd.DataFrame(content, columns=columns)
#時間戳
timestamp = datetime.datetime.now().strftime('%Y%m%d')
timestamp = timestamp[:4] + '-' + timestamp[4:6] + '-' + timestamp[6:9]

filename = 'ptt gossip ' + timestamp+ '.csv'
#轉成csv檔
df.to_csv(filename, index=False)
print(f'文件{filename}已成功保存至{os.getcwd()}')

確認沒問題過後,剩下的就是主要函式了。

最後主要函式getmsg(),利用前面的getHtml()函式回傳,熬湯。fina_all找出所有<div class=title>的tag

def getmsg(url, pages, data_csv=False, keyword=''):
'''
:param url: 字串 --> 網址
:param keyword: 字串 --> 關鍵字搜尋,預設返回全部
:param data_csv: 布林值-->是否輸出成表格
:return:
'''
#儲存內容轉成表格
content = []
soup = BeautifulSoup(getHtml(url), 'lxml')
rows = soup.find_all('div', class_='title')

rows應該是回傳這樣的內容

rows

利用迴圈將rows的內容分別提取出來,剛剛寫的get_article_href可以提取出底下a tag的href並回傳連結,再用文章鏈結熬湯。這邊我使用select,select的功能其實和find_all一樣,select主要是針對css去做搜尋。

前面我們提到我們所需要的資訊都在<span class=”article-meta-value”>裡面,用select找就是select(‘span.article-meta-value’),回傳的格式應該會像下面這樣。

[<span class=”article-meta-value”>marunouchi (marunouchi)</span>, <span class=”article-meta-value”>Gossiping</span>, <span class=”article-meta-value”>[問卦] 大安區公寓會漲到一坪200萬嗎?</span>, <span class=”article-meta-value”>Sat Mar 13 11:27:45 2021</span>]

看得出來總共回傳四個tag,第一個是作者,第二個是看板,第三個是標題,第四個是發佈時間,所以我們就可以直接打印出我們要的資訊。

def getmsg(url, pages, data_csv=False, keyword=''):
'''
:param url: 字串 --> 網址
:param keyword: 字串 --> 關鍵字搜尋,預設返回全部
:param data_csv: 布林值-->是否輸出成表格
:return:
'''
#儲存內容轉成表格
content = []
soup = BeautifulSoup(getHtml(url), 'lxml')
rows = soup.find_all('div', class_='title')
for row in rows:
article_url = get_article_href(row, keyword)
article_soup = BeautifulSoup(getHtml(article_url), 'lxml')
article_info = article_soup.select('span.article-meta-value')
if article_info:
print(f'作者:{article_info[0].string}')
print(f'標題:{article_info[2].string}')
print(f'時間:{article_info[3].string}')
print()

接著要來加上翻頁功能,利用for迴圈來實現,在迴圈的最後加上我們之前寫所nextpage()函式,將url替換成nextpage(url),下次一迴圈,就會使用翻頁厚的url來爬取資料。

def getmsg(url, pages, data_csv=False, keyword=''):
'''
:param url: 字串 --> 網址
:param keyword: 字串 --> 關鍵字搜尋,預設返回全部
:param data_csv: 布林值-->是否輸出成表格
:return:
'''
#儲存內容轉成表格
content = []
#要爬取的頁數
pages = pages
for page in range(pages):
soup = BeautifulSoup(getHtml(url), 'lxml')
rows = soup.find_all('div', class_='title')

for row in rows:
article_url = get_article_href(row, keyword)
article_soup = BeautifulSoup(getHtml(article_url), 'lxml')
article_info = article_soup.select('span.article-meta-value')
print(article_info)
if article_info:
print(f'作者:{article_info[0].string}')
print(f'標題:{article_info[2].string}')
print(f'時間:{article_info[3].string}')
print()
url = nextpage(url)

最後的最後再加上轉成csv的功能就大功告成了

def getmsg(url, pages, data_csv=False, keyword=''):
'''
:param url: 字串 --> 網址
:param keyword: 字串 --> 關鍵字搜尋,預設返回全部
:param data_csv: 布林值-->是否輸出成表格
:return:
'''
content = []
pages = pages
for page in range(pages):
soup = BeautifulSoup(getHtml(url), 'lxml')
rows = soup.find_all('div', class_='title')

for row in rows:
article_url = get_article_href(row, keyword)
article_soup = BeautifulSoup(getHtml(article_url), 'lxml')
article_info = article_soup.select('span.article-meta-value')
print(article_info)
if article_info:
print(f'作者:{article_info[0].string}')
print(f'標題:{article_info[2].string}')
print(f'時間:{article_info[3].string}')
print()
# 是否要轉成csv檔,要的話再存入content list
if data_csv:
c = [article_info[0].string,
article_info[2].string,
article_info[3].string]
content.append(c) url = nextpage(url)
#利用之前寫的csvmsg函式來轉csv檔
if data_csv:
csvmsg(content)

完整的code

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
import os
import datetime

url = 'https://www.ptt.cc/bbs/Gossiping/index.html'
headers = {'cookie': 'over18=1'}

def getHtml(url):
try:
headers = {'cookie': 'over18=1'}
resp = requests.get(url, headers=headers)
resp.encoding = 'utf-8'
resp.raise_for_status()

return resp.text
except:
return '獲取Html訊息失敗'



def getmsg(url, pages, data_csv=False, keyword=''):
'''
:param url: 字串 --> 網址
:param keyword: 字串 --> 關鍵字搜尋,預設返回全部
:param data_csv: 布林值-->是否輸出成表格
:return:
'''
content = []
pages = pages
for page in range(pages):
soup = BeautifulSoup(getHtml(url), 'lxml')
rows = soup.find_all('div', class_='title')

for row in rows:
article_url = get_article_href(row, keyword)
article_soup = BeautifulSoup(getHtml(article_url), 'lxml')
article_info = article_soup.select('span.article-meta-value')
print(article_info)
if article_info:
print(f'作者:{article_info[0].string}')
print(f'標題:{article_info[2].string}')
print(f'時間:{article_info[3].string}')
print()

if data_csv:
c = [article_info[0].string,
article_info[2].string,
article_info[3].string]
content.append(c)
url = nextpage(url)

if data_csv:
csvmsg(content)


def nextpage(url):
soup = BeautifulSoup(getHtml(url), 'lxml')
link = soup.find_all('a', class_='btn wide')[1]['href']
link = 'https://www.ptt.cc' + link

return link


def get_article_href(row, keyword=''):
wordcheck = row.find('a', text= re.compile(keyword))
if wordcheck:
address = wordcheck['href']
article_url = 'https://www.ptt.cc' + address
return article_url


def csvmsg(content):
columns = ['作者', '標題', '發佈時間']
df = pd.DataFrame(content, columns=columns)

timestamp = datetime.datetime.now().strftime('%Y%m%d')
timestamp = timestamp[:4] + '-' + timestamp[4:6] + '-' + timestamp[6:9]

filename = 'ptt gossip ' + timestamp+ '.csv'
df.to_csv(filename, index=False)
print(f'文件{filename}已成功保存至{os.getcwd()}')

最後測試內容,尋找關鍵字’房’,5頁的八卦版貼文,並輸出成表格

sample code:

--

--