2020-06-04更新
下面附上笔者提供的源码(已经验证过功能。后续会在同一个工程中更新Facebook和Insgram的相关爬虫代码)。
https://github.com/zhangjz777/yfi_source
下面是工程关于Youtube相关的代码注意事项介绍。
- 如遇到此报错“java.lang.NoSuchMethodError: javax.servlet.ServletContext.getVirtualServerName()Ljava/lang/String”,是由于pom引入的jar包中包含的低版本servlet-api 导致的,请自行排除多余的依赖。详情可以参考这篇文章:SpringBoot报错:java.lang.NoSuchMethodError: javax.servlet.ServletContext.getVirtualServerName()Ljava/lang/String;
- 请确保你的IDE中,有lombok插件支持。
- 请确保你已正确申请youtube api key,代码中的key是笔者的,现已弃用。
- 因笔者水平有限,若有什么代码问题,请文明交流,留在你的评论或者私信。
一、前期调研
开始决定做Youtube的时候先是查阅了百度和Google上的一些搜索结果。当我以youtube爬虫作为搜索关键词时,结果并不尽人意:在这其中部分爬虫是以下载视频为目的,没有获取视频其他信息,因此并不是我所需要的。还有部分商业网站提供付费接口服务,但是秉着能省则省的原则,这种肯定是不会考虑的,因此只能另辟奇径。当我切换以“youtube api”为关键词搜索时,发现了下面这个。
没错,Google官方提供了关于Youtube API给开发者使用。也给予此,开发者也没有必要自己花费大力气去破解youtube的api。因此,本篇文章本质上是一篇讲解youtube 官方API使用教程的文章,若已有相关开发经验的读者可以阅读另外两篇,如果你尚未了结此api库的使用方法那这篇文章或许可以给你一些参考,同时我也会附上一些我在实际开发过程中遇到的一些问题和相关的注意事项供你参考。
二、Youtube API的使用
本文中的使用方式是在Java工程中,其他语言的使用教程请参考官方文档。
1、如何才能使用YouTube API?
(1)登录Google云平台
首先你需要登录Google的云平台(请自备Google邮箱账号)
https://console.developers.google.com/apis/api/youtube.googleapis.com
(2)开通Youtube API V3服务
登录成功后在图示中找到Youtube API v3开通
按照如下顺序创建API秘钥用于发起请求时的权限验证
(3)阅读API开发文档
将API复制到别处保存后,就可以前往API使用文档页面了。
https://developers.google.com/youtube/v3/docs
这里包含了常用的业务模型,在笔者的实际开发过程中使用了Channel、Playlist、Videos、Search进行操作。
2、API的使用方式
(1)在Java项目中引入jar包依赖
下面使用到的版本号可根据官方文档提供的填入
<!-- YouTube Data V3 support --><dependency><groupId>com.google.apis</groupId><artifactId>google-api-services-youtube</artifactId><version>v3-rev182-1.22.0</version></dependency><!-- Required for any code that makes calls to the YouTube Analytics API --><dependency><groupId>com.google.apis</groupId><artifactId>google-api-services-youtubeAnalytics</artifactId><version>v3-rev182-1.22.0</version></dependency><!-- Required for any code that makes calls to the YouTube Reporting API --><dependency><groupId>com.google.apis</groupId><artifactId>google-api-services-youtubereporting</artifactId><version>v1-rev10-1.22.0</version></dependency>
另外请确保你有可以用的本地网络代理可以访问外网,具体使用方式
其中host表示,你设置的代理ip,port表示代理使用的端口。
System.setProperty("http.proxyHost", host);System.setProperty("http.proxyPort", port);System.setProperty("https.proxyHost", host);System.setProperty("https.proxyPort", port);
(2)代码示例
查询指定channel id的频道下的所有视频,根据视频发布时间排序
参考文档
https://developers.google.com/youtube/v3/docs/search
首先你需要的channel id 如何获取呢?根据我的观察,应该是有两种情况的。
频道首页url自带channel id
图中链接为:https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
即channel 后边的 “UCXuqSBlHAE6Xw-yeJA0Tunw” 即为当前频道的channel id,可以在api 中指定该参数。
频道首页url不包含channel id
图中链接为:https://www.youtube.com/user/EVOTV
然而,官方提供的api库里,并没有根据user name 来搜索视频的api。这时,该怎么处理呢?
其实很简单,在当前页面下打开调试窗口(F12),在Elements这一栏中搜索channel,找到与当前频道名字相同的链接,这就是该用户“隐藏”的channel id。
代码请求参考如下
YouTube youtube = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, request -> {}).setApplicationName("youtube-cmdline-search-sample").build();YouTube.Search.List search = youtube.search().list("id,snippet");String apiKey = "你在Google云平台申请到的api key";search.setKey(apiKey);// 接口返回数据模型 search.setType("video");// 设置需要接口返回的字段
search.setFields("items(id/kind,id/videoId,snippet/title,snippet/thumbnails/default/url),nextPageToken,pageInfo,prevPageToken");// 返回的最大记录条数search.setMaxResults(50);// 设置要查询的channel idsearch.setChannelId(channelId);search.setOrder("date");SearchListResponse searchResponse;while (true) {try {searchResponse = search.execute();break;} catch (GoogleJsonResponseException e) {if (403 == e.getDetails().getCode()) {// 配额用尽,使用下一个LogUtil.error("youtube api 配额用尽,尝试替换api key");seatch.setKey("替换新的api key");return;} else {LogUtil.error("其它异常,结束任务");throw e;}}}List<SearchResult> searchResultList = searchResponse.getItems();List<SearchResult> allRecord = new ArrayList<>();if (searchResultList != null) {PageInfo pageInfo = searchResponse.getPageInfo();// 根据分页获取全部数据allRecord.addAll(searchResultList);while (true) {// 设置分页的参数search.setPageToken(searchResponse.getNextPageToken());searchResponse = search.execute();if (searchResponse == null ||AppUtil.isNull(searchResponse.getItems())) {break;}List<SearchResult> items = searchResponse.getItems();if (AppUtil.isNull(items)) {break;}allRecord.addAll(items);if (items.size() < 50) {break;}}if (AppUtil.isNull(allRecord)) {return;}// 获取所有的video idList<String> videoIds = allRecord.stream().map(SearchResult::getId).map(ResourceId::getVideoId).collect(Collectors.toList());videoIds.forEach(System.out::println);}
查询指定video的详细信息(使用参数video id)
参考文档
https://developers.google.com/youtube/v3/docs/videos
video id的来源
在视频详情页中:https://www.youtube.com/watch?v=P_7piye1Who
v后边的参数“P_7piye1Who”即为当前视频的video id。
获取使用前一个api 获取频道下的视频,返回的也是video id。
YouTube youtubeVideo = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, request -> {}).setApplicationName("youtube-cmdline-search-sample").build();YouTube.Videos.List search = youtubeVideo.videos().list("id,snippet");
// 设置要查询的字段信息
search.setFields("items(id,snippet/publishedAt,snippet/title,snippet/description,snippet/tags,snippet/channelId,snippet/thumbnails)");String apiKey = "申请到的API key";if (apiKey == null) {return null;}search.setKey(apiKey);search.setMaxResults(50);StringBuilder videoIdStr = new StringBuilder();if (videoIds.size() > 1) {for (int i = 0; i < videoIds.size(); i++) {videoIdStr.append(videoIds.get(i));if (i != videoIds.size() - 1) {videoIdStr.append(",");}}} else {videoIdStr.append(videoIds.get(0));}// 此处可以放入最多50个id进行查询,以逗号分隔search.setId(videoIdStr.toString());VideoListResponse response;while (true) {try {response = search.execute();break;} catch (GoogleJsonResponseException e) {if (403 == e.getDetails().getCode()) {// 配额用尽,使用下一个LogUtil.error("youtube api 配额用尽,尝试替换api key");String newApiKey = "尝试替换新的配额";} else {LogUtil.error("其它异常,结束任务");throw e;}}}if (response == null) {return null;}List<Video> items = response.getItems();if (AppUtil.isNull(items)) {return null;}for (Video item : items) {// 标签数据List<String> tags = item.getSnippet().getTags();StringBuilder stringBuilder = new StringBuilder();if (AppUtil.isNotNull(tags)) {for (String tag : tags) {stringBuilder.append(tag).append(",");}}String description = item.getSnippet().getDescription();if (description == null) {description = "";}System.out.println("description:" + description);System.out.println("channel id:" + channel.getId());System.out.println("title:" + item.getSnippet().getTitle());System.out.println("thumbnails" + item.getSnippet().getThumbnails().getDefault().getUrl());System.out.println("releaseTime:" + new Date(item.getSnippet().getPublishedAt().getValue()));}
查询指定频道的信息(使用channel id)
参考文档
https://developers.google.com/youtube/v3/docs/channels
YouTube youtubeChannel = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, request -> {}).setApplicationName("youtube-cmdline-search-sample").build();YouTube.Channels.List search = youtubeChannel.channels().list("id,snippet");search.setFields("items(snippet/publishedAt,snippet/title,snippet/description,snippet/thumbnails)");String apiKey = "从Google云平台获取的api key";search.setKey(apiKey);search.setMaxResults(50); String channelId = "需要查询的channel id";search.setId(channelId);ChannelListResponse response;while (true) {try {response = search.execute();break;} catch (GoogleJsonResponseException e) {if (403 == e.getDetails().getCode()) {// 配额用尽,使用下一个LogUtil.error("youtube api 配额用尽,尝试替换api key");String newApiKey = getUsableApiKey(apiKey);return null;} else {LogUtil.error("其它异常,结束任务");throw e;}}}if (response == null || AppUtil.isNull(response.getItems())) {return null;}List<Channel> items = response.getItems();if (AppUtil.isNull(items)) {return null;}Channel channel = items.get(0);System.out.println("channelId:" + channelId);System.out.println("频道名称:" + channel.getSnippet().getTitle());System.out.println("缩略图 thumbnails:" + channel.getSnippet().getThumbnails().getDefault().getUrl());System.out.println("频道简介:" + channel.getSnippet().getDescription());
}
查询指定播放列表下的视频
参考文档
https://developers.google.com/youtube/v3/docs/playlistItems
播放列表是Youtube区别于Facebook和Ins而独有的,笔者理解播放列表其实相当于对视频合集的分类。
那如何获取playlist id呢?其实与video id一样,点进去一个播放列表详情页,查看它的url
https://www.youtube.com/watch?v=2i1PdfaAKFA&list=PL8mG-RkN2uTwChYF-gaygFQero5g5IXgr
其中list= 后边的 “PL8mG-RkN2uTwChYF-gaygFQero5g5IXgr”即为当前播放列表的playlist id。同时,也可以通过官方提供的api获取一个频道下所有的播放列表,这里不再赘述。
下面是演示代码
YouTube youtube = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, request -> {}).setApplicationName("youtube-cmdline-search-sample").build();YouTube.PlaylistItems.List search = youtube.playlistItems().list("id,snippet");String apiKey = "获取的youtube api key";search.setKey(apiKey);// 需要接口返回的字段信息
search.setFields("items(snippet/resourceId/videoId),nextPageToken,pageInfo,prevPageToken");search.setMaxResults(50);String playlistId = "获取的播放列表";search.setPlaylistId(playlistId);PlaylistItemListResponse searchResponse;while (true) {try {searchResponse = search.execute();break;} catch (GoogleJsonResponseException e) {if (403 == e.getDetails().getCode()) {// 配额用尽,使用下一个LogUtil.error("youtube api 配额用尽,尝试替换api key");String newApiKey = "尝试获取新的api ";if (newApiKey == null) {// 结束当前任务LogUtil.error("系统当前配额已用完");return;}if (!apiKey.equals(newApiKey)) {// 返回新的api-key则再次尝试search.setKey(newApiKey);} else {// api-key相同则退出return;}} else {LogUtil.error("其它异常,结束任务");throw e;}}}List<PlaylistItem> searchResultList = searchResponse.getItems();List<PlaylistItem> allRecord = new ArrayList<>();if (searchResultList != null) {PageInfo pageInfo = searchResponse.getPageInfo();if (pageInfo.getTotalResults() < 50) {// 添加第一页数据List<String> allVideoIds = searchResultList.stream().map(PlaylistItem::getSnippet).map(PlaylistItemSnippet::getResourceId).map(ResourceId::getVideoId).collect(Collectors.toList());// 打印所有视频idallVideoIds.forEach(System.out::println);return;} else {// 获取多页数据allRecord.addAll(searchResultList);while (true) {// 请求下一页的数据search.setPageToken(searchResponse.getNextPageToken());searchResponse = search.execute();if (searchResponse == null ||AppUtil.isNull(searchResponse.getItems())) {break;}List<PlaylistItem> items = searchResponse.getItems();if (AppUtil.isNull(items)) {break;}allRecord.addAll(items);if (items.size() < 50) {break;}}}}if (AppUtil.isNull(allRecord)) {return;}List<String> videoIds = allRecord.stream().map(PlaylistItem::getSnippet).map(PlaylistItemSnippet::getResourceId).map(ResourceId::getVideoId).collect(Collectors.toList());videoIds.forEach(System.out::println);
(3)API相关注意事项
分页相关注意事项
以上绝大部分的请求api 的最大返回条数为50条,分页大小设置超过接口限制则会返回错误提示。
再请求下一页数据时,必须用到上一次请求的 一个参数,像下面这样:
search.setPageToken(searchResponse.getNextPageToken());searchResponse = search.execute();
配额限制相关
每个Google账户申请到的api key每日有10000个配额的限制。
每个接口消耗的配额并不是相同的,比如Search接口每次消耗100个单位的配额,而Video的相关只消耗1-2个单位。具体每个接口的消耗配额数量可以参考文档。
因此,可以事先做好配额估算,是非常必要的,可以提前多申请几个api key 用于轮换。
另一个需要注意的点是,配额消耗完,接口并不是返回错误信息提醒,而是直接抛出异常。因此,需要在代码中做捕获处理,像这样:
try {searchResponse = search.execute();break;} catch (GoogleJsonResponseException e) {if (403 == e.getDetails().getCode()) {System.out.println("配额用尽,使用下一个");}}
另外,你也可以在Google云平台上申请扩充配额,但是这个流程想当繁琐复杂,对于个人开发者来说几乎不能实现。此外高并发的请求API会导致Google拒绝相应,请仔细阅读官方文档。
配额消耗情况表格:
三、订阅功能实现
看到这里可能有读者会问:假如我想实现一个订阅功能呢?我希望订阅一个频道后,Youtube可以将更新信息同步推送给我指定的服务器。
答案是可以的。
笨重但比较直接的想法是采用轮询机制,即每隔一段时间去请求频道下的视频接口,根据返回的视频名称判断是否是新视频。这样做的缺点显而易见,一是除非轮询时间间隔特别短,否则基本没法保证时效性。二是频繁的访问查询接口会浪费掉大量的api 配额,因此这不是一种优雅的解决方案。
为了避免这个问题官方提供了发布订阅系统,一种基于Webhooks实现的订阅推送(对于Webhooks机制不清楚的同学可以了解后再去尝试),可以实现几乎实时的更新推送。
详细资料参考官方的这篇文档
https://developers.google.com/youtube/v3/guides/push_notifications
其主要流程就是,在下面这个网址中添加订阅频道和回调地址
https://pubsubhubbub.appspot.com/subscribe
这样你就会在你的服务器上接收到这样的更新信息
<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015"xmlns="http://www.w3.org/2005/Atom"><link rel="hub" href="https://pubsubhubbub.appspot.com"/><link rel="self" href="https://www.youtube.com/xml/feeds/videos.xml?channel_id=CHANNEL_ID"/><title>YouTube video feed</title><updated>2015-04-01T19:05:24.552394234+00:00</updated><entry><id>yt:video:VIDEO_ID</id><yt:videoId>VIDEO_ID</yt:videoId><yt:channelId>CHANNEL_ID</yt:channelId><title>Video title</title><link rel="alternate" href="http://www.youtube.com/watch?v=VIDEO_ID"/><author><name>Channel title</name><uri>http://www.youtube.com/channel/CHANNEL_ID</uri></author><published>2015-03-06T21:40:57+00:00</published><updated>2015-03-09T19:05:24.552394234+00:00</updated></entry>
</feed>
四、页面展示
当我们获取到了足够的数据后,如何在我们页面上嵌入Youtube的视频呢?其实很简单,通过iframe直接嵌入到你的document中就可以实现了(注意替换你的video id)。
<iframe src="https://www.youtube.com/embed/KZ7Z2x4FIWw?autoplay=0&autohide=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" style="height: 2rem;"></iframe>
最终实现效果:
五、总结
对于Youtube API的使用笔者也是第一次接触,使用上难免会有不准确之处,欢迎评论区交流,指正笔者的错误。