Hentai Meets Spider——我的青春爬虫物语果然有问题

在很久之前我就有一个看尽天下优秀本子的宏愿,但是越是玩弄计谋,就越会发现人类的能力是有极限的。。。除非超越人类!我不做lasjflaldjlljjasdfl

咳咳,以上只是某人无聊的玩笑。在接触爬虫后一直在思考找哪个网站当作练手羡慕,而正巧这时看到了Dimpurr大佬的ExHentai 探险指南,打开了新世界的大门,于是决定把EXHentai当作受害者自己的练手项目!

如Dim在文章中所说,ExHentai是E-Hentai的里站,所以如果我们想要访问ExHentai的话其实是需要先注册一个ExHentai的账号,而E站的规则貌似是在一个表站账号注册一个月后才能访问里站,所以我们的第一步应该是注册一个E-Hentai账号然后发酵一个月。


One Mouth Later…


很好,现在我们已经拥有了一个发酵了一个月的珍品E站账号,如果你在打开里站EXHentai的时候仍然看到了那只忧郁的熊猫的话,请参考Dim的 ExHentai 探险指南

环境准备

环境准备

工欲善其事,必先利其器。现在又很多成熟的爬虫工具,但并不是所有的爬虫工具都适合要做的项目。最初是打算使用Scrapy框架来爬取数据的,Scrapy框架的功能也的确强大,可以用尽量少的代码获得尽可能多的产出。但当我给Scrapy加入了自己的各种middleware后,事情就脱离了我的掌控,发生了各种各样的bug,于是基本上一大半的事件都在Google和畅游StackOverflow,缺少游戏体验。在判断想要熟练使用Scrapy的各种功能需要花费太多精力后我决定放弃使用Scrapy,转而投向Requests的怀抱。

因为预计存储的数据有600k+条,使用json等轻量化存储格式好像并不是那么合适,所以我选择使用MySQL来存储爬到的数据。为了提高爬虫效率,我决定使用多线程来爬数据,因为threading是内置在python环境中的所以就不用另外配置了。用以提取页面数据的工具有很多,比如BeautifulSoup、XPath、正则表达式等,鉴于自己过于弱鸡对XPath和正则表达式极不熟悉所以我选择使用亲和的BeautifulSoup。

最终我的初步环境配置如下所示

Python 3.6.7
MySQL 8.0.13
requests 2.13.0
PyMySQL 0.9.3
bs4 0.0.1

数据爬取

数据爬取
数据爬取

由于并没有什么爬虫经验,甚至在决定爬EX站时都不知道自己打算拿这些数据做什么,所以决定先一股脑地把所有看上去能用的数据爬取下来,之后再慢慢料理。总之根据E站的信息特征,最后决定将本子的标题、发布日期、语言、评分、喜爱人数和特征标签等都爬取下来。

在决定好要爬取的内容后要做的就是创建MySQL的存储模式

CREATE DATABASE exhentai;

use exhentai;

