iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
0

注意! 注意! 注意!

此篇程式碼為分享範例,請勿直接執行程式碼。若想測試爬蟲,請以其他目標網站測試,並遵守爬蟲禮儀,謝謝。


歡迎來到第六天,今天要承接第四天衍伸到更深層的爬蟲。一個網站(這裡定義為同一個域名底下的所有網頁)不會只有單純的首頁與文章。像 IT 邦幫忙是一個很強大的網站,包含了很多有趣、很強大的功能,但這裡只簡單的舉其中一個類型 - 技術問答,在 IT 邦幫忙中累積了無數前輩們的討論與文章,若今天的爬蟲目的為爬取所有問題內容,那我們該怎麼處理呢?

觀察

經過了前兩篇的分享,在爬蟲時的第一個動作是定義好目標,接下來就是觀察目標頁面的架構。首先我們目標為爬取所有問答相關的內容,會發現在「技術問答」這個大標底下,又有「最新」、「熱門」、「已解決」這三個分類,而在分類底下又有許多頁。而隨意點進一篇問答文章時會發現問答類型文章的 URL 架構為 https://ithelp.ithome.com.tw/questions/{文章 ID} 。再了解目標頁面的架構與相互關係後,就可以開始擬定爬蟲策略。

https://ithelp.ithome.com.tw/upload/images/20200919/20128931fgdnyid0q8.png

策略

最直觀的方式是模擬人類行為做爬蟲,也就是順著架構往下一層層的爬。另一種是盲目式的爬,由於我們觀察到每一個頁面中,都有另外推薦文章、熱門文章等,因此我們也可以設定一隻蟲是收集整個頁面的 <a>,一但滿足https://ithelp.ithome.com.tw/questions/{文章 ID} 的條件,就進行爬蟲,一路往下爬。今天我會以第一種策略作為舉例。

規劃功能

首先「最新」、「熱門」、「已解決」這三個分類是固定的,因此可以設定這三個分頁為目標網址,但每個分類底下的頁面數並非固定,這會是我們需要另外著墨的功能,另外由於文章數過多,無法保證這三個分類內的文章不會相互重複,因此要另外設定一個檢查機制,去避免浪費請求資源。

開始寫扣

依照我們的策略,首先第一步是蒐集所有問答文章的 URL,而在這個步驟由於每個分類所擁有的頁面不同,因此需要解決如何完整讀取所有頁面的資料。由於 IT 邦幫忙的頁面中會顯示該分類的最後一頁頁碼,因此可以直接讀取數字並透過 GET 的 Parameters 進行完整的爬蟲。當然另一種方式就是以「下一頁」做為目標,不斷的讀取該標籤的 URL 往下爬。這裡將以第一種方式做舉例。

https://ithelp.ithome.com.tw/upload/images/20200919/201289313owmtGmanN.png

import requests
from lxml import etree

def page_numbers(html):
    next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
    final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
    return int(final_page)
    

當我們解決尋找最後一頁,接下來要處理的是擷取每個分類的文章網址。

def get_article_url(url, final_page):
    urls = set()
    for i in range(2, final_page+1):
        response = requests.get(url, params = {'page':i})
        html = etree.HTML(response.text)
        urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
    return urls

接下來就可以將所有程式碼合併。

from time import sleep
import requests
from lxml import etree

def page_numbers(html):
    next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
    final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
    return int(final_page)
  
def get_article_url(url, last_page):
    urls = set()
    for i in range(last_page):
        response = requests.get(url, params = {'page':i+1})
        html = etree.HTML(response.text)
        urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
        sleep(100) # 做隻有禮貌的蟲
    return urls
  
target_url = ["https://ithelp.ithome.com.tw/questions",
             "https://ithelp.ithome.com.tw/questions?tab=hot",
             "https://ithelp.ithome.com.tw/questions?tab=solved"]
article_urls = set()

for url in target_url:
    response = requests.get(url)
    html = etree.HTML(response.text)
    last_page =  page_numbers(html)
    article_urls = article_urls.union(get_article_url(url,last_page))

之後我們就可以到目前為止我們就拿到所有問答文章的 URL,由於這次在抓取文章 URL 時沒有同時間把標題留下,因此接下來需要整理之前寫的程式碼

def article_parse(link):
    response = requests.get(link)
    html = etree.HTML(response.text)
    mk = html.xpath("//div[@class='markdown__style']")[0]
    header = html.xpath("//h2[@class = 'qa-header__title']/text()")[0]
    return {header : etree.tostring(mk, method='text', encoding='unicode', pretty_print=True).strip()}

最後合併所有的程式碼

from time import sleep
import requests
from lxml import etree

def page_numbers(html):
    next_tag = html.xpath("//a[@rel='next']")[0] # 定位出下一頁
    final_page = next_tag.getparent().getprevious().find('a').text # 下一頁按鈕的前一個按鈕及為最後一頁的數字
    return int(final_page)
  
def get_article_url(url, last_page):
    urls = set()
    for i in range(last_page):
        response = requests.get(url, params = {'page':i+1})
        html = etree.HTML(response.text)
        urls = urls.union(set(html.xpath("//a[@class = 'qa-list__title-link']/@href")))
        sleep(100) # 做隻有禮貌的蟲
    return urls

def article_parse(link):
    response = requests.get(link)
    html = etree.HTML(response.text)
    mk = html.xpath("//div[@class='markdown__style']")[0]
    header = html.xpath("//h2[@class = 'qa-header__title']/text()")[0]
    return {header : etree.tostring(mk, method='text', encoding='unicode', pretty_print=True).strip()}
  
target_url = ["https://ithelp.ithome.com.tw/questions",
             "https://ithelp.ithome.com.tw/questions?tab=hot",
             "https://ithelp.ithome.com.tw/questions?tab=solved"]
article_urls = set()

for url in target_url:
    response = requests.get(url)
    html = etree.HTML(response.text)
    last_page =  page_numbers(html)
    article_urls = article_urls.union(get_article_url(url,last_page))

article = {}
for url in article_urls:
    article = {**article,**article_parse(url)}
    sleep(100) # 做隻有禮貌的蟲

這樣就完成所有的程式碼了!今天就到這裡,明天見!


上一篇
[Day 5] 番外篇 - 爬蟲禮儀
下一篇
[Day 7] 番外篇 - 工程師的生活就是這麼樸實無華
系列文
資料蒐集與分散式運算 30 天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言