前言 最近看了下 openrestry ,它是 ngnix 和 lua 的结合体,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。可以构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。搜索它的视频资料,一个在网易云课堂上,只有 1/3 的内容,另外一个在 stuq 上,是全部内容。我就想把视频爬下来。
分析 分析视频地址 通过 chrome 开发工具的 Elements 发现没有直接暴露地址,是用 flash 播放的,同时可以知道视频的 id 用 vid 来表示。 通过 Network 可以发现几个有用的请求。它们是 getvideofile player.swf playinfo . 真实的视频地址就在 playinfo 这个请求里面。分析这个请求的查询参数 可以知道只有 2 个参数是必须的,一个是 vid (视频 id), 一个是 m 它是固定的。当时还反编译了一下 player.swf 看了下其他参数是什么。其实这个接口没有怎么防范,通过不停的减少参数就可以把这个分析出来。
分析视频列表 查看网页源代码可以看到有视频的 id,有 2 个问题是,一个是要登录,一个是它用的是 vue 。而且也没有发现有 api 请求,所以只能用 selenium 来模拟请求了。
实现 我把抓取视频 id 和下载视频分开,用 selenium 来抓取视频 id 和文件名列表,保存为 json,在用 aiohttp 来下载视频文件,所以会快些。
stuq_spider.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import timeimport jsonfrom selenium import webdriverfrom selenium.webdriver.common.keys import Keysclass StuqSpider : def __init__ (self) : self.driver = webdriver.Chrome('/Users/tom/workspace/chromedriver' ) def crawl_course (self, courseid) : coursewares = self.get_coursewares(courseid) data = [] for url in coursewares: info = self.get_video_info(url) data.append(info) self.store_data(data) def login_weibo (self, username, password) : self.driver.get('https://passport.stuq.org/user/login' ) self.driver.find_element_by_xpath('//a[@class="button weibo"]' ).click() userid = self.driver.find_element_by_id('userId' ) userid.send_keys(username) passwd = self.driver.find_element_by_id('passwd' ) passwd.send_keys(password) time.sleep(5 ) self.driver.find_element_by_xpath( '//*[@id="outer"]/div/div[2]/form/div/div[2]/div/p/a[1]' ).send_keys(Keys.ENTER) time.sleep(20 ) def get_coursewares (self, courseid) : coursewares = [] self.driver.implicitly_wait(5 ) self.driver.get('http://www.stuq.org/course/' + courseid + '/study' ) for a in self.driver.find_elements_by_xpath('//p[@class="video"]/a' ): coursewares.append(a.get_attribute('href' )) return coursewares def get_video_info (self, url) : self.driver.implicitly_wait(2 ) self.driver.get(url) h2 = self.driver.find_element_by_tag_name('h2' ).text videoid = self.driver.find_element_by_xpath( '//section[@id="cc-video"]/div' ).get_attribute('id' ).split('_' )[2 ] return [h2, videoid] def store_data (self, data) : with open('stuq.json' , 'w' ) as fp: json.dump(data, fp) def close (self) : self.driver.close() self.driver.quit() username = '' password = '' courseid = '1015' spider = StuqSpider() try : spider.login_weibo(username, password) spider.crawl_course(courseid) finally : spider.close()
bokecc_crawler.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import aiohttpimport asyncioimport jsonimport timeimport os.pathimport xml.etree.ElementTree as ETbase_url = 'https://p.bokecc.com/servlet/playinfo?m=1&vid=' chunk_size = 64 class Crawler : def __init__ (self, max_task=4 , loop=None) : self.max_task = 4 self.loop = loop or asyncio.get_event_loop() self.q = asyncio.Queue(loop=self.loop) self.session = aiohttp.ClientSession(loop=self.loop) self.t0 = self.t1 = 0 def add_task (self, task) : self.q.put_nowait(task) async def crawl (self) : workers = [ asyncio.Task(self.work(), loop=self.loop) for _ in range(self.max_task) ] self.t0 = time.time() await self.q.join() self.t1 = time.time() for w in workers: w.cancel() async def work (self) : try : while True : filename, vid = await self.q.get() print('working:' , filename, vid) xmldata = await self.fetch(base_url + vid) root = ET.fromstring(xmldata) copys = root.findall(".//copy" ) playurl = copys[-1 ].attrib['playurl' ] await self.download_video(playurl, filename) self.q.task_done() except asyncio.CancelledError: pass async def fetch (self, url) : async with self.session.get(url) as response: return await response.text() async def download_video (self, url, filename) : async with self.session.get(url) as response: with open(os.path.join('videos' , filename), 'wb' ) as fd: while True : chunk = await response.content.read(chunk_size) if not chunk: break fd.write(chunk) def close (self) : self.session.close() def report (self) : print('total time: ' , self.t1 - self.t0) loop = asyncio.get_event_loop() crawler = Crawler(loop=loop) with open('stuq.json' ) as fp: data = json.load(fp) for filename, vid in data: filename += '.flv' crawler.add_task((filename, vid)) try : loop.run_until_complete(crawler.crawl()) finally : crawler.report() crawler.close()
爬取成果 爬了 20 分钟,下了 29 个视频,共 2.1G。