python学习——一个用Python写的小作业

上周三的时候去面试了一家有意思的公司,也没什么正式的面试,也就是和团队的boss聊了聊,因为是一些完全做过的东西,所以boss只提了一个需求,让我回去花点时间解决。因为确实是一个问题的不大的事情,所以就将它当做了一个小作业。需求是:搭建一个简单的原型系统,在自己的电脑上,在一天之内抓取10w条(url ,title)的数据对,存在自己本地的数据库里,然后写一个查询的页面。

如果有做过的前辈/同学/朋友,应该觉得这是一个简单的事情。因为也确实如此,即使没有做过这方面的东西,用python的话,也是很容易上手,慢慢可以搞定的。

以下的内容或存在用词用语不当的问题,由于本人的专业水平有限,对于造成的不适请见谅。

首先是写爬虫的部分,用python的urllib来做的话,轻而易举。http://blog.csdn.net/tingyuanss/article/details/7588700,这一篇博文中,博主在“描述如何实现”的部分,写的很清晰易懂,基本上看了后,即使完全没概念的人也能大致理解网络爬虫是干嘛的。还有一篇 http://blog.chinaunix.net/uid-11538492-id-2869940.html,里面有一个有意思的设计,就是每个爬虫都有自己的最大抓取量,抓取到阈值时,爬虫线程就主动停下来,准备退出,虽然我在写自己的爬虫部分时没有实现这一点,但我仍然觉得这是一个有意思的设计。OK,回到这里的需求,抓取(url , title)的数据对。所以我需要获得10w份网页源码,解析获得url(s) 和title。最直观的想法是多线程,来提高处理效率。但怎样利用多线程好呢?一开始我在我的虚拟机里进行了简单的尝试,通过观察,我发现时间上的消耗主要发生在两个部分:抓取(网页源码)和读(网页源码),这两个步骤对应的操作是 urllib.urlopen(...) 和 read()。前者主要消耗网卡资源,后者更多的消耗CPU资源(这里我忽略了对内存资源的考虑) ,并且如果只跑这样一个程序,相对于有些时段缓慢的网速,CPU还是有不少闲置的。所以我决定将这两个操作分别写成两类线程中来做,而不是完全由一种线程来完成,这样基本可以保证urllib.urlopen(..)可以更高频的被调用,能对网卡资源有更多的利用。并且利用Queue.Queue,可以写出三个队列,分别用来存储url,response,和(url, title)。专门负责爬取的线程,将存储url的队列作为主要的数据输入源,通过从这个队列上获取url,然后调用urlopen,将获取的response添加到response队列上,然后再获取下一条url...;专门负责读response的线程,从response队列上,获取response,读取后,解析出需要的信息,分别添加到url队列上和(url,title)队列上。所以基于生产-消费者模型,爬虫部分整个在逻辑流程上呈现一种爬取线程,队列,读线程,队列组成的环。

以下的代码只作为一种基础的参考,其中仍存在一些问题。

import threading, urllib, Queue, socket, time

# the crawler just do crawl, they dont parse that
class Crawler(threading.Thread) :
	def __init__(self,in_queue,out_queue,ban_queue,fin) :
		threading.Thread.__init__(self)
		self._in_queue = in_queue # the clear url queue
		self._out_queue = out_queue # the (url_response, url) queue
		self._ban_queue = ban_queue # the timed out url
		self._fin = fin # fin is Event set by other thread

	def run(self) :
		while not self._fin.isSet() :
			try :
				url = self._in_queue.get()
				if url in self._ban_queue.queue :
					continue
				resp = urllib.urlopen(url)
				self._out_queue.put((resp,url))
				self._in_queue.task_done()
			except IOError :
				if not url in self._ban_queue.queue :
					self._ban_queue.put(url)
				self._in_queue.task_done()
			except Exception, e :
				print 'Exception happened in Crawler...',e
				break
		self._in_queue.task_done()

	def join(self):
		threading.Thread.join(self)

# the parser just do parse the response get from urlopen
class Parser(threading.Thread) :
	def __init__(self,in_queue,out_queue,data_queue,fin) :
		threading.Thread.__init__(self)
		self._in_queue = in_queue # the (url_response,url) queue
		self._out_queue = out_queue # the clear url queue
		self._data_queue = data_queue # the (url, title) queue
		self._fin = fin
		self._url_record = []

	def run(self) :
		while not self._fin.isSet() :
			try :
				resp, url = self._in_queue.get()
				if url in self._url_record :
					continue
				self._url_record.append(url)
				contant = resp.read()
				title = contant.split('</title>',1)[0].split('<title>',1)[-1]
				urls = []
				_urls = contant.split('href="')[1:]
				for i in _urls :
					_url = i.split('"')[0]
					if _url.startswith('.ico') or _url.startswith('.css') or _url.__contains__('linkid') :
						continue
					elif _url.startswith('http') :
						urls.append(_url)
				for i in urls :
					if i not in self._url_record :
						self._out_queue.put(i)
				self._data_queue.put((url,title,charset))
				self._in_queue.task_done()
			except Exception, e :
				print 'Exception happened in parser...',e
				if not self._fin.isSet() :
					continue
				else :
					break

	def join(self) :
		threading.Thread.join(self)

