使用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.新英雄-馬超:五虎將的最後一位英雄,通過&ldquo;投擲-拾取&rdquo;,強化攻擊在複雜的戰場中穿梭。<br />2.新玩法-王者模擬戰(即將上線):在機關沙盤中,招募英雄,排兵布陣,與其他七位玩家比拼策略!<br />3.新系統-萬象天工:整合以往所有的娛樂模式玩法,冒險之旅玩法。未來,用戶使用編輯器&ldquo;天工&rdquo;創作的優質原創玩法,將有可能會加入到萬象天工;<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&amp;ref=appstore.mobile_download&amp;nonce=4803361670017098198%3A26139170&amp;appClientId=2882303761517485445&amp;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
  • 更換cpythonjpython
  • 加同步鎖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納入隊列之中,還可以將解析以及保存都寫進執行緒,以提高程式的執行效率。

更多爬蟲技術點擊訪問

歡迎各位一起交流