Python 多线程抓取网页

From: http://www.cnblogs.com/coser/archive/2012/03/16/2402389.html

 

最近,一直在做网络爬虫相关的东西。 看了一下开源C++写的larbin爬虫,仔细阅读了里面的设计思想和一些关键技术的实现。

1、larbin的URL去重用的很高效的bloom filter算法; 
2、DNS处理,使用的adns异步的开源组件; 
3、对于url队列的处理,则是用部分缓存到内存,部分写入文件的策略。 
4、larbin对文件的相关操作做了很多工作 
5、在larbin里有连接池,通过创建套接字,向目标站点发送HTTP协议中GET方法,获取内容,再解析header之类的东西 
6、大量描述字,通过poll方法进行I/O复用,很高效 
7、larbin可配置性很强 
8、作者所使用的大量数据结构都是自己从最底层写起的,基本没用STL之类的东西 
...... 
还有很多,以后有时间在好好写篇文章,总结下。

   这两天,用python写了个多线程下载页面的程序,对于I/O密集的应用而言,多线程显然是个很好的解决方案。刚刚写过的线程池,也正好可以利用上了。其实用python爬取页面非常简单,有个urllib2的模块,使用起来很方便,基本两三行代码就可以搞定。虽然使用第三方模块,可以很方便的解决问题,但是对个人的技术积累而言没有什么好处,因为关键的算法都是别人实现的,而不是你自己实现的,很多细节的东西,你根本就无法了解。 我们做技术的,不能一味的只是用别人写好的模块或是api,要自己动手实现,才能让自己学习得更多。

  我决定从socket写起,也是去封装GET协议,解析header,而且还可以把DNS的解析过程单独处理,例如DNS缓存一下,所以这样自己写的话,可控性更强,更有利于扩展。对于timeout的处理,我用的全局的5秒钟的超时处理,对于重定位(301or302)的处理是,最多重定位3次,因为之前测试过程中,发现很多站点的重定位又定位到自己,这样就无限循环了,所以设置了上限。具体原理,比较简单,直接看代码就好了。

   自己写完之后,与urllib2进行了下性能对比,自己写的效率还是比较高的,而且urllib2的错误率稍高一些,不知道为什么。网上有人说urllib2在多线程背景下有些小问题,具体我也不是特别清楚。

先贴代码:

fetchPage.py  使用Http协议的Get方法,进行页面下载,并存储为文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
'''
Created on 2012-3-13
Get Page using GET method
Default using HTTP Protocol , http port 80
@author: xiaojay
'''
importsocket
importstatistics
importdatetime
importthreading
socket.setdefaulttimeout(statistics.timeout)
classError404(Exception):
    '''Can not find the page.'''
    pass
classErrorOther(Exception):
    '''Some other exception'''
    def__init__(self,code):
        #print 'Code :',code
        pass
classErrorTryTooManyTimes(Exception):
    '''try too many times'''
    pass
