python使用代理+多线程爬取速卖通评论(二)

废话少说

在上一篇文章python使用代理+多线程爬取速卖通评论(一)中,我已经成功分析出了速卖通评论请求数据的策略,但是为了防止我们的爬虫触发速卖通的反爬策略,我们决定采取使用代理IP的方式来进行伪装,同时为了提高爬取速度,我决定开多个线程进行数据爬取。
这篇文章,更多的是我在实现多线程爬取过程中的思考过程和收获,以及代码大概的说明,完整的代码我已放到github,大概300行,如有bug或者更好更优雅的实现,我会及时更新。
需要代码的看这里,代码是默认保存到数据库的,你可以本地建一下数据库和表,也可以使用我提供的save_data_to_csv()方法,直接保存到csv文件中。

使用代理IP发送请求

监控同一IP访问频率是非常常见的反爬手段之一,你用同一个IP在短时间内大量访问目标网站,而且没有sleep的话,你的ip很容易被服务器禁止访问。所以为了反反爬,我们要学会如何使用代理IP来发送请求,这也是我第一次学习使用代理IP爬数据,超easy。
对于我们个人来说,如果只是自己爬小量数据用于研究,分析的话,可以直接从代理IP网站爬取免费的代理IP。
比如国内高匿代理IP,如图

python使用代理+多线程爬取速卖通评论(二)_第1张图片
image.png

我们直接把首页的IP爬取下来就够用了,当然免费的肯定没有付费的好用,有些IP不可以用,但是说实话我还没有碰到几个不能用的。这个爬取很简单,直接附代码了,爬取到本地之后,按行保存到本地一个txt文件中

from bs4 import BeautifulSoup
import queue
url='http://www.xicidaili.com/nn/'
headers={
      'user-agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',        
}
ip_data=requests.get(url,headers=headers)
soup=BeautifulSoup(ip_data.text,'html.parser')
ips=soup.select('tr')
ip_list=[]
for i in range(1,len(ips)):
    ip_info=ips[i]
    tds=ip_info.select('td')
    ip_list.append(tds[1].text+':'+tds[2].text)
with open("iplist.txt","a",encoding='utf-8') as f:
        for ip in ip_list:
            f.write(ip)
            f.write('\n')
python使用代理+多线程爬取速卖通评论(二)_第2张图片
image.png

接着你就可以用代理IP来爬数据了,听起来感觉很复杂,但是对应到代码上,也就多加一个参数(当然只限定python+requests,其它不了解)

def get_ip_list(self):
    temp_ip_list=[]
    with open('iplist.txt', 'r') as f:
        while True:
            ip=f.readline().replace('\n','')
            # 记得加 'http://'
            temp_ip_list.append('http://'+ip)
            if not ip:
                break
    return temp_ip_list       
def get_random_ip(self):        
    proxy_ip=random.choice(self.ip_list)
    proxies={
        'http':proxy_ip
    }
    return proxies

因为我是从完整代码里截取的,所以里边有self,第一个函数用于将每一个ip从iplist.txt这个文件读取到一个python list中,然后第二个函数用于从该list中随机获取一个Ip,而真正使用代理ip发请求是超级简单,只是在原来的基础上,多一个proxies参数

proxies=self.get_random_ip()
requests.get(url,headers=headers,proxies=proxies)

多线程爬取

我回家连的就是家里wifi,本来爬的就慢,动不动就超时了,再加上,为了保险,每爬一个页面,我都sleep1秒钟,这样一来,爬取的速度我感觉有点慢,所以就考虑要不要多开几个线程,但是因为对之前从来没写过多线程程序,对多线程的认识就是一些模糊的概念,因此在编程中间还碰到一些问题,但是后来解决问题之后,对并发编程,线程同步,生产者与消费者模型,线程安全等有一个更进一步的认识。
代码整体结构如图


python使用代理+多线程爬取速卖通评论(二)_第3张图片
image.png

CommentSpyder 是爬虫类,主要负责爬取数据和解析数据
Saver是存储类,主要负责储存数据
get_total_page()函数用于获取评论总页数
get_url()函数用于构造token请求地址
update_ip_list()函数用于更新iplist.txt文件中的代理IP,需要手动执行
crawl()为封装好的爬取函数
main()函数为主函数


python使用代理+多线程爬取速卖通评论(二)_第4张图片
image.png

在main函数中,首先发出请求获取总页数,然后根据总页数给每个线程平均分配自己所要爬取的评论页码范围,默认开10个线程,同时在开一个线程,用于往数据库或者csv文件写数据,然后这10个线程相当于生产者-消费者模型中的生产者,saver线程相当于是消费者,这11个线程共享一个pyhon提供的线程安全的队列,生产者爬到数据之后写入该队列,然后消费者从该队列取数据,并一条一条插入数据库或者保存到csv中。
说一下我踩过的坑
踩坑1:我在爬虫类初始化的时候首先发一个请求,获取token,这样在爬取每一页的时候就不必每次去取token了,但是我在写多线程的时候,一开始是这么写的,只贴相关代码
spyder=CommentSpyder(url,productid,owner_memberid,companyid,result_queue,start_page,end_page)
crawl_thread = threading.Thread(target = spyder.crawlComments,args=(url,productid,owner_memberid,companyid,result_queue,start_page,end_page))

