情況最簡單下的爬蟲案例

Jason990420發表於2020-03-06

檔案建立日期: 2020/03/06
最後修訂日期: None
相關軟體資訊:

Win 10 Python 3.8.2 PySimpleGUI 4.16.0 BS4 4.8.2

說明: 本文請隨意引用或更改, 只須標示出處及作者, 作者不保證內容絶對正確無誤, 如造成任何後果, 請自行負責.

標題: 情況最簡單下的爬蟲案例

常看一些網路上的小說, 但都會碰到某些章節沒下載或廣告等等問題, 所以就寫了一個簡單的爬蟲, 只針對簡單的請求, 沒有要設定Header, Cookie, 登入, 驗證或VIP使用者等, 就可以取得網頁資料的小說網頁

要求:

  1. 小說目錄網址, 不需翻頁, 以https://www.wfxs.org/html/2/為例
  2. 取得各章的網址
  3. 建立一個目錄供存整本小說
  4. 每章各建立一個文字檔案
  5. 為加速完成, 使用多執行緒
  6. 簡單顯示進行中的執行緒, 完成數及總數

執行緒說明

用了好幾種方法, 常會碰到主程式結束, 但執行緒未全部完成, 章節的總數總是不對, 因此自己建立記錄區, 自行管理, 終於解決這個問題, 明確所有的執行緒都已完成.

輸出

情況最簡單下的爬蟲案例

情況最簡單下的爬蟲案例

說明及程式碼

  1. 使用的庫
from pathlib import Path
from bs4 import BeautifulSoup as bs
from copy import deepcopy
import urllib.request as request
import _thread
import PySimpleGUI as sg
  1. 建立網頁處理的類
class WEB():

    def __init__(self):
        self.base = 'https://www.wfxs.org'  # 網頁的根目錄
        self.root = ''                      # 小說儲存的根目錄
        self.queue = {}                     # 記錄目前正在工作的執行緒
        self.max = 40                       # 最大執行緒數
        self.buffer = {}                    # 執行緒完成的章節小說內容, 待存檔
        self.temp = []                      # 章節執行緒出錯, 待重排入執行緒
        self.count = 0                      # 已存檔章節數
        self.not_allow = '''?|><"*'+,;=[]\x00/\\''' # 不合格的檔名文字
  • 建立目錄: 如果該目錄已存在, 後面加上數字以區別
    def create_subdirectory(self, name):
        i, path = 1, Path(name)
        while path.is_dir():
            i += 1
            path = Path(name+str(i))
        self.root = path
        path.mkdir()
  • 讀取該章節小說內容
    由於內容放在head中, 如果直接以head.text讀取, 也會取到子tag中的文字, 因此先移除其他所的子tag, 再以head.text讀取.
    <html ……><head><title> … </title><meta … /> … 章內容文字</head>
    def chapter_content(self, html):
        for tag in html.head: tag.extract()
        chapter_text = self.form(html.head.text).strip()
        return chapter_text
  • 將文字中的<br>以及多餘的空行移除
    def form(self, text):
        text = text.replace('\xA0', '')
        while '\n\n' in text:
            text = text.replace('\n\n', '\n')
        return text
  • 獲取一個新未使用的記錄執行緒鍵值
    def get_a_key(self):
        for i in range(self.max):
            if i not in self.queue:
                return i
        return None
  • 讀取目錄中的作者名
    find_all, tag為meta, 引數name:author; 讀取content的內容, 從右邊分割字串, 取最右邊一個.
    <meta content="絕品天醫 版權歸 葉天南"name="author"/>
    def get_auther(self, html):
        return html.find_all(
            name ='meta', attrs={'name':'author'}
            )[0].get('content').rsplit(sep=None, maxsplit=1)[-1]
  • 讀取目錄中所有的章節名及鏈結
    find_all, tag為<dd>, 章節名為tag<a>text, 鏈結為tag<a>href值, 如果章節名空字串, 略過.
    <dd><a href="/html/2/3063.html">第十五章 慶元診所</a></dd>
    def get_chapters(self, html):
        chapters = html.find_all(name='dd')
        result = []
        for chapter in chapters:
            title = chapter.a.text.split('(')[0]
            if title != '':
                link = self.base + chapter.a.get('href')
                result.append([self.valid(title), link])
        return result
  • 讀取目錄中對該書的簡介
    簡介在tag<p>, 引數class="tl pd8 pd10"<br>後的text
    <p class="tl pd8 pd10">作者︰葉天南寫的小說《絕品天醫》… <br/>.……..
    def get_description(self, html):
        return self.form(html.find(
            name='p',
            attrs={'class':"tl pd8 pd10"}).br.text)
  • 讀取目錄中書名
    書名在tag<h1>中的text, 因為要以書名來建立目錄, 所以要移除書名中的非法字母
    <h1 class="tc h10">絕品天醫</h1>
    def get_name(self, html):
        return self.valid(html.h1.text)
  • 載入目錄中的書名, 作者, 簡介, 各章名及其鏈結
    def load_catalog(self, url):
        status, html     = self.load_html(url)
        if status != 200:
            return None, None, None, None, None
        name        = self.get_name(html)
        auther      = self.get_auther(html)
        description = self.get_description(html)
        chapters    = self.get_chapters(html)
        return status, name, auther, description, chapters
  • 載入章節的小說內文, 再放入存檔用的緩衝區, 只要網頁載入的結果不是200程式碼, 就從執行緒記錄區移除, 並放入後面重排入執行緒.
    def load_chapter(self, key, chapter, url):
        status, html = self.load_html(url)
        if status != 200:
            self.temp.append([chapter, url])
            del self.queue[key]
        else:
            chapter_text = self.chapter_content(html)
            self.buffer[key] = [chapter, chapter_text]
        return
  • 根據網址讀入HTML檔, 如果出錯或狀態程式碼不是200的都返回None, 指示錯誤, 後面再重新讀取
    該網頁的編碼為big5, 譯碼如果有錯, ignore, 該字會略過不處理.
    <meta http-equiv="Content-Type" content="text/html; charset=big5" />
    def load_html(self, url):
        try:
            response = request.urlopen(url)
            status   = response.getcode()
        except:
            return None, ''
        else:
            if status == 200:
                data = str(response.read(), encoding='big5', errors='ignore')
                html = bs(data, features='html.parser')
                return status, html
            else:
                return None, ''
  • 刪除執行緒記錄
    def queue_delete(self, key):
        del self.queue[key]
  • 執行緒加入記錄中, 並啟動, 批註中為非執行緒作法
    def queue_insert(self, chapter, url):
        key = self.get_a_key()
        self.queue[key] = [chapter, url]
        # self.load_chapter(key, chapter, url)
        _thread.start_new_thread(self.load_chapter, (key, chapter, url))
  • 檢查執行緒記錄是否已達到上限, 用來限制最大限執行緒數, 不會再加入新的限程
    def queue_is_full(self):
        return True if len(self.queue) == self.max else False
  • 檢查執行緒記錄是否空, 用來確認所有的執行緒都已完成.
    def queue_not_empty(self):
        return True if len(self.queue) != 0 else False
  • 儲存小說書的說明檔, 內含書名, 作者, 簡介, 如果檔案已存在, 附加額外數字以區別
    def save_book(self, name, auther, description):
        i, path = 1, self.root.joinpath(name+'.txt')
        while path.is_file():
            i += 1
            path = self.root.joinpath(name+str(i)+'.txt')
        text = '\n'.join(('書名: %s'%name, '作者: %s'%auther, 
                          '簡介: %s'%description))
        with open(path, 'wt', encoding='utf-8') as f:
            f.write(text)
  • 儲存小說的章節內文, 如果檔名存在, 附加額外數字以區別, 在存檔緩衝區以及執行緒記錄中, 刪除該章節.
    def save_chapter(self):
        buffer = deepcopy(self.buffer)
        for key, value in buffer.items():
            i, path = 1, self.root.joinpath(value[0]+'.txt')
            while path.is_file():
                i += 1
                path = self.root.joinpath(value[0]+str(i)+'.txt')
            with open(path, 'wt', encoding='utf-8') as f:
                f.write(value[1])
            self.count += 1
            del self.buffer[key]
            del self.queue[key]
  • 將檔名中的非法字母移除, 避免存檔錯誤
    def valid(self, text):
        return ''.join((char for char in text if char not in self.not_allow))
  • 主程式
    • 如果目錄無法載入, 結束程式
    • 儲存小說書的說明檔
    • 建立簡單GUI, 顯示進度, 並控制隨時可以結束, 或保證所有的行程都已執行完畢
url = 'https://www.wfxs.org/html/2/'    # 小說目錄網址
W = WEB()

status, name, auther, description, chapters = W.load_catalog(url)
if status == None:
    print('%s open failed !' % url)
    quit()

W.create_subdirectory(name)
W.save_book(name, auther, description)

font = ('Courier New', 16, 'bold')
layout = [[sg.Text('', font=font, auto_size_text=False, key='Text1', 
                   size=(W.max, 1))],
          [sg.Text('', font=font, auto_size_text=False, key='Text2', 
                   size=(W.max, 1))]]
window = sg.Window('Novel Download', layout=layout, finalize=True)

size = len(chapters)
all = deepcopy(chapters)
while len(all) != 0:
    W.temp = []
    for chapter, url in all:
        W.save_chapter()
        state, values = window.read(timeout=1)
        if state == None:
            window.close()
            quit()
        window['Text1'].update(value='■'*len(W.queue))
        window['Text2'].update(
            value='{}/{} chapters saved'.format(W.count, size))
        while W.queue_is_full():    # 如果執行緒記錄區已滿, 存章節
            W.save_chapter()
        W.queue_insert(chapter, url)    # 插入執行緒, 下載章節
    # 如果執行緒記錄區不是空的, 存章節, 跑完所有的執行緒, 再重新跑出錯的章節
    while W.queue_not_empty():
        W.save_chapter()
    all = deepcopy(W.temp)

while True:
    state, values = window.read(timeout=100)
    if state == None:
        break
    window['Text1'].update(value='■'*len(W.queue))
    window['Text2'].update(value='{}/{} chapters saved'.format(W.count, size))
    W.save_chapter()    # 儲存章節, 直到執行緒全部執行完畢
window.close()
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Jason Yang

相關文章