從零開始寫Python爬蟲 --- 2.6 爬蟲實踐:重構排行榜小說爬蟲&Mysql資料庫

這次我們要在scrapy框架下重構我們上次寫的排行榜小說爬蟲(https://zhuanlan.zhihu.com/p/26756909) 並將爬取的結果存儲到mysql資料庫中。另外,這是爬蟲專欄第二部分:Scrapy框架 的最後一篇文章啦~

目標分析:

我們的目標十分明確:

由於上次自己寫的bs4小說爬蟲效率堪憂,

我又不肯自己寫多線程(其實是不會!逃)

所以我們來利用Scrapy強大的並發功能吧!

但是,用到並發其實會有個坑,下文會著重說明。

那麼我們只要:

找到小說每一章的鏈接地址,

將每章小說的標題、正文部分存入資料庫。

數據篩選:

由於是代碼的重構,其實在上次的文章中我們就已經把整個爬蟲如何運作的邏輯完成了,這次只需要用Scrapy框架的方法重寫一遍就行。另外,也會拋棄bs4庫,投降Xpath的懷抱。

好來,我們來看具體怎麼寫吧。

遇到的麻煩:

說起來,一開始我是不肯用資料庫的,

我覺得直接把小說爬下來寫入文本不就結了嗎?

然而,理想很豐滿 現實很骨幹!

寫入的文本是這樣的:

發現了沒有? 小說的順序是不固定的,序章之後居然就是65章了。

我去查了一下原因:

scrapy非同步處理Request請求,Scrapy發送請求之後,不會等待這個請求的響應,他會同時發送其他請求或者做別的事情。

整數因為這個特性,Scrapy才能有這麼快的速度,他在cpu等待IO操作的時間,發起了一個新的線程。

如何解決?

遇到關於順序這個蛋疼的問題,我想了很多辦法:

比如設置request的priority(優先順序),讓每次request按照優先順序排隊發出,然而這樣並不能改變寫進文本的順序。

在思考一番之後,我決定通過將數據寫入mysql資料庫,來解決排序的問題:

當然,還是由於Scrapy的並發系統,就算是寫入資料庫,也不能按順序入庫,

結果是這樣的:

我拍腦袋一想,為什麼要按順序入庫呢,

我在查詢數據的時候,給他按章節名來排序不就結了?

我真機智,快為我點個贊!

然而,如果按照章節名排序,出來的結果是:

是的 mysql是瑞典人開發的,

默認是utf8編碼,

不支持中文排序也是很正常的!

這裡我們是不是陷入了trouble呢?

當然不是,我們給每章小說都定義一個id欄位,

最後通過id來給章節排序,不就完了嗎?

這裡我選擇:

將每一章的章節名中的數字,

轉換為阿拉伯數字,

再傳入id欄位~

看代碼吧:

注意,需要將這個模塊 放在和spider同級目錄,方便一會我們寫spider的時候導入

實現了中文向阿拉伯數字轉換用於從小說章節名提取id來排序chs_arabic_map = {: 0, : 1, : 2, : 3, : 4, : 5, : 6, : 7, : 8, : 9, : 10, : 100, : 10 ** 3, : 10 ** 4, : 0, : 1, : 2, : 3, : 4, : 5, : 6, : 7, : 8, : 9, : 10, : 100, : 10 ** 3, : 10 ** 4, : 10 ** 8, : 10 ** 8, : 1, 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 7: 7, 8: 8, 9: 9}num_list = [1,2,4,5,6,7,8,9,0,,,,,,,,,,,,,,]def get_tit_num(title): result = for char in title: if char in num_list: result+=char return resultdef Cn2An(chinese_digits): result = 0 tmp = 0 hnd_mln = 0 for count in range(len(chinese_digits)): curr_char = chinese_digits[count] curr_digit = chs_arabic_map[curr_char] # meet 「億」 or 「億」 if curr_digit == 10 ** 8: result = result + tmp result = result * curr_digit # get result before 「億」 and store it into hnd_mln # reset `result` hnd_mln = hnd_mln * 10 ** 8 + result result = 0 tmp = 0 # meet 「萬」 or 「萬」 elif curr_digit == 10 ** 4: result = result + tmp result = result * curr_digit tmp = 0 # meet 「十」, 「百」, 「千」 or their traditional version elif curr_digit >= 10: tmp = 1 if tmp == 0 else tmp result = result + curr_digit * tmp tmp = 0 # meet single digit elif curr_digit is not None: tmp = tmp * 10 + curr_digit else: return result result = result + tmp result = result + hnd_mln return result # testprint (Cn2An(get_tit_num(第一千三百九十一章 你妹妹被我咬了!)))

解決了用於排序的id的問題,我們就可以開始寫代碼了

項目的創建:

# 創建項目scrapy startproject biquge# 進入文件夾cd biquge# 生成爬蟲文件scrapy genspider xsphspider# 看一下目錄樹:.├── biquge│ ├── __init__.py│ ├── __pycache__│ │ ├── __init__.cpython-36.pyc│ │ ├── items.cpython-36.pyc│ │ ├── pipelines.cpython-36.pyc│ │ └── settings.cpython-36.pyc│ ├── items.py│ ├── middlewares.py│ ├── pipelines.py│ ├── settings.py│ └── spiders│ ├── __init__.py│ ├── __pycache__│ │ ├── __init__.cpython-36.pyc│ │ ├── sjzh.cpython-36.pyc│ │ └── xsphspider.cpython-36.pyc│ ├── sjzh.py│ └── xsphspider.py└── scrapy.cfg

編寫Items:

還是和原來一樣,我們先定義好沒一個爬取的item(小說章節)有哪些欄位是我們需要的:

import scrapyclass BiqugeItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() # 小說名字 bookname = scrapy.Field() #章節名 title = scrapy.Field() #正文 body = scrapy.Field() #排序用id order_id = scrapy.Field()

編寫Spider:

由於我們的spider爬取順序是這樣的:

首先: 爬取排行榜頁面,找到每一本小說的頁面

接著: 爬取小說頁面, 找到小說每一章的鏈接

最後: 爬取每一章節頁面,找到文章標題和正文內容

我們再來複習一下 spider是怎麼運作的:

首先: 從start_urls里發起請求,返回response

接著: 自動調用 parse函數

中間: 一系列我們自己添加的功能

最後: 返回item,給PIPELINE處理

為了實現我們定好的spider邏輯,我們得調用Scrapy內置的requests函數,

來介紹一下Scrapy.request函數:

class Request(url, callback=None, method=GET, headers=None, body=None, cookies=None, meta=None, encoding=utf-8, priority=0, dont_filter=False, errback=None)# 這裡其實和我們一直用的request模塊也差不多,最主要需要注意的參數:# callback 這個參數的意思是回調函數,就是會自動運行的函數,並將request獲得的response自動傳進去。

來看一下具體的代碼:

比起之前的爬蟲,稍微長一點,仔細看能看懂的,

都有詳細的注釋

# -*- coding: utf-8 -*-import scrapyfrom biquge.items import BiqugeItem# 導入我們自己寫的函數from .sjzh import Cn2An,get_tit_numclass XsphspiderSpider(scrapy.Spider): name = "xsphspider" allowed_domains = ["qu.la"] start_urls = [http://www.qu.la/paihangbang/] novel_list = [] def parse(self, response): # 找到各類小說排行榜名單 books = response.xpath(.//div[@class="index_toplist mright mbottom"]) # 找到每一類小說排行榜的每一本小說的下載鏈接 for book in books: links = book.xpath(.//div[2]/div[2]/ul/li) for link in links: url = http://www.qu.la + link.xpath(.//a/@href).extract()[0] self.novel_list.append(url) # 簡單的去重 self.novel_list = list(set(self.novel_list)) for novel in self.novel_list: yield scrapy.Request(novel, callback=self.get_page_url) def get_page_url(self, response): 找到章節鏈接 page_urls = response.xpath(.//dd/a/@href).extract() for url in page_urls: yield scrapy.Request(http://www.qu.la + url,callback=self.get_text) def get_text(self, response): 找到每一章小說的標題和正文 並自動生成id欄位,用於表的排序 item = BiqugeItem() # 小說名 item[bookname] = response.xpath( .//div[@class="con_top"]/a[2]/text()).extract()[0] # 章節名 ,將title單獨找出來,為了提取章節中的數字 title = response.xpath(.//h1/text()).extract()[0] item[title] = title # 找到用於排序的id值 item[order_id] = Cn2An(get_tit_num(title)) # 正文部分需要特殊處理 body = response.xpath(.//div[@id="content"]/text()).extract() # 將抓到的body轉換成字元串,接著去掉 之類的排版符號, text = .join(body).strip().replace(u3000, ) item[body] = text return item

編寫PIPELINE:

  • mysql資料庫:

由於這裡我們需要將數據寫入Mysql資料庫,這裡需要自己有一點mysql基本操作的知識:

如果對於資料庫一點都不懂,這裡有一本比較好的教程,跟著做一遍,大體就都明白了。

mysql 5.7參考手冊: MySQL 5.7 參考手冊 · GitBook

  • 具體代碼:

import pymysql class BiqugePipeline(object): def process_item(self, item, spider): 將爬到的小數寫入資料庫 # 首先從items里取出數據 name = item[bookname] order_id = item[order_id] body = item[body] title = item[title] # 與本地資料庫建立聯繫 # 和本地的scrapyDB資料庫建立連接 connection = pymysql.connect( host=localhost, # 連接的是本地資料庫 user=root, # 自己的mysql用戶名 passwd=********, # 自己的密碼 db=bqgxiaoshuo, # 資料庫的名字 charset=utf8mb4, # 默認的編碼方式: cursorclass=pymysql.cursors.DictCursor) try: with connection.cursor() as cursor: # 資料庫表的sql sql1 = Create Table If Not Exists %s(id int,zjm varchar(20),body text) % name # 單章小說的寫入 sql = Insert into %s values (%d ,%s,%s) % ( name, order_id, title, body) cursor.execute(sql1) cursor.execute(sql) # 提交本次插入的記錄 connection.commit() finally: # 關閉連接 connection.close() return item

配置settings:

將我們寫的PIPELINE加入settings:

ITEM_PIPELINES = { biquge.pipelines.BiqugePipeline: 300,}

中斷後如何恢復任務?

由於這次我們需要爬得數據量非常的大,

就算有強大的多線程也不是一時半會就能爬完的,

所以這裡我們得知道如果爬蟲爬到一半斷了,我們如何從斷的地方接著工作,

而不是從頭開始

  • Job 路徑

要啟用持久化支持,你只需要通過 JOBDIR 設置 job directory 選項。這個路徑將會存儲 所有的請求數據來保持一個單獨任務的狀態(例如:一次spider爬取(a spider run))。必須要注意的是,這個目錄不允許被不同的spider 共享,甚至是同一個spider的不同jobs/runs也不行。也就是說,這個目錄就是存儲一個 單獨 job的狀態信息。

  • 如何使用?

要啟用一個爬蟲的持久化,運行以下命令:

scrapy crawl somespider -s JOBDIR=crawls/somespider-1

然後,你就能在任何時候安全地停止爬蟲(按Ctrl-C或者發送一個信號)。

恢復這個爬蟲也是同樣的命令:

scrapy crawl somespider -s JOBDIR=crawls/somespider-1

結果展示:

由於沒有爬太長時間,我就關閉掉了,就爬了一點點:

# 登錄mysql資料庫mysql -uroot -p # 選中小說資料庫use bqgxiaoshuo;# 查看爬到的小說Lshow tables;

看一下小說章節的排序:

select zjm from "小說名" order by id;

可以看到已經基本完成排序的工作了。

當然,這個爬蟲只是初步實現了基本功能,

實際上還有很多bug和需要優化的地方,

這些大家可以自己在源代碼的基礎上添加功能啦,

如果有人能基於這個真的做出一個小說閱讀app那就更好了!

到這裡,我們的Scrapy爬蟲的學習記錄就要告一段落了,

從下一篇文章開始,我將會介紹模擬瀏覽器爬蟲~

每天的學習記錄都會 同步更新到:

微信公眾號: findyourownway

知乎專欄:從零開始寫Python爬蟲 - 知乎專欄

blog : www.ehcoblog.ml

Github: Ehco1996/Python-crawler

推薦閱讀:

從零開始寫Python爬蟲 --- 爬蟲實踐:螺紋鋼數據&Cookies
開啟知乎收藏夾看圖模式
Python模擬登陸萬能法-微博|知乎
從零開始寫Python爬蟲 --- 爬蟲應用:今天吃什麼?

TAG:Python | 爬虫 | scrapy |