defdownPage(hostname ,filename , trytimes=0):
    try:
        #To avoid too many tries .Try times can not be more than max_try_times
        iftrytimes >=statistics.max_try_times :
            raiseErrorTryTooManyTimes
    exceptErrorTryTooManyTimes :
        returnstatistics.RESULTTRYTOOMANY,hostname+filename
    try:
        s =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        #DNS cache
        ifstatistics.DNSCache.has_key(hostname):
            addr =statistics.DNSCache[hostname]
        else:
            addr =socket.gethostbyname(hostname)
            statistics.DNSCache[hostname] =addr
        #connect to http server ,default port 80
        s.connect((addr,80))
        msg  ='GET '+filename+' HTTP/1.0\r\n'
        msg +='Host: '+hostname+'\r\n'
        msg +='User-Agent:xiaojay\r\n\r\n'
        code =''
        f =None
        s.sendall(msg)
        first =True
        whileTrue:
            msg =s.recv(40960)
            ifnotlen(msg):
                iff!=None:
                    f.flush()
                    f.close()
                break
            # Head information must be in the first recv buffer
            iffirst:
                first =False               
                headpos =msg.index("\r\n\r\n")
                code,other =dealwithHead(msg[:headpos])
                ifcode=='200':
                    #statistics.fetched_url += 1
                    f =open('pages/'+str(abs(hash(hostname+filename))),'w')
                    f.writelines(msg[headpos+4:])
                elifcode=='301'orcode=='302':
                    #if code is 301 or 302 , try down again using redirect location
                    ifother.startswith("http") :               
                        hname, fname =parse(other)
                        downPage(hname,fname,trytimes+1)#try again
                    else:
                        downPage(hostname,other,trytimes+1)
                elifcode=='404':
                    raiseError404
                else:
                    raiseErrorOther(code)
            else:
                iff!=None:f.writelines(msg)
        s.shutdown(socket.SHUT_RDWR)
        s.close()
        returnstatistics.RESULTFETCHED,hostname+filename
    exceptError404 :
        returnstatistics.RESULTCANNOTFIND,hostname+filename
    exceptErrorOther:
        returnstatistics.RESULTOTHER,hostname+filename
    exceptsocket.timeout:
        returnstatistics.RESULTTIMEOUT,hostname+filename
    exceptException, e:
        returnstatistics.RESULTOTHER,hostname+filename
defdealwithHead(head):
    '''deal with HTTP HEAD'''
    lines =head.splitlines()
    fstline =lines[0]
    code =fstline.split()[1]
    ifcode =='404': return(code,None)
    ifcode =='200': return(code,None)
    ifcode =='301'orcode =='302':
        forline inlines[1:]:
            p =line.index(':')
            key =line[:p]
            ifkey=='Location':
                return(code,line[p+2:])
    return(code,None)
     
defparse(url):
    '''Parse a url to hostname+filename'''
    try:
        u =url.strip().strip('\n').strip('\r').strip('\t')
        ifu.startswith('http://') :
            u =u[7:]
        elifu.startswith('https://'):
            u =u[8:]
        ifu.find(':80')>0:
            p =u.index(':80')
            p2 =p +3
        else:
            ifu.find('/')>0:
                p =u.index('/')
                p2 =p
            else:
                p =len(u)
                p2 =-1
        hostname =u[:p]
        ifp2>0:
            filename =u[p2:]
        else: filename ='/'
        returnhostname, filename
    exceptException ,e:
        print"Parse wrong : ", url
        printe
defPrintDNSCache():
    '''print DNS dict'''
    n =1
    forhostname instatistics.DNSCache.keys():
        printn,'\t',hostname, '\t',statistics.DNSCache[hostname]
        n+=1
defdealwithResult(res,url):
    '''Deal with the result of downPage'''
    statistics.total_url+=1
    ifres==statistics.RESULTFETCHED :
        statistics.fetched_url+=1
        printstatistics.total_url , '\t fetched :', url
    ifres==statistics.RESULTCANNOTFIND :
        statistics.failed_url+=1
        print"Error 404 at : ", url
    ifres==statistics.RESULTOTHER :
        statistics.other_url +=1
        print"Error Undefined at : ", url
    ifres==statistics.RESULTTIMEOUT :
        statistics.timeout_url +=1
        print"Timeout ",url
    ifres==statistics.RESULTTRYTOOMANY:
        statistics.trytoomany_url+=1
        printe ,"Try too many times at", url
if__name__=='__main__':   
    print 'Get Page using GET method'

下面,我将利用上一篇的线程池作为辅助,实现多线程下的并行爬取,并用上面自己写的下载页面的方法和urllib2进行一下性能对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
'''
Created on 2012-3-16
@author: xiaojay
'''
importfetchPage
importthreadpool
importdatetime
importstatistics
importurllib2
'''one thread'''
defusingOneThread(limit):
    urlset =open("input.txt","r")
    start =datetime.datetime.now()
    foru inurlset:
        iflimit <=0: break
        limit-=1
        hostname , filename =parse(u)
        res=fetchPage.downPage(hostname,filename,0)
        fetchPage.dealwithResult(res)
    end =datetime.datetime.now()
    print"Start at :\t", start
    print"End at :\t", end
    print"Total Cost :\t", end -start
    print'Total fetched :', statistics.fetched_url
     
'''threadpoll and GET method'''
defcallbackfunc(request,result):
    fetchPage.dealwithResult(result[0],result[1])
defusingThreadpool(limit,num_thread):
    urlset =open("input.txt","r")
    start =datetime.datetime.now()
    main =threadpool.ThreadPool(num_thread)
    forurl inurlset :
        try:
            hostname , filename =fetchPage.parse(url)
            req =threadpool.WorkRequest(fetchPage.downPage,args=[hostname,filename],kwds={},callback=callbackfunc)
            main.putRequest(req)
        exceptException:
            printException.message       
    whileTrue:
        try:
            main.poll()
            ifstatistics.total_url >=limit : break
        exceptthreadpool.NoResultsPending:
            print"no pending results"
            break
        exceptException ,e:
            printe
    end =datetime.datetime.now()
    print"Start at :\t", start   
    print"End at :\t", end
    print"Total Cost :\t", end -start
    print'Total url :',statistics.total_url
    print'Total fetched :', statistics.fetched_url
    print'Lost url :', statistics.total_url -statistics.fetched_url
    print'Error 404 :',statistics.failed_url
    print'Error timeout :',statistics.timeout_url
    print'Error Try too many times ',statistics.trytoomany_url
    print'Error Other faults ',statistics.other_url
    main.stop()
'''threadpool and urllib2 '''
defdownPageUsingUrlib2(url):
    try:
        req =urllib2.Request(url)
        fd =urllib2.urlopen(req)
        f =open("pages3/"+str(abs(hash(url))),'w')
        f.write(fd.read())
        f.flush()
        f.close()
        returnurl ,'success'
    exceptException:
        returnurl , None
     
defwriteFile(request,result):
    statistics.total_url +=1
    ifresult[1]!=None:
        statistics.fetched_url +=1
        printstatistics.total_url,'\tfetched :', result[0],
    else:
        statistics.failed_url +=1
        printstatistics.total_url,'\tLost :',result[0],
defusingThreadpoolUrllib2(limit,num_thread):
    urlset =open("input.txt","r")
    start =datetime.datetime.now()  
    main =threadpool.ThreadPool(num_thread)   
     
    forurl inurlset :
        try:
            req =threadpool.WorkRequest(downPageUsingUrlib2,args=[url],kwds={},callback=writeFile)
            main.putRequest(req)
        exceptException ,e:
            printe
         
    whileTrue:
        try:
            main.poll()
            ifstatistics.total_url  >=limit : break
        exceptthreadpool.NoResultsPending:
            print"no pending results"
            break
        exceptException ,e:
            printe
    end =datetime.datetime.now()   
    print"Start at :\t", start
    print"End at :\t", end
    print"Total Cost :\t", end -start
    print'Total url :',statistics.total_url
    print'Total fetched :', statistics.fetched_url
    print'Lost url :', statistics.total_url -statistics.fetched_url
    main.stop()
if__name__ =='__main__':
    '''too slow'''
    #usingOneThread(100)
    '''use Get method'''
    #usingThreadpool(3000,50)
    '''use urllib2'''
    usingThreadpoolUrllib2(3000,50)

 

实验分析:

实验数据:larbin抓取下来的3000条url,经过Mercator队列模型(我用c++实现的,以后有机会发个blog)处理后的url集合,具有随机和代表性。使用50个线程的线程池。 
实验环境:ubuntu10.04,网络较好,python2.6 
存储:小文件,每个页面,一个文件进行存储 
PS:由于学校上网是按流量收费的,做网络爬虫,灰常费流量啊!!!过几天,可能会做个大规模url下载的实验,用个几十万的url试试。

实验结果:

使用urllib2 ,usingThreadpoolUrllib2(3000,50)

Start at :    2012-03-16 22:18:20.956054 
End at :    2012-03-16 22:22:15.203018 
Total Cost :    0:03:54.246964 
Total url : 3001 
Total fetched : 2442 
Lost url : 559 
下载页面的物理存储大小:84088kb

使用自己的getPageUsingGet ,usingThreadpool(3000,50)

Start at :    2012-03-16 22:23:40.206730 
End at :    2012-03-16 22:26:26.843563 
Total Cost :    0:02:46.636833 
Total url : 3002 
Total fetched : 2484 
Lost url : 518 
Error 404 : 94 
Error timeout : 312 
Error Try too many times  0 
Error Other faults  112 
下载页面的物理存储大小:87168kb

小结: 自己写的下载页面程序,效率还是很不错的,而且丢失的页面也较少。但其实自己考虑一下,还是有很多地方可以优化的,比如文件过于分散,过多的小文件创建和释放定会产生不小的性能开销,而且程序里用的是hash命名,也会产生很多的计算,如果有好的策略,其实这些开销都是可以省略的。另外DNS,也可以不使用python自带的DNS解析,因为默认的DNS解析都是同步的操作,而DNS解析一般比较耗时,可以采取多线程的异步的方式进行,再加以适当的DNS缓存很大程度上可以提高效率。不仅如此,在实际的页面抓取过程中,会有大量的url ,不可能一次性把它们存入内存,而应该按照一定的策略或是算法进行合理的分配。 总之,采集页面要做的东西以及可以优化的东西,还有很多很多。

附件下载:程序代码(水平有限,仅供参考)

略改进版:minicrawler(里面的beautifulSoup需要改成3.x版本的)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/403774.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决vue的滚动条监听事件无效 解决vue的滚动条scrollTop距离总是为0无效问题

话不多说 直接上代码&#xff08;方法可以直接复制拿去&#xff0c; html部分需要改成你的元素的ref和点击回到顶部的方法名称&#xff09; html <section ref"scrollbox" class"inner-body"><div>这里放了很多内容 出现了滚动条</div&g…

动态规划练习 13

题目&#xff1a;Longest Ordered Subsequence (POJ 2533) 链接&#xff1a;http://acm.pku.edu.cn/JudgeOnline/problem?id2533 #include <iostream> #include <vector> using namespace std; int LIS(const vector<int> &data) { vector<int> n…

奔跑吧Linux内核初识

断更新博客有一段时间了。入职两年了一家创业公司&#xff0c;那是真心的累&#xff0c;当然了获得了技术上很大的提升。搞了两年的vr产品&#xff0c;唯一遗憾的是&#xff0c;平台是ST单片机&#xff0c;远离了系统级别的知识。回看刚出校园时的三年计划&#xff0c;和第一年…

装载问题

1、回溯法 (1)描述:回溯法是一种选优搜索法&#xff0c;按选优条件向前搜索&#xff0c;以达到目标。但当探索到某一步时&#xff0c;发现原先选择并不优或达不到目标&#xff0c;就退回一步重新选择&#xff0c;这种走不通就退回再走的技术为回溯法。 (2)原理: 回溯法在问题的…

android开发(13) 尝试在流布局中移动控件

我们常用的linearlayout,等都属于流布局&#xff0c;在流布局中如何移动控件呢&#xff1f; 我决定做个尝试。虽然可以使用绝对布局&#xff0c;但我不倾向使用这个布局。那么看看我的方式吧。 记得margin这个属性吗&#xff0c;我们就用来它来控制控件的位置&#xff0c;改动它…

阴影 border: 0 0 0 1px #4caaff;

阴影 border: 0 0 0 1px #4caaff;

第一章 处理器体系结构

1.请简述精简指令集RISC和复杂指令集CISC的区别 2.请简述数值 0x123456789 在大小端字节序处理器的存储器中的存储方式 3.请简述在你所熟悉的处理器&#xff08;比如双核Cortex-A9&#xff09;中一条存储读写指令的执行全过程 4.请简述内存屏障&#xff08;memory barrier&am…

Linux双网卡绑定实现

概述&#xff1a;通过网卡绑定&#xff0c;处理网卡单点故障&#xff0c;实验的操作系统是Redhat Linux Enterprise 5.3.绑定的前提条件&#xff1a;芯片组型号相同&#xff0c;而且网卡应该具备自己独立的BIOS芯片。网卡绑定时有四种模式&#xff0c;其中常用的是模式0和模式1…

给element的select添加复选框

需求&#xff1a;要求给select多选的时候&#xff0c;给下拉框前加上复选框样式 element select原样式 需要更改后的样式 html <el-selectv-model"searchObj.knowledgeIds"class"select-box"filterablemultiplecollapse-tagsstyle"margin-left…

TCP选项:TCP_NODELAY和TCP_CORK

From: http://blog.163.com/zhangjie_0303/blog/static/990827062012718316231/ Nagle算法 TCP_NODELAY和TCP_CORK Nagle算法 根据创建者John Nagle命名。该算法用于对缓冲区内的一定数量的消息进行自动连接。该处理过程 (称为Nagling)&#xff0c;通过减少必须发送的封包的…

loop 伪设备 挂在文件系统

dd if/dev/zero of./test.img bs1M count512 losetip /dev/loop1 test.img mkfs.ext3 -L ingben /dev/loop1 dumpe2fs /dev/loop1 tune2fs -l /dev/loo1 mount /dev/loop1 /mnt hexdump -C /dev/loop1 > /dumptext.log1

mysql导入导出数据

用以下命令&#xff0c;导入导出Mysql几百M&#xff0c;几G的数据库都没有问题。注意“--default-character-setutf8”是设置编码的。 1&#xff0c;数据库备份命令&#xff1a; mysqldump -h localhost -u root -p --default-character-setutf8 dbname >backup.sq…

修改复选框样式

//默认input[type"checkbox"] {margin-top: 7px;cursor: pointer;position: relative;width: 14px;height: 14px;font-size: 14px;margin-right: 8px;background-color:#fff;}//选中后修改input[type"checkbox"]::after {position: absolute;top: 0;//修改…

java jni ubuntu 环境搭建时遇到的坑

1&#xff1a;版本不一致遇到的坑 javah的版本需要同javac的版本一致。如果版本的问题搞不定&#xff0c;直接用andorid source build之后的环境即可 2&#xff1a;javah使用遇到的坑 jni中字段描述符可以使用javah生成 javah -jni -classpath . JNIdemo 其中 -classpath…

linux 内核配置过程中遇到的问题

大家都知道在修改内核需要两步 配置和编译 在配置过程中 用到的命令 make config、make menuconfig、make xconfig 前两个是文本界面 最后一个是图形界面 不建议用最后一个 因为占用的资源太多 有点卡 但如果你的硬件配置极高 我也不否定 你来用 转入正题 我在配置时 make menu…

c如何返回数组给java

jintArray c_hello(JNIEnv *env, jobject cls, jintArray arr) { jint a[4]{12,13,14,15}; jintArray arry; arry (*env)->NewIntArray(env,4); (*env)->SetIntArrayRegion(env,arry,0,4,a); return arry; } 实际上也…

JNDI的XML相关配置(context.xml和web.xml)

1. 在tomcat目录下conf/context.xml文件中 加入一下代码   <Resource name"jdbc/sqlconnpool" auth"Container" type"javax.sql.DataSource" driverClassName"com.microsoft.sqlserver.jdbc.SQLServerDriver" …

Vue 作用域插槽

原博出处&#xff1a; 作者&#xff1a;SentMes 链接&#xff1a;SentMes作者书写的作用于插槽链接 https://www.jianshu.com/p/0c9516a3be80 来源&#xff1a;简书 ** ** ** 十分感谢原作者&#xff0c;写的十分详细&#xff0c;原作者辛苦了&#xff01; 深入理解vue中的s…

nuc972的ramfs的配置yaffs2,ubi文件系统

按照技术支持的推荐&#xff0c;使用ramfs文件系统。那么就可以在uboot的nuc970_evb.h中将JEFS yaffs ubi 的相关支持去掉就可以了。这样理应能减少很大部分的uboot大小。剩下就是配置内核中的ramfs配置。 General setup ---> [ ] Initial RAM filesystem and RAM disk (i…