以豌豆莢為例,用 Scrapy 爬取分類多級頁面


本文轉載自以下網站:以豌豆莢為例,用 Scrapy 爬取分類多級頁面 https://www.makcyun.top/web_scraping_withpython17.html

 

需要學習的地方:

1.分析網站數據結構 (主要)

2.使用Scrapy框架構造代碼參數

3.作圖

 

使用 Scrapy 爬取豌豆莢全網 70000+ App。

摘要:使用 Scrapy 爬取豌豆莢全網 70000+ App,並進行探索性分析。

寫在前面:若對數據抓取部分不感興趣,可以直接下拉到數據分析部分。

1 分析背景

之前我們使用了 Scrapy 爬取並分析了酷安網 6000+ App,為什么這篇文章又在講抓 App 呢?

因為我喜歡折騰 App,哈哈。當然,主要是因為下面這幾點:

第一、之前抓取的網頁很簡單

在抓取酷安網時,我們使用 for 循環,遍歷了幾百頁就完成了所有內容的抓取,非常簡單,但現實往往不會這么 easy,有時我們要抓的內容會比較龐大,比如抓取整個網站的數據,為了增強爬蟲技能,所以本文選擇了「豌豆莢」這個網站。

目標是: 爬取該網站所有分類下的 App 信息並下載 App 圖標,數量在 70,000 左右,比酷安升了一個數量級。

第二、再次練習使用強大的 Scrapy 框架

之前只是初步地使用了 Scrapy 進行抓取,還沒有充分領會到 Scrapy 有多么牛逼,所以本文嘗試深入使用 Scrapy,增加隨機 UserAgent、代理 IP 和圖片下載等設置。

第三、對比一下酷安和豌豆莢兩個網站

相信很多人都在使用豌豆莢下載 App,我則使用酷安較多,所以也想比較一下這兩個網站的 App 特點。

話不多說,下面開始抓取流程。

▌分析目標

首先,我們先來了解一下要抓取的豌豆莢網頁是什么樣的,可以看到該網站上的 App 分成了很多類,包括:「應用播放」、「系統工具」等,一共有 14 個大類別,每個大類下又細分了多個小類,例如,影音播放下包括:「視頻」、「直播」等。

點擊「視頻」進入第二級子類頁面,可以看到每款 App 的部分信息,包括:圖標、名稱、安裝數量、體積、評論等。

在之前的一篇文章中(見下方鏈接),我們分析了這個頁面:采用 AJAX 加載,GET 請求,參數很容易構造,但是具體頁數不確定,最后分別使用了 For 和 While 循環抓取了所有頁數的數據。

∞ Python For 和 While 循環爬取不確定頁數的網頁

接着,我們可以再進入第三級頁面,也就是每款 App 的詳情頁,可以看到多了下載數、好評率、評論數這幾樣參數,抓取思路和第二級頁面大同小異,同時為了減小網站壓力,所以 App 詳情頁就不抓取了。

所以,這是一個分類多級頁面的抓取問題,依次抓取每一個大類下的全部子類數據。

學會了這種抓取思路,很多網站我們都可以去抓,比如很多人愛爬的「豆瓣電影」也是這樣的結構。

▌分析內容

數據抓取完成后,本文主要是對分類型數據的進行簡單的探索性分析,包括這么幾個方面:

  • 下載量最多 / 最少的 App 總排名
  • 下載量最多 / 最少的 App 分類 / 子分類排名
  • App 下載量區間分布
  • App 名稱重名的有多少
  • 和酷安 App 進行對比

▌分析工具

  • Python
  • Scrapy
  • MongoDB
  • Pyecharts
  • Matplotlib

2 數據抓取

▌網站分析

我們剛才已經初步對網站進行了分析,大致思路可以分為兩步,首先是提取所有子類的 URL 鏈接,然后分別抓取每個 URL 下的 App 信息就行了。

可以看到,子類的 URL 是由兩個數字構成,前面的數字表示分類編號,后面的數字表示子分類編號,得到了這兩個編號,就可以抓取該分類下的所有 App 信息,那么怎么獲取這兩個數值代碼呢?

回到分類頁面,定位查看信息,可以看到分類信息都包裹在每個 li 節點中,子分類 URL 則又在子節點 a 的 href 屬性中,大分類一共有 14 個,子分類一共有 88 個

到這兒,思路就很清晰了,我們可以用 CSS 提取出全部子分類的 URL,然后分別抓取所需信息即可。

另外還需注意一點,該網站的 首頁信息是靜態加載的,從第 2 頁開始是采用了 Ajax 動態加載,URL 不同,需要分別進行解析提取。

