欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Facebook Java爬虫获取视频数据

程序员文章站 2022-05-17 09:28:40
...

前言部分

前置说明

​ 截止到本文发表前,该爬虫方法因为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数据

Facebook Java爬虫获取视频数据

请注意视频频道地址: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。

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

Facebook Java爬虫获取视频数据

​ 其中,前六条随着首页加载进来的数据也并不是直接出现在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;
    }
相关标签: 爬虫 java html