以视频爬取实例讲解Python爬虫神器Beautiful Soup用法
1.安装BeautifulSoup4
easy_install安装方式,easy_install需要提前安装
easy_installbeautifulsoup4
pip安装方式,pip也需要提前安装.此外PyPi中还有一个名字是BeautifulSoup的包,那是BeautifulSoup3的发布版本.在这里不建议安装.
pipinstallbeautifulsoup4
Debain或ubuntu安装方式
apt-getinstallPython-bs4
你也可以通过源码安装,下载BS4源码
Pythonsetup.pyinstall
2.小试牛刀
#coding=utf-8 ''' @通过BeautifulSoup下载百度贴吧图片 ''' importurllib frombs4importBeautifulSoup url='http://tieba.baidu.com/p/3537654215' #下载网页 html=urllib.urlopen(url) content=html.read() html.close() #使用BeautifulSoup匹配图片 html_soup=BeautifulSoup(content) #图片代码我们在[Python爬虫基础1--urllib](http://blog.xiaolud.com/2015/01/22/spider-1st/"Python爬虫基础1--urllib")里面已经分析过了 #相较通过正则表达式去匹配,BeautifulSoup提供了一个更简单灵活的方式 all_img_links=html_soup.findAll('img',class_='BDE_Image') #接下来就是老生常谈的下载图片 img_counter=1 forimg_linkinall_img_links: img_name='%s.jpg'%img_counter urllib.urlretrieve(img_link['src'],img_name) img_counter+=1
很简单,代码注释里面已经解释的很清楚了.BeautifulSoup提供了一个更简单灵活的方式,去分析网站源码,更快获取图片link.
3.爬取实例
3.1基本的抓取技术
在写一个爬虫脚本时,第一件事情就是手动观察要抓取的页面来确定数据如何定位。
首先,我们要看一看在http://pyvideo.org/category/50/pycon-us-2014上的PyCon大会视频列表。检查这个页面的HTML源代码我们发现视频列表的结果差不多是长这样的:
<divid="video-summary-content"> <divclass="video-summary"><!--firstvideo--> <divclass="thumbnail-data">...</div> <divclass="video-summary-data"> <div> <strong><ahref="#linktovideopage#">#title#</a></strong> </div> </div> </div> <divclass="video-summary"><!--secondvideo--> ... </div> ... </div>
那么第一个任务就是加载这个页面,然后抽取每个单独页面的链接,因为到YouTube视频的链接都在这些单独页面上。
使用requests来加载一个web页面是非常简单的:
importrequests response=requests.get('http://pyvideo.org/category/50/pycon-us-2014')
就是它!在这个函数返回后就能从response.text中获得这个页面的HTML。
下一个任务是抽取每一个单独视频页面的链接。通过BeautifulSoup使用CSS选择器语法就能完成它,如果你是客户端开发者的话你可能对这会很熟悉。
为了获得这些链接,我们要使用一个选择器,它能抓取在每一个id为video-summary-data的<div>中所有的<a>元素。由于每个视频都有几个<a>元素,我们将只保留那些URL以/video开头的<a>元素,这些就是唯一的单独视频页面。实现上述标准的CSS选择器是div.video-summary-dataa[href^=/video]。下面的代码片段通过BeautifulSoup使用这个选择器来获得指向视频页面的<a>元素:
importbs4 soup=bs4.BeautifulSoup(response.text) links=soup.select('div.video-summary-dataa[href^=/video]')
因为我们真正关心的是这个链接本身而不是包含它的<a>元素,我们可以使用列表解析来改善上述代码。
links=[a.attrs.get('href')forainsoup.select('div.video-summary-dataa[href^=/video]')]
现在,我们已经有了一个包含所有链接的数组,这些链接指向了每个单独页面。
下面这段脚本整理了目前我们提到的所有技术:
importrequests importbs4 root_url='http://pyvideo.org' index_url=root_url+'/category/50/pycon-us-2014' defget_video_page_urls(): response=requests.get(index_url) soup=bs4.BeautifulSoup(response.text) return[a.attrs.get('href')forainsoup.select('div.video-summary-dataa[href^=/video]')] print(get_video_page_urls())
如果你运行上面这段脚本你将会获得一个满是URL的数组。现在我们需要去解析每个URL以获得更多关于每场PyCon会议的信息。
3.2抓取相连页面
下一步是加载我们的URL数组中每一个页面。如果你想要看看这些页面长什么样的话,这儿是个样例:http://pyvideo.org/video/2668/writing-restful-web-services-with-flask。没错,那就是我,那是我会议中的一个!
从这些页面我们可以抓取到会议的标题,在页面的顶部能看到它。我们也可以从侧边栏获得演讲者的姓名和YouTube的链接,侧边栏在嵌入视频的右下方。获取这些元素的代码展示在下方:
defget_video_data(video_page_url): video_data={} response=requests.get(root_url+video_page_url) soup=bs4.BeautifulSoup(response.text) video_data['title']=soup.select('div#videoboxh3')[0].get_text() video_data['speakers']=[a.get_text()forainsoup.select('div#sidebara[href^=/speaker]')] video_data['youtube_url']=soup.select('div#sidebara[href^=http://www.youtube.com]')[0].get_text()
关于这个函数需要注意的一些事情:
从首页抓取的URL是相对路径,所以root_url需要加到前面。
大会标题是从id为videobox的<div>里的<h3>元素中获得的。注意[0]是必须的,因为调用select()返回的是一个数组,即使只有一个匹配。
演讲者的姓名和YouTube链接的获取方式与首页上的链接获取方式类似。
现在就剩下从每个视频的YouTube页面抓取观看数了。接着上面的函数写下去其实是非常简单的。同样,我们也可以抓取like数和dislike数。
defget_video_data(video_page_url): #... response=requests.get(video_data['youtube_url']) soup=bs4.BeautifulSoup(response.text) video_data['views']=int(re.sub('[^0-9]','', soup.select('.watch-view-count')[0].get_text().split()[0])) video_data['likes']=int(re.sub('[^0-9]','', soup.select('.likes-count')[0].get_text().split()[0])) video_data['dislikes']=int(re.sub('[^0-9]','', soup.select('.dislikes-count')[0].get_text().split()[0])) returnvideo_data
上述调用soup.select()函数,使用指定了id名字的选择器,采集到了视频的统计数据。但是元素的文本需要被处理一下才能变成数字。考虑观看数的例子,在YouTube上显示的是"1,344views"。用一个空格分开(split)数字和文本后,只有第一部分是有用的。由于数字里有逗号,可以用正则表达式过滤掉任何不是数字的字符。
为了完成爬虫,下面的函数调用了之前提到的所有代码:
defshow_video_stats(): video_page_urls=get_video_page_urls() forvideo_page_urlinvideo_page_urls: printget_video_data(video_page_url)
3.3并行处理
上面到目前为止的脚本工作地很好,但是有一百多个视频它就要跑个一会儿了。事实上我们没做什么工作,大部分时间都浪费在了下载页面上,在这段时间脚本时被阻塞的。如果脚本能同时跑多个下载任务,可能就会更高效了,是吗?
回顾当时写一篇使用Node.js的爬虫文章的时候,并发性是伴随JavaScript的异步特性自带来的。使用Python也能做到,不过需要显示地指定一下。像这个例子,我将开启一个拥有8个可并行化进程的进程池。代码出人意料的简洁:
frommultiprocessingimportPool defshow_video_stats(options): pool=Pool(8) video_page_urls=get_video_page_urls() results=pool.map(get_video_data,video_page_urls)
multiprocessing.Pool类开启了8个工作进程等待分配任务运行。为什么是8个?这是我电脑上核数的两倍。当时实验不同大小的进程池时,我发现这是最佳的大小。小于8个使脚本跑的太慢,多于8个也不会让它更快。
调用pool.map()类似于调用常规的map(),它将会对第二个参数指定的迭代变量中的每个元素调用一次第一个参数指定的函数。最大的不同是,它将发送这些给进程池所拥有的进程运行,所以在这个例子中八个任务将会并行运行。
节省下来的时间是相当大的。在我的电脑上,第一个版本的脚本用了75秒完成,然而进程池的版本做了同样的工作只用了16秒!
3.4完成爬虫脚本
我最终版本的爬虫脚本在获得数据后还做了更多的事情。
我添加了一个--sort命令行参数去指定一个排序标准,可以指定views,likes或者dislikes。脚本将会根据指定属性对结果数组进行递减排序。另一个参数,--max代表了要显示的结果数的个数,万一你只想看排名靠前的几条而已。最后,我还添加了一个--csv选项,为了可以轻松地将数据导到电子制表软件中,可以指定数据以CSV格式打印出来,而不是表对齐格式。
完整脚本显示在下方:
importargparse importre frommultiprocessingimportPool importrequests importbs4 root_url='http://pyvideo.org' index_url=root_url+'/category/50/pycon-us-2014' defget_video_page_urls(): response=requests.get(index_url) soup=bs4.BeautifulSoup(response.text) return[a.attrs.get('href')forainsoup.select('div.video-summary-dataa[href^=/video]')] defget_video_data(video_page_url): video_data={} response=requests.get(root_url+video_page_url) soup=bs4.BeautifulSoup(response.text) video_data['title']=soup.select('div#videoboxh3')[0].get_text() video_data['speakers']=[a.get_text()forainsoup.select('div#sidebara[href^=/speaker]')] video_data['youtube_url']=soup.select('div#sidebara[href^=http://www.youtube.com]')[0].get_text() response=requests.get(video_data['youtube_url']) soup=bs4.BeautifulSoup(response.text) video_data['views']=int(re.sub('[^0-9]','', soup.select('.watch-view-count')[0].get_text().split()[0])) video_data['likes']=int(re.sub('[^0-9]','', soup.select('.likes-count')[0].get_text().split()[0])) video_data['dislikes']=int(re.sub('[^0-9]','', soup.select('.dislikes-count')[0].get_text().split()[0])) returnvideo_data defparse_args(): parser=argparse.ArgumentParser(description='ShowPyCon2014videostatistics.') parser.add_argument('--sort',metavar='FIELD',choices=['views','likes','dislikes'], default='views', help='sortbythespecifiedfield.Optionsareviews,likesanddislikes.') parser.add_argument('--max',metavar='MAX',type=int,help='showthetopMAXentriesonly.') parser.add_argument('--csv',action='store_true',default=False, help='outputthedatainCSVformat.') parser.add_argument('--workers',type=int,default=8, help='numberofworkerstouse,8bydefault.') returnparser.parse_args() defshow_video_stats(options): pool=Pool(options.workers) video_page_urls=get_video_page_urls() results=sorted(pool.map(get_video_data,video_page_urls),key=lambdavideo:video[options.sort], reverse=True) max=options.max ifmaxisNoneormax>len(results): max=len(results) ifoptions.csv: print(u'"title","speakers","views","likes","dislikes"') else: print(u'Views+1-1Title(Speakers)') foriinrange(max): ifoptions.csv: print(u'"{0}","{1}",{2},{3},{4}'.format( results[i]['title'],','.join(results[i]['speakers']),results[i]['views'], results[i]['likes'],results[i]['dislikes'])) else: print(u'{0:5d}{1:3d}{2:3d}{3}({4})'.format( results[i]['views'],results[i]['likes'],results[i]['dislikes'],results[i]['title'], ','.join(results[i]['speakers']))) if__name__=='__main__': show_video_stats(parse_args())
下方输出的是在我写完代码时前25个观看数最多的会议:
(venv)$pythonpycon-scraper.py--sortviews--max25--workers8 Views+1-1Title(Speakers) 3002270Keynote-GuidoVanRossum(GuidoVanRossum) 2564210Computersciencefundamentalsforself-taughtprogrammers(JustinAbrahms) 2369170Ansible-Python-PoweredRadicallySimpleITAutomation(MichaelDehaan) 2165276AnalyzingRapLyricswithPython(JulieLavoie) 2158243ExploringMachineLearningwithScikit-learn(JakeVanderplas,OlivierGrisel) 2065130FastPython,SlowPython(AlexGaynor) 2024240GettingStartedwithDjango,acrashcourse(KennethLove) 1986470It'sDangeroustoGoAlone:BattlingtheInvisibleMonstersinTech(JuliePagano) 1843240DiscoveringPython(DavidBeazley) 1672220AllYourDucksInARow:DataStructuresintheStandardLibraryandBeyond(BrandonRhodes) 1558171Keynote-FernandoPérez(FernandoPérez) 144960DescriptorsandMetaclasses-UnderstandingandUsingPython'sMoreAdvancedFeatures(MikeMüller) 1402120FlaskbyExample(MiguelGrinberg) 134260PythonEpiphanies(StuartWilliams) 1219500to00111100withweb2py(G.CliffordWilliams) 1169180CheapHelicoptersInMyLivingRoom(NedJacksonLovely) 1146110IPythonindepth:highproductivityinteractiveandparallelpython(FernandoPerez) 1127502D/3DgraphicswithPythononmobileplatforms(NikoSkrypnik) 108180Generators:TheFinalFrontier(DavidBeazley) 1067120DesigningPoeticAPIs(ErikRose) 106460Keynote-JohnPerryBarlow(JohnPerryBarlow) 1029100WhatIsAsync,HowDoesItWork,AndWhenShouldIUseIt?(A.JesseJiryuDavis) 981110TheSorryStateofSSL(HynekSchlawack) 961122FarewellandWelcomeHome:PythoninTwoGenders(NaomiCeder) 95860GettingStartedTesting(NedBatchelder)