▌Scrapy抓取

我們要爬取兩部分內容,一是 APP 的數據信息,包括前面所說的:名稱、安裝數量、體積、評論等,二是下載每款 App 的圖標,分文件夾進行存放。

由於該網站有一定的反爬措施,所以我們需要添加隨機 UA 和代理 IP,關於這兩個知識點,我此前單獨寫了兩篇文章進行鋪墊,傳送門:

∞ Scrapy 中設置隨機 User-Agent 的方法匯總

∞ Python 爬蟲的代理 IP 設置方法匯總

這里隨機 UA 使用 scrapy-fake-useragent 庫,一行代碼就能搞定,代理 IP 直接上阿布雲付費代理,幾塊錢搞定簡單省事。

下面,就直接上代碼了:

items.py

import scrapy

class WandoujiaItem(scrapy.Item):
cate_name = scrapy.Field() #分類名
child_cate_name = scrapy.Field() #分類編號
app_name = scrapy.Field() # 子分類名
install = scrapy.Field() # 子分類編號
volume = scrapy.Field() # 體積
comment = scrapy.Field() # 評論
icon_url = scrapy.Field() # 圖標url

middles.py

中間件主要用於設置代理 IP。

import base64
proxyServer = "http://http-dyn.abuyun.com:9020"
proxyUser = "你的信息"
proxyPass = "你的信息"

proxyAuth = "Basic " + base64.urlsafe_b64encode(bytes((proxyUser + ":" + proxyPass), "ascii")).decode("utf8")
class AbuyunProxyMiddleware(object):
def process_request(self, request, spider):
request.meta["proxy"] = proxyServer
request.headers["Proxy-Authorization"] = proxyAuth
logging.debug('Using Proxy:%s'%proxyServer)

pipelines.py

該文件用於存儲數據到 MongoDB 和下載圖標到分類文件夾中。

存儲到 MongoDB:

MongoDB 存儲
class MongoPipeline(object):
def __init__(self,mongo_url,mongo_db):
self.mongo_url = mongo_url
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls,crawler):
return cls(
mongo_url = crawler.settings.get('MONGO_URL'),
mongo_db = crawler.settings.get('MONGO_DB')
)

def open_spider(self,spider):
self.client = pymongo.MongoClient(self.mongo_url)
self.db = self.client[self.mongo_db]

def process_item(self,item,spider):
name = item.__class__.__name__
# self.db[name].insert(dict(item))
self.db[name].update_one(item, {'$set': item}, upsert=True)
return item

def close_spider(self,spider):
self.client.close()

按文件夾下載圖標:

# 分文件夾下載
class ImagedownloadPipeline(ImagesPipeline):
def get_media_requests(self,item,info):
if item['icon_url']:
yield scrapy.Request(item['icon_url'],meta={'item':item})

def file_path(self, request, response=None, info=None):
name = request.meta['item']['app_name']
cate_name = request.meta['item']['cate_name']
child_cate_name = request.meta['item']['child_cate_name']

path1 = r'/wandoujia/%s/%s' %(cate_name,child_cate_name)
path = r'{}\{}.{}'.format(path1, name, 'jpg')
return path

def item_completed(self,results,item,info):
image_path = [x['path'] for ok,x in results if ok]
if not image_path:
raise DropItem('Item contains no images')
return item

settings.py

BOT_NAME = 'wandoujia'
SPIDER_MODULES = ['wandoujia.spiders']
NEWSPIDER_MODULE = 'wandoujia.spiders'

MONGO_URL = 'localhost'
MONGO_DB = 'wandoujia'

# 是否遵循機器人規則
ROBOTSTXT_OBEY = False
# 下載設置延遲 由於買的阿布雲一秒只能請求5次,所以每個請求設置了 0.2s延遲
DOWNLOAD_DELAY = 0.2

DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware': 100, # 隨機UA
'wandoujia.middlewares.AbuyunProxyMiddleware': 200 # 阿布雲代理


ITEM_PIPELINES = {
'wandoujia.pipelines.MongoPipeline': 300,
'wandoujia.pipelines.ImagedownloadPipeline': 400,
}

# URL不去重
DUPEFILTER_CLASS = 'scrapy.dupefilters.BaseDupeFilter'

wandou.py

主程序這里列出關鍵的部分:

def __init__(self):
self.cate_url = 'https://www.wandoujia.com/category/app'
# 子分類首頁url
self.url = 'https://www.wandoujia.com/category/'
# 子分類 ajax請求頁url
self.ajax_url = 'https://www.wandoujia.com/wdjweb/api/category/more?'
# 實例化分類標簽
self.wandou_category = Get_category()
def start_requests(self):
yield scrapy.Request(self.cate_url,callback=self.get_category)

