使用Python抓取動態網站數據
- 2019 年 10 月 7 日
- 筆記
青山哥哥伸頭看,看我塵中吃苦茶 園信
這裡將會以一個例子展開探討多執行緒在爬蟲中的應用,所以不會過多的解釋理論性的東西,並發詳情點擊連接
爬取某應用商店
當然,爬取之前請自行診斷是否遵循君子協議,
遵守就爬不了數據查看robots協議只需要在域名後綴上rebots.txt
即可 例如:

1. 目標
- URL:
http://app.mi.com/category/15
- 獲取「遊戲」分類的所有APP名稱、簡介、下載鏈接
2. 分析
2.1 網頁屬性
首先,需要判斷是不是動態載入
點擊翻頁,發現URL後邊加上了#page=1
,這也就是說,查詢參數為1的時候為第二頁,寫一個小爬蟲測試一下
import requests url = "http://app.mi.com/category/15"headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"} html = requests.get(url=url, headers=headers).content.decode("utf-8") print(html)
在輸出的html中搜索「王者榮耀」,發現並沒有什麼問題,那麼第二頁呢?將上述程式碼中的url = "http://app.mi.com/category/15"
改為url = "http://app.mi.com/category/15#page=1"
再次搜索第二頁的內容」爐石傳說」,發現並沒有搜索出來,那麼該網站可能是動態載入
- 抓包分析 打開chrome自帶的竊聽器,切換到network,點擊翻頁

可以看到該GET請求後綴很多參數

經過多次測試發現
page
為頁數,但是值需要減1才是真實的頁數categoryId
為應用分類pageSize
尚不明確,所以將抓到包的URL打開看一下

不難發現,pageSize
為每一頁顯示APP資訊的個數,並且返回了一個json字串
2.2 分析json
複製一段json過來
{"count":2000, "data": [ {"appId":108048, "displayName":"王者榮耀", "icon":"http://file.market.xiaomi.com/thumbnail/PNG/l62/AppStore/0eb7aa415046f4cb838cfe5b5d402a5efc34fbb25", "level1CategoryName":"網遊RPG", "packageName":"com.tencent.tmgp.sgame" }, {}, ... ] }
所有的資訊都不知道是幹啥的,暫時保存
2.3 二級頁面
點擊」王者榮耀」,跳轉到APP詳情,看下URL是什麼樣子
http://app.mi.com/details?id=com.tencent.tmgp.sgame
然後這裡會驚奇的發現,id的查詢參數和上邊的packageName
的值一樣,所以詳情頁就需要拼接URL
2.4 獲取資訊
- APP名稱 <div class="intro-titles"><p>深圳市騰訊電腦系統有限公司</p><h3>王者榮耀</h3>……</div>
- APP簡介 <p class="pslide">《王者榮耀》是騰訊第一5V5團隊公平競技手游,國民MOBA手游大作!5V5王者峽谷、公平對戰,還原MOBA經典體驗;契約之戰、五軍對決、邊境突圍等,帶來花式作戰樂趣!10秒實時跨區匹配,與好友開黑上分,向最強王者進擊!多款英雄任憑選擇,一血、五殺、超神,實力碾壓,收割全場!敵軍即將到達戰場,王者召喚師快來集結好友,準備團戰,就在《王者榮耀》!</p><h3 class="special-h3">新版特性</h3><p class="pslide">1.新英雄-馬超:五虎將的最後一位英雄,通過“投擲-拾取”,強化攻擊在複雜的戰場中穿梭。<br />2.新玩法-王者模擬戰(即將上線):在機關沙盤中,招募英雄,排兵布陣,與其他七位玩家比拼策略!<br />3.新系統-萬象天工:整合以往所有的娛樂模式玩法,冒險之旅玩法。未來,用戶使用編輯器“天工”創作的優質原創玩法,將有可能會加入到萬象天工;<br />4.新功能-職業選手專屬認證:百餘位KPL職業選手遊戲內官方認證;<br />5.新功能-不想同隊:王者50星以下的排位賽,在結算介面可設置不想同隊的玩家;<br />6.新功能-系統AI託管:玩家在遭遇掛機後可選擇AI託管,但AI不會CARRY比賽;<br />7.新皮膚:沈夢溪-鯊炮海盜貓。</p>
- APP下載地址 <div class="app-info-down"><a href="/download/108048?id=com.tencent.tmgp.sgame&ref=appstore.mobile_download&nonce=4803361670017098198%3A26139170&appClientId=2882303761517485445&appSignature=66MyRvEdLh4RcytLpGkjciBDW_XgoMYMe9g39Hf4f2g" class="download">直接下載</a> </div>
2.4 確認技術
由以上分析可以得出,使用lxml提取數據將會是不錯的選擇,有關xpath使用請點擊跳轉
xpath語法如下:
- 名稱:
//div[@class="intro-titles"]/h3/text()
- 簡介:
//p[@class="pslide"][1]/text()
- 下載鏈接:
//a[@class="download"]/@href
3. 程式碼實現
import requestsfrom lxml import etreeclass MiSpider(object): def __init__(self): self.bsase_url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30" # 一級頁面的URL地址 self.headers = {"User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;"} # 獲取響應對象 def get_page(self, url): reponse = requests.get(url=url, headers=self.headers) return reponse # 解析一級頁面,即json解析,得到APP詳情頁的鏈接 def parse_page(self, url): html = self.get_page(url).json() # two_url_list:[{"appId":"108048","dispayName":"..",...},{},{},...] two_url_list = html["data"] for two_url in two_url_list: two_url = "http://app.mi.com/details?id={}".format(two_url["packageName"]) # 拼接app詳情鏈接 self.parse_info(two_url) # 解析二級頁面,得到名稱、簡介、下載鏈接 def parse_info(self, two_url): html = self.get_page(two_url).content.decode("utf-8") parse_html = etree.HTML(html) # 獲取目標資訊 app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip() app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip() app_url = "http://app.mi.com" + parse_html.xpath('//a[@class="download"]/@href')[0].strip() print(app_name, app_url, app_info) # 主函數 def main(self): for page in range(67): url = self.bsase_url.format(page) self.parse_page(url)if __name__ == "__main__": spider = MiSpider() spider.main()
接下來將數據存儲起來,存儲的方式有很多csv、MySQL、MongoDB
數據存儲
這裡採用MySQL資料庫將其存入
建表SQL
/* Navicat MySQL Data Transfer Source Server : xxx Source Server Type : MySQL Source Server Version : 50727 Source Host : MySQL_ip:3306 Source Schema : MIAPP Target Server Type : MySQL Target Server Version : 50727 File Encoding : 65001 Date: 13/09/2019 14:33:38 */ CREATE DATABASE MiApp CHARSET=UTF8; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for app -- ---------------------------- DROP TABLE IF EXISTS `app`; CREATE TABLE `app` ( `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'APP名稱', `url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'APP下載鏈接', `info` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'APP簡介' ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
1. pymysql
簡單介紹一下pymysql 的使用,該模組為第三方,需要用pip安裝,安裝方法不再贅述。
1.1 內置方法
pymysql方法
connect()
連接資料庫,參數為連接資訊(host, port, user, password, charset)
pymysql對象方法
cursor()
游標,用來定位資料庫cursor.execute(sql)
執行sql語句db.commit()
提交事務cursor.close()
關閉游標db.close()
關閉連接
1.2 注意事項
只要涉及數據的修改操作,必須提交事務到資料庫
查詢資料庫需要使用fet
方法獲取查詢結果
1.3 詳情
更多詳情可以參考pymsql
2. 存儲
創建配置文件(config.py)
''' 資料庫連接資訊 '''HOST = "xxx.xxx.xxx.xxx"PORT = 3306USER = "xxxxx"PASSWORD = "xxxxxxx"DB = "MIAPP"CHARSET = "utf8mb4"
表結構
mysql> desc MIAPP.app; +-------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+-------+ | name | varchar(20) | YES | | NULL | | | url | varchar(255) | YES | | NULL | | | info | text | YES | | NULL | | +-------+--------------+------+-----+---------+-------+3 rows in set (0.00 sec)
SQL語句
insert into app values(name,url,info);
完整程式碼
import requestsfrom lxml import etreeimport pymysqlfrom config import *class MiSpider(object): def __init__(self): self.bsase_url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30" # 一級頁面的URL地址 self.headers = {"User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;"} self.db = pymysql.connect(host=HOST, port=PORT, user=USER, password=PASSWORD, database=DB, charset=CHARSET) # 連接資料庫 self.cursor = self.db.cursor() # 創建游標 self.i = 0 # 用來計數,無其他作用 # 獲取響應對象 def get_page(self, url): reponse = requests.get(url=url, headers=self.headers) return reponse # 解析一級頁面,即json解析,得到APP詳情頁的鏈接 def parse_page(self, url): html = self.get_page(url).json() # two_url_list:[{"appId":"108048","dispayName":"..",...},{},{},...] two_url_list = html["data"] for two_url in two_url_list: two_url = "http://app.mi.com/details?id={}".format(two_url["packageName"]) # 拼接app詳情鏈接 self.parse_info(two_url) # 解析二級頁面,得到名稱、簡介、下載鏈接 def parse_info(self, two_url): html = self.get_page(two_url).content.decode("utf-8") parse_html = etree.HTML(html) # 獲取目標資訊 app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip() app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip() app_url = "http://app.mi.com" + parse_html.xpath('//a[@class="download"]/@href')[0].strip() ins = "insert into app(name,url,info) values (%s,%s,%s)" # 需要執行的SQL語句 self.cursor.execute(ins, [app_name, app_url, app_info]) self.db.commit() self.i += 1 print("第{}APP {}成功寫入資料庫".format(self.i, app_name)) # 主函數 def main(self): for page in range(67): url = self.bsase_url.format(page) self.parse_page(url) # 斷開資料庫 self.cursor.close() self.db.close() print("執行結束,共{}個APP成功寫入".format(self.i))if __name__ == "__main__": spider = MiSpider() spider.main()

多執行緒
爬取上述資訊似乎有點慢,如果數據多的話太耗時,而且電腦資源也得不到充分的利用
這就需要用多執行緒的理念,關於多進程和多執行緒的概念網上比比皆是,只需要明白一點
進程可以包含很多個執行緒,進程死掉,執行緒不復存在
打個比方,假設有一列火車,把這列火車理解成進程的話,那麼每節車廂就是執行緒,正是這許許多多的執行緒才共同組成了進程
python中有多執行緒的概念
假設現在有兩個運算:
n += 1n -= 1
在python內部實際上這樣運算的
x = n x = n + 1n = x x = n x = n + 1n = x
執行緒有一個特性,就是會爭奪電腦資源,如果一個執行緒在剛剛計算了x = n
這時候另一個執行緒n = x
運行了,那麼這樣下來全就亂了, 也就是說n加上一千個1再減去一千個1結果不一定為1,這時就考慮執行緒加鎖問題了。
每個執行緒在運行的時候爭搶共享數據,如果執行緒A正在操作一塊數據,這時B執行緒也要操作該數據,屆時就有可能造成數據紊亂,從而影響整個程式的運行。 所以Python有一個機制,在一個執行緒工作的時候,它會把整個解釋器鎖掉,導致其他的執行緒無法訪問任何資源,這把鎖就叫做GIL全局解釋器鎖,正是因為有這把鎖的存在,名義上的多執行緒實則變成了單執行緒,所以很多人稱GIL是python雞肋性的存在。 針對這一缺陷,很多的標準庫和第三方模組或者庫都是基於這種缺陷開發,進而使得Python在改進多執行緒這一塊變得尤為困難,那麼在實際的開發中,遇到這種問題本人目前用四種解決方式:
- 用
multiprocessing
代替Thead
- 更換
cpython
為jpython
- 加同步鎖
threading.Lock()
- 消息隊列
queue.Queue()
如果需要全面性的了解並發,請點擊並發編程,在這裡只簡單介紹使用
1. 隊列方法
# 導入模組from queue import Queue# 使用q = Queue() q.put(url) q.get() # 當隊列為空時,阻塞q.empty() # 判斷隊列是否為空,True/False
2. 執行緒方法
# 導入模組from threading import Thread# 使用流程t = Thread(target=函數名) # 創建執行緒對象t.start() # 創建並啟動執行緒t.join() # 阻塞等待回收執行緒# 創建多執行緒for i in range(5): t = Thread(target=函數名) t.start() t.join()
3. 改寫
理解以上內容就可以將原來的程式碼改寫多執行緒,改寫之前加上time
來計時

多執行緒技術選用:
- 爬蟲涉及IO操作較多,貿然改進程會造成電腦資源的浪費。
pass - 更換
jpython
簡直沒必要。pass - 加鎖可以實現,不過針對IO還是比較慢,因為操作文件的話,必須加鎖。
pass - 使用消息隊列可有效的提高爬蟲速率。
執行緒池的設計:
- 既然爬取的頁面有67頁,APP多達2010個,則考慮將URL入列
def url_in(self): for page in range(67): url = self.bsase_url.format(page) self.q.put(page)
下邊是完整程式碼
import requestsfrom lxml import etreeimport timefrom threading import Threadfrom queue import Queueimport jsonimport pymysqlfrom config import *class MiSpider(object): def __init__(self): self.url = "http://app.mi.com/categotyAllListApi?page={}&categoryId=15&pageSize=30" self.headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3895.5 Safari/537.36"} # 創建URL隊列 self.url_queue = Queue() # 把所有要爬取的頁面放進隊列 def url_in(self): for page in range(67): url = self.url.format(page) # 加入隊列 self.url_queue.put(url) # 執行緒事件函數 def get_data(self): while True: # 如果結果 為True,則隊列為空了 if self.url_queue.empty(): break # get地址,請求一級頁面 url = self.url_queue.get() html = requests.get(url=url, headers=self.headers).content.decode("utf-8") html = json.loads(html) # 轉換為json格式 # 解析數據 app_list = [] # 定義一個列表,用來保存所有的APP資訊 [(name,url,info),(),(),...] for app in html["data"]: # 應用鏈接 app_link = "http://app.mi.com/details?id=" + app["packageName"] app_list.append(self.parse_two_page(app_link)) return app_list def parse_two_page(self, app_link): html = requests.get(url=app_link, headers=self.headers).content.decode('utf-8') parse_html = etree.HTML(html) app_name = parse_html.xpath('//div[@class="intro-titles"]/h3/text()')[0].strip() app_url = "http://app.mi.com" + parse_html.xpath('//div[@class="app-info-down"]/a/@href')[0].strip() app_info = parse_html.xpath('//p[@class="pslide"][1]/text()')[0].strip() info = (app_name, app_url, app_info) print(app_name) return info # 主函數 def main(self): # url入隊列 self.url_in() # 創建多執行緒 t_list = [] for i in range(67): t = Thread(target=self.get_data) t_list.append(t) t.start() for i in t_list: i.join() db = pymysql.connect(host=HOST, user=USER, password=PASSWORD, database=DB, charset=CHARSET) cursor = db.cursor() ins = 'insert into app values (%s, %s, %s)' app_list = self.get_data() print("正在寫入資料庫") cursor.executemany(ins, app_list) db.commit() cursor.close() db.close()if __name__ == '__main__': start = time.time() spider = MiSpider() spider.main() end = time.time() print("執行時間:%.2f"% (end - start))
當然這裡的設計理念是將URL納入隊列之中,還可以將解析以及保存都寫進執行緒,以提高程式的執行效率。
更多爬蟲技術點擊訪問
歡迎各位一起交流