寫在最前:互聯網並非法外之地,爬蟲僅供技術交流
運行環境
- python 3.7.4
- requests 2.10.0
爬取目標
- EDA技術與應用(2020秋)1.1.2 EDA技術概述 教學視頻
分析視頻字幕接口
找接口就只能憑借經驗去network里面翻找,或者借助於瀏覽器調試,沒有過多的技巧。
一、從資源回溯尋找接口
-
帶有視頻接口的json文件URL分析
https://www.xuetangx.com/api/v1/lms/service/playurl/7ED5FE6BE6C6DAC39C33DC5901307461/?appid=10000
跟其他視頻比較,可以得出:
有一個請求參數,這個參數似乎是固定的,所以不用管。
而
7ED5FE6BE6C6DAC39C33DC5901307461
是一個路徑變量,不同視頻有着不同的該參數。 -
帶有字幕接口的json文件URL分析
https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/
這個數據是通過POST請求的,數據查看后發現需要一個json對象
{"c_d":"7ED5FE6BE6C6DAC39C33DC5901307461"}
而且這個
c_d
與上文視頻的路徑變量一致。所以最后得出請求方案:
c_d = val # method: GET # 視頻動態URL url_video = "https://www.xuetangx.com/api/v1/lms/service/playurl/{}/?appid=10000".format(c_d) # method: POST # 字幕URL url_subtitle = "https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/" data = {"c_d":"7ED5FE6BE6C6DAC39C33DC5901307461"}
-
視頻接口和字幕接口URL分析
上面的兩個json文件直接提供的視頻和字幕的接口URL,所以即使他們的URL還帶了其它的參數,我們也不再需要關心這些。
可能會擔心的就是鑒權問題,但是我已經嘗試過了,字幕和視頻的接口以及這兩個json文件都不需要專門的頭部信息進行鑒權。
我們只要找到上面的兩個文件,就可進行視頻字幕的下載。
所以我們現在需要找到
c_d
。
二、從未知變量回溯尋找接口
-
帶有c_d(ccid)的json文件
我們可以在這個文件下的
data
中的content_info
中的media
下找到一個ccid
與c_d
相同,所以我們可以把這里獲得的ccid
當成變量給下游的URL。同時我們還在這個文件下找到了視頻相關附件的鏈接,
https://qn-next.xuetangx.com/15679498483925.pptx
,同樣不需要鑒權就可以下載。 -
帶有c_d(ccid)的json文件URL分析
https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906
我們可以看到這個新的路徑給爬取增加了不少難度,它多出了兩個路徑變量(4227236/6195112)和一個請求參數(sign=NCIAE08091001906`)。
而且從這里開始就已經需要鑒權了,頭文件得帶上相應的參數才可以進行訪問。
-
帶有id的json文件
https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906
中的6195112
是這個文件的leaf_list
中每一個json對象的id
。我們成功的解決了下游URL
的一個變量。 -
帶有c_d(ccid)的json文件URL分析
https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid=4227236&sign=NCIAE08091001906
很幸運的是,下游URL的兩個未解決變量在這里出現了,經過這個URL,總體的未知變量沒有增多。
經過兩個路由后,我們最后可以得出這樣的請求方案:
cid = val1 sign = val2 # method: GET # 章節動態URL url_chapter = "https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid={}&sign={}".format(cid, sign) leaf_id = response(url_chapter) # method: GET # 小節動態URL url_leaf = "https://www.xuetangx.com/api/v1/lms/learn/leaf_info/{}/{}/?sign={}".format(cid, vid, sign)
三、回溯到頭再順流而下
-
未解決的問題
-
"NCIAE08091001906"到底是什么?cid是課程id嗎?
我們可以通過退出再登錄,使用其它賬戶來判斷它們是否與用戶身份相關;通過等待一段時間看它們是否改變,判斷是否與時間有關。我們會發現它們既與用戶身份無關也與時間無關
我們還可以通過瀏覽器的調試模式去判斷這一點。
最后我們可以得出sign和cid都是課程識別碼。
雖然你可以在進入這門課程學習后,在頂上的URL找到這兩個參數。但我依舊想更清楚的解釋它們是什么,sign(course_sign)是一門課程的標識,而cid(classroom_id)是一門課程每個學期的標識。這些信息都可以在更高的源頭追溯到。
但這次我們就先追溯到這里。
-
關於鑒權的問題。
我們在爬蟲的時候需要考慮清楚地告訴對方服務器我們是什么?
所以我們需要去看瀏覽器為我們生成的請求頭和其它請求條件呢,這我們可以自己搭一個本地服務,去看
requests
的請求頭和瀏覽器的有什么區別。再通過不斷試錯,找到當前請求需要的請求頭和其它請求條件。
很慶幸的是,
學堂在線
我們需要補充修改的請求頭參數非常簡單。示例如下:-
方式一
# 這里的代碼請不要嘗試,sessionid我已經安全退出,失去效力。 # 沒有安全退出的話可以保存兩周,在此期間可以任意爬取。當然這也跟瀏覽器的設置有關。 headers = { "xtbz": "xt" } cookies = { "sessionid": "z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } requests.get("https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906",headers=headers,cookies=cookies)
-
方式二
# 方式一直接帶上cookie是更好的選擇,至少在學堂在線是這樣的。 headers = { "xtbz": "xt", "cookies": "sessionid=z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } requests.get("https://www.xuetangx.com/api/v1/lms/learn/leaf_info/4227236/6195112/?sign=NCIAE08091001906",headers=headers,cookies=cookies)
-
-
-
正式順流而下
-
找到sign和cid
-
找到cookies
只需要sessionid就好,其它瀏覽器找cookies自行百度。
-
根據sign和cid請求數據
import json,requests,time cid = "4227236" sign = "NCIAE08091001906" # 請求頭 僅供參考 headers = { "xtbz": "xt" } cookies = { "sessionid": "z3rvy7fpp4tqbc4opmzkq1amlvmqde7d" } # 章節信息 url_chapter = "https://www.xuetangx.com/api/v1/lms/learn/course/chapter?cid={}&sign={}".format(cid, sign) time.sleep(0.2) chapter = json.loads(requests.get(url_chapter,headers=headers,cookies=cookies).content) ## 第一章的第一節的所有小節 leaf_list = chapter['data']['course_chapter'][0]['section_leaf_list'][0]['leaf_list'] ## 第一章的第一節的所有視頻小節 video_leaf_list = list(filter(lambda item:item['leaf_type']==0, leaf_list)) ## 第一章的第一節的第一個視頻小節的id vid = video_leaf_list[0]['id'] # 視頻小節信息 url_leaf = "https://www.xuetangx.com/api/v1/lms/learn/leaf_info/{}/{}/?sign={}".format(cid, vid, sign) time.sleep(0.2) video = json.loads(requests.get(url_leaf,headers=headers,cookies=cookies).content) ## ppt等附件 url_file = video['data']['content_info']['download'][0]['file_url'] time.sleep(0.2) file = requests.get(url_file).content with open('1.pptx','wb') as f: f.write(file) ccid = video['data']['content_info']['media']['ccid'] ## 視頻 time.sleep(0.2) url_video = json.loads(requests.get("https://www.xuetangx.com/api/v1/lms/service/playurl/{}/?appid=10000".format(ccid)).content)['data']['sources']['quality10'][0] time.sleep(0.2) content_video = requests.get(url_video).content with open('1.mp4','wb') as f: f.write(content_video) ## 字幕 time.sleep(0.2) url_subtitle = json.loads(requests.post("https://www.xuetangx.com/api/v1/lms/service/s_t_g_p/",data={"c_d": ccid},headers=headers).content)['data'][0]['data'] time.sleep(0.2) content_subtitle = requests.get(url_subtitle).text with open('1.txt','w') as f: f.write(content_subtitle)
-
寫在最后
上面的代碼主要是提供一個思路,實際只用於抓取EDA技術與應用(2020秋)第一章的第一節的第一個視頻小節,因為我們不可以保證每一門課的第一章的第一節都有視頻小節,也不能保證每一個小節都有附件,每一個視頻都有字幕,爬取其它視頻還要做容錯處理。
如果想一次爬所有視頻也可以實現,用for循環就可以。請記得不要過度頻繁地發送請求,會給服務器造成巨大的壓力,服務器針對此也有很多的反爬手段。
爬取的字幕是json數據,想要變成字幕文件還得做相應處理。
這篇文章還有后續,會繼續完善相應功能。