def crawl() :
	# init
	socket.setdefaulttimeout(8)
	url_list = Queue.Queue()
	resp_list = Queue.Queue()
	data_list = Queue.Queue()
	ban_list = Queue.Queue()
	finish = threading.Event()
	crawler0 = Crawler(url_list,resp_list,ban_list,finish)
	crawler1 = Crawler(url_list,resp_list,ban_list,finish)
	crawler2 = Crawler(url_list,resp_list,ban_list,finish)
	parser0 = Parser(resp_list,url_list,data_list,finish)
	parser1 = Parser(resp_list,url_list,data_list,finish)
	threads = [crawler0, crawler1, crawler2, parser0, parser1]
	init_url = ['http://www.baidu.com','http://www.sina.com.cn','http://www.hao123.com','http://www.qq.com']
	total_num = 100000
	for i in init_url :
		url_list.put(i)
	# start threads
	for i in threads :
		i.start()
	count = 0
	while data_list.qsize() < total_num :
		time.sleep(10)
		count += 1
		for i in threads :
			if not i.isAlive() :
				break # early terminate
		print 'data(%d/%d) done...time(%d * 10s)' % (data_list.qsize(), total_num, count)
	finish.set()
	for i in threads :
		i.join()

if __name__=='__main__' :
	crawl()

可以添加一些打印消息,或者使用logging模块,将一些信息写到log文件里方便检查,不过作为基础,上面的代码应该能说明这个小作业的一种思路了。上面这部分代码,我在我的本(联想的Y470)上,从晚上0点开始,跑了约8h10min,利用学校夜间良好的网络状况,抓取了10w条数据。上面的代码存在这样一些问题:对获得response的解析的部分是值得怀疑的。我确实对前端编程不太了解,并且正则表达式的使用确实应该多加练习,所以我只好通过观察一些页面的网页源码,然后利用上面所示的分片的方法来做了,这部分实在需要怀疑。其次,就是这里的线程数了,Crawler和Parser的比例,我做的测试次数有限,所以无法判断怎样的一个C和P的比例是合适的。


之后就是数据入库了。我被推荐使用mongodb,为了方便,我安装并简单学习了pymongo。除了"$set", "$push"等等这些修改器我没打算进一步细究,pymongo模块真的是简单好用好上手,真心感谢这些了不起的贡献者。其他的就不说了,这里我遇到的主要问题有:首先是url中的'.',pymongo下document的键值对中key是不能含有点号的,即'.' ;其次是编码问题了,上面爬虫部分隐藏的另一个大问题就是没有获取charset,因此获取的title值存在编码问题,我对编码没有了解,但显然pymongo下document中的键值对是unicode编码的,并且当我写入utf-8编码的中文字符串时也没有什么问题,其他的编码问题,我还没有进行尝试,因为翻看的一些文章,已经让我对python2.x的编码问题感到头晕了(据说python3的编码问题得到了好转,不知道具体情况时怎么样的)。点号的问题很好解决,可以str.replace来解决,用类似“ /#/#/ “这样奇怪的字符组合来代替,我想第二次replace回去的时候发生误replace的概率应该比较小,并且在这里学习一下合法的url编码也是值得的 http://www.cnblogs.com/leaven/archive/2012/07/12/2588746.html


最后就是写一个查询的web页面了。用web.py的话,即使没有经验也能感到很好上手,只是对于 templates/xxx.html,如果没有写过HTML方面的东西,还需要投入点时间。我没有这方面的经验,并且也耐性也差,所以当想到我可能需要动态的根据查询结果在返回页面上显示10条,或者20条记录时,我发现几个简单的例子上内容根本无法满足我的需求,于是我打算看看web.py的template.py的代码,希望能得到帮助。

按照 Class Render --> def __getattr__ --> _load_template --> return Template(...)的顺序,我发现web.py给出的使用模板的例子 render = web.template.render('templates/') ,其实是可以这样使用的:你完全可以打开一个.html文件,在文件流的适当的位置加入适当的html语句,然后将这个流作为参数,交由web.template.Template(类)来创建一个对象,然后在需要的地方,通过在这个对象后面加括号的方式调用它,这样你就在某种意义上,拥有了动态的页面,而不拘泥于编写.html文件了。这也是目前我在使用web.py的过程中发现的值得一说的有趣的东西。


以上,就是所谓的一个python小作业,希望能对和我一样没有前端经验的同学有所裨益。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值