Python 爬虫多线程问题咨询

2019-04-25 15:22:06 +08:00
 seoguess

刚开始研究多线程,看了很多视频跟文章,但还是概念混淆,所以过来论坛求助。

1、

对于爬虫而言,for 循环多线程抓取 N 个页面,然后获取内容到 MongoDB 数据库,错误链接写入 error.txt,是线程安全的吗?如果不安全,我通过在类里面的采集入库跟写入文件加锁,是不是可以解决?我之前没加锁,抓了 36W 条数据,感觉没发现有什么问题。

入库部分如下:

# 启动抓取函数
def run(self):
    try:
        rst_json = self.claw_detail()
        if rst_json != None:
            gLock.acquire()
            result = table_lines.insert_one(rst_json)
            gLock.release()
        else:
            result = 'null'
    except:
        with open(file_path + "/error.txt", 'a+') as f1:
            f1.write(self.hotelid)
            f1.write('\n')
            f1.flush()
        result = 'error'
        time.sleep(5)
    finally:
        time.sleep(1)

    return result

2、

多层 for 循环嵌套的时候,这么用多线程可以吗? t.join()这么用没有问题吧? t.setDaemon(True)需要设置吗?

main 函数

def main(): for dateTuple in dateList:

    threads = []
    
    for i in id_lines.find():
        hotelId = i.get('hotelId')
        threads.append(hotelId)


    for hotelid in threads:
        t = ClawData(hotelid,headersCookie)
        #t.setDaemon(True)
        t.start()

    for hotelid in threads:
        t.join()

    time.sleep(3)

非常感谢!

2712 次点击
所在节点    Python
14 条回复
seoguess
2019-04-25 16:18:42 +08:00
如果 for 循环加上加锁不安全的话,那么 Queue 队列应该如何应用?

非生产者消费者模式感觉太复杂了,抓取的类只需要传入 cookie 跟对应的 id 来生成 url。

或者我把 url 跟 cookie 变成一个 tuple,然后判断 not Queue.empty(),然后通过 for 循环 + Queue.get()多线程去抓取内容可行?

求解惑,谢谢!
Juszoe
2019-04-25 19:02:17 +08:00
python 有全局锁 GIL,不用考虑线程不安全的问题,IO 型任务多线程随便用,除非你要把一段代码做成原子操作
scriptB0y
2019-04-25 19:07:35 +08:00
@Juszoe 错啦,Python 的 GIL 带来的作用是虚拟机的 opcode 是线程安全的,但是并不是每一行代码是线程安全的。请看这个例子:

https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/chapter2/06_Thread_synchronization_with_Lock_and_Rlock.html
scriptB0y
2019-04-25 19:11:31 +08:00
对于楼主的问题,不是线程安全的,会有多个线程同时操作数据库,但是只要你的任务之间不相关,没有共享资源,即使线程不安全也是没有问题滴。

但是有几点建议楼主可以参考:
1. 写文件肯定是非线程安全的,你这种用法应该使用 logging 模块,第一他是 thread safe 的,第二它是专门写 log 的。
2. 你不需要自己操作这么低级的 API 啦,可以看下 concurrent 模块,里面封装了线程池,用起来更方便! https://docs.python.org/3/library/concurrent.futures.html
Juszoe
2019-04-25 19:16:51 +08:00
@scriptB0y #3 学到了,可能我之前的线程都是任务不相关的,所以没有感觉到
scriptB0y
2019-04-25 19:19:42 +08:00
@Juszoe 是的,即使是相关的,也是需要一定的几率才会发现线程不安全的现象。Python3 GIL 调度策略换了,出现的几率更低了。
seoguess
2019-04-25 19:43:34 +08:00
@scriptB0y 谢谢!我研究一下。
Leigg
2019-04-25 19:45:03 +08:00
没有同步或者资源竞争问题不讨论线程安全,setDaemon 需要设置 true,否则当程序异常停止时,会有僵尸线程。
seoguess
2019-04-25 21:24:25 +08:00
@Leigg 谢谢!如果我的 main 函数大致结构是:

for cookie in cookies: # 1、获取 N 个不同的生成的 cookie
for id in id_list: #2、获取不同的 id 来生成 url
t = threading.Thread(claw(cookie,id), args) # 3、多线程获取内容、入库、记录错误
t.start()


是不是逻辑上,我把第三步中的采集入库、记录错误上锁了,1、2 中对应的 cookie 跟 id 不会出现多线程引起的数据错乱?
Leigg
2019-04-25 21:35:21 +08:00
记录错误用 logging,不能用 with open ;
入库记得 db 连接用连接池,或者入库单独一个函数,总之不要每个线程创建一个 db 连接,没有其他的同步或者资源竞争问题吧?
seoguess
2019-04-26 10:16:22 +08:00
@scriptB0y 我用 concurrent 模块重新修改了下代码,发现效率比我之前的代码差了好多....
for 循环: #获取 cookie:
threads = [ (i.get('hotelId'),headersCookie) for i in id_lines.find() ]
pool = ThreadPoolExecutor()
future_tasks = [ pool.submit(start_claw, t) for t in threads ]
wait(future_tasks, return_when=ALL_COMPLETED)

time.sleep(3)


3K 左右的链接,用时 382 秒

for 循环: #获取 cookie:
threads = []

for i in id_lines.find():
hotelId = i.get('hotelId')
threads.append(hotelId)


for hotelid in threads:
t = ClawData(hotelid,headersCookie)
t.setDaemon(True) #防止程序异常退出时,有僵尸进程存在
t.start()

for hotelid in threads:
t.join()

time.sleep(3)

用时:52 秒

请问为啥效率可以差别这么大?
zy342500
2019-04-26 17:43:58 +08:00
@seoguess pool = ThreadPoolExecutor(max_workers=20) 这样写 给他设置工作线程数
seoguess
2019-04-26 22:56:20 +08:00
@zy342500 谢谢,我以为放空的话就是没有限制。

max_workers=100,跑完用时 79 秒
max_workers=1000,跑完用时 49 秒
seoguess
2019-04-26 23:13:06 +08:00
原来 max_worker 为空的情况下,默认线程为 cpu 核数量*5,难怪花了 300+秒。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/558615

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX