Python Web scraper在进行多处理时静默挂起
我正在抓取一个包含几十个基本URL的站点,这些基本URL最终链接到我解析的数千个xml页面,转换成一个Pandas数据框架,并最终保存到SQLite数据库。为了节省时间,我对下载/解析阶段进行了多处理,但是脚本在一定数量的页面(不确定有多少;在100到200之间)之后会自动挂起(停止收集页面或解析XML) 使用相同的解析器,但按顺序执行所有操作(无多处理)不会产生任何问题,因此我怀疑我在多处理方面做错了什么。可能是创建了太多的Parse_url类实例并阻塞了内存 以下是流程概述:Python Web scraper在进行多处理时静默挂起,python,multiprocessing,Python,Multiprocessing,我正在抓取一个包含几十个基本URL的站点,这些基本URL最终链接到我解析的数千个xml页面,转换成一个Pandas数据框架,并最终保存到SQLite数据库。为了节省时间,我对下载/解析阶段进行了多处理,但是脚本在一定数量的页面(不确定有多少;在100到200之间)之后会自动挂起(停止收集页面或解析XML) 使用相同的解析器,但按顺序执行所有操作(无多处理)不会产生任何问题,因此我怀疑我在多处理方面做错了什么。可能是创建了太多的Parse_url类实例并阻塞了内存 以下是流程概述: engine
engine = create_engine('sqlite:///path_to_db') # sqlalchemy
class Parse_url():
def __init__(self, url):
self.url = url
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
return True
def parse(self):
# parse xml, return dataframes
def collect_xml_links(start_url):
# collect and return a list of links to XML pages on this starting URL
def parse_urls(url):
with Parse_url(url) as parser:
collection_of_dfs = parser.parse()
return collection_of_dfs
def write_info_to_sql(result, db_name, engine):
# write info to SQLite database
start_urls = [url1, url2, url3, ... ]
with Pool(4) as pool:
results = pool.map(collect_xml_links, start_urls)
for urls in results:
url_list.extend(urls) # This works and returns urls
for i in range(0, len(url_list), 50): # Chunks of 50 to report progress
url_list_slice = url_list[i:i+50]
with Pool(4) as pool:
results = pool.map(parse_urls, url_list_slice)
for result in results:
write_info_to_sql(result, db_name, engine)
当脚本挂起时,它似乎总是使用相同数量的页面来挂起,但我不确定它是否完全相同。终止脚本会给出一个毫无帮助的回溯,指向results=pool.map(parse\u url,url\u list\u slice)
行
我的多处理设置是否存在明显问题?是否有可能我生成了太多的Parse_url类实例?很确定这并不理想,但它确实有效。假设问题是多进程创建的对象太多,我添加了一个明确的“del”步骤,如下所示:
for i in range(0, len(url_list), 50): # Chunks of 50 to report progress
url_list_slice = url_list[i:i+50]
with Pool(4) as pool:
results = pool.map(parse_urls, url_list_slice)
for result in results:
write_info_to_sql(result, db_name, engine)
del(result) # explicitly delete the dataframes when done with them
我不确定这些对象为什么会持久化,因为似乎没有对它们的引用,所以应该对它们进行垃圾收集。我尝试了几种其他方法来删除对它们的引用,例如
results = []
批处理完成后,仍然存在挂起。在第二个循环中,您在每个不理想的迭代中创建一个
池。PythonGC非常懒惰,因此您的软件在迭代过程中积累了大量资源
多处理.Pool
是为可重用性而设计的,因此您只能在脚本中创建一次
with Pool(4) as pool:
results = pool.map(collect_xml_links, start_urls)
for urls in results:
url_list.extend(urls) # This works and returns urls
for i in range(0, len(url_list), 50): # Chunks of 50 to report progress
url_list_slice = url_list[i:i+50]
results = pool.map(parse_urls, url_list_slice)
for result in results:
write_info_to_sql(result, db_name, engine)
我看不到任何地方定义了collect\u xml\u链接
或start\u URL
,修复程序在哪里?仍然没有def collect\u xml\u链接
或iterablestart\u url
。猜测类实例正在累积,我在write\u info\u to\u sql(result,db\u name,engine)
之后添加了del(result)
。这似乎已经解决了问题,但我想知道是否有更好的解决方案实际上这里的信息太少了。代码过于精简;可能是您正在以死锁的方式使用XML解析器。为什么Parse\u url
是一个上下文管理器?Python GC只关注循环引用;通过引用计数,立即清除资源。此外,这些资源是在单独的进程中创建的,而不是在主进程中创建的。我用错了词。我指的资源是池资源(管道、线程、映射结果等)。创建和销毁池的快速循环会给人一种资源泄漏的感觉,因为池中的线程延长了相关对象的生命周期。join
方法确保逻辑将等待所有资源释放,但OP不会调用它。尽管如此,推荐的方法是创建一个池
,并在整个软件生命周期中使用它。值得一提的是,按照这个建议,停止了挂起,刮板现在正确地完成了~5000个XML页面,据我所知这很有帮助!