def get_category(self,response):
cate_content = self.wandou_category.parse_category(response)
# ...

這里,首先定義幾個 URL,包括:分類頁面、子分類首頁、子分類 AJAX 頁,也就是第 2 頁開始的 URL,然后又定義了一個類 Get_category() 專門用於提取全部的子分類 URL,稍后我們將展開該類的代碼。

程序從 start_requests 開始運行,解析首頁獲得響應,調用 get_category() 方法,然后使用 Get_category() 類中的 parse_category() 方法提取出所有 URL,具體代碼如下:

class Get_category():
def parse_category(self, response):
category = response.css('.parent-cate')
data = [{
'cate_name': item.css('.cate-link::text').extract_first(),
'cate_code': self.get_category_code(item),
'child_cate_codes': self.get_child_category(item),
} for item in category]
return data

# 獲取所有主分類標簽數值代碼
def get_category_code(self, item):
cate_url = item.css('.cate-link::attr("href")').extract_first()
pattern = re.compile(r'.*/(\d+)') # 提取主類標簽代碼
cate_code = re.search(pattern, cate_url)
return cate_code.group(1)

# 獲取所有子分類名稱和編碼
def get_child_category(self, item):
child_cate = item.css('.child-cate a')
child_cate_url = [{
'child_cate_name': child.css('::text').extract_first(),
'child_cate_code': self.get_child_category_code(child)
} for child in child_cate]
return child_cate_url

# 正則提取子分類編碼
def get_child_category_code(self, child):
child_cate_url = child.css('::attr("href")').extract_first()
pattern = re.compile(r'.*_(\d+)') # 提取小類標簽編號
child_cate_code = re.search(pattern, child_cate_url)
return child_cate_code.group(1)

這里,除了分類名稱 cate_name 可以很方便地直接提取出來,分類編碼和子分類的子分類的名稱和編碼,我們使用了 get_category_code() 等三個方法進行提取。提取方法使用了 CSS 和正則表達式,比較簡單。

最終提取的分類名稱和編碼結果如下,利用這些編碼,我們就可以構造 URL 請求開始提取每個子分類下的 App 信息了。

{'cate_name': '影音播放', 'cate_code': '5029', 'child_cate_codes': [
{'child_cate_name': '視頻', 'child_cate_code': '716'},
{'child_cate_name': '直播', 'child_cate_code': '1006'},
...
]},
{'cate_name': '系統工具', 'cate_code': '5018', 'child_cate_codes': [
{'child_cate_name': 'WiFi', 'child_cate_code': '895'},
{'child_cate_name': '瀏覽器', 'child_cate_code': '599'},
...
]},
...

接着前面的 get_category() 繼續往下寫,提取 App 的信息:

def get_category(self,response): 
cate_content = self.wandou_category.parse_category(response)
# ...
for item in cate_content:
child_cate = item['child_cate_codes']
for cate in child_cate:
cate_code = item['cate_code']
cate_name = item['cate_name']
child_cate_code = cate['child_cate_code']
child_cate_name = cate['child_cate_name']

page = 1 # 設置爬取起始頁數
if page == 1:
# 構造首頁url
category_url = '{}{}_{}' .format(self.url, cate_code, child_cate_code)
else:
params = {
'catId': cate_code, # 類別
'subCatId': child_cate_code, # 子類別
'page': page,
}
category_url = self.ajax_url + urlencode(params)
dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code}
yield scrapy.Request(category_url,callback=self.parse,meta=dict)

這里,依次提取出全部的分類名稱和編碼,用於構造請求的 URL。由於首頁的 URL 和第 2 頁開始的 URL 形式不同,所以使用了 if 語句分別進行構造。接下來,請求該 URL 然后調用 self.parse() 方法進行解析,這里使用了 meta 參數用於傳遞相關參數。

def parse(self, response):
if len(response.body) >= 100: # 判斷該頁是否爬完,數值定為100是因為無內容時長度是87
page = response.meta['page']
cate_name = response.meta['cate_name']
cate_code = response.meta['cate_code']
child_cate_name = response.meta['child_cate_name']
child_cate_code = response.meta['child_cate_code']

if page == 1:
contents = response
else:
jsonresponse = json.loads(response.body_as_unicode())
contents = jsonresponse['data']['content']
# response 是json,json內容是html,html 為文本不能直接使用.css 提取,要先轉換
contents = scrapy.Selector(text=contents, type="html")