CREATE TABLE `exhentai_info` (
    `manga_pure_id` varchar(30) NOT NULL,
    `manga_id` varchar(30) NOT NULL,
    `head` varchar(500) DEFAULT NULL,
    `subhead` varchar(500) DEFAULT NULL,
    `kind` varchar(20) DEFAULT NULL,
    `uploader` varchar(30) DEFAULT NULL,
    `time` datetime DEFAULT NULL,
    `parent` varchar(30) DEFAULT NULL,
    `parent_href` varchar(30) DEFAULT NULL,
    `visible` varchar(30) DEFAULT NULL,
    `language` varchar(30) DEFAULT NULL,
    `file_size` float DEFAULT NULL,
    `length` int DEFAULT NULL,
    `favorited` int DEFAULT NULL,
    `rating_count` int DEFAULT NULL,
    `average_rating` float DEFAULT NULL,
    `artist_feature` varchar(2000) DEFAULT NULL,
    `group_feature` varchar(2000) DEFAULT NULL,
    `female_feature` varchar(2000) DEFAULT NULL,
    `male_feature` varchar(2000) DEFAULT NULL,
    `language_feature` varchar(2000) DEFAULT NULL,
    `character_feature` varchar(2000) DEFAULT NULL,
    `misc_feature` varchar(2000) DEFAULT NULL,
    `parody_feature` varchar(2000) DEFAULT NULL,
    `removed` int DEFAULT 0,
    `failed` int DEFAULT 0,
    `crawled` int DEFAULT 0,
    PRIMARY KEY (`manga_pure_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `record` (
    `id` INT NOT NULL,
    `page` INT NOT NULL,
    `date` TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中exhentai_info表中的’removed’、’failed’、’crawled’分别标记该本子已被移除、爬取失败和被成功爬取,其他项则分别是每个本子的基本信息;record表则用来记录已经爬到目录的第几页,毕竟天有不测风云,不知道爬虫什么时候就会突然崩掉,万一出现问题好从上次停止的地方开始。

E站的反爬虫措施比较仁慈,没有诸如验证码之类的措施,只有ip检测的机制。 请求的head直接用Chrome的开发者工具调试出的heade就好,完全不用改,一个账号用不同的ip请求几十万次也完全不会被封,不得不说这真的相当仁慈。

经过测试大致推断出了E站的机制:同一ip一小时内请求超过3600次就会封禁ip,第一次1h,第二次封禁24h。为了快速爬取大量数据,必须使用ip代理才行。

在一番搜索之后发现商用的ip代理价格较高(贫穷的我orz),而那些免费代理提供的ip质量实在过低,请求成功率过于感人。自己倒是也尝试了写一个ip代理池,但是质量emmmmmmm。所以我上github搜索了一圈,看看有没有好用的代理池,最后找到了崔庆才大神的开源项目IPProxyPool,使用它完成了代理。

    def getProxy(self):
        r = requests.get('http://127.0.0.1:8991/')
        ip_ports = json.loads(r.text)
        num = random.randint(0, len(ip_ports)-1)
        ip = ip_ports[num][0]
        port = ip_ports[num][1]
        proxies={
            'http':'http://%s:%s'%(ip,port),
            'https':'http://%s:%s'%(ip,port)
        }
        return proxies

    def deleteProxy(self, proxies):
        ip = re.match(r'http://(\S+):\d+', proxies['http']).group(1)
        requests.get('http://127.0.0.1:8991/delete?ip=%s' % ip)
        print(threading.current_thread().name + ': ' + 'Successfully delete %s' % ip)

    def getHtml(self, url):
        while True:
            try:
                proxies = self.getProxy()
                html = self.session.get(url = url, headers = self.head, proxies = proxies, timeout = 10)

                if BeautifulSoup(html.text, 'html5lib').find('title') == None:
                    self.deleteProxy(proxies)
                else:
                    return html
            except:
                print("Failed with get %s" % url)

IPProxyPool的使用方法十分简单,提供了获取ip和去除无用代理的接口,使用这些接口我定义了一个简单粗暴的用以请求html的方法,当检测出使用的ip已经被E站封掉时就发送请求将这个代理从代理池中移除 ,尽可能地减少了代码的冗余 。顺带一提,最初在使用Scrapy的时候我增加了一个检测请求是否成功的middleware和一个设置代理的middleware,理论上来说当请求失败时前者应当返回一个新的request,这个新的request会进入等待队列等待发送,但是不知道是由于什么魔法这个新的request死活都不被spider调用,也就是说某个request如果失败的话就无法请求第二次了,虽然理论上来说还有一些其他机制来绕过这个问题来重新发送request,但是在这个过程中我对人生产生了怀疑于是背弃Scrapy重新投向requests的怀抱。如果又dalao知道问题的原因的话请务必告诉我!

在爬取过程中遇到的另一个问题是emoji的存储,理论上来说emoji也是utf8编码应该可以存储在MySQL中,但是我创建的数据库模式似乎无法存储长度大于4字节的字符,而这个见鬼的❤️的长度大于四个字节,然后MySQL就无法存储了,本来是想找到emoji的utf8对应表然后定点去除呢,但是发现emoji似乎也挺乱的没有什么统一的规则。在一番查阅之后发现有一个十分简单粗暴却有效的方法,那就是把所有长度大于四个字节的字符直接去掉,非常有效。

    def subEmojiPattern(self):
        try:  
            #python UCS-4 build的处理方式
            return re.compile(u'[\U00010000-\U0010ffff]')  
        except re.error:  
            #python UCS-2 build的处理方式 
            return re.compile(u'[\uD800-\uDBFF][\uDC00-\uDFFF]')

剩下还遇到一些零零碎碎的小问题,比如windows和linux环境下python的编码方式不一致之类的,就不一一赘述了。

最后把爬虫扔到服务器上慢慢跑,爬了大概两三天爬完了所有数据,总计约66万条。

600K+条数据

顺带一提,在爬取数据时我使用的线程数是64,在使用代理的情况下每分钟爬取到的数据在100-120之间。理论上来说应该是用不到这么多线程的,因此我之后打算测试一下在使用代理和不使用代理的情况下不同线程数的爬取效率。

本次爬虫就到此结束了,之后打算对这些数据进行可视化分析,如果有时间的话会把这两部分也写成博文。

最后,将代码奉上!

http://blog.lastation.me/wp-content/uploads/2019/05/EXSpider.zip

2 thoughts on “Hentai Meets Spider——我的青春爬虫物语果然有问题

  1. 你好,能分享下源码吗?无法下载呢。
    最近在学习爬虫,想用来练练

    1. 好久没看博客没想到原来的源码链接失效了,现已重新上传文件,欢迎交流啊

Leave a Reply

Your email address will not be published. Required fields are marked *