爬虫八:从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。

获取cookie

打开FireFox浏览器,登录QQ空间,启动FireBug,选择FireBug中的Cookies页签,点击页签中的cookies按钮菜单,选择“导出本站点的cookie”即可完成cookie的导出。

image

导出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是有过期时间的。如果想长时间抓取网页,就需要每隔一段时间就更新一次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)    收藏  举报

导航