我有一个csv文件(" SomeSiteValidURLs.csv"),其中列出了我需要抓取的所有链接。代码正在运行,将通过csv中的url,抓取信息并记录/保存在另一个csv文件中(" Output.csv")。但是,由于我计划在网站的大部分内容(对于> 10,000,000页)进行此操作,因此速度非常重要。对于每个链接,爬行并将信息保存到csv大约需要1秒,这对于项目的大小来说太慢了。所以我已经整合了多线程模块,令我惊讶的是它根本没有加速,它仍然需要1个人链接。我做错什么了吗?还有其他方法可以加快处理速度吗?
没有多线程:
import urllib2
import csv
from bs4 import BeautifulSoup
import threading
def crawlToCSV(FileName):
with open(FileName, "rb") as f:
for URLrecords in f:
OpenSomeSiteURL = urllib2.urlopen(URLrecords)
Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
OpenSomeSiteURL.close()
tbodyTags = Soup_SomeSite.find("tbody")
trTags = tbodyTags.find_all("tr", class_="result-item ")
placeHolder = []
for trTag in trTags:
tdTags = trTag.find("td", class_="result-value")
tdTags_string = tdTags.string
placeHolder.append(tdTags_string)
with open("Output.csv", "ab") as f:
writeFile = csv.writer(f)
writeFile.writerow(placeHolder)
crawltoCSV("SomeSiteValidURLs.csv")
使用多线程:
import urllib2
import csv
from bs4 import BeautifulSoup
import threading
def crawlToCSV(FileName):
with open(FileName, "rb") as f:
for URLrecords in f:
OpenSomeSiteURL = urllib2.urlopen(URLrecords)
Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
OpenSomeSiteURL.close()
tbodyTags = Soup_SomeSite.find("tbody")
trTags = tbodyTags.find_all("tr", class_="result-item ")
placeHolder = []
for trTag in trTags:
tdTags = trTag.find("td", class_="result-value")
tdTags_string = tdTags.string
placeHolder.append(tdTags_string)
with open("Output.csv", "ab") as f:
writeFile = csv.writer(f)
writeFile.writerow(placeHolder)
fileName = "SomeSiteValidURLs.csv"
if __name__ == "__main__":
t = threading.Thread(target=crawlToCSV, args=(fileName, ))
t.start()
t.join()
答案 0 :(得分:11)
你没有正确地并行化这个。你真正想做的是让你的for循环中的工作在许多工人中同时发生。现在你将所有工作移动到一个后台线程中,它同步完成整个事情。这根本不会改善性能(实际上只会略微伤害它)。
这是一个使用ThreadPool来并行化网络操作和解析的示例。尝试一次跨多个线程写入csv文件是不安全的,所以我们将返回已经写回父级的数据,并让父级将所有结果写入文件末尾。
import urllib2
import csv
from bs4 import BeautifulSoup
from multiprocessing.dummy import Pool # This is a thread-based Pool
from multiprocessing import cpu_count
def crawlToCSV(URLrecord):
OpenSomeSiteURL = urllib2.urlopen(URLrecord)
Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
OpenSomeSiteURL.close()
tbodyTags = Soup_SomeSite.find("tbody")
trTags = tbodyTags.find_all("tr", class_="result-item ")
placeHolder = []
for trTag in trTags:
tdTags = trTag.find("td", class_="result-value")
tdTags_string = tdTags.string
placeHolder.append(tdTags_string)
return placeHolder
if __name__ == "__main__":
fileName = "SomeSiteValidURLs.csv"
pool = Pool(cpu_count() * 2) # Creates a Pool with cpu_count * 2 threads.
with open(fileName, "rb") as f:
results = pool.map(crawlToCSV, f) # results is a list of all the placeHolder lists returned from each call to crawlToCSV
with open("Output.csv", "ab") as f:
writeFile = csv.writer(f)
for result in results:
writeFile.writerow(result)
请注意,在Python中,线程实际上只会加速I / O操作 - 由于GIL,CPU绑定操作(如解析/搜索BeautifulSoup
正在执行)实际上无法并行执行线程,因为一次只有一个线程可以执行基于CPU的操作。因此,您仍然可能看不到您希望采用这种方法的速度。当您需要在Python中加速CPU绑定操作时,您需要使用多个进程而不是线程。幸运的是,您可以轻松地看到此脚本如何使用多个进程而不是多个线程执行;只需将from multiprocessing.dummy import Pool
更改为from multiprocessing import Pool
即可。不需要进行其他更改。
修改强>
如果您需要将其扩展为包含10,000,000行的文件,则需要稍微调整此代码 - Pool.map
将传递给它的可迭代转换为列表,然后再将其发送出去对于你的工人来说,这显然不会很好地与10,000,000个参赛名单一起工作;在记忆中拥有整件事可能会让你的系统陷入困境。将所有结果存储在列表中的问题相同。相反,您应该使用Pool.imap
:
imap(func,iterable [,chunksize])
map()的lazier版本。
chunksize参数与map()使用的参数相同 方法。对于使用较大值的chunksize可以进行很长时间的迭代 使用默认值1来快速完成工作。
if __name__ == "__main__":
fileName = "SomeSiteValidURLs.csv"
FILE_LINES = 10000000
NUM_WORKERS = cpu_count() * 2
chunksize = FILE_LINES // NUM_WORKERS * 4 # Try to get a good chunksize. You're probably going to have to tweak this, though. Try smaller and lower values and see how performance changes.
pool = Pool(NUM_WORKERS)
with open(fileName, "rb") as f:
result_iter = pool.imap(crawlToCSV, f)
with open("Output.csv", "ab") as f:
writeFile = csv.writer(f)
for result in result_iter: # lazily iterate over results.
writeFile.writerow(result)
使用imap
,我们永远不会将所有f
同时放入内存,也不会立即将所有结果存储在内存中。我们记忆中最多的是chunksize
行f
,这应该更容易管理。