爬虫八:从0开始(二):多线程多进程爬取、图片、cookie
四、Python网络爬虫4 – 多线程抓取
在进行抓取的时候,时间的消耗主要是在请求等待的时间上,所以一个最容易想到的优化方式就是使用多线程。
线程threading,略。
线程池
在抓取网页的时候,一个简单的思路就是为每个网页启动一个线程。在要抓取的网页比较少的时候——比如百十来个——这样子还是可行的。但是网页比较多的时候,这样做就不太合理了。因为线程的创建启动和运行都会消耗很多的资源,线程启动太多会耗尽资源导致机器卡死。而且,创建线程后只执行一次也是一种浪费。为了减少线程的创建、实现线程的重复利用,我们需要引入线程池。
可以使用python的ThreadPoolExecutor来创建线程池:
from concurrent.futures import ThreadPoolExecutor def func(): print("this is multi thread") def start_pool(): pool = ThreadPoolExecutor(64) for i in range(10): pool.submit(func) if __name__ == '__main__': start_pool()
也很简单,甚至不比单线程多一行代码。在代码里创建了一个总数为64的线程池,然后在一个循环中每次取出一条线程来执行func函数,没有空闲线程时则会进入等待。
按照这样的思路,我们也可以使用Queue来自己创建线程池:
class CustomThread(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.__queue = queue def run(self): while True: cmd = self.__queue.get() cmd() self.__queue.task_done() def custom_pool(): queue = Queue(5) for i in range(queue.maxsize): t = CustomThread(queue) t.setDaemon(True) t.start() for i in range(20): queue.put(func) queue.join()
在上面的代码里创建了一个长度为5的队列,然后参照队列的长度创建了几个线程,并立刻启动。每个线程随时待命,一旦队列里面有了要执行的任务就会拿过来立即执行,并在执行完成后发送通知给队列。queue.join()方法则会在队列中的所有任务执行完成前阻塞住线程,待所有任务执行完成后再继续执行后面的代码。
并行:
前面所述的方案是并发处理的方案。并发处理的方案可以充分利用CPU。不过现在的CPU通常都是多核的,为了利用多核CPU的特点,可以考虑使用并行处理的方案。下面是一个进程的演示:
from multiprocessing import Process def process(func): p = Process(target=func) p.start()
当然也有对应的进程池了:
from multiprocessing.pool import Pool def multiprocess(func): pool = Pool(processes=16) for i in range(16): pool.apply_async(func) pool.close() pool.join()
五、Python网络爬虫5 – 图片抓取:
使用beautiful爬取图片:http://pp.163.com/longer-yowoo/pp/10069141.html
#!python # encoding: utf-8 from urllib import urlopen from bs4 import BeautifulSoup def get(url): response = urlopen(url) html = response.read().decode('gbk') response.close() return html def detect(html): soup = BeautifulSoup(html, "html.parser") images = soup.select("img[data-lazyload-src]") return images def main(): html = get("http://pp.163.com/longer-yowoo/pp/10069141.html") links = detect(html) for link in links: print link.attrs["data-lazyload-src"] if __name__ == "__main__": main()
# OUT: http://img2.ph.126.net/FbKF5NvZsxdi4Yu9L2GT-g==/3264265305013617812.jpg # OUT: http://img1.ph.126.net/AjFxUDXAZlu98PBWUPFcSA==/1618762591162995241.jpg # OUT: http://img0.ph.126.net/6KmMoZabvft1_kHYAEZN5Q==/6599332561585979784.jpg
在上面的代码中soup.select(“img[data-lazyload-src]”)一句查询了所有包含data-lazyload-src属性的img标签。在捕捉到图片标签后,又取出data-lazyload-src属性并打印了出来,一共有六个。
然后就是如何抓取图片了。先来看看之前的一段代码:
html = response.read().decode("gbk")
这段代码的作用是抓取网页内容并转换为字符串。其中,response是http反馈信息,read方法的作用是读取出http返回的字节流,decode则是将字节流转换为字符串。字符串本质是字节流,图片也是。那么,如何获取图片也就清楚了:就是通过http获取到图片的字节流,再将字节流保存到硬盘即可。看下是如何实现的:
def download(url, pic_path): response = urlopen(url) img_bytes = response.read() f = open(pic_path, "wb") f.write(img_bytes) f.close()
完整代码:
#!python # encoding: utf-8 from urllib import urlopen from bs4 import BeautifulSoup def get(url): response = urlopen(url) html = response.read().decode('gbk') response.close() return html def detect(html): soup = BeautifulSoup(html, "html.parser") images = soup.select("img[data-lazyload-src]") return images def download(url, pic_path): response = urlopen(url) img_bytes = response.read() with open(pic_path, "wb") as f: f.write(img_bytes) def main(): html = get("http://pp.163.com/longer-yowoo/pp/10069141.html") links = detect(html) i = 0 for link in links: url = link.attrs["data-lazyload-src"] download(url, '/pics/%s.jpg' % i) i += 1 if __name__ == "__main__": main()
上面的代码仍可以优化下:要下载的文件的名称及扩展名最好是从下载链接中动态获取。
六、Python网络爬虫6 – 网页编码:
在抓取网页时遇到了一段报错信息:
Traceback (most recent call last): File "D:/pythonDevelop/spider/pic_grab.py", line 14, in <module> print(get("http://pp.163.com/longer-yowoo/pp/10069141.html")) File "D:/pythonDevelop/spider/pic_grab.py", line 8, in get content = response.read().decode("utf8") UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 69: invalid continuation byte
在错误信息中提示了网页的编码不是utf-8。那么如何确认网页的编码形式呢?有如下几种方式:
- 从网页源码中查找chaset信息;
- 使用FireBug。重新打开网页,使用FireBug的NetWork抓取网页加载过程,查看目标网页的头信息,找到Content-Type,其中的charset信息就是;
- 使用Firefox右键菜单中的“查看页面信息”功能:点击网页空白处 –> 右键菜单 –> 查看页面信息,在弹出窗口中选择 常规 –> 文字编码 也可以查看网页编码信息。
检测到网页的编码是gbk。修改后就可以了。
七、Python网络爬虫7 – 使用cookie
很多时候,我们要查看的内容必须要先登录才能找到,比如知乎的回答,QQ空间的好友列表、微博上关注的人和粉丝等。要使用爬虫直接登录抓取这些信息时,有一个不太好解决的难题,就是这些网站设置的登录规则以及登录时的验证码识别。不过,我们可以想办法绕过去,思路是这样的:先使用浏览器登录,从浏览器获取登录后的“凭证”,然后将这个“凭证”放到爬虫里,模拟用户的行为继续抓取。这里,我们要获取的凭证就是cookie信息。
这次我们尝试使用python和cookie来抓取QQ空间上的好友列表。使用的工具是FireFox浏览器、FireBug和Python。
打开FireFox浏览器,登录QQ空间,启动FireBug,选择FireBug中的Cookies页签,点击页签中的cookies按钮菜单,选择“导出本站点的cookie”即可完成cookie的导出。
导出cookie会以一个名为cookies.txt文本文件形式存在。
程序实现
然后我们会使用获取的cookie新建一个opener来替换之前请求时使用的默认的opener。将获取的cookies拷贝到程序目录下,编写脚本如下:
#!python # encoding: utf-8 from http.cookiejar import MozillaCookieJar from urllib.request import Request, build_opener, HTTPCookieProcessor DEFAULT_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"} DEFAULT_TIMEOUT = 360 def grab(url): cookie = MozillaCookieJar() cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True) req = Request(url, headers=DEFAULT_HEADERS) opener = build_opener(HTTPCookieProcessor(cookie)) response = opener.open(req, timeout=DEFAULT_TIMEOUT) print(response.read().decode('utf8')) if __name__ == '__main__': grab(<a href="http://user.qzone.qq.com/QQ号/myhome/friends">http://user.qzone.qq.com/QQ号/myhome/friends</a>)
因为我们使用的是FireFox浏览器导出的cookie文件,所以这里使用的cookieJar是MozillaCookieJar。
执行脚本…然而报错了:
Traceback (most recent call last): File "D:/pythonDevelop/spider/use_cookie.py", line 17, in <module> start() File "D:/pythonDevelop/spider/use_cookie.py", line 9, in start cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True) File "D:\Program Files\python\python35\lib\http\cookiejar.py", line 1781, in load self._really_load(f, filename, ignore_discard, ignore_expires) File "D:\Program Files\python\python35\lib\http\cookiejar.py", line 2004, in _really_load filename) http.cookiejar.LoadError: 'cookies.txt' does not look like a Netscape format cookies file
问题出在cookies文件上,说是不像一个Netscape格式的cookie文件。不过也好解决,只需要在cookies文件开始一行添加如下内容即可:
# Netscape HTTP Cookie File
通过这行内容提示python cookie解析器这是一个FireFox浏览器适用的cookie。
再次执行,还是会报错,因为比较长我就只贴关键的部分出来:
http.cookiejar.LoadError: invalid Netscape format cookies file 'cookies.txt': '.qzone.qq.com\tTRUE\t/\tFALSE\tblabla\tdynamic'
意思是cookie中某些行存在格式错误。具体错在哪儿,需要先了解下FireFox浏览器的cookie格式。MozillaCookieJar认为每行cookie需要包含以下信息,每条信息以制表符分隔:
| 名称 | domain | domain_specified | path | secure | expires | name | value |
| 类型 | 字符串 | 布尔型 | 字符串 | 布尔型 | 长整型 | 字符串 | 字符串 |
| 说明 | 域名 | — | 适用路径 | 是否使用安全协议 | 过期时间 | 名称 | 值 |
其中domain_specified是什么意思我不很清楚,以后弄明白了再补上。再来看看我们获取的cookie的部分行:
user.qzone.qq.com FALSE / FALSE 814849905_todaycount 0 user.qzone.qq.com FALSE / FALSE 814849905_totalcount 0 .qzone.qq.com TRUE / FALSE 1473955201 Loading Yes .qzone.qq.com TRUE / FALSE 1789265237 QZ_FE_WEBP_SUPPORT 0
前两行格式是错误的,后两行格式是正确的。前两行缺少“expires”属性。该怎么办呢——补上就好了呗。在其他的cookie中随意选一个时间补上就OK了。
补全cookie后,再次执行是正常的,没有报错。但是没有如预期的打印出好友信息,因为网址错了。使用firebug可以找出正确的网址:
https://h5.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/tfriend/friend_ship_manager.cgi?uin=QQ号&do=1&rd=0.44948123599838985&fupdate=1&clean=0&g_tk=515169388
这样就抓取到好友列表了。好友列表是一个json字符串。

