Facebook Java爬虫获取视频数据

前言部分

前置说明

​ 截止到本文发表前,该爬虫方法因为htmlunit的问题,已经无法正常运行,由于本人后续不再维护相关功能,所以不会修复此问题。如果是迫切需要解决方案的读者可以忽略本文,寻求其它解决方案。如果对此 方案略感兴趣,可以阅读源码和方法进行参考。

​ 另笔者并不是专业爬虫开发,因此本文是以Java开发尝试实现的一个简单程序。使用到的工具为htmlunit + jsoup。该方案,主要实现的功能是:获取到指定频道下的所有视频的详细信息(并不下载视频文件)。获取到的数据结构如下所示(部分长数据做了删减以###标识,在前言文章中有全部数据以供参考),读者可以参考并筛选是否有自己需要的字段信息。另外,如果读者有其他解决方案,期待在评论区留言,不吝赐教。

源码地址:GitHub

{
                  "node": {
                     "feed_unit": {
                        "__typename": "Story",
                        "encrypted_tracking": "AZXEE###",
                        "viewability_config": [
                           8,
                           5
                        ],
                        "attachments": [
                           {
                              "media": {
                                 "__typename": "Video",
                                 "publish_time": 1532093178,
                                 "container_story": null,
                                 "play_count": 32,
                                 "total_posts": 1,
                                 "post_play_count": 32,
                                 "feedback": {
                                    "id": "ZmVlZGJhY2s6MjQ5MDAxNjkyMzY3OTYz",
                                    "top_reactions": {
                                       "edges": [
                                          {
                                             "i18n_reaction_count": "13",
                                             "node": {
                                                "localized_name": "\u3044\u3044\u306d\uff01",
                                                "reaction_type": "LIKE",
                                                "id": "1635855486666999"
                                             }
                                          }
                                       ]
                                    },
                                    "i18n_reaction_count": "13",
                                    "important_reactors": {
                                       "nodes": [
                                          
                                       ]
                                    },
                                    "reaction_count": {
                                       "count": 13
                                    },
                                    "viewer_actor": null,
                                    "viewer_feedback_reaction_info": null
                                 },
                                 "id": "249001692367963",
                                 "image": {
                                    "uri": "https://scontent-nrt1-1.x###"
                                 },
                                 "has_viewer_watched_show_video": false,
                                 "is_live_streaming": false,
                                 "url": "https://www.facebook.com/JIMSKO07/videos/249001692367963/",
                                 "video_channel": {
                                    "__typename": "VirtualVideosChannel",
                                    "id": "dmlkZW9DaGFubmVsOjM4NzQzNzg4ODEwNjMwMToyNDkwMDE2OTIzNjc5NjM="
                                 },
                                 "owner": {
                                    "__typename": "Page",
                                    "id": "210402066227926",
                                    "name": "Racing jaya"
                                 },
                                 "is_show_video": false,
                                 "playable_duration_in_ms": 58942,
                                 "viewer_last_play_position_ms": null,
                                 "broadcast_status": null,
                                 "smart_preview_video": {
                                    "id": "249003012367831",
                                    "original_width": 400,
                                    "original_height": 224,
                                    "broadcaster_origin": null,
                                    "broadcast_status": null,
                                    "is_live_streaming": false,
                                    "is_looping": true,
                                    "loop_count": 23,
                                    "is_spherical": false,
                                    "permalink_url": "https://www.facebook.com/JIMSKO07/videos/249003012367831/",
                                    "captions_url": null,
                                    "dash_prefetch_experimental": [
                                       "478219935963026v"
                                    ],
                                    "can_use_oz": true,
                                    "playable_url_dash": "https://www.facebook.com/video/playback/dash_mpd_debug.mpd?v=249003012367831&dummy=.mpd",
                                    "dash_manifest": "\u003C?xm###",
                                    "min_quality_preference": null,
                                    "playable_url": "https://video-nrt1-1.xx.fbcdn.net/v/t42.9040-2/37336605_19181526###",
                                    "playable_url_quality_hd": null,
                                    "autoplay_gating_result": "gatekeeper",
                                    "viewer_autoplay_setting": "default_autoplay",
                                    "can_autoplay": false,
                                    "drm_info": "{\"drm_helper\":null,\"video_license_uri_map\":{},\"graph_###",
                                    "captions_settings": null
                                 },
                                 "playable_duration": 59,
                                 "live_viewer_count_read_only": 0,
                                 "breaking_status": false,
                                 "is_premiere": false,
                                 "is_subscribed_to_live_video_schedule": false,
                                 "broadcast_schedule": null,
                                 "name": "",
                                 "savable_description": {
                                    "text": "",
                                    "image_ranges": [
                                       
                                    ],
                                    "inline_style_ranges": [
                                       
                                    ],
                                    "aggregated_ranges": [
                                       
                                    ],
                                    "ranges": [
                                       
                                    ]
                                 },
                                 "playlist_video_channel": {
                                    "__typename": "Page",
                                    "id": "210402066227926"
                                 },
                                 "video_view_model": {
                                    "episode_number": null
                                 },
                                 "sotto_content": null
                              }
                           }
                        ],
                        "attached_story": null,
                        "id": "UzpfSTIxMDQwMjA2NjIyNzkyNjpWSzoyNDkwMDE2OTIzNjc5NjM="
                     },
                     "__typename": "VideoHomeFeedUnitSectionComponent"
                  },
                  "cursor": null
               }

一、开始前的准备

(1)网络环境

​ 请确保代码运行的机器可以访问外网或者可以使用的代理。

(2)添加maven依赖

            <dependency>
                <groupId>net.sourceforge.htmlunit</groupId>
                <artifactId>htmlunit</artifactId>
              <version>2.25</version>
            </dependency>
            <dependency>
                <groupId>org.jsoup</groupId>
                <artifactId>jsoup</artifactId>
                <version>1.9.2</version>
            </dependency>

(3)关于Facebook数据

请注意视频频道地址:https://www.facebook.com/watch/cnliziqi/

​ 经过观察多个频道,可以得出结论:“watch”后的字符串“cnliziqi”是唯一值,即可作为channel_id(当然,实际上Facebook肯定有真正意义上的channel_id,只是这里我们为了方便这样称呼)。

二、相关概念

(1)关于HtmlUnit

关于HtmlUnit的相关概念可以参考下边的博文介绍,笔者不再额外表述。

HtmlUnit的使用
简介
HtmlUnit是一个无界面浏览器Java程序。它为HTML文档建模,提供了调用页面、填写表单、单击链接等操作的API。就跟你在浏览器里做的操作一样。

HtmlUnit不错的JavaScript支持(不断改进),甚至可以使用相当复杂的AJAX库,根据配置的不同模拟Chrome、Firefox或Internet Explorer等浏览器。

HtmlUnit通常用于测试或从web站点检索信息。

HtmlUnit使用场景
httpClient的局限性
我们一般可以使用apache的HttpClient组件进行HTML页面信息的获取,HttpClient实现的http请求返回的响应一般是纯文本的document页面,即最原始的html页面。

对于一个静态的html页面来说,使用httpClient足够将我们所需要的信息爬取出来了。但是对于现在越来越多的动态网页来说,更多的数据是通过异步JS代码获取并渲染到的,最开始的html页面是不包含这部分数据的。

通过HtmlUnit库,加载一个完整的Html页面,然后就可以将其转换成我们常用的字串格式,用其他工具如Jsoup来获取其中的元素了。当然也可以直接在HtmlUnit提供的对象中获取网页元素,甚至是操作如按钮、表单等控件。除了不能像可见浏览器一样用鼠标键盘浏览网页之外,我们可以用HtmlUnit来模拟操作其他的一切操作,像登录网站,撰写博客等等都是可以完成的。当然网页内容爬取是最简单的一个应用了。
————————————————
版权声明:本文为CSDN博主「私念Moposion」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40908515/article/details/90674287

(2) 关于Jsoup

​ 关于Jsoup,做过java爬虫的应该都了解过,它是一个用来解析静态页面的HTML解析器,在本文中主要用来解析从Facebook的频道首页中解析出一些我们需要的数据。本文不再赘述其相关概念和用法,详细可参考其官方文档:

https://www.open-open.com/jsoup/

(3)Facebook的数据问题(Web端)

​ 笔者在分析过Facebook首页及其数据加载流程后发现,其首页视频数据除了前几条数据是跟随页面一起加载进来的,剩下的数据(指下拉加载)是通过接口请求加载出来的,也就是说并不能通过jsoup直接请求页面然后解析dom。

​ 当笔者下拉加载更多的视频数据时,动态请求的分页接口如下所示:

​ 其中,前六条随着首页加载进来的数据也并不是直接出现在dom元素中,是需要其他js函数的加载才有。因此,我们就需要一个能够异步加载的工具(即HtmlUnit),等获取数据的js运行完成后再找到我们需要的数据。后边的数据,我们就通过视频分页接口自己去获取了。

三、程序实例

(1)根据频道id获取该频道下的所有视频

前面已经提到,一个频道的全量视频数据其实是分为两部分:

​ 1、与首页一起加载出的,最新的几条视频记录

​ 2、下拉刷新,通过接口获得的历史视频记录

​ 所以我们这里也要分两个步骤去获取。请参考下面代码:

/**
     * @desc 获取首页数据
     * @param channelId channelId (ex:   cnliziqi)
     * @return 关键数据节点信息
     */
    public String getVideoByChannelId(String channelId) {
        try {
            Thread.sleep(RandomUtils.nextLong(3, 8) * 1000);
            // 构造WebClient 模拟Chrome 浏览器
            WebClient webClient = new WebClient(BrowserVersion.CHROME);
            // 支持JavaScript
            webClient.getOptions().setJavaScriptEnabled(true);
            webClient.getOptions().setCssEnabled(false);
            webClient.getOptions().setActiveXNative(false);
            webClient.getOptions().setCssEnabled(false);
            webClient.getOptions().setThrowExceptionOnScriptError(false);
            webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
            webClient.getOptions().setTimeout(15000);
            String host = "127.0.0.1";
            String port = "1087";
            System.setProperty("http.proxyHost", host);
            System.setProperty("http.proxyPort", port);
            System.setProperty("https.proxyHost", host);
            System.setProperty("https.proxyPort", port);
            ProxyConfig proxyConfig = new ProxyConfig();
            proxyConfig.setProxyHost(host);
            proxyConfig.setProxyPort(Integer.parseInt(port));
            // 设置WebClient的代理
            webClient.getOptions().setProxyConfig(proxyConfig);
            // 拼接你要访问的视频首页地址
            HtmlPage rootPage = webClient.getPage("https://www.facebook.com/watch/" + channelId + "/");
            //设置一个运行JavaScript的时间
            webClient.waitForBackgroundJavaScript(5000);
            String html = rootPage.asXml();
            Document document = Jsoup.parse(html);
            if (document == null) {
                LogUtil.error("获取facebook页面失败.channelId:" + channelId);
                return null;
            }
            Elements scripts = document.getElementsByTag("script");
            if (AppUtil.isNull(scripts)) {
                LogUtil.error("获取facebook页面失败.channelId:" + channelId);
                return null;
            }
            String sourceStr = null;
            for (Element script : scripts) {
                // 只截取script节点的前面一部分,节省匹配时间
                String scriptHtml = script.html().substring(0, Math.min(script.html().length(), 200));
                // 前几条视频所在的节点,可以使用浏览器分析dom找到
                if (scriptHtml.contains("bigPipe.onPageletArrive({bootloadable:{\"CometVideoHomeSottoCatalogNonSubscriberUpsellSection")) {
                    sourceStr = script.html();
                    break;
                }
            }
            // 获取实际视频信息
            return executeHomePageData(sourceStr);
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return null;
    }

/**
     * @desc 处理首页的数据
     * @param sourceStr 首页的节点数据
     * @return 最终获取的视频信息
     */
    private String executeHomePageData(String sourceStr) {
        if (AppUtil.isNull(sourceStr)) {
            return "获取首页数据为空";
        }
        try {
            // 获取首页数据
            int sourceStart = sourceStr.indexOf("section_components:{edges:") + 26;
            int sourceEnd = sourceStr.indexOf(",page_info:{has_next_page");
            String jsonData = sourceStr.substring(sourceStart, sourceEnd);
            JSONArray jsonArray = JSONArray.parseArray(jsonData);
            if (AppUtil.isNull(jsonArray)) {
                LogUtil.error("解析首页数据失败.");
                return "";
            }
            // 尝试通过获取的数据保存视频
            List<FacebookVideo> facebookVideoList = saveVideoBySourceData(jsonArray);
            if (AppUtil.isNull(facebookVideoList)) {
                return "";
            }
            List<FacebookVideo> resultList = new ArrayList<>(facebookVideoList);
            // 尝试通过分页获取全部信息
            int cursorStart = sourceStr.indexOf("end_cursor") + 12;
            String cursorStr = sourceStr.substring(cursorStart, cursorStart + 120);
            int sectionIdStart = sourceStr.indexOf("WWW_PLAYLIST_VIDEO_LIST") - 21;
            String sectionId = sourceStr.substring(sectionIdStart - 48, sectionIdStart);
            Map<String, Object> map = new HashMap<>();
            // 构造接口请求参数,每次请求一百条数据
            String var = "{\"count\":100,\"cursor\":\"cursorStr\",\"scale\":1,\"sectionID\":\"sourceSectionId\",\"useDefaultActor\":false}";
            String var1 = var.replace("cursorStr", cursorStr);
            var1 = var1.replace("sourceSectionId", sectionId);
            map.put("variables", var1);
            map.put("doc_id", "2651465051639629");
            // 主要在此请求中的代理设置(参考方法实现)
            String dataJson = HttpClientUtil.doPostForm("https://www.facebook.com/api/graphql/", map);
            if (AppUtil.isNull(dataJson)) {
                LogUtil.error("通过Facebook-API获取数据失败.mapInfo:[" + JSONObject.toJSONString(map) + "]");
                return JSONObject.toJSONString(resultList);
            }
            while (true) {
                // 防止被反扒机制限制,使用自动随机睡眠一定时间后再请求接口
                Thread.sleep(RandomUtils.nextLong(3, 8) * 1000);
                JSONObject jsonObject = JSONObject.parseObject(dataJson);
                JSONArray dataArray = jsonObject.getJSONObject("data").getJSONObject("node")
                        .getJSONObject("section_components").getJSONArray("edges");
                // 保存视频信息
                List<FacebookVideo> videoList = saveVideoBySourceData(dataArray);
                if (AppUtil.isNull(videoList)) {
                    break;
                } else {
                    resultList.addAll(videoList);
                }
                JSONObject pageInfo = jsonObject.getJSONObject("data").getJSONObject("node")
                        .getJSONObject("section_components").getJSONObject("page_info");
                if (!pageInfo.getBoolean("has_next_page")) {
                    break;
                }
                // 下一次请求需要上一次请求的一个参数
                String var2 = var.replace("cursorStr", pageInfo.getString("end_cursor"));
                var2 = var2.replace("sourceSectionId", sectionId);
                map.put("variables", var2);
                dataJson = HttpClientUtil.doPostForm("https://www.facebook.com/api/graphql/", map);
            }
            return JSONObject.toJSONString(resultList);
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return "";
    }

/**
     * @desc 根据json 数组解析需要的数据
     * @param jsonArray json数组
     * @return 构造好的数据
     */
    private List<FacebookVideo> saveVideoBySourceData(JSONArray jsonArray) {
        List<FacebookVideo> arrayList = new ArrayList<>();
        try {
            for (Object o : jsonArray) {
                JSONObject data = (JSONObject) o;
                JSONArray attachments = data.getJSONObject("node").getJSONObject("feed_unit").getJSONArray("attachments");
                if (AppUtil.isNull(attachments)) {
                    continue;
                }
                JSONObject attachment = (JSONObject) attachments.get(0);
                FacebookVideo video = new FacebookVideo();
                video.setVideoId(attachment.getJSONObject("media").getString("id"));
                String title = attachment.getJSONObject("media").getString("name");
                String mainBody = attachment.getJSONObject("media").getJSONObject("savable_description").getString("text");
                if (AppUtil.isNull(title) && AppUtil.isNull(mainBody)) {
                    continue;
                }
                String thumbnail = attachment.getJSONObject("media").getJSONObject("image").getString("uri");
                Long publishTime = attachment.getJSONObject("media").getLong("publish_time");
                if (publishTime == null) {
                    continue;
                }
                video.setThumbnail(thumbnail);
                video.setDescription(mainBody);
                video.setReleaseTime(new Date(publishTime * 1000));
                arrayList.add(video);
            }
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return arrayList;
    }

(2)根据频道id+视频id获取单条视频信息

/**
     * @desc 根据video id获取视频详情
     * @param videoId 视频id(注意 此video组成实际为 频道 id + video id ex:cnliziqi/videos/3080176115398147)
     * @return 视频详情
     */
    public String getVideoDetailByVideoId(String videoId) {
        if (AppUtil.isNull(videoId)) {
            return null;
        }
        try {
            // 注意多条记录请求时的反爬
//            Thread.sleep(RandomUtils.nextLong(3, 8) * 1000);
            // 构造WebClient 模拟Chrome 浏览器
            WebClient webClient = new WebClient(BrowserVersion.CHROME);
            // 支持JavaScript
            webClient.getOptions().setJavaScriptEnabled(true);
            webClient.getOptions().setCssEnabled(false);
            webClient.getOptions().setActiveXNative(false);
            webClient.getOptions().setCssEnabled(false);
            webClient.getOptions().setThrowExceptionOnScriptError(false);
            webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
            webClient.getOptions().setTimeout(15000);
            String host = "127.0.0.1";
            String port = "1087";
            System.setProperty("http.proxyHost", host);
            System.setProperty("http.proxyPort", port);
            System.setProperty("https.proxyHost", host);
            System.setProperty("https.proxyPort", port);
            ProxyConfig proxyConfig = new ProxyConfig();
            proxyConfig.setProxyHost(host);
            proxyConfig.setProxyPort(Integer.parseInt(port));
            webClient.getOptions().setProxyConfig(proxyConfig);
            HtmlPage rootPage = webClient.getPage("https://www.facebook.com/" + videoId + "/");
            webClient.waitForBackgroundJavaScript(5000);
            String html = rootPage.asXml();
            Document document = Jsoup.parse(html);
            if (document == null) {
                return null;
            }
            // 从document中解析缩略图地址
            Elements imageEle = document.getElementsByAttributeValue("name", "twitter:image");
            if (AppUtil.isNull(imageEle)) {
                return null;
            }
            String thumbnail = imageEle.get(0).attr("content");
            // 解析需要的信息(此数据包含作者信息,但缺少简介,因此需要额外获取)
            Elements jsonEle = document.getElementsByAttributeValue("type", "application/ld+json");
            if (AppUtil.isNull(jsonEle)) {
                return null;
            }
            String cdata = jsonEle.get(0).childNodes().get(0).toString();
            JSONObject sourceJson = JSONObject.parseObject(StringUtils.substringBetween(cdata, "//<![CDATA[", "//]]>"));
            // 上传时间
            String uploadDate = sourceJson.getString("uploadDate");
            if (AppUtil.isNull(uploadDate)) {
                if (jsonEle.size() < 2) {
                    return null;
                }
                sourceJson = JSONObject.parseObject(jsonEle.get(1).text());
                uploadDate = sourceJson.getString("uploadDate");
                if (AppUtil.isNull(uploadDate)) {
                    return null;
                }
            }
            String title = StringUtils.substringBefore(sourceJson.getString("name"), "|");
            String description = sourceJson.getString("description");
            String channelUrl = sourceJson.getJSONObject("publisher").getString("url");
            String channelId = channelUrl.split("/")[3];
            FacebookVideo video = FacebookVideo.builder()
                    .videoId(videoId)
                    .channelId(channelId)
                    .description(description)
                    .releaseTime(DateUtil.parseDate(uploadDate, DateUtil.YYYY_MM_DD_T_HH_MM_SS_ZZ))
                    .thumbnail(thumbnail)
                    .build();
            return JSONObject.toJSONString(video);
        } catch (Exception e) {
            LogUtil.error(e);
        }
        return null;
    }
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值