从零开始写一只爬虫 · 我是人类

从零开始写一只爬虫

我们这回换一个网站吧。不去折磨那个可怜的招聘网站了。毕竟以后还可能再用到它……万一被拉黑了就不好了……

这次我们来抓取一个免费托管 dns 的网站。

为了证明我是人,我需要模拟一下浏览器

如何把自己变成外星人?只要自己穿上一件外星人的衣服,说外星人的语言,写外星人的文字就行了。别人把你识别为外星人,你就是个外星人。

现在,我们需要做的,就是给我们的请求穿上个 “外星人的衣服”。这个 “衣服” 就是 “请求头”,里面带有请求和响应的 “概览” 比如说,浏览器类型,浏览器版本,浏览器语言,是否支持 gzip 压缩,http 状态(200、404、500 等),好多好多。

Windows 下有好几个专业的抓取请求头的东西(比如 Fiddler 之类的)。Linux 下面我不知道有没有,但是在逛 Ubuntu 软件中心的时候,我发现了神器 Wireshark,去年计算机网络课的时候拿他抓 TCP 报文、IP 数据报来着。

然后我使用了 WireShark 抓取请求头……妈呀,吓死宝宝了,专业抓包工具就是不含糊,各种请求的细节都抓取到了。但是我只关心我们的请求头部分:

WireShark抓包结果

好的,全都复制下来,然后这样写:

headers = {
    "Host": "freedns.afraid.org",
    "Connection": "keep-alive",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate, sdch",
    "DNT": "1",
    "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4",
    "Referer": "http://freedns.afraid.org/domain/?ls=1",
}

用法么,下面再说。

为了证明我是人,我需要模拟个登录。

先说明啊,我可不会输入验证码。。。把图片显示出来然后自己输入验证码?高难度的样子。

现在,我需要访问 http://freedns.afraid.org/subdomain/这个页面:如果已经登录了,那么页面会正常显示。但如果没有登录,则会被带到 http://freedns.afraid.org/zc.php?from=L3N1YmRvbWFpbi8=这个登录页面去。而据我观察,这个网站的登录是拿 Cookie 来实现的。

那好,怎么才能吃到 Cookie 呢?响应头里面有……真心有。一般来说,登录这件事啊,就是,你点击登录之后,浏览器把用户名密码发给服务器,服务器发回来一段响应,响应头里面包含一小段 Cookie。在 Cookie 有效期内,你都是 “已登录” 状态。不过不用我们操心,强大的 Python 有对应的库。将你的 “登录” 请求发给服务器,服务器发回响应,里面的 Cookie 能自动被存到一个叫做 Cookie Jar 的东西里面。以后再请求其他网页,带上这个 Cookie Jar 就好了。很是方便。

然后我们要做的,就是找到点击 “登录” 之后,我们的用户名、密码交给谁了。看 HTML 代码,form 里面有啥,咱就提交啥。不过这样并不保险:有的时候 JS 会在 Form 里面动态地加上一些隐藏域。所以,最好的办法还是抓!亲,回 Windows 下面用 Fiddler 抓吧,找到登录的返回码为 200 的包就好了。吾等渣渣已经被强大的 WireShark 虐爆了……

然后找到登录页面上面的 form,我们可以看到,需要提交的东西有:用户名 username、密码(还是明文密码)password、记住我 remember、从哪里来 from 和做什么操作 action。每个网站需要的数据都不一样,自行处理。

和上面的伪装成浏览器的东西整合一下,这一小段登录代码如下:

def login(self):
    auth_url = 'http://freedns.afraid.org/zc.php?from=L2RvbWFpbi8='
    # 登陆用户名和密码
    loginInfo = {
        'username': 'YOUR_ID_HERE',
        'password': 'YOUR_PASSWORD_HERE',
        'remember': 1,
        'from': 'L2RvbWFpbi8=',
        'action': 'auth'
    }
    # urllib 进行编码
    login_data = urllib.parse.urlencode(loginInfo).encode()
    headers = {
        "Host": "freedns.afraid.org",
        "Connection": "keep-alive",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, sdch",
        "DNT": "1",
        "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4",
        "Referer": "http://freedns.afraid.org/domain/?ls=1",
    }
    # 初始化一个 CookieJar 来处理 Cookie
    cookieJar = http.cookiejar.CookieJar()
    # 实例化一个全局 opener
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookieJar))
    # 获取 cookie
    req = urllib.request.Request(auth_url, login_data, headers)
    result = opener.open(req)
    return opener

获取到 Cookie 之后,在请求头里面,我们会发现至少一个 cookie_pair。That's what we need~

(直接把 cookie 拿到,然后写到请求头里面应该也是可行的吧……)

我是人,我会从页面中发现链接并自动点击

这个嘛……开始的时候不是说了,作为例子的话使用枚举的么。现在同样可以采用 “链接发现” 这种形式的。做的改动不多,将链接提取出来,放到队列里面就好了。

虽然在这里例子里面我们还是用不到……

这里需要注意两个问题:

第一是,访问过的链接不能重复访问。这个可以用一个 set 来记录。已经访问的页面。

注意注意注意!数量太大的话会爆内存的!小范围做例子还是可以的。大范围……我也不会……求大神带我飞。

第二是,相对链接,处理会比较麻烦……比如需要先获取到本页面的 url,再替换掉最后的部分,再…………Ok,我不想写了……求放过我。

