利用Python爬取Twinkl上的教学资源

在线教育网站Twinkl最近宣布,任何人都可以获得为期一个月的会员资格(注册网址:www.twinkl.com/offer ,注册时输入代码: USATWINKLHELPS)。Twinkl上的教育资源主要面向学前、小学教育,资源格式一般为PPT和PDF,制作精美,学前班和小学教师应该会非常喜欢。最近写了个批量爬取脚本,放上来交流一下。

一、思路分析

暂时没有发现 Twinkl上的资源是否存在一个全局的索引规则,所以我们可以首先选定某一个分类目录,从而批量下载该目录下的所有资源。如下图,每一个分类目录下有若干页面。每个页面有12个资源,除了第一页的链接需要手动获取外,后面第N页的链接格式为:第一页链接/N。

查看页面源代码,发现每一个资源都对应一个li标签,例如:

<li class="resource-preview col-12 col-sm-6 col-lg-4 col-xl-3 ">
			<a href="/resource/ma-t-l-141-line-handwriting-activity-sheets">
			    
				<img src="https://images.twinkl.co.uk/tw1n/image/private/t_345/image_repo/0c/81/ma-t-l-141-line-handwriting-activity-sheets-mandarin-chinese_ver_1.jpg" data-width="480" data-src="https://images.twinkl.co.uk/tw1n/image/private/t_345/image_repo/0c/81/ma-t-l-141-line-handwriting-activity-sheets-mandarin-chinese_ver_1.jpg" class="delayed-image-load image-replace" title="" style="">
				<noscript><img src="https://images.twinkl.co.uk/tw1n/image/private/t_345/image_repo/0c/81/ma-t-l-141-line-handwriting-activity-sheets-mandarin-chinese_ver_1.jpg" alt="画线运笔练习 - 运笔练习,手写,肌肉能力训练,直线,曲线,圆圈,波浪线,之字形折线," width="345" height="173"/></noscript>
				<h2>画线运笔练习</h2><h3>More languages</h3></a><a class="saveIcon moreLanguages blue button" data-id="2796856" href="/my-saved-resources">保存以后用</a>
			
		</li>

其中入口链接为<a>标签的href属性,缩略图链接为<img>标签的src属性或data-src属性,资源标题为<h2>标签内的文本。

进入任意资源页面,很容易可以看到下载入口链接下面的id为download_link的<a>标签内:

<span class="download flagPresentSpan">
					<a id="download_link" data-download="no" data-nopremiumfollow="1" href="/sign-up" class="button bigDownload green flagPresent loggedOut">Unlimited 会员<div>下载</div></a>
					
				</span>

访问此链接,可以通过post请求获得真实下载地址(这一步会校验登陆状态和会员资格)。下面图中location这一项即为真实下载地址,可以利用脚本获取。

此外,这里还可以顺便获得用户cookie,以便模拟登陆:

到这里,基本就可以理顺思路了。

二、功能实现

1.导入库

这里要用到如下库:

from bs4 import BeautifulSoup import re import requests import time

2.获取所有目录页面

def getUrl(initialUrl, headers):
    pageUrlList = []
    r = requests.get(initialUrl, headers = headers)
    soup = BeautifulSoup(r.text, from_encoding = 'utf-8')
    div = soup.find('div', class_='pager_bar')
    pageNum = int(div['data-num-pages'])
    print 'Total page number:' + str(pageNum)
    pageUrlList.append(initialUrl)
    if pageNum >= 2:
        for i in range(2,pageNum+1):
            newPageUrl = initialUrl + '/' + str(pageNum)
            pageUrlList.append(newPageUrl)
    return pageUrlList, pageNum

此函数输入值为手动设置的初始链接(初始链接一般设为某一目录下的第一页),输出值为所有页面的序号和对应链接。

3.获取资源列表

def getItem(pageUrl, headers):
    itemUrlList = []
    rr = requests.get(pageUrl, headers = headers)
    rsoup = BeautifulSoup(rr.text, from_encoding = 'utf-8')
    for link in rsoup.find_all('a', href = re.compile(r'/resource/.*')):
        title = link.find('h2').text
        itemUrltemp = link['href']
        itemUrl = 'https://www.twinkl.com' + itemUrltemp
        picUrlTemp = link.find('img')
        try:
            picUrl = picUrlTemp['src']
        except:
            picUrl = picUrlTemp['data-src']
        temp = [title, itemUrl, picUrl]
        itemUrlList.append(temp)
    return itemUrlList

输入值为上一步获得的页面链接,批量获取每一个页面中每一个资源对应的入口链接和缩略图链接,最后汇总为一个列表。实际测试时总是在获取缩略图时报错,检查发现缩略图属性名称不统一,所以用了一个异常处理语句。

4.获取真实下载链接

def getDownload(itemUrl):
    rrr = requests.get(itemUrl, headers = headers)
    rrsoup = BeautifulSoup(rrr.text, from_encoding = 'utf-8')
    downTemp = rrsoup.find('a', id = 'download_link')['href']
    downloadUrl = 'https://www.twinkl.com.hk' + downTemp
    print '下载入口获取成功'
    return downloadUrl

def convertUrl(downloadUrl):
    downloadInfo = requests.post(downloadUrl, headers=headers, allow_redirects=False)
    trueUrl = downloadInfo.headers['location']
    print '下载链接获取成功'
    return trueUrl

这两个函数分别用来获取下载入口链接和对应的真实地址。

5.下载资源

def download(trueDownloadUrl, title, picUrl):
    suffix = 'exe'
    suffixList = ['.doc', '.ppt', '.zip', '.rar', '.pdf']
    for l in suffixList:
        if l in trueDownloadUrl:
            suffix = l
    fileName = title + str(suffix)
    picName = title + '.jpg'
    down = requests.get(trueDownloadUrl)
    with open(fileName,"wb") as f:
        f.write(down.content)
    f.close()
    picDown = requests.get(picUrl)
    with open(picName, "wb") as p:
        p.write(picDown.content)
    p.close()
    print '下载完成'
    return None

由于在下载链接中看不到文件格式,所以预设了几个可能的后缀名,通过判断语句来确定。

6.调取函数运行

def managerItem(itemUrlList):
    for j in range(0, len(itemUrlList)):
            print 'j=' + str(j)
            itemUrl = itemUrlList[j][1]
            picUrl = itemUrlList[j][2]
            title = itemUrlList[j][0].replace('/','').replace('*','')
            print title
            try:
                downloadUrl = getDownload(itemUrl)
                trueDownloadUrl = convertUrl(downloadUrl)
                download(trueDownloadUrl, title, picUrl)
            except Exception as e:
                print '报错'
                print e
                time.sleep(60)
                print '等待 60秒'
                errorItemUrl.append(itemUrlList[j])
            time.sleep(60)

def managerPage(pageUrlList, headers):
    for i in range(0, len(pageUrlList)):
        pageUrl = pageUrlList[i]
        try:
            itemUrlList = getItem(pageUrl, headers)
        except Exception as e:
            print 'item报错'
            print e 
            time.sleep(30)
            print '等待 30秒'
            errorPageUrl.append(pageUrlList[i])
            continue
        print 'Items in page ' + str(i+1) + ' obtained.'
        managerItem(itemUrlList)

调取上面的函数来实现爬取功能。若出现报错,则异常处理语句可保证脚本正常运行,同时自动等待一定时间。

7.主程序

#start
print 'Start'
initialUrl = r'https://www.twinkl.com.cn/resources/age-group-3-5-mandarin-chinese-china-resources/communication-and-language-age-group-3-5-mandarin-chinese-china-resources/writing-communication-and-language-age-group-3-5-mandarin-chinese-china-resources'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent': user_agent}
headers['cookie'] = 'XXXXX'
pageUrlList, pageNum = getUrl(initialUrl, headers)
print ' Page links obtained.'
errorPageUrl = []
errorItemUrl = []
managerPage(pageUrlList, headers)
if errorPageUrl:
    print '重新开始下载失败页面'
    managerPage(errorPageUrl, headers)
if errorItemUrl:
    print '重新开始下载失败项目'
    managerItem(errorItemUrl)
print 'All done!'

直接利用cookie模拟登陆。可重新开始下载报错的项目。

三、其他说明

  • 经测试,整个脚本基本可以稳定运行,但是爬取到第八页之后经常出现报错现象;有时还有网络请求错误。有想法的朋友可以交流下。
  • 这个脚本仅模拟手工操作过程,不会对服务器造成额外负担。
点滴分享. permalink.

本站采用Disqus评论系统,可能需要科学上网才能正常评论。