contents = contents.css('.card')
for content in contents:
# num += 1
item = WandoujiaItem()
item['cate_name'] = cate_name
item['child_cate_name'] = child_cate_name
item['app_name'] = self.clean_name(content.css('.name::text').extract_first())
item['install'] = content.css('.install-count::text').extract_first()
item['volume'] = content.css('.meta span:last-child::text').extract_first()
item['comment'] = content.css('.comment::text').extract_first().strip()
item['icon_url'] = self.get_icon_url(content.css('.icon-wrap a img'),page)
yield item

# 遞歸爬下一頁
page += 1
params = {
'catId': cate_code, # 大類別
'subCatId': child_cate_code, # 小類別
'page': page,
}
ajax_url = self.ajax_url + urlencode(params)
dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code}
yield scrapy.Request(ajax_url,callback=self.parse,meta=dict)

最后,parse() 方法用來解析提取最終我們需要的 App 名稱、安裝量等信息,解析完成一頁后,page 進行遞增,然后重復調用 parse() 方法循環解析,直到解析完全部分類的最后一頁。

最終,幾個小時后,我們就可以完成全部 App 信息的抓取,我這里得到 73,755 條信息和 72,150 個圖標,兩個數值不一樣是因為有些 App 只有信息沒有圖標。

圖標下載:

下面將對提取的信息,進行的數據分析。

3 數據分析

▌總體情況

首先來看一下 App 的安裝量情況,畢竟 70000 多款 App,自然很感興趣 哪些 App 使用地最多,哪些又使用地最少

代碼實現如下:

plt.style.use('ggplot')
colors = '#6D6D6D' #字體顏色
colorline = '#63AB47' #紅色CC2824 #豌豆莢綠
fontsize_title = 20
fontsize_text = 10

# 下載量總排名
def analysis_maxmin(data):
data_max = (data[:10]).sort_values(by='install_count')
data_max['install_count'] = (data_max['install_count'] / 100000000).round(1)
data_max.plot.barh(x='app_name',y='install_count',color=colorline)
for y, x in enumerate(list((data_max['install_count']))):
plt.text(x + 0.1, y - 0.08, '%s' %
round(x, 1), ha='center', color=colors)

plt.title('安裝量最多的 10 款 App ?',color=colors)
plt.xlabel('下載量(億次)')
plt.ylabel('App')
plt.tight_layout()
# plt.savefig('安裝量最多的App.png',dpi=200)
plt.show()

看了上圖,有兩個「沒想到」:

  • 排名第一的居然是一款手機管理軟件

    對豌豆莢網上的這個第一名感到意外,一是,好奇大家都那么愛手機清理或者怕中毒么?畢竟,我自己的手機都「裸奔」了好些年;二是,第一名居然不是鵝廠的其他產品,比入「微信」或者「QQ」。

  • 榜單放眼望去,以為會出現的沒有出現,沒有想到的卻出現了

    前十名中,居然出現了書旗小說、印客這些比較少聽過的名字,而國民 App 微信、支付寶等,甚至都沒有出現在這個榜單中。

帶着疑問和好奇,分別找到了「騰訊手機管家」和「微信」兩款 App 的主頁:

騰訊手機管家下載和安裝量:

微信下載和安裝量:

這是什么情況?

騰訊管家 3 億多的下載量等同於安裝量,而微信 20 多億的下載量,只有區區一千多萬的安裝量,兩組數據對比,大致反映了兩個問題:

  • 要么是騰訊管家的下載量實際並沒有那么多

  • 要么是微信的下載量寫少了

不管是哪個問題,都反映了一個問題:該網站做得不夠走心啊

為了證明這個觀點,將前十名的安裝量和下載量都作了對比,發現很多 App 的安裝量都和下載量是一樣的,也就是說:這些 App 的實際下載量並沒有那么多,而如果這樣的話,那么這份榜單就有很大水分了。

難道,辛辛苦苦爬了那么久,就得到這樣的結果?

不死心,接着再看看安裝量最少的 App 是什么情況,這里找出了其中最少的 10 款:

掃了一眼,更加沒想到了:

「QQ 音樂」竟然是倒數第一,竟然只有 3 次安裝量!

確定這和剛剛上市、市值千億的 QQ 音樂是同一款產品?

再次核實了一下:

沒有看錯,是寫着 3人安裝

這是已經不走心到什么程度了? 這個安裝量,鵝廠還能「用心做好音樂」?

說實話,到這兒已經不想再往下分析下去了,擔心爬扒出更多沒想到的東西,不過辛苦爬了這么久,還是再往下看看吧。