下面的例子里面只能找出绝对路径。我只是写出来做个 Demo 而已。虽然你会发现那个函数完全没有被调用。

到这里,这是我们目前的全部代码

import csv
import http
import socket
import threading
import urllib
import requests
from bs4 import BeautifulSoup


class CsvSaver:
    def __init__(self, path='./', filename='result.txt', method='w+'):
        self.path = path
        self.filename = filename
        self.method = method
        # 打开的文件
        self.FILE = None
        self.WRITER = None

    def open(self):
        self.FILE = open(self.path + self.filename, self.method)
        self.WRITER = csv.writer(self.FILE, quoting=csv.QUOTE_ALL)

    def write(self, data):
        '参数是一个 list'
        self.WRITER.writerow(data)
        self.FILE.flush()

    def close(self):
        self.FILE.close()


class Spider:
    def __init__(self, dataSaver, failSaver=None, thread=5, timeout=2, ):
        self.thread = thread
        # 队列
        self.queue = []
        # 已经爬行过的集合
        self.history = []
        # 爬行数据记录
        self.dataSaver = dataSaver
        # 失败 Log 记录
        self.failSaver = failSaver
        # 线程们
        self.THREADS = []
        # 互斥锁
        self.queueLock = threading.Lock()
        # 带 cookie 的请求器
        self.requester = self.login()
        # 设置超时
        socket.setdefaulttimeout(timeout)

    def login(self):
        auth_url = 'http://freedns.afraid.org/zc.php?from=L2RvbWFpbi8='
        # 登录用户名和密码
        loginInfo = {
            'username': 'YOUR_ID_HERE',
            'password': 'YOUR_PASSWORD_HERE',
            'remember': 1,
            'from': 'L2RvbWFpbi8=',
            'action': 'auth'
        }
        # urllib 进行编码
        login_data = urllib.parse.urlencode(loginInfo).encode()
        headers = {
            "Host": "freedns.afraid.org",
            "Connection": "keep-alive",
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
            "Accept": "*/*",
            "Accept-Encoding": "gzip, deflate, sdch",
            "DNT": "1",
            "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4",
            "Referer": "http://freedns.afraid.org/domain/?ls=1",
        }
        # 初始化一个 CookieJar 来处理 Cookie
        cookieJar = http.cookiejar.CookieJar()
        # 实例化一个全局 opener
        opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookieJar))
        # 获取 cookie
        req = urllib.request.Request(auth_url, login_data, headers)
        result = opener.open(req)
        print('登录成功')
        return opener

    def addUrlBlock(self, start=100, end=200):
        for index in range(start, end):
            url = 'http://freedns.afraid.org/subdomain/edit.php?edit_domain_id=' + str(index)
            self.addUrl(url)

    def addUrl(self, url):
        # 得到锁
        self.queueLock.acquire()
        if not url in self.history:
            # 添加 URL
            self.queue.append(url)
        # 释放锁
        self.queueLock.release()

    def getUrl(self):
        while self.queue:
            url = None
            # 得到锁
            self.queueLock.acquire()
            # 获得 URL
            if self.queue:
                url = self.queue.pop(0)
                self.history.append(url)
            # 释放锁
            self.queueLock.release()
            yield url
        raise StopIteration

    def findInfo(self, nowUrl, data):
        status = data.find('option', value=True).text.strip()
        self.dataSaver.write(status.split(' '))

    def findUrl(self, nowUrl, data):
        aSet = data.find_all('a')
        for a in aSet:
            self.addUrl(a.href)

    def runThread(self):
        for url in self.getUrl():
            try:
                # 得到想要的信息
                data = BeautifulSoup(self.requester.open(url).read())
                # 处理信息
                self.findInfo(url, data)
                print("Success %s" % url)
                # 发现链接
                # self.findUrl(url, data)

            except:
                print("Fail %s" % url)
                self.failSaver.write(["Fail", url])

    def run(self):
        self.dataSaver.open()
        if self.failSaver:
            self.failSaver.open()

        self.THREADS = []
        # 生成线程
        for i in range(self.thread):
            self.THREADS.append(threading.Thread(target=self.runThread))
        # 开启线程
        for i in range(self.thread):
            self.THREADS[i].start()
        # 等待线程完成
        for i in range(self.thread):
            self.THREADS[i].join()

        self.dataSaver.close()
        if self.failSaver:
            self.failSaver.close()


if __name__ == '__main__':
    dataStore = CsvSaver(filename='result.csv')
    logStore = CsvSaver(filename='log.csv')
    spider = Spider(dataSaver=dataStore, failSaver=logStore, thread=3)
    spider.addUrlBlock(300, 310)
    spider.run()
    print('全部完成')

这个代码和上一篇中的代码,不同之处在于,替换掉了原来的请求器。不过这样一来我就不知道如何控制请求超时这个问题了,只能在全局设置请求超时时间了。

一个锁锁住两个东西(队列,和已经访问过的集合),是不是有点违背 “操作系统” 的课程内容?好吧我就这样用了,简单粗暴一点吧。

还有就是,这个网站如果要抓取的话,id 号在 190000 左右的成功率会高一点。有兴趣可以自己运行一下看看。总量在 120W 左右,所有数据拿下来得好几天。(因为粗暴使用的 Python 的多线程很慢……)

留下评论