1. 程式人生 > >Python爬蟲之爬取動態頁面資料

Python爬蟲之爬取動態頁面資料

很多網站通常會用到Ajax和動態HTML技術,因而只是使用基於靜態頁面爬取的方法是行不通的。對於動態網站資訊的爬取需要使用另外的一些方法。

先看看如何分辨網站時靜態的還是動態的,正常而言含有“檢視更多”字樣或者開啟網站時下拉才會載入內容出來的進本都是動態的,簡便的方法就是在瀏覽器中檢視頁面相應的內容、當在檢視頁面原始碼時找不到該內容時就可以確定該頁面使用了動態技術。

對於動態頁面資訊的爬取,一般分為兩種方法,一種是直接從JavaScript中採集載入的資料、需要自己去手動分析Ajax請求來進行資訊的採集,另一種是直接從瀏覽器中採集已經載入好的資料、即可以使用無介面的瀏覽器如PhantomJS來解析JavaScript。

1、直接從JavaScript中採集載入的資料

示例1——爬取MTime影評資訊:

隨便開啟一個電影的URL:http://movie.mtime.com/99547/


一開始出現轉圈的載入,即可判斷是動態載入的。

關注到“票房”這裡:


檢視原始碼並找不到票房的字樣:


因此可斷定該內容是使用Ajax非同步載入生成的。

開啟FireBug,在“網路”>“JavaScript”中檢視含有敏感字元的介面連結,因為是和電影相關的,就先檢視含有“Movie.api?Ajax_Callback=......”字樣的連結,可以檢視到其中一個含有影評和票房等資訊:


為了進行確認哪些引數是會變化的,再開啟一個新的電影的URL並進行相同的操作進行檢視:


為了方便,直接上BurpSuite的Compare模組進行比較:


可以直接看到,只有以上三個引數的值是不一樣的,其餘的都是相同的。其中Ajax_RequestUrl引數值為當前movie的URL,t的值為當前時間,Ajax_CallBackArgument0的值為當前電影的序號、即其URL中後面的數字。

因此就可以構造Ajax請求的URL來爬取資料,回到top 100的主頁http://www.mtime.com/top/movie/top100/,分析其中的標籤等然後編寫程式碼遍歷top 100所有的電影相關票房和影評資訊,注意的是並不是所有的電影都有票房資訊,這裡需要判斷即可。

程式碼如下:

#coding=utf-8
import requests
import re
import time
import json
from bs4 import BeautifulSoup as BS
import sys
reload(sys)
sys.setdefaultencoding('utf8')

headers = {
	'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36',
}

def Get_Movie_URL():
	urls = []
	for i in range(1,11):
		# 第一頁的URL是不一樣的,需要另外進行處理
		if i != 1:
			url = "http://www.mtime.com/top/movie/top100/index-%d.html" % i
		else:
			url = "http://www.mtime.com/top/movie/top100/"
		r = requests.get(url=url,headers=headers)
		soup = BS(r.text,'lxml')
		movies = soup.find_all(name='a',attrs={'target':'_blank','href':re.compile('http://movie.mtime.com/(\d+)/'),'class':not None})
		for m in movies:
			urls.append(m.get('href'))
	return urls

def Create_Ajax_URL(url):
	movie_id = url.split('/')[-2]
	t = time.strftime("%Y%m%d%H%M%S0368", time.localtime())
	ajax_url = "http://service.library.mtime.com/Movie.api?Ajax_CallBack=true&Ajax_CallBackType=Mtime.Library.Services&Ajax_CallBackMethod=GetMovieOverviewRating&Ajax_CrossDomain=1&Ajax_RequestUrl=%s&t=%s&Ajax_CallBackArgument0=%s" % (url,t,movie_id)
	return ajax_url

def Crawl(ajax_url):
	r = requests.get(url=ajax_url,headers=headers)
	if r.status_code == 200:
		r.encoding = 'utf-8'
		result = re.findall(r'=(.*?);',r.text)[0]
		if result is not None:
			value = json.loads(result)

			movieTitle = value.get('value').get('movieTitle')
			TopListName = value.get('value').get('topList').get('TopListName')
			Ranking = value.get('value').get('topList').get('Ranking')
			movieRating = value.get('value').get('movieRating')
			RatingFinal = movieRating.get('RatingFinal')
			RDirectorFinal = movieRating.get('RDirectorFinal')
			ROtherFinal = movieRating.get('ROtherFinal')
			RPictureFinal = movieRating.get('RPictureFinal')
			RStoryFinal = movieRating.get('RStoryFinal')
			print movieTitle
			if value.get('value').get('boxOffice'):
				TotalBoxOffice = value.get('value').get('boxOffice').get('TotalBoxOffice')
				TotalBoxOfficeUnit = value.get('value').get('boxOffice').get('TotalBoxOfficeUnit')
				print '票房:%s%s' % (TotalBoxOffice,TotalBoxOfficeUnit)
			print '%s——No.%s' % (TopListName,Ranking)
			print '綜合評分:%s 導演評分:%s 畫面評分:%s 故事評分:%s 音樂評分:%s' %(RatingFinal,RDirectorFinal,RPictureFinal,RStoryFinal,ROtherFinal)
			print '****' * 20

def main():
	urls = Get_Movie_URL()
	for u in urls:
		Crawl(Create_Ajax_URL(u))

	# 問題所在,請求如下單個電影連結時時不時會爬取不到資料
	# Crawl(Create_Ajax_URL('http://movie.mtime.com/98604/'))

if __name__ == '__main__':
	main()

執行結果為:


注意到其中一些電影如No.6的是時不時才會爬取得到的,具體Json資料也是loads下來了,就是不能夠每次都可以解析出來,具體的原因還沒分析出來。

示例2——爬取肯德基門店資訊:

以肯德基的餐廳地址為例:http://www.kfc.com.cn/kfccda/storelist/index.aspx


可以看到當前城市顯示的是廣州。

檢視頁面原始碼:



發現在“當前城市”之後只有“上海”的字樣而沒有廣州,而且城市是通過JS載入進來的,即該頁面使用了動態載入技術。

到FireBug中檢視JS相應的請求內容:


可以看到其中一個JS請求是用於獲取城市地址的,將該URL記下用於後面的地址的自動獲取然後再解析Json格式的資料即可。

獲取了city資訊,就應該是獲取所在city的門店資訊了,到XHR中檢視到如下請求:



其為POST請求,提交的內容為city資訊等,且返回的響應內容就是含有門店資訊的Json格式的內容。

因此記錄下該POST請求的URL和引數名,其中cname引數為city的值、直接從上一個Ajax請求中獲取即可,pageIndex引數是指定第幾頁(注意門店的換頁操作也是使用Ajax載入的),pageSize引數指定每頁顯示幾家店鋪、這裡為預設的10。

程式碼如下:

#coding=utf-8
import requests
import re
import json

url = 'http://www.kfc.com.cn/kfccda/storelist/index.aspx'
ajax_url = 'http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=js'
store_url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname'

r = requests.get(ajax_url)
result = re.findall(r'=(.*?);',r.text)[0]
value = json.loads(result)
city = value.get('city')

for i in range(1,11):
	data = {
		'cname':city,
		'pageIndex':str(i),
		'pageSize':'10',
		'pid':''
	}
	r2 = requests.post(url=store_url,data=data)
	result2 = json.loads(r2.text)
	tables = result2.get('Table1')
	for t in tables:
		print t.get('storeName')
		print t.get('addressDetail')
		print '**' * 20

執行結果:


注意,直接在命令列輸出結果才會完整,不要直接在Sublime中輸出否則結果會出現漏掉的:-)

2、直接從瀏覽器中採集已經載入好的資料

這裡用到一個組合,即:PhantomJS+Selenium+Python,都需要一個個去安裝,PhantomJS負責渲染解析JavaScript,Selenium負責驅動瀏覽器以及和Python互動,Python負責後期處理。

安裝selenium:pip install selenium,另外在呼叫瀏覽器驅動時可能會報錯,這時就需要下載相應的補丁,地址為:http://www.seleniumhq.org/download/

下載phantomjs解壓然後在呼叫時執行路徑executable_path上寫上phantomjs.exe所在的路徑即可。

Selenium練習例子:

使用Firefox瀏覽器的驅動來開啟百度,檢視輸入框的標籤:


可知可以通過webdriver的find_element_by_name()方法來獲取標籤元素進行操作。

在程式碼中判斷是否包含“百度”字樣,然後輸入“Kali Linux”進行搜尋,檢視頁面是否含有“Kali”字元,最後退出:

#coding=utf-8
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time

url = 'https://www.baidu.com/'

driver = webdriver.Firefox()
driver.implicitly_wait(3) # 隱式等待
driver.get(url)

assert u"百度" in driver.title

elem = driver.find_element_by_name("wd") # 找到輸入框的元素
elem.clear() # 清空輸入框裡的內容
elem.send_keys(u"Kali Linux") # 在輸入框中輸入'Kali Linux'
elem.send_keys(Keys.RETURN) # 在輸入框中輸入回車鍵
time.sleep(3)

assert u"Kali" in driver.page_source

driver.close()

期間會出現報錯資訊:selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH. 

這是因為沒有geckodriver.exe檔案或者沒有將其配置到環境變數中。

先去下載,個人下載的是win64版的:https://github.com/mozilla/geckodriver/releases

接著解壓,將geckodriver.exe放在一個已經設定了環境變數的目錄或者直接再設定新的環境變數的目錄都可以,這裡是直接放在Python目錄中的Scripts目錄中。

接著執行就沒有問題了:


示例1——爬取肯德基門店資訊:

這裡還是使用肯德基門店的例子,直接呼叫webdriver的find_element_by_xpath()方法,對於餐廳名稱及其地址的xpath的獲取,可以直接使用FireBug中的FirePath來獲取:


接著可以使用PhantomJS或者Firefox驅動都可以。

程式碼如下:

#coding=utf-8
from selenium import webdriver
import time

url = 'http://www.kfc.com.cn/kfccda/storelist/index.aspx'

driver = webdriver.PhantomJS(executable_path='E:\Python27\Scripts\phantomjs-2.1.1-windows\\bin\phantomjs.exe')
# 也可以使用Firefox驅動,區別在於有無介面的顯示
# driver = webdriver.Firefox()

# driver.implicitly_wait(10) # 隱式等待

driver.get(url)  

# 執行緒休眠,和隱式等待的區別在於前者執行每條命令的超時時間是一樣的而sleep()只會在呼叫時wait指定的時間
time.sleep(3)

for i in range(1,11):
	shopName_xpath = ".//*[@id='listhtml']/tr[" + str(i+1) + "]/td[1]"
	shopAddress_xpath = ".//*[@id='listhtml']/tr[" + str(i+1) + "]/td[2]"
	shopName = driver.find_element_by_xpath(shopName_xpath).text
	shopAddress = driver.find_element_by_xpath(shopAddress_xpath).text
	print shopName
	print shopAddress
	print '**' * 20

driver.close()

執行結果:


想換頁的話直接獲取該下一頁的標籤然後再模擬點選即可。

這裡還是要注意,直接在命令列輸出結果才會完整,不要直接在Sublime中輸出否則結果會出現漏掉的:-)

示例2——爬取去哪兒網酒店資訊:

網頁為:http://hotel.qunar.com/


檢視原始碼中表單部分內容:


可以根據圖中框出的屬性來進行元素的提取,然後通過webdriver進行相應的操作。

接著對酒店資訊的標籤進行特徵提取,這裡城市選的是深圳:


根據框中的特徵使用BeautifulSoup來進行提取即可。

酒店名所在標籤:


酒店地址所在標籤:


酒店評分所在標籤:


酒店價格所在標籤:


接著進行自動點選下一頁操作,檢視原始碼:


可以看到li標籤的class值為“item next ”,即中間有空格隔著,也就是其class值有多個而不是一個的意思,這樣就不能使用find_element_by_class_name()而是使用find_element_by_css_selector()來獲取元素,當然也可以使用find_element_by_xpath()和FirePath結合使用、但是xpath解析出來的引數會隨著頁面而改變,為了方便就直接使用find_element_by_css_selector(),如上述情況,多個class值的寫法為find_element_by_css_selector(".item.next")

程式碼如下:

#coding=utf-8
import requests
import re
import time
import datetime
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup as BS

def Crawl_For_Hotel(driver, to_city, from_date, to_date):
	ele_tocity = driver.find_element_by_id('toCity')
	ele_fromdate = driver.find_element_by_id('fromDate')
	ele_todate = driver.find_element_by_id('toDate')
	ele_search = driver.find_element_by_class_name('search-btn')
	ele_tocity.clear()
	ele_tocity.send_keys(to_city)
	ele_tocity.click()
	ele_fromdate.clear()
	ele_fromdate.send_keys(from_date)
	ele_todate.clear()
	ele_todate.send_keys(to_date)
	ele_search.click()
	page_num = 0
	while True:
		try:
			WebDriverWait(driver, 10).until(
					EC.title_contains(unicode(to_city))
				)
		except Exception, e:
			print e
			break

		time.sleep(5)

		# 讓driver執行JS指令碼,將頁面拉到底部
		js = "window.scrollTo(0, document.body.scrollHeight);"
		driver.execute_script(js)
		time.sleep(5)

		htm_const = driver.page_source
		soup = BS(htm_const,'lxml')
		names = soup.find_all(name='a',attrs={'class':'e_title js_list_name', 'href':not None, 'title':not None, 'target':'_blank'})
		addrs = soup.find_all(name='span',attrs={'class':'area_contair'})
		scores = soup.find_all(name='p',attrs={'class':'score'})
		prices = soup.find_all(name='div',attrs={'class':'hotel_price'})

		for name,addr,score,price in zip(names,addrs,scores,prices):

			# 處理地址標籤及其拼接問題
			ads = re.findall(r'</b>(.*?)</em>',str(addr))
			addr_new = ''
			for a in ads:
				addr_new = a if addr_new == '' else addr_new + ',' + a

			score_new = re.findall(r'<b>(.*?)</b>',str(score))[0]

			price_new = re.findall(r'<b>(.*?)</b>',str(price))[0]

			print name.string
			print '價格:%s元起' % price_new
			print '評分:%s / 5分' % score_new
			print '地址:' + addr_new
			print '**' * 20

		try:
			next_page = WebDriverWait(driver, 10).until(
				# EC.visibility_of(driver.find_element_by_xpath(".//*[@id='searchHotelPanel']/div[6]/div[1]/div/ul/li[10]/a/span[1]"))
				EC.visibility_of(driver.find_element_by_css_selector(".item.next"))
				)
			next_page.click()
			page_num += 1
			time.sleep(10)
		except Exception, e:
			print e
			break

def main():
	url = 'http://hotel.qunar.com/'
	today = datetime.date.today().strftime('%Y-%m-%d')
	tomorrow = datetime.datetime.today() + datetime.timedelta(days=1)
	tomorrow = tomorrow.strftime('%Y-%m-%d')

	driver = webdriver.Firefox()
	# driver = webdriver.PhantomJS(executable_path='E:\Python27\Scripts\phantomjs-2.1.1-windows\\bin\phantomjs.exe')
	driver.set_page_load_timeout(50)
	driver.get(url)
	driver.maximize_window() # 將瀏覽器最大化顯示
	driver.implicitly_wait(10) # 控制間隔時間,等待瀏覽器反應

	Crawl_For_Hotel(driver,u'深圳',today,tomorrow)

if __name__ == '__main__':
	main()

執行結果:


示例3——爬取酷狗頁面實現下載歌曲

參考的文章:http://www.freebuf.com/sectool/151282.html

大致過程為,當點選播放某一首歌時,頁面會請求一個MP3檔案所在的URL來進行播放,這個URL可以使用BurpSuite截斷或者瀏覽器的開發者工具看到,然後直接訪問也可以進行歌曲的下載。在這裡就只看如何通過webdriver來獲取該URL然後實現下載。

開啟酷狗主頁面:http://www.kugou.com/

檢視其輸入框以及搜尋按鈕的元素標籤,同樣是使用FirePath來檢視其xpath:



隨意搜尋一首歌,這裡搜的是“smileyface”,然後檢視第一行的歌曲的元素標籤資訊:



開啟FireBug的網路,然後點選該連結進行播放操作:


檢視Network的內容,發現其中一條請求的是一個亂取名字的URL檔案,即mp3檔案,可以看到它請求包的一些特徵:



直接複製該URL進行訪問,可以直接下載歌曲,即是我們需要查詢的URL。

接著點選頁面的下載按鈕看看,會提示需要在客戶端才能進行下載操作:


可以看到,是可以直接繞過這個限制直接下載歌曲的。

接著檢視頁面原始碼,看到audio標籤的屬性src的值是不會顯示出來的:


只有檢視元素時才可以看到連結,即需要動態載入:


接著,直接對該URL使用urllib庫的urlretrieve()方法下載即可。

但是,有一些付費歌曲在線上頁面上是找不到其相應播放的URL的,需要下載客戶端才可以播放:


所以要在下載時進行判斷以免下載了不是MP3的檔案。

程式碼如下:

#coding=utf-8
import requests
import re
import time
import urllib
import sys
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

def Download_The_Song(url):
	try:
		print "[*]Start to download the song..."
		path = "%s.mp3" % name
		urllib.urlretrieve(url, path, Schedule)
		print '\n[+]Download successfully!'
	except Exception, e:
		print '[-]Failed to download.'
		print e

def Crawl_For_The_URL():
	print
	song_name = raw_input("[*]Please input the name of song for search: ")
	global name

	url = 'http://www.kugou.com/'
	driver = webdriver.Chrome()
	driver.maximize_window()
	driver.get(url)
	# time.sleep(3)

	# 輸入歌曲名並進行搜尋操作
	ele_input = driver.find_element_by_xpath('html/body/div[1]/div[1]/div[1]/div[1]/input')
	ele_input.clear()
	ele_input.send_keys(song_name.decode('gbk'))
	ele_search = driver.find_element_by_xpath('html/body/div[1]/div[1]/div[1]/div[1]/div/i')
	ele_search.click()
	songs_list_url = driver.current_url

	# 獲取歌曲列表的名稱
	driver.get(songs_list_url)
	# time.sleep(3)
	i = 1
	while True:
		try:
			ele_song = driver.find_element_by_xpath(".//*[@id='search_song']/div[2]/ul[2]/li[%d]/div[1]/a"%i)
			print "%d." % i + ele_song.get_attribute('title')
			i += 1
		except NoSuchElementException as msg:
			break

	# 獲取使用者輸入的數字進行相應歌曲下載URL的提取
	print
	num_input = raw_input("[*]Please choose the number of the song to download: ")
	num = int(num_input)
	driver.get(songs_list_url)
	# time.sleep(3)
	ele_choosed_song = driver.find_element_by_xpath(".//*[@id='search_song']/div[2]/ul[2]/li[%d]/div[1]/a"%num)
	name = ele_choosed_song.get_attribute('title')
	ele_choosed_song.click()
	driver.switch_to_window(driver.window_handles[1])
	download_url = driver.find_element_by_xpath(".//*[@id='myAudio']").get_attribute('src')
	driver.close()
	return download_url

# 用於顯示下載進度
def Schedule(a, b, c):
	per = 100 * a * b / c  
	if per > 100:
		per = 100
	percentage = "Downloading...... %.2f %%" % per
	sys.stdout.write('\r'+'[*]'+percentage)

def main():
	url = Crawl_For_The_URL()

	# 若返回的URL請求的檔名字尾不是MP3格式,則可能是webdriver的問題,或者是該歌曲是付費歌曲、需要在客戶端才能播放
	if url.split('.')[-1] == "mp3":
		Download_The_Song(url)
	else:
		print "[-]Can't download the song .Maybe the type of webdriver is incorrect ,or the song needs to be paid for play."

if __name__ == '__main__':
	main()

執行結果:

大多數歌曲都是可以直接下載的:


接著看下Eason大佬的新歌,一般都是需要付費的:


這裡奇怪的一點是使用Firefox驅動或者PhantomJS是有問題的而使用Chrome的就沒有問題,Firefox說的是找不到該元素:


具體的不同瀏覽器驅動的問題後面再分析看看。

參考來源:《Python爬蟲開發與專案實戰》