耗时一周,我统计出来了npm下载量前30的仓库,第一竟是它!
作为一个前端开发人员,我们每天都在使用npm,但是你曾经是否和我一样好奇,下载量最大的包是哪个?每天下载多少次?他们的github star是多少?上周我偶然看到了一个库 glob, 每周竟然下载8000万次,与此同时,react只有1500万次,glob是最高的吗,第一又是谁呢?
统计结果展示
耗时一周,我统计出来了npm下载量前30的仓库,第一竟是它!supports-color!总下载量 26,108,633,482 次,但 github star 竟然只有 319 个。另外,我做了一个网站,统计了最近一周、最近一月、最近一年、总下载量等各个维度的图表,还没有做优化加载可能有点慢,网站地址 https://www.npmrank.net/
无图无真相,下面是网站截图

分析npm官网接口,获取某个包的下载量
打开浏览器控制台分析npm接口发现,同一个地址,比如 https://www.npmjs.com/package/lodash , 从npm首页 Popular libraries 中推荐的库点进去,接口返回的是JSON格式的数据,而从地址栏输入链接进去,返回是服务端渲染好的html。多次控制变量法未能定位是哪个header的原因,我就先不管了(当然不是睡大觉)
- 找到返回JSON的接口,copy -> copy as fetch 
- 粘贴到console 
- 复制header到postman,同时看到有下载量数据 
- 打开postman右侧的代码块,找到python代码 
- 复制到test.py,去掉某些空的header 
OK,这样获取某一个仓库的接口就完成了,通过这个接口我们可以拿到github地址,仓库版本,最近一年每周的下载量等
根据npm官方api,获取不同时间段的下载量
上面官网的接口只是最近一年各周的下载量,有没有其他时间段的呢,找了一圈后发现npm官网提供了这样的接口,官方api文档 ,
通过上面提供的接口,我们可以获取上周、上月、任何一个时间段的下载量,但是需要注意的是,官方api每次最多返回18个月的数据,最早是2015-01-10号的数据,所以统计总下载量时要分段获取每年的下载量后再累加,如果你想统计自己的包被安装了多少次,也是可以滴,接下来就是获取很多包名,循环下载后统计了
获取19年的排行
我在网上搜了一下npm download rank,发现只有 anvaka 19年做的统计符合想要的结果,他下载了npm全部的包并做了各种维度的分析,这个md是他统计的 top 1000依赖的包,不过被依赖的越多下载量越大,误差应该不会很大
- 保存文件到本地 SOURCE_FILE 
- 获取包名和仓库地址并存到sqlite数据库 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19- '''
 从md中拿到库名并存到数据库
 '''
 with open(SOURCE_FILE, 'r') as f:
 lines = f.readlines()
 for line in lines:
 name = re.findall(r'\[(.*?)\]', line)
 href = re.findall(r'\((.*?)\)', line)
 print('line\n', line)
 if name and href:
 get_pkgbase_query = '''SELECT * FROM pkgbase WHERE id = ?'''
 record_base = sql_obj.get(get_pkgbase_query, (name[0],), one=True)
 if record_base is None:
 insert_data_query = '''
 INSERT INTO pkgbase
 ('id', 'npm_url', 'github_url', 'homepage_url', 'version', 'license', 'github_star', 'size', created, updated)
 VALUES(?,?,?,?,?,?,?,?,?,?)
 '''
 sql_obj.update(insert_data_query, (name[0], NPM_BASE_URL + name[0], '', '', '', '', 0, '', 0, 0)) 