看了首尾,我們再看看整體,了解一下全部 App 的安裝數量分布,這里去除了有很大水分的前十名 App。

很驚訝地發現,竟然有 多達 67,195 款,占總數的 94% 的 App 的安裝量不足 1萬!

如果這個網站的所有數據都是真的話,那么上面排名第一的手機管家,它 一款就差不多抵得上這 6 萬多款 App 的安裝量了!

對於多數 App 開發者,只能說:現實很殘酷,辛苦開發出來的 App,用戶不超過 1萬人的可能性高達近 95%

代碼實現如下:

def analysis_distribution(data):
data = data.loc[10:,:]
data['install_count'] = data['install_count'].apply(lambda x:x/10000)
bins = [0,1,10,100,1000,10000]
group_names = ['1萬以下','1-10萬','10-100萬','100-1000萬','1000萬-1億']
cats = pd.cut(data['install_count'],bins,labels=group_names)
cats = pd.value_counts(cats)
bar = Bar('App 下載數量分布','高達 94% 的 App 下載量低於1萬')
bar.use_theme('macarons')
bar.add(
'App 數量',
list(cats.index),
list(cats.values),
is_label_show = True,
xaxis_interval = 0,
is_splitline_show = 0,
)
bar.render(path='App下載數量分布.png',pixel_ration=1)

▌分類情況

下面,我們來看看各分類下 App 情況,不再看安裝量,而看數量,以排出干擾。

可以看到 14 個大分類中,每個分類的 App 數量差距都不大,數量最多的「生活休閑」是「攝影圖像」的兩倍多一點。

接着,我們進一步看看 88 個子分類的 App 數量情況,篩選出數量最多和最少的 10 個子類:

可以發現兩點有意思的現象:

  • 「收音機」類別 App 數量最多,達到 1,300 多款

    這個很意外,當下收音機完全可以說是個老古董了,居然還有那么人去開發。

  • App 子類數量差距較大

    最多的「收音機」是最少的「動態壁紙」近 20 倍,如果我是一個 App 開發者,那我更願意去嘗試開發些小眾類的 App,競爭小一點,比如:「背單詞」、「小兒百科」這些。

看完了總體和分類情況,突然想到一個問題:這么多 App,有沒有重名的呢?

驚奇地發現,叫「一鍵鎖屏」的 App 多達 40 款,這個功能 App 很難再想出別的名字了么? 現在很多手機都支持觸控鎖屏了,比一鍵鎖屏操作更加方便。

接下來,我們簡單對比下豌豆莢和酷安兩個網站的 App 情況。

▌對比酷安

二者最直觀的一個區別是在 App 數量上,豌豆莢擁有絕對的優勢,達到了酷安的十倍之多,那么我們自然感興趣:

豌豆莢是否包括了酷安上所有的 App ?

如果是,「你有的我都有,你沒有的我也有」,那么酷安就沒什么優勢了。統計之后,發現豌豆莢 僅包括了 3,018 款,也就是一半左右,剩下的另一半則沒有包括。

這里面固然存在兩個平台上 App 名稱不一致的現象,但更有理由相信 酷安很多小眾的精品 App 是獨有的,豌豆莢並沒有。

代碼實現如下:

include = data3.shape[0]
notinclude = data2.shape[0] - data3.shape[0]
sizes= [include,notinclude]
labels = [u'包含',u'不包含']
explode = [0,0.05]
plt.pie(
sizes,
autopct = '%.1f%%',
labels = labels,
colors = [colorline,'#7FC161'], # 豌豆莢綠
shadow = False,
startangle = 90,
explode = explode,
textprops = {'fontsize':14,'color':colors}
)
plt.title('豌豆莢僅包括酷安上一半的 App 數量',color=colorline,fontsize=16)
plt.axis('equal')
plt.axis('off')
plt.tight_layout()
plt.savefig('包含不保包含對比.png',dpi=200)
plt.show()

接下來,我們看看所包含的 App 當中,在兩個平台上的下載量是怎么樣的:

可以看到,兩個平台上 App 下載數量差距還是很明顯。

最后,我面再看看豌豆莢上沒有包括哪些APP:

可以看到很多神器都沒有包括,比如:RE、綠色守護、一個木函等等。豌豆莢和酷安的對比就到這里,如果用一句話來總結,我可能會說:

豌豆莢太牛逼了, App 數量是酷安的十倍,所以我選酷安。

以上,就是利用 Scrapy 爬取分類多級頁面的抓取和分析的一次實戰。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM