常言道:有朋自远方来不亦乐乎吗?有朋友来我们这里玩是一件很开心的事情,所以我们一定要努力做好地主带朋友来玩!那么问题来了,什么时候去哪里最合适,哪里玩的地方最多呢?今天教大家如何使用线程池爬取同一行程的景点信息和评论数据,并制作词云和数据可视化!!!带你了解各个城市的旅游景点信息。在开始爬取数据之前,我们先来了解一下线程。线程进程:进程是代码对数据集的操作,是系统进行资源分配和调度的基本单位。线程:是一个轻量级的进程,是程序执行的最小单位,是一个进程的执行路径。一个进程中至少有一个线程,一个进程中的多个线程共享进程的资源。线程生命周期在创建多线程之前,我们先来了解一下线程的生命周期,如下图所示:从图中可以看出,线程可以分为五个状态——新建、就绪、运行、阻塞、终止.首先,创建一个新线程并启动线程。线程进入就绪态后,处于就绪态的线程不会立即运行。只有在获得CPU资源后才会进入运行状态。进入运行态后,线程可能会丢失CPU资源或遇到休眠,io操作(读写等操作)线程进入就绪态或阻塞态。直到休眠,io操作结束,或者CPU资源重新获得,才会进入running状态,操作完成后进入termination状态。注意:新创建的线程系统需要分配资源,终止的线程系统需要回收资源,那么如何减去创建/终止线程的系统开销呢?这时候我们可以创建一个线程池来复用线程,可以减少系统开销。花费。在创建线程池之前,我们先来了解一下如何创建多线程。创建多线程创建多线程可以分为四个步骤:创建函数;创建线程;启动线程;等待结束;创建功能。f'https://www.cnblogs.com/#p{page}'forpageinrange(1,50)]defget_parse(url):response=requests.get(url)print(url,len(response.text))首先导入requests网络请求库,将我们所有要抓取的url保存在列表中,然后自定义函数get_parse发送网络请求,打印请求的url和响应的字符长度。创建线程我们在上一步中创建了爬虫函数,接下来我们将创建线程。具体代码如下:importthreading#multithreadingdefmulti_thread():threads=[]forurlinurls:threads.append(threading.Thread(target=get_parse,args=(url,)))首先我们导入threading模块,自定义multi_thread函数,然后创建一个空列表threads来存放线程任务,通过threading.Thread()方法创建线程。其中:target为运行函数;args是运行函数所需的参数。注意args中的参数要以元组的形式传入,然后通过.append()方法将线程添加到threads空列表中。启动线程线程已经创建,接下来要启动线程。启动线程非常简单。具体代码如下:forthreadinthreads:thread.start()首先我们通过for循环获取线程列表中的线程任务,通过.start()启动线程。等待结束启动线程之后,接下来就是等待线程结束。具体代码如下:forthreadinthreads:thread.join()和启动线程一样,先通过for循环获取threads列表中的线程任务,然后使用.join()方法等待要完成的线程。多线程已创建。接下来,我们将测试多线程的速度。具体代码如下:if__name__=='__main__':t1=time.time()multi_thread()t2=time.time()print(t2-t1)运行结果如下图所示:它只多线程爬取50个博客园网页需要1秒多,多线程发送的网络请求的URL是随机的。我们来测试一下单线程的运行时间。具体代码如下:if__name__=='__main__':t1=time.time()foriinurls:get_parse(i)t2=time.time()print(t2-t1)运行结果如图下图:单线程爬取50个博客园网页耗时9秒多,单线程发送网络请求的url是有序的。上面我们说了,新的线程系统需要分配资源,终止的线程系统需要回收资源。为了减少系统开销,我们可以创建一个线程池。线程池原理一个线程池由两部分组成,如下图所示:线程池:预先构建N个线程,这些线程会被复用;任务队列:当有新任务时,该任务会被放入任务队列中间。当任务队列中有任务时,线程池中的线程会从任务队列中取出任务并执行。任务执行完后,线程会去执行下一个任务,直到没有任务执行完,线程才会返回线程池等待任务。使用线程池来处理突发性大请求或需要大量线程完成的任务(处理时间短的任务)。好了,了解了线程池的原理之后,我们开始创建线程池。线程池的创建Python提供了ThreadPoolExecutor类来创建线程池。语法如下:ThreadPoolExecutor(max_workers=None,thread_name_prefix='',initializer=None,initargs=())其中:max_workers:最大线程数;thread_name_prefix:允许用户控制线程池创建的threading.Thread工作线程名称,方便调试;initializer:是一个可选的可调用对象,在每个工作线程开始时调用;initargs:传递给初始化器的元组参数。注意:在启动max_workers工作线程之前,空闲的工作线程也会被重用。ThreadPoolExecutor类中提供了map()和submit()函数,用于插入任务队列。其中:map()函数map()语法格式为:map(调用方法,参数队列)具体示例如下:importrequestsimportconcurrent.futuresimporttimeurls=[f'https://www.cnblogs.com/#p{page}'forpageinrange(1,50)]defget_parse(url):response=requests.get(url)returnresponse.textdefmap_pool():withconcurrent.futures.ThreadPoolExecutor(max_workers=20)作为池:shtml=pool.map(get_parse,urls)htmls=list(zip(urls,htmls))forurl,htmlinhtmls:print(url,len(html))if__name__=='__main__':t1=time.time()map_pool()t2=time.time()print(t2-t1)首先我们导入requests网络请求库和concurrent.futures模块,将所有url放到urls列表中,然后自定义get_parse()方法返回网络请求返回的数据,然后自定义map_pool()方法创建代理池,其中代理池最大max_workers为20,调用map()方法将网络请求任务放入任务队列,并结合重新将数据和URL转成元组,放入htmls列表中。运行结果如下图所示:可以发现map()函数返回的结果与传入参数的顺序是对应的。注意:当我们在自定义方法get_parse()中直接打印结果时,打印结果乱序。submit()函数submit()函数的语法格式如下:submit(调用方法,参数)具体例子如下:defsubmit_pool():withconcurrent.futures.ThreadPoolExecutor(max_workers=20)aspool:futures=[pool.submit(get_parse,url)forurlinurls]futures=zip(urls,futures)forurl,futureinfutures:print(url,len(future.result()))结果如图下图:注意:submit()函数的输出结果需要调用result()方法。好了,线程知识就到这里了,接下来开始我们的爬虫。登山前分析首先,我们进入同行程的景点网页,打开开发者工具,如下图:经过搜索,我们发现每个景点的基本信息(详情页URL,景点id等)存储在下图中的URL链接中,其URL链接为:https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page=2&kw=&pid=6&cid=80&cyid=0&sort=&isnow=0&spType=&lbtypes=&IsNJL=0&classify=0&grade=&dctrack=1%CB%871629537670551030%CB%8720%CB%873%CB%872557287248299209%CB%870&iid=0.6901326566387387后我们可以对url进行增删改查简化:https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page=1&pid=6&cid=80&cyid=0&isnow=0&IsNJL=0其中page是我们翻页的重要参数。打开网址链接,如下图所示:通过上面的网址链接,我们可以获取到很多景点的基本信息,随机打开一个景点的详情页并打开开发者模式,搜索后保存评论数据在如下图所示的URL链接中,其URL链接如下:https://www.ly.com/scenery/AjaxHelper/DianPingAjax.aspx?action=GetDianPingList&sid=12851&page=1&pageSize=10&labId=1&sort=0&iid=0.48901069375088其中:action,labId,iid,sort为常量,sid为景区id,page控制翻页,pageSize为每页获取的数据量。上一步我们知道了景区id的存放位置,所以构造评论数据的URL就很简单了。实战演练这次我们的爬虫步骤是:获取景点基本信息,获取评论数据,创建MySQL数据库,保存数据,创建线程池数据分析,获取景点基本信息。如下:defget_parse(url):response=requests.get(url,headers=headers)Xpath=parsel.Selector(response.text)data=Xpath.xpath('/html/body/div')foriindata:scenery_data={'title':i.xpath('./div/div[1]/div[1]/dl/dt/a/text()').extract_first(),'sid':i.xpath('//div[@class="list_l"]/div/@sid').extract_first(),'Grade':i.xpath('./div/div[1]/div[1]/dl/dd[1]/span/text()').extract_first(),'Detailed_address':i.xpath('./div/div[1]/div[1]/dl/dd[2]/p/text()').extract_first().replace('Address:',''),'characteristic':i.xpath('./div/div[1]/div[1]/dl/dd[3]/p/text()').extract_first(),'价格':i.xpath('./div/div[1]/div[2]/div[1]/span/b/text()').extract_first(),'place':i.xpath('./div/div[1]/div[1]/dl/dd[2]/p/text()').extract_first().replace('Address:','')[6:8]}首先自定义方法get_parse()发送网络请求,使用parsel.Selector()方法解析响应的文本数据,然后通过xpath获取数据获取点评数据获取到景区基本信息后,通过景区基本信息中的sid构建点评信息的URL链接。主要代码如下:defget_data(Scenery_data):foriinrange(1,3):link=f'https://www.ly.com/scenery/AjaxHelper/DianPingAjax.aspx?action=GetDianPingList&sid={Scenery_data["sid"]}&page={i}&pageSize=100&labId=1&sort=0&iid=0.20105777381446832'response=request.get(link,headers=headers)Json=response.json()commtent_detailed=Json.get('dpList')#有有评论如果commtent_detailed!=none:none:foriinCommtent_detailed:comment_information={'dptitle':saceery_data['title''''dpcontent','dpcontent':i.get('dpcontent')('DPDATE')[5:7],'Lineaccess':I.get('LineaCcess')}#}#没有评论数据elifcommtent_detailed==None:Comment_information={'dptitle':Scenery_data['title'],'dpContent':'无评论','dpDate':'NoComments','lineAccess':'NoComments'}首先自定义方法get_data()传入刚刚获取的景点基本信息data,然后使用基本信息的sid景点构造评论数据URL链接,在构造评论数据URL时,需要设置pageSize和page两个变量,获取多条评论和翻页。构建好URL链接后,发送网络请求。需要注意的是有些景点是没有注释的,需要通过if语句来设置。创建MySQL数据库这次我们将数据存储在MySQL数据库中。由于数据较多,我们将数据分为两张数据表,一张是景区基本信息表,一张是景区点评数据表。主要代码如下显示:#createdatabasedefcreate_db():db=pymysql.connect(host=host,user=user,passwd=passwd,port=port)cursor=db.cursor()sql='createdatabaseifnotexistscommtentdefaultcharactersetutf8'cursor.execute(sql)db.close()create_table()#创建景点信息数据表defcreate_table():db=pymysql.connect(host=host,user=user,passwd=passwd,port=port,db='commtent')cursor=db.cursor()sql='createtableifnotexistsScenic_spot_data(titlevarchar(255)notnull,linkvarchar(255)notnull,Gradevarchar(255)notnull,Detailed_addressvarchar(255)notnull,characteristicvarchar(255)notnull,priceintnotnull,placevarchar(255)notnull)'cursor.execute(sql)db.close()首先我们调用pymysql。connect()方法连接数据库,通过.cursor()获取游标,然后通过.execute()方法执行单条sql语句,执行成功后返回受影响的行数,然后关闭数据库连接,最后调用自定义方法create_table()创建景点信息数据面。这里我们只给出创建景点信息数据表的代码,因为数据表的创建只是sql语句略有不同,其他都是一样的。可以参考这段代码创建各个景点的点评数据表。保存数据创建数据库和数据表后,下一步就是保存数据。主要代码如下:#保存景区数据到景区数据表defsaving_scenery_data(srr):db=pymysql.connect(host=host,user=user,password=passwd,port=port,db='commtent')cursor=db.cursor()sql='insertintoScenic_spot_data(title,link,Grade,Detailed_address,characteristic,price,place)values(%s,%s,%s,%s,%s,%s,%s)'try:cursor.execute(sql,srr)db.commit()except:db.rollback()db.close()首先我们调用pymysql的.connect()方法来连接数据库,通过.cursor()获取游标,然后通过.execute()方法执行单条SQL语句。执行成功后,返回受影响的行数。使用try-except语句。当保存数据不成功时,会调用rollback()方法,它会撤消当前事务中所做的所有更改,并释放此连接对象当前持有的任何数据库锁。注:srr为传入的景点信息数据。创建一个线程池。单线程爬虫已经写好了。接下来,我们将创建一个函数来创建我们的线程池,使单线程爬虫成为多线程。主要代码如下:urls=[f'https://www.ly.com/scenery/NewSearchList.aspx?&action=getlist&page={i}&pid=6&cid=80&cyid=0&isnow=0&IsNJL=0'foriinrange(1,6)]defmulti_thread():并发。futures.ThreadPoolExecutor(max_workers=8)aspool:h=pool.map(get_parse,urls)if__name__=='__main__':create_db()multi_thread()创建线程池的代码很简单,就一个with语句并调用map()方法运行结果如下图所示:好了,数据已经获取到,接下来进行数据分析。数据可视化首先我们来分析一下当月每个景点的参观人数,这样我们就不用担心去错时间了。我们发现,10月、2月和1月到访广州长隆飞禽公园的人数占比最大。分析完月份,再来看看评论情况:可以发现大部分评论都是好评。可以说:去长隆鸟类乐园,人人有口皆碑。看完评论,评论的内容是什么?好了,获取景点信息和评论,制作词云和数据可视化就这些了。以上就是本次分享的全部内容。觉得文章还不错的话,请关注公众号:Python编程学习圈,每日干货分享,发送“J”还能领取大量学习资料。或者去编程学习网了解更多编程技术知识。