- 循环请求存储基本数据 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34- '''
 更新下载量
 '''
 async def main():
 all_data_query = '''SELECT * FROM pkgbase'''
 records = sql_obj.get(all_data_query)
 for index, record in enumerate(records):
 while True:
 print('id', record['id'], index)
 try:
 '''
 获取下载量并写入数据库
 '''
 href = NPM_BASE_URL + record['id']
 npm_response = requests.request("GET", href, headers=npm_headers)
 npm_data = npm_response.json()
 # pkgbase
 github_url = npm_data['packageVersion'].get('repository', '')
 homepage_url = npm_data['packageVersion'].get('homepage', '')
 version = npm_data['packument'].get('version', '')
 license = npm_data['packument'].get('license', '')
 # 有仓库两个license
 license = license if type(license) == str else '-'
 versions = npm_data['packument'].get('versions') if npm_data['packument'].get('versions') else []
 updated = datetime.datetime.fromtimestamp(versions[0]['date']['ts'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
 created = datetime.datetime.fromtimestamp(versions[len(versions) - 1]['date']['ts'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
 update_pkgbase_query = '''
 UPDATE pkgbase
 SET github_url = ?, homepage_url = ?, version = ?, license = ?, updated = ?, created = ?
 WHERE id = ?
 '''
 sql_obj.update(update_pkgbase_query, (github_url, homepage_url, version, license, updated, created, record['id']))
- 更新各个时间段的下载量 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51- '''
 获取某一时期的下载量
 '''
 def get_point_downloads(date_range, package_name):
 href = f'{NPM_BASE_API_POINT_URL}{date_range}/{package_name}'
 response = requests.request("GET", href)
 data = response.json()
 return data['downloads']
 '''
 获取全部下载量,npm每次最多返回18个月的数据,所以分段下载后再累加
 '''
 def get_point_all_downloads(package_name):
 start_time = 2015
 end_time = datetime.datetime.now().year
 all_downloads = 0
 for year in range(start_time, end_time + 1):
 dltype = f'{year}'
 date_range = f'{year}-01-01:{year + 1}-01-01'
 print('date_range', date_range)
 downloads = get_point_downloads(date_range, package_name)
 all_downloads += downloads
 print('new downloads',downloads)
 add_data_query = '''
 INSERT INTO pkgdownload
 ('id', 'dltype', 'downloads', 'timepoint')
 VALUES(?,?,?,?)
 '''
 sql_obj.update(add_data_query, (package_name, dltype, downloads, datetime.datetime.now()))
 return all_downloads
 ...
 # pkgdownload
 base_dltype = ['last-day', 'last-week', 'last-month', 'last-year', 'all-time']
 for dltype in base_dltype:
 if dltype == 'all-time':
 downloads = get_point_all_downloads(record['id'])
 else:
 downloads = get_point_downloads(dltype, record['id'])
 print('dltype', dltype)
 print('downloads', downloads)
 replaced_dltype = re.sub(r'\-', '_', dltype)
 add_pkgdownload_query = '''
 INSERT INTO pkgdownload
 ('id', 'dltype', 'downloads', 'timepoint')
 VALUES(?,?,?,?)
 '''
 sql_obj.update(add_pkgdownload_query, (record['id'], replaced_dltype, downloads, datetime.datetime.now()))
获取包的github数据
本来官网接口中返回的有ghapi字段,如 https://api.github.com/repos/lodash/lodash ,里面有stargazers_count字段就是star数,但是该接口每小时限速60次,所以无奈只能用爬虫了,代码如下
| 1 |  | 
第一次使用爬虫库 bs4 的 BeautifulSoup 模块,获取 github star 只有两行代码,也太方便了吧
就在刚才发现npm也有接口会返回github star数,如 https://api.github.com/repos/lodash/lodash/pulls?per_page=1 里的 stargazers_count ,等有时间我替换一下
开启服务
经过上面一通操作,我们现在有了pkgbase、pkgdownload 这样两张表,内容如下

接下来写两个接口,一个是返回下载量排名的的类型,过去一周,过去一年,总下载量等,供前端筛选,使用quart简单起个服务
| 1 |  | 
根据排名类型,返回对应的排行数据
| 1 |  | 

彩蛋
如果你看了上面开启服务的的代码,你可能会发现获取排行数据的接口其实还有一个top参数,最大是200条,但是由于图表不方便展示这么多的数据,如果你想自己看一下前200都有哪些包,可以复制接口改一下,如 https://www.npmrank.net/api/ranking/packages/last-day?top=200 ,如果你想查看超过200的排行,可以打开database.db的pkgdownload表查看
结束
以上就是获取npm排行的整个流程了,如果感觉有意思的话欢迎点个赞或者star,后端仓库地址 npmrank ,在线体验网页链接 https://www.npmrank.net/