python配合pyppeteer实现动态网页数据抓取
关键词: 动态网页,python, pyppeteer, chrome, javascript, html5, xpath, json, idm, wget,
爬虫学的好,监狱进的早。
爬虫学的好,监狱进的早。
爬虫学的好,监狱进的早。
首先,静态页面或网站不在本文讨论之列,方法多到不计其数,最差的情况下人肉都可以。对于python和pypeteer的初级适用也不在本文讨论之类,类似的文档随处可见。 先来看看动态页面的定义: 动态页面技术是与静态页面技术相对应的,也就是说,网页URL的后缀不是·htm、.html、.shtml、.xml等静态网页的常见形式,而是以·asp、.jsp、.php、.perl、.cgi等形式为后缀,并且在动态网页网址中有一个标志性的符号——“?”。 动态页面并不维护单独的页面,它是以数据库为基础,在收到用户请求时,根据请求参数现场生成页面。对于最终用户来说,保存网页或查看source,其中的内容和网页显示差异巨大,甚至没有任何有用的元素或数据,那十有八九可以确认碰到动态页面了。
并不是非要挑战这个高难度动作,只是碰巧访问的网站这样,索性就研究了一下类似技术。不要尝试抓取正规的网站,成功和监狱只是一墙之隔。还好,练手的这个网站除了是通过ajax渲染的网站外,既不正经,也不正规。 动态网页的结构构成 动态网页的构成元素,主要有以下几个, HTML,CSS, Javascript, web服务器和数据库。其中前三个因素,决定了在浏览器上的渲染行为及最终结果,也是动态抓取数据要重点关注的部分。动态网页内容以及元素,都可以通过chrome的检查功能或页面开发工具来查看。


上图就一个典型的动态页面结构,其中需要抓取的数据以列表的形式,有的是<li, 有的是<div, 还有的是<span,,嵌在HTML中。只要有恰当的技术和方法,可以很轻松的把这些数据抓回来。并没有例外,BAT,微软,甚至豆瓣编辑器都是这样组织的。 抓取动态数据,我们选择python和pypteteer结合的方式。至于python和pypteteer结合的程序框架,随意在网上复制一个就可以。
页面列表元素的循环抓取 在动态网页里,每个列表都是一个css选择器,它最大的困扰就是数量不是固定的,特别是最后一页,设定固定数值循环抓取一定会出错。这里提供俩个思路去处理。还有些判断nth-child(n)大小再遍历的方法,就有点太蠢了。 a, 通过beautifulSoup的findall album = await page.content() soup = bs(album,'html.parser').find_all(id = 'dataList_img') for item in soup[0].find_all('li'): #bs find的result是list,find必须遍历每个项。 item_title = item.get_text().strip('0123456789 /℃') ......
先通过find_all获取id='dataList_img'下的全部数据,再find_all其中的每个li进行处理 b, 通过pypeteteer的xpath datalist = await page.xpath('//*[@id="dataList_img"]/ul/li') for item in datalist: t = await item.xpath("./div[2]/p/text()") item_title1 = await (await t[0].getProperty("textContent")).jsonValue() 通过pyppeteer的xpath定位其中的每个li元素,再进行循环。
动态网页的element选取和动作操作 对于页面元素,可以有多种方法去选取,包括J, JJ和Jx。任何一个方式都可以,选中就可以click操作。对于元素,取属性的方式有很多,对于含data-v的html页面会比较麻烦,很大可能取不到。

对于列表中的子项元素,直接抓取有点难度,首先nth-child在不断变化,每个子项的xpath也不同,尝试用含变量的nth-child定位方式,/root/a/b[position()=$variable], 也是然并卵。现在有了xpath,利用xpath的相对路径。这些问题就迎刃而解了。 datalist = await page.xpath('//*[@id="dataList_img"]/ul/li') for item in datalist: t = await item.xpath("./div[2]/p/text()") #注意,其中的item表明该xpath的基准位置 item_title1 = await (await t[0].getProperty("textContent")).jsonValue() b = await item.xpath('./div/img') img_tag = await (await b[0].getProperty('outerHTML')).jsonValue() #包含自身,用于最低层的tag img_url = re.findall(re3, img_tag) #匹配正则表达式,查找对应url. #for img in itemlink: imglink = img.get('data-src') #通过bs的遍历方式取url. 这里要解释一下,网上例程里抓取img地址,无一例外都是通过抓取<img src=''>中的src属性,包括京东和百度都没问题,但是,这些代码在动态页面里不工作,一个属性都抓不到。 img_url = await (await item.getProperty("src")).jsonValue() #这个方法并不灵。

如上图,对于每个news页面,我们抓取这三部分就好, 类型,标题,以及文章主体。
选择元素进行操作,并捕获click后弹出的新页面 我们平时打开网页,常见操作无非是看到喜欢的,直接点开,或者没有喜欢的,那就点下一页。那这些操作,通过pyppeteer怎么实现呢? 这个问题有点复杂,我们要分开俩步来谈,先谈怎么点开自己喜欢的内容。点击的内容,可能是一个按钮,也可能是一个元素,或者是下一页。这些内容就是pyppeteer里需要先选定的元素,click就是针对这些元素的一个event,可以是XPath,也可以是Selector,都可以。注意:这些元素都属于页面内的。

注意观察,代码和页面元素的对应关系,在chrome里是非常容易查找的。 page.Jx:等待 xPath 对应的元素出现,返回对应的 ElementHandle 实例 page.querySelector :等待选择器对应的元素出现,返回对应的 ElementHandle 实例 选中元素,之后的click就简单多了,直接点击,然后等待。 elem = await page.waitForSelector(nextpage) # elem = await page.xpath('//*[@id="dataList_img"]/ul/li[4]') await elem.click() await asyncio.sleep(3) 两个方法都是可以的。XPath和Selector(注意大小写)一定要在chrome里复制,否则有可能认不出,或认错。至于为啥要等,动态页面渲染需要时间,太快进去也是空白页面。关注的怎么等待。 如何去获取页面中的某个元素呢?python不认识$, 但是认识J, JJ和Jx page.J('#uniqueId'):获取某个选择器对应的第一个元素 page.JJ('div'):获取某个选择器对应的所有元素 page.Jx('//img'):获取某个 xPath 对应的所有元素 page.waitForXPath('//img'):等待某个 xPath 对应的元素出现 page.waitForSelector('#uniqueId'):等待某个选择器对应的元素出现



至于元素本身及属性,可以参考下段内容。 outerHTML <li data-v-3b0ba6cb="" style="width: 33.3333%;"><div data-v-3b0ba6cb="" class="imgBox columnNum3"> Jspath document.querySelector("#dataList_img > ul > li:nth-child(1)") selector #dataList_img > ul > li:nth-child(1) xpath //*[@id="dataList_img"]/ul/li[1] fullpath /html/body/div/div[2]/div[3]/div[1]/div[2]/div/ul/li[1] element <li data-v-3b0ba6cb="" style="width: 33.3333%;"><div data-v-3b0ba6cb="" class="imgBox columnNum3">
如何捕获新开页面, 要根据click之后的结果分三种情况来处理。 1, 同页面内前进的容易处理,因为页面本身没变化,可以很容易通过一下操作处理。 await page.goBack() await page.goForward() await page.reload() await page.screenshot() await page.close() await page.evaluate() 2, 通过url方式打开新页面的,这个也不难,可以前期构造url直接goto,还节约了一个click。 workpage = await browser.newPage() await workpage.goto(work_url, {'waitUntil' : 'domcontentloaded'}) 后续操作,直接在workpage上进行就可以。 3, 最难处理的第三种,click动作本身弹开的新页面,没有url,也没有newpage的标签,只能通过targetcreated事件去捕获。 result_page = asyncio.get_event_loop().create_future() # create new promise # bind promise to watch event targetcreated, must before click to link browser.once('targetcreated', lambda target: result_page.set_result(target)) await (await result_page).page() # 捕获新开页面, 并进行处理。 类似代码在js里实现的很多,但是在python里,只有上面这段代码才有效。网上有很多人说过,通过获取page的list,利用下标定位,再bringFront的方法,在这个场景,都是无效的。 如何判断新页面已成功打开呢?可以通过Waitfor一个特定的事件或元素。比如: await page.waitForXPath('//img'); await page.waitForSelector('#uniqueId'); await page.waitForResponse('https://d.youdata.netease.com/api/dash/hello'); await page.waitForRequest('https://d.youdata.netease.com/api/dash/hello'); 异常处理 抓取动态页面时,最常见的错误是动作太快,页面未来得及渲染,还有一种可能页面本身就是空的,没有抓取数据需要的XPath。 a, 判断是否刷到空页面 d = await page.xpath('//*[@id="dataDetail_news"]/div[2]') if len(d) == 0: #说明碰到了空页面 continue 遇到空页面,通过continue跳过当前循环,继续下一轮操作。 b, 动作太快,或者网站太慢,页面来不及准备 j = 0 titlenew = await (await t[0].getProperty('textContent')).jsonValue() while (titlenew == previous and j < 5): #title相同,说明新的content还没准备好。 print('Wait 2 seconds') await asyncio.sleep(2) titlenew = await (await t[0].getProperty('textContent')).jsonValue() j = j + 1 previous = titlenew 如何判断页面来不及准备呢,可以选取一个需要抓取的数据(建议字符串类型),两次抓取的数据相同,那就说明太快了,需要再等2秒。可是我们不能无限度等下去啊,再引入一个参数j,在j >= 5时,也就是等待10秒后,跳出循环。 通过以上俩个方式,可以规避动态页面抓取错误中的80%以上。 c, 其他未曾预料的错误 python程序执行时,大部分错误被抑制,特别是通过Exception来定义后。但是我们需要捕获程序中的错误,来了解运行状态。因此需要在Exception里添加个打印错误的代码,例子见下面。 爬取数据,记录,日志的保存 建议对爬取记录,或图片或文字,进行编号索引,一方面便于确定执行状态,另外在程序中断后,也知道后续从哪里开始。很简单,构造一个列表,保存起来就可以。 from sys import stderr from traceback import print_exc try: result.append([index, imgprefix, item_title, imgextfix, total]) except Exception as e: print_exc(file=stderr) finally: csv.writer(open('xm-newslist.csv', 'a+', newline='', encoding = 'gb18030', errors='ignore' )).writerows(result) 正则表达式:
re1 = re.compile(r'cover\.(?:jpg|png)$') # 精确匹配以covercover.jpg或cover.png结尾的字符串 re2 = re.compile('[0-9]{1,}P') #匹配[3P], 44P,或3456P这样的字符串,通常指图片数量。 re3 = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+') re3, 匹配http或https开始的完整url。
windows系统的回车换行 windows10已经能很好的处理回车换行符号了,已经无须特意去转换\r\n的序列。但windows本身的定义及顺序如下: 回车\换行, \r\n, 0x0D0A
windows下文件命名非法字符剥除 有很多字符,在windows里禁止用于文件和目录名称,官方定义是7个。不合法字符要进行删除。 title = re.sub(r'[*"/\?:|<>]', '', title) 处理中文字符的乱码 python3默认用unicode,处理中文字符可能会出现乱码,特别是在写入文件时。解决方式如下: 1,指定编码文件 encoding='gb18030' 或,2,py文件指定为utf-8编码。 #!/usr/bin/env python # -*- coding: utf-8 -*- gbk或utf-8无法处理的字符,可以通过以下方法查看具体是啥字母 print('\ufffc'.encode('gb18030'))
批量下载链接文件 我们不可能在python运行时,抓取或下载网站的图片或文件,数据量一大,跑起来就好好几个钟头甚至上天,太慢了。但我们可以通过python抓取到文件或图片链接,后期通过下载工具来处理。 动态页面,就是通过代码实现的页面,既然是代码实现,那就一定有规则,只要找到数据存放规则,那就很容易通过python来处理了。 对于album里的图片,要么命名有规则,从01开始排。如果文件名是散乱的随机名称,那页面里一定有地方存放着包含文件名的json文件。以某c开头的视觉网站来说,其json文件解析方法如下: import json album = json.loads(imgjson) for item in album: url = url_prefix + item.get('img') 最初想法是通过调用idm来实现文件的下载,我们在python里构造idm下载命令,在通过call或os.system调用,实践证明,一旦文件数量上万,这种方法完全行不通,其一,调用idm本身太慢添加一个文件要1秒之多,14万个文件啊。其二,idm的多线程及频繁重传,很容易触发网站的ban操作。 ====以下为调用idm代码来下载,效率太低======= #command = [IDM, '/d', url + filename, '/p', dest, '/f', filename, '/a'] # call(command) # or os.system(command) 不得以转为wget方式,确认wget的批量下载命令如下: wget -O filename -x -P local -i inputfile -a wget.log 因此我们构造inputfile,以及wget命令本身就可以了。 # =====以下为构造wget下载指令, a, 创建inputfile, b, 构造下载命令。 def xmbatchdownload(index, url, title, fmt, count): i = 1 downlist = str(index).zfill(4) + '.txt' logfile = str(index).zfill(4) + '.log' dlistfile = open(downlist, 'w') #构造inputfile,写入同类型的全部下载链接。依照01开始,共计count张图片 for i in range(1,int(count)+1,1): filename = str(i).zfill(2) + '.' + fmt localname = url[-17:-1] + '-' + filename dest = DWNPATH + title.strip() dlistfile.write(url + filename + '\n') dlistfile.close() command = [WGET, '-P', dest, '-i', downlist, '-a', logfile] wget_cmd = ' '.join(command) f.writelines(wget_cmd + '\n') #将构造好的wget下载命令,写入文件f。 之后以批处理方式在windows执行下载命令,效果好太多了。首先wget以单进程方式运行,批量下载时,无须重新连接,直接复用已建立好的连接,这对于对于https方式访问的网站安全太多了。其次以index方式命名,非常方便知道下载状态,可以拆分成多个批处理,同时进行,也能随时排错。 至少14万个小文件,差不多10G多大小,一晚上就下好了。 ========over=========