一开始一直没觉得有什么问题,但是当我发现多线程跑和单线程跑的时间差不多的时候,我突然想起了,学pyhon基础的时候有一个GIL(python全局解释器锁),然后又从别人博客中看到所谓的“python多线程是鸡肋的言论”,于是我恍然大悟,“怪不得多线程时间和单线程时间差不多嘞,原来python多线程没什么鸟用”。
但是当我百度输入python多线程爬虫,还是有很多人用python的多线程来写代码,如果真的没用,为什么还有这么多人采取多线程,所以我还是多思考了一会,终于想清楚了原因。
因为我为每个线程实例化了一个爬虫对象,而在爬虫对象初始化的过程中,会发出网络请求取得token,而我给thread添加的target中只有python爬取数据的代码,所以这十个爬虫对象请求token的过程是线程阻塞的,这也是为什么我总感觉线程是一个个按顺序运行的,我一开始还误以为是全局解释器锁的原因,每次只能有一个线程获得锁,很显然是我错了,python的多线程鸡肋只是鸡肋在无法利用多核CPU,但是即使单核CPU,在做IO密集型操作时,多线程效率还是远远高于单线程
我也曾一度钻进牛角尖,我想不通,单核CPU多线程的时间为什么会比单线程短...
因为,学习多线程的时候,经常讲到一个时间片的切换,微观上是一个个操作来的,只是切换足够快,快到看上去就好像计算机在同时做两个操作。那么既然实际上是按顺序一个个运行的,只是看上去在并行,那么多线程时间怎么会缩短呢?假设有两个任务A和B
A中包含a1,a2俩个操作,分别耗时1s,2s
B中包含b1,b2,b3三个操作,分别耗时1s,2s,3s
同步运行的话肯定是9s(当然简化了模型)
就算开了多线程,单核CPU,不管你切换的有多快,但是本质上你一次只做一个操作,你完成了a1,切换到b1,不管怎么切换,最终运行时间也应该等于9秒才对啊。
而通过写这个多线程爬虫,也让我想通了这个问题,我之所以有上述错误的想法就是因为我忽略了IO往往存在大量的阻塞时间。
任务AB耗费的总时间等于AB操作+IO阻塞的时间(如网络IO,磁盘IO),而相比IO阻塞时间,cpu执行操作的时间几乎可以忽略不计。
那么再以上面那个例子来讲一下
同步执行的情况下
a1 1秒,等待IO10秒
a2 2秒,等待IO20秒
b1 1秒,等待IO10秒
b2 2秒,等待IO20秒
b3 3秒,等待IO30秒
总耗时99秒
而使用多线程的话,
a1 1秒 ,遇到IO阻塞,释放GIL锁,而不会傻等在这里,线程B获得GIL,转去执行b1,说到这里,后面就不用说了吧,这样下来总时间肯定少于99秒。所以时间可以缩短全是因为IO阻塞的存在。
踩坑2:一开始我只开了10个线程,在爬到数据并解析后立刻插入数据库,但是数据库这边有时候会报链接不可以获得的错误,我猜测肯定是数据库访问频率某个瞬间太高了,后来就想要不用个队列,爬下来先写到队列里,然后再开一个线程,专门用于从队列中慢慢读,并保存到数据库,写着写着,哇,这不就是操作系统上讲的消费者与生产者模型嘛。
踩坑3:一开始我在保存数据的时候,想要打印一个信息,即这是第几条数据,但是经常会出现多个线程打印同一个数字,这是因为我没有进行加锁,当我加锁之后,对该变量的读取和加1操作每一个时刻只有一个线程可以运行,从而打印出了正确的顺序,这似乎没什么,稍微了解一下锁的概念就可以知道,但是后边我在用一个共享队列的时候,我并没有加锁,但是我发现从来没有出现多个线程同时访问一条数据的情况,我试了很多遍,一次都没有出现,我突然,(真的是突然),想起了一个词“线程安全”,前段时间看java,总是说哪些容器是线程安全的,哪些是不安全的,肯定就是这儿的这个意思,我百度一查,果然如此,import queue进来后,我使用的是python自带的线程安全队列,该队列内部实现了锁原语,所以保证了不会有多个线程对其同时进行读写,如果你换成list,肯定就有问题了。

最后

踩坑越多,收获越大,我知道我的智商只是正常人的智商,无论我怎么思考也解决不了世界难题,但是思考总是可以让我进步,让我更优秀,所以希望我永远热爱思考,永远享受想通问题时的畅快!

你可能感兴趣的:(python使用代理+多线程爬取速卖通评论(二))