一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
评论爬虫
起因
昨天有个朋友说自己写的爬虫没法实现翻页爬取,原因大概就是循环中并没有更新页数的变量。由于晚上已经很晚了,所以我就直接加个变量进去能跑通就行,今天正好有时间,把这个优化一下(水一篇博客)
分析
这次爬虫主要功能就是实现微博评论爬取,从他给我的代码里面已经看到了api,打开查看一下是json格式的数据
从内容看应该是关于疫情再度来袭网友们对医护人员赞美的评论。
为了不被反爬限制,作为案例代码,这里我把页面变化假设是max=
(其实应该是max_id&page一起决定),返回的是json格式,又有这么简单的页面变化方式,话不多说直接开始。
基本实现
这部分以最快速度实现爬虫,不考虑效率,直接串行。首先得到全部页面数,然后遍历得到每一页的json数据,再对json数据进行解析、保存。整个流程基本没有难点,引用正则对评论中的特殊字符进行处理就好,这里我直接引用的是他给我的代码中的字符处理并且把他的代码功能模块化了,直接给出来。
import requests
import pandas as pd
import time
import re
header = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9','Content-Type':'text/html;charset=utf-8','User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36 Edg/100.0.1185.29'}
Cookie = {'Cookie':'_T_WM=62953886347; XSRF-TOKEN=5c89c1; WEIBOCN_FROM=1110006030; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9WW1eMIsQaly_bK2_D_rFOrO5NHD95QfS02NSo.0e0z4Ws4Dqcj_i--ciK.Ri-8si--fi-z7iKysi--ciKL2i-z7i--ciKL2i-z7i--fiK.fi-2c; MLOGIN=1; SCF=ArbNNj9HacdcNYnuVNUQWJo_hYNuNhy9-jQQKs97pTLbVdUKb0HK5uQPLmmip7i33KV4JzC6MELS_lwPt92tZIk.; SUB=_2A25PT2DCDeRhGeNJ6lUX-S3PyTWIHXVssACKrDV6PUJbktAKLVmikW1NS_p_yW_ld5ywy4HBCtTjonFbqctB1vf5; SSOLoginState=1649086610; mweibo_short_token=2795505bef'}
def get_pages():
url = "https://m.weibo.cn/comments/hotflow?id=4754620872659321&mid=4754620872659321&max_id_type=0"
response = requests.get(url, headers=header, cookies=Cookie).json()
if response['ok']==0:
print("请求失败")
return 0
return response['data']['max']
def get_json(url):
response = requests.get(url, headers=header, cookies=Cookie).json()
if response['ok'] == 0:
print("请求失败")
return None
else:
return response
def json_parse(json):
res=[]
for data in json['data']['data']:
text=data['text']
comment = re.sub('[^\u4e00-\u9fa5]', '', text)
comment=re.sub(r'(<span.*>.*</span>)*(<a.*>.*</a>)?','',comment)
res.append([data['created_at'],data['user']['id'],data['user']['screen_name'],comment])
#return res
df=pd.DataFrame(res)
df.to_csv('./comments.csv',header=False,encoding='utf-8-sig',index=False,mode='a+')
def main():
total_pages = get_pages()
# 设置爬取50页
set_page=50
crawl_pages=set_page if set_page<=total_pages else total_pages
for i in range(1,crawl_pages+1):
print(f"正在爬取第{i}页")
base_url="https://m.weibo.cn/comments/hotflow?id=4753468341096374&mid=4754620872659321&max="+str(i)+"&max_id_type=0"
json_parse(get_json(base_url))
if __name__ == '__main__':
start=time.time()
main()
print(f"总共耗时:{time.time()-start} s")
进阶实现
python目前异步机制其实已经算还可以了,对于爬虫这种io密集型的操作,异步请求效率肯定是高于串行的。
有了上面的基本代码,相对应的改成异步请求也并不困难。首先,既然是异步实现那我们就需要异步请求的框架— aiohttp
相对应的我们写入文件也需要异步写入—aiofiles
pip install aiohttp,aiofiles
就好
然后把requests换成aiohttp请求,然后请求所有需要爬取的页面,并且用解析处理的异步函数嵌套请求的异步函数,最后全部添加进tasks,剩下的只用等待任务完成就好了。
# 异步版本
import asyncio
import aiohttp
import aiofiles
import time
import re
import requests
header = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9','Content-Type':'text/html;charset=utf-8','User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36 Edg/100.0.1185.29'}
Cookie = {'Cookie':'_T_WM=62953886347; XSRF-TOKEN=5c89c1; WEIBOCN_FROM=1110006030; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9WW1eMIsQaly_bK2_D_rFOrO5NHD95QfS02NSo.0e0z4Ws4Dqcj_i--ciK.Ri-8si--fi-z7iKysi--ciKL2i-z7i--ciKL2i-z7i--fiK.fi-2c; MLOGIN=1; SCF=ArbNNj9HacdcNYnuVNUQWJo_hYNuNhy9-jQQKs97pTLbVdUKb0HK5uQPLmmip7i33KV4JzC6MELS_lwPt92tZIk.; SUB=_2A25PT2DCDeRhGeNJ6lUX-S3PyTWIHXVssACKrDV6PUJbktAKLVmikW1NS_p_yW_ld5ywy4HBCtTjonFbqctB1vf5; SSOLoginState=1649086610; mweibo_short_token=2795505bef'}
# 这里只有一次请求所以还是用requests
def get_pages():
url = "https://m.weibo.cn/comments/hotflow?id=4754620872659321&mid=4754620872659321&max_id_type=0"
response = requests.get(url, headers=header, cookies=Cookie).json()
if response['ok']==0:
print("请求失败")
return 0
return response['data']['max']
async def get_json(url):
async with aiohttp.ClientSession(headers=header,cookies=Cookie) as session:
async with session.get(url,headers=header) as response:
result=await response.json()
if result['ok'] == 0:
print("请求失败")
return None
return result
async def json_parse(url):
print(url)
json=await get_json(url)
for data in json['data']['data']:
text=data['text']
comment = re.sub('[^\u4e00-\u9fa5]', '', text)
comment=re.sub(r'(<span.*>.*</span>)*(<a.*>.*</a>)?','',comment)
async with aiofiles.open("./comments_V2.csv", mode='a', encoding='utf-8-sig') as f:
await f.writelines(f"{data['created_at']},{data['user']['id']},{data['user']['screen_name']},{comment} \n")
await f.close()
async def main():
total_pages = get_pages()
# 设置爬取50页
set_page=50
crawl_pages=set_page if set_page<=total_pages else total_pages
url_lists=[f"https://m.weibo.cn/comments/hotflow?id=4753468341096374&mid=4754620872659321&max={str(i)}&max_id_type=0" for i in range(1,crawl_pages+1)]
loop=asyncio.get_event_loop()
tasks=[json_parse(url) for url in url_lists]
#loop.run_until_complete(asyncio.gather(*tasks))
await asyncio.gather(*tasks)
if __name__ == '__main__':
start = time.time()
asyncio.run(main())
print(f"总共耗时:{time.time() - start} s")
两种方式得到的数据量是一样的,但是时间上相差一倍多,而且请求的页数越大两者时间相差的也就越大。
题外话:有人说aiofiles写入和原生的写入其实差别不大,这里我没有具体测试,不过既然是异步我还是选择异步框架支持吧。
结束
最近在外面赶项目,重新捡起了多年没写的c++。一些偏底层的东西写起来确实难受,平时在学校自己写点代码从来不用那么考虑效率、程序的实时性……Python写多了也忘了多线程之间的处理细节,很多时候信号量之间互斥情况并没有考虑到,写完的代码不是这里没有考虑到加锁就是那里忘了wait。相对来说还是go写起来舒服,实在不行还有只读只写的channel能帮我消除一部分竞态的考虑。有大神带着写项目从中自己有收获了很多,等项目写完回学校打算重新复习一下之前学的多线程相关的知识,好好记录一下学习笔记。