支持正版,一切只爲學習
之前寫的筆趣閣爬蟲有不少同學說不能爬了,我後來發現是網站改版的緣故,前些日子把書裏的爬蟲項目都整的差不多了,現在又有些不知道爬什麼好了,剛好在這段時間把筆趣閣爬蟲代碼重寫一下。
(PS:我看自己之前寫的代碼感覺寫的好醜哦,官方吐槽,最爲致命,以前的鏈接)
做了以下改進:
- 將爬蟲代碼寫入對象中,把用戶選擇判斷的語句寫在外部,使得爬蟲本身更加簡潔;
- 優化互動的相關代碼,操作更舒適;
- 採用多進程爬取小說各章節的內容,速度更快;
- 加入UA代理,儘可能反反爬蟲,安全性更高 。
前期準備,需要在cmd裏下載安裝以下第三方庫:
pip install requests
pip install python-docx
一、基本思路
以下是爬取筆趣閣小說的主體思路,爬蟲代碼在這個思路上擴展構建。
二、構建爬蟲
筆趣閣的網站衆多,這是我爬取的筆趣閣網址:http://www.biquge.tv/
我將爬蟲寫到 biqugeCrawl 類中。
1、借東風,借用網站搜索書籍的功能
以搜索聖墟爲例,網站的搜索是靠這個searchkey這個參數。在biqugeCrawl類中定義一個search_book() 函數專用於查找小說功能。
1.1、 大家看到這個%什麼的或許會很陌生,這是因爲URL 只允許一部分 ASCII 字符(數字字母和部分符號),其他的字符(如漢字)是不符合 URL 標準的,所以我們這裏使用 urllib庫 的 parse.quote() 將其轉換爲URL編碼。
轉換後還是不符合要求,懷疑是編碼格式的問題,去網頁的源碼中查看,如下顯示爲GBK編碼,所以我們輸入的書名還要先轉換爲GBK格式。
name = input("請輸入書名:")
name = parse.quote(name.encode('gbk'))
1.2、 接下來我們拼接搜索的url,並請求它,得到相應的搜索結果的html網頁。
__random_ua()是隨機返回user-agent的函數,這樣在一定程度上可以避免被反爬。代碼很簡單,自己看一下。
serach_url = "http://www.biquge.tv/modules/article/search.php?searchkey={}"
url = serach_url.format(name)
response = requests.get(url=url, headers=self.__random_ua())
1.3、 如果搜到了書籍,也不直接下載,因爲好多小說名字相似趁熱度的,所以我這裏獲取第一個小說名及作者名返回讓用戶確認是否下載。另外,因爲搜索不當,可能沒有書籍,也可能是關鍵字太短了,我用try-except 另行處理,返回None值,並提示沒有該書。
# 搜索書籍
root = etree.HTML(response.content)
try:
book_name = root.xpath('//*[@id="nr"]/td[1]/a/text()')[0]
book_author = root.xpath('//*[@id="nr"]/td[3]/text()')[0]
book_url = root.xpath('//*[@id="nr"]/td[1]/a/@href')[0]
return [book_name, book_author, book_url]
except:
return None
# 用戶確認
book = biquge.search_book()
if book is not None:
flag = input("搜索到的書名爲《{}》,作者名爲{},請確認是否下載【Y/N】".format(book[0], book[1]))
while 1:
if flag == 'Y' or flag == 'y':
biquge.download(book[0], book[2])
break
elif flag == 'N' or flag == 'n':
break
else:
flag = input("請正確輸入【Y/N】!")
else:
print("查無此書")
2、遇明主,用戶確認下載小說
上一步寫得search_book() 函數在搜到小說得情況下返回了三個數據:小說名、作者名、小說得url,經由用戶確認,調用biqugeCrawl.download() ,獲取小說章節鏈接,並採用多線程下載內容。
2.1、 請求網頁,獲取鏈接,因爲它有九個最新章節得在裏面,所以我們使用xpath解析出來得列表要略去這九個,直接用切片切掉。
response = requests.get(url=url, headers=self.__random_ua())
root = etree.HTML(response.content)
# 拼接完整得章節url
content_urls = list(map(lambda x: "http://www.biquge.tv/" + x, root.xpath('//*[@id="list"]/dl/dd/a/@href')[9:]))
content_names = root.xpath('//*[@id="list"]/dl/dd/a/text()')[9:]
# 創建以書名命名的文件夾
path = '《' + book_name + '》'
if not os.path.exists(path):
os.mkdir(path)
2.2、 有了相應得鏈接,這裏就要使用多進程了,速度大大的增加。
utls_names = [i for i in zip(content_urls, content_names)]
# 同時進行得進程數量
pool = Pool(processes=5)
# 將utls_names中得元素分配給parse()函數
pool.map(self.parse, utls_names)
3、取荊州,下載小說內容,保存到docx文檔中
上面一步是獲取到了小說章節的目錄,並採用多進程的方法調用了biqugeCrawl.parse() 函數,這個函數的作用就是下載並保存小說內容。
3.1、 解析出小說的主體內容,觀察網頁源碼,可以發現,小說的內容都在<div id="content"></div>
html標籤中,我用正則表達式將其解析出來,並對文本做了一些處理。
response = requests.get(url=url, headers=self.__random_ua())
html = response.content.decode('gbk')
content = "".join(re.findall('<div id="content">(.*?)</div>', html, re.S))
content = re.sub("<.*?>", "", re.sub(" ", " ", content))
3.2、 獲取到的小說內容肯定是要保存起來的,我這裏保存到了docx文檔文件中。這裏又用xapth解析出了小說名,作爲文本保存路徑的一部分。
我後面想想那小說章節名也可以不傳進來,在這裏解析就是了,算了,無傷大雅
root = etree.HTML(response.content)
book_name = root.xpath('//div[@class="con_top"]/a[2]/text()')[0]
# 創建document對象
document = Document()
# 將文本寫入文檔
document.add_paragraph(content)
document.save("《{}》/《{}》.docx".format(book_name, name))
print('《' + name + '》已下載完成')
爬蟲的思路都理清楚了,還有一些旁支末節詳見完整代碼,有什麼不清楚也可以直接來問我。
三、運行測試
做了一些簡單的測試,按照提示來操作都是沒問題的,就算不按照提示來,整些奇奇怪怪的操作也是沒問題的,很舒服,而且速度也快,我爬取《聖墟》一千五百多章大概耗時三分鐘,也可以更快,不過我們只是學習爬蟲的技術又不是搞破壞,太快的話對服務器的壓力也很大的,還有被封IP的可能,可以參考【構建簡單IP代理池】,使用代理ip,這樣更安全。
IP代理池我自己是改了好幾版了,用起來還是很舒服的,不用擔心被封ip的事情了,把ip數據存到數據庫中,上手簡單,建議學習搭建。
四、總結
這爬取筆趣閣本來就是個小爬蟲,我感覺很適合讓剛入爬蟲的同學學習的,雖然相比那種只爬取單個網頁的入門學習方式要多些困難,不過克服困難後還是會有很大的進步的,這是我整理的【爬蟲實戰項目集合】,有興趣的可以看看,由淺入深的學習爬蟲,最簡單的和較爲複雜的都有。
對於這個爬筆趣閣的項目,我認爲有一個很大的槽點,它是多進程爬取出來的,文件按時間排序不行,按名稱排序又是下面這樣,emm,太難看了,還是之前老版本的非多進程好看,但就是慢些,沒有思路解決這個問題,求教。
另外,我想在爬蟲里加上暫定下載的功能,以及斷網處理的功能(這兩個功能是一個原理,搞一個任務隊列),這爬取筆趣閣小爬蟲是小爬蟲,但小事情多做幾次,就會越發的精細,好了,也沒啥講的了,祝大家寫程序沒有bug,天天開心。
啊,對啦對啦,點個贊,點個關注吧,蟹蟹支持。
五、完整代碼
import os
from urllib import parse
import requests
from docx import Document
from lxml import etree
import re
import random
from multiprocessing import Pool
import time
class biqugeCrawl():
m_url = "http://www.biquge.tv/"
# 查詢書籍函數
def search_book(self):
serach_url = "http://www.biquge.tv/modules/article/search.php?searchkey={}"
name = input("請輸入書名:")
name = parse.quote(name.encode('gbk'))
url = serach_url.format(name)
response = requests.get(url=url, headers=self.__random_ua())
root = etree.HTML(response.content)
try:
book_name = root.xpath('//*[@id="nr"]/td[1]/a/text()')[0]
book_author = root.xpath('//*[@id="nr"]/td[3]/text()')[0]
book_url = root.xpath('//*[@id="nr"]/td[1]/a/@href')[0]
return [book_name, book_author, book_url]
except:
return None
# 下載函數
def download(self, book_name, url):
response = requests.get(url=url, headers=self.__random_ua())
root = etree.HTML(response.content)
content_urls = list(map(lambda x: "http://www.biquge.tv/" + x, root.xpath('//*[@id="list"]/dl/dd/a/@href')[9:]))
content_names = root.xpath('//*[@id="list"]/dl/dd/a/text()')[9:]
# 創建以書名命名的文件夾
path = '《' + book_name + '》'
if not os.path.exists(path):
os.mkdir(path)
utls_names = [i for i in zip(content_urls, content_names)]
pool = Pool(processes=5)
pool.map(self.parse, utls_names)
print("《{}》下載完畢!".format(book_name))
# 爬取文章內容函數
def parse(self, url_name):
url = url_name[0]
name = url_name[1]
try:
response = requests.get(url=url, headers=self.__random_ua())
html = response.content.decode('gbk')
root = etree.HTML(response.content)
book_name = root.xpath('//div[@class="con_top"]/a[2]/text()')[0]
content = "".join(re.findall('<div id="content">(.*?)</div>', html, re.S))
content = re.sub("<.*?>", "", re.sub(" ", " ", content))
# 創建document對象
document = Document()
# 將文本寫入文檔
document.add_paragraph(content)
document.save("《{}》/《{}》.docx".format(book_name, name))
print('《' + name + '》已下載完成')
except Exception as e:
with open("./log.txt", "a+", encoding="utf-8") as file:
file.write("*"*30+"\n"+str(e))
print("出現異常,下載中斷,請查看log文件!")
pass
# 隨機UA
def __random_ua(self):
UA = ["Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0",
"Mozilla/5.0 (X11; U; Linux Core i7-4980HQ; de; rv:32.0; compatible; JobboerseBot;Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 5.1; rv:36.0) Gecko/20100101 Firefox/36.0",
"Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.12) Gecko/20050915 Firefox/1.0.7",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/20100101 Firefox/17.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0",
"Mozilla/5.0 (Windows NT 6.0; rv:34.0) Gecko/20100101 Firefox/34.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0",
"Mozilla/5.0 (Windows NT 5.1; rv:40.0) Gecko/20100101 Firefox/40.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20041107 Firefox/1.0",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0",
"Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/20.6.14",
"Mozilla/5.0 (Windows NT 5.1; rv:30.0) Gecko/20100101 Firefox/30.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0",
"Mozilla/5.0 (X11; U; Linux Core i7-4980HQ; de; rv:32.0; compatible; JobboerseBot; Gecko/20100101 Firefox/38.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0"
]
headers = {
"User-Agent": {}
}
headers["User-Agent"] = random.choice(UA)
return headers
if __name__ == '__main__':
biquge = biqugeCrawl()
print("----筆趣閣小說爬蟲----")
print("1-------------搜索小說")
print("2----------------退出")
flag = input("請輸入指令選擇相應功能:")
while 1:
error_str = ""
if flag == "1":
book = biquge.search_book()
if book is not None:
flag = input("搜索到的書名爲《{}》,作者名爲{},請確認是否下載【Y/N】".format(book[0], book[1]))
while 1:
if flag == 'Y' or flag == 'y':
biquge.download(book[0], book[2])
break
elif flag == 'N' or flag == 'n':
break
else:
flag = input("請正確輸入【Y/N】!")
else:
print("查無此書")
elif flag == "2":
exit(1)
else:
error_str = "指令錯誤,"
flag = input("{}請重新輸入指令選擇相應功能【1.搜索;2.退出】:".format(error_str))