至于如何解析json,会在下一节进行说明。
cookie是有过期时间的。如果想长时间抓取网页,就需要每隔一段时间就更新一次cookie。如果都是从FireFox浏览器来手动获取显得有些笨了。从浏览器获取的cookie只是作为一个入口,之后再进行请求还是要依靠python主动获取cookie。下面是一段获取cookie的程序:
#!python # encoding: utf-8 from http.cookiejar import CookieJar from urllib.request import Request, HTTPCookieProcessor, build_opener DEFAULT_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"} DEFAULT_TIMEOUT = 360 def get(url): cookie = CookieJar() handler = HTTPCookieProcessor(cookie) opener = build_opener(handler) req = Request(url, headers=DEFAULT_HEADERS) response = opener.open(req, timeout=DEFAULT_TIMEOUT) for item in cookie: print(item.name + " = " + item.value) response.close()
在示例程序中演示了如何获取cookie,并打印了cookie的name和value两项属性。通过实例可以看到每次执行http请求都会重新获取cookie,因此可以将我们的程序调整一下:执行第一次请求时使用我们通过浏览器获取的cookie,之后的每次请求都可以使用上次请求时获取的cookie。调整后的程序:
#!python # encoding: utf-8 from http.cookiejar import MozillaCookieJar, CookieJar from urllib.request import Request, build_opener, HTTPCookieProcessor, urlopen DEFAULT_HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"} DEFAULT_TIMEOUT = 360 def gen_login_cookie(): cookie = MozillaCookieJar() cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True) return cookie def grab(cookie, url): req = Request(url, headers=DEFAULT_HEADERS) opener = build_opener(HTTPCookieProcessor(cookie)) response = opener.open(req, timeout=DEFAULT_TIMEOUT) print(response.read().decode("utf8")) response.close() def start(url1, url2): cookie = gen_login_cookie() grab(cookie, url1) grab(cookie, url2) if __name__ == '__main__': u1 = "https://user.qzone.qq.com/QQ号/myhome/friends" u2 = "https://h5.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/tfriend/friend_ship_manager.cgi?uin=QQ号&do=2&rd=0.44948123599838985&fupdate=1&clean=0&g_tk=515169388" start(u1, u2)
就这样。
其他
其实在登录QQ空间时使用cookie还有另一种法子——通过观察,也可以在http 请求头中添加cookie信息。
获取请求头中cookie的方式:打开FireFox浏览器,打开FireBug并激活FireBug的network页签,在FireFox浏览器上登录QQ空间,然后在FireBug中找到登录页请求,然后就可以找到请求头中的cookie信息了。

将cookie信息整理成一行,添加到请求头中就可以直接访问了。这个方法相对简单,减少了修改cookie文件的步骤。
此外,在一篇博客文章中还找到了直接登录QQ空间的方案。这算是已知最好的法子了,只要腾讯不改变登录规则就能很简单的执行请求获取cookie。
posted on 2018-04-02 12:03 myworldworld 阅读(449) 评论(0) 收藏 举报
浙公网安备 33010602011771号