Извлечение общедоступных сообщений со страницы Facebook без ключа API/приложения/токена/секрета

Сразу хочу уточнить, что у меня нет аккаунта в Facebook и я не собираюсь его создавать. Кроме того, то, чего я пытаюсь добиться, совершенно законно в моей стране и США.

Вместо того, чтобы использовать API Facebook для получения последних сообщений временной шкалы страницы Facebook, я хочу отправить запрос на получение непосредственно на URL-адрес страницы (например, этой страницы) и извлеките сообщения из исходного кода HTML.
(Я хочу получить текст и время создания сообщения.)

Когда я запускаю это в веб-консоли:

document.getElementsByClassName('userContent')

Я получаю список элементов, содержащих текст последних сообщений.

Но я хотел бы извлечь эту информацию из сценария nodejs. Вероятно, я мог бы сделать это довольно легко, используя безголовый браузер, такой как puppeteer или подобный, но это создало бы массу ненужных накладных расходов. Мне бы очень хотелось использовать простой подход, такой как загрузка HTML-кода, передача его в cheerio и использование jQuery-подобного API cheeriio для извлечения сообщений.

Вот моя попытка попробовать именно это:

// npm i request cheerio request-promise-native
const rp = require('request-promise-native'); // requires installation of `request`
const cheerio = require('cheerio');

rp.get('https://www.facebook.com/pg/officialstackoverflow/posts/').then( postsHtml => {
    const $ = cheerio.load(postsHtml);

    const timeLinePostEls = $('.userContent');
    console.log(timeLinePostEls.html()); // should NOT be null
    const newestPostEl = timeLinePostEls.get(0);
    console.log(newestPostEl.html()); // should NOT be null
    const newestPostText = newestPostEl.text();
    console.log(newestPostText);
    //const newestPostTime = newestPostEl.parent(??).child('.livetimestamp').title;
    //console.log(newestPostTime);
}).catch(console.error);

к сожалению $('.userContent') не работает. Однако мне удалось убедиться, что данные, которые я ищу, встроены где-то в этот HTML-код.

Но я не мог придумать хороший подход к регулярному выражению или тому подобное для извлечения этих данных.

В зависимости от содержания поста количество тегов HTML в посте сильно различается.

Вот простой пример поста, содержащего одну ссылку:

<div class="_5pbx userContent _3576" data-ft="&#123;&quot;tn&quot;:&quot;K&quot;&#125;"><p>We&#039;re proud to be named one of Built In NYC&#039;s Best Places to Work in 2019, ranking in the top 10 for Best Midsize Places to Work and top 3 (!) for Best Perks and Benefits. See what it took to make the list and check out our profile to see some of our job openings. <a href="https://l.facebook.com/l.php?u=https%3A%2F%2Fbit.ly%2F2H3Kbr2&amp;h=AT29h2HyDsEk0rHRWqJA-Fa4M1qi3nJT1NBi95othaR3qeFuFAMNiVS2Dgtv5KR5m0xqjw6kfwZdhZt0_D3UQT1Oel2UhxRql-KwkA1xqWvrql4u1jDhzrkGVT_XxoUd8_w8_fzLZzzhz23a8yPCK6IPfWKB76_CEFjG3b78y4dFJvY9Z08AYlR01dmi5_FvWVEVytkN-123u6alYE8pqL6Jb6dtIQUTWGXYJPaNMrtxkCUZniEVXEcILkwHGSuHqCTAarboyMP55F1vhYO3OAiVMkvjbN274fVq92YvbK3bi90bU9T-5ADWHDUJ-CwcofSBTW47chstQeY0n_UluD_rBIPLsfXVSnCtpRkR2kXi9zzHLnNeIYeNssv3i7UKS_f5Z2pnVT6xe3zJbNpB68doH1Z__I9nsTCNIyFyKx2VxabecoL03DIawbRrzBoxLAwzNPLACBjTkpEQhdVn4_wdAIjXRL4cLQDcZkLEoG_sspBgRePH23TFbNufQOBly-FNtLHnkUDO2Ca-FYvAGXpcu6J4B1aH3XFPB803lsz-GRdACyOFOgXDXJfwr4WtWzUHxfiOPULWiI43yI5L4aU6wYRhPjxua3RuRZ8oj9fXa1w4Jrht94Ue2wfKtz8" target="_blank" data-ft="&#123;&quot;tn&quot;:&quot;-U&quot;&#125;" rel="noopener nofollow" data-lynx-mode="async">http://*******/2H3Kbr2</a></p></div>

В более удобочитаемом виде это выглядит примерно так:

<div class="_5pbx userContent _3576" data-ft="&#123;&quot;tn&quot;:&quot;K&quot;&#125;">
    <p>
        We&#039;re proud to be named one of Built In NYC&#039;s Best Places to Work in 
        2019, ranking in the top 10 for Best Midsize Places to Work and top 3 (!) for 
        Best Perks and Benefits. See what it took to make the list and check out our 
        profile to see some of our job openings.
        <a href="VERY_LONG_URL.........." target="_blank" data-ft="&#123;&quot;tn&quot;:&quot;-U&quot;&#125;" rel="noopener nofollow" data-lynx-mode="async">SHORT_LINK.....</a>
    </p>
</div>

Это регулярное выражение похоже работает нормально, но я не думаю, что оно очень надежное:

/<div class="[^"]+ userContent [^"]+" data-ft="[^"]+">(.+?)<\/div>/g

Если бы, например, сообщение содержало другой элемент div, то оно не работало бы должным образом. В дополнение к этому у меня нет возможности узнать время/дату, когда сообщение было создано с использованием этого подхода?

Любые идеи, как я мог бы относительно надежно извлечь самые последние 2-3 сообщения, включая дату/время создания?


person Forivin    schedule 18.01.2019    source источник
comment
Если вы проголосовали близко, объясните, почему, чтобы я мог скорректировать свой вопрос.   -  person Forivin    schedule 18.01.2019
comment
Скрапинг запрещен на Facebook, независимо от того, является ли он законным в вашей стране. не уверен, почему есть близкое голосование, хотя ваш вопрос довольно подробный. это просто не разрешено, это почти единственный правильный ответ;)   -  person luschn    schedule 18.01.2019
comment
В моей стране это разрешено. Было много судебных дел. Oracle, например, проиграла судебный процесс (они не хотели, чтобы люди загружали Java с их веб-сайта с помощью скрипта).   -  person Forivin    schedule 18.01.2019
comment
Вы можете поговорить об этом с юристом, но здесь вам точно не помогут с чем-то, что явно запрещено на Facebook. одни и те же правила для всех на Facebook, независимо от того, из какой вы страны.   -  person luschn    schedule 18.01.2019
comment
другими словами: их платформа, их правила. как честный разработчик, вы должны уважать это, независимо от того, есть ли судебные разбирательства, разрешающие это в вашей стране.   -  person luschn    schedule 18.01.2019
comment
Нет, их лицензионное соглашение не может иметь преимущественную силу перед законом. Каждый браузер, сканер поисковых систем и даже тонны браузерных расширений получают доступ к Facebook таким образом.   -  person Forivin    schedule 18.01.2019


Ответы (1)


Хорошо, я наконец понял это. Я надеюсь, что это будет полезно для других. Эта функция извлечет 20 последних сообщений, включая время создания:

// npm i request cheerio request-promise-native
const rp = require('request-promise-native'); // requires installation of `request`
const cheerio = require('cheerio');

function GetFbPosts(pageUrl) {
    const requestOptions = {
        url: pageUrl,
        headers: {
            'User-Agent': 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0'
        }
    };
    return rp.get(requestOptions).then( postsHtml => {
        const $ = cheerio.load(postsHtml);
        const timeLinePostEls = $('.userContent').map((i,el)=>$(el)).get();
        const posts = timeLinePostEls.map(post=>{
            return {
                message: post.html(),
                created_at: post.parents('.userContentWrapper').find('.timestampContent').html()
            }
        });
        return posts;
    });
}
GetFbPosts('https://www.facebook.com/pg/officialstackoverflow/posts/').then(posts=>{
    // Log all posts
    for (const post of posts) {
        console.log(post.created_at, post.message);
    }
});

Поскольку сообщения Facebook могут иметь сложное форматирование, сообщение представляет собой не обычный текст, а HTML. Но вы можете удалить форматирование и просто получить текст, заменив message: post.html() на message: post.text().

Изменить. Если вы хотите получить более 20 последних сообщений, это будет сложнее. Первые 20 постов обслуживаются статически на исходной html-странице. Все последующие сообщения извлекаются через ajax кусками по 8 сообщений. Это может быть достигнуто следующим образом:

// make sure your node.js version supports async/await (v10 and above should be fine)
// npm i request cheerio request-promise-native
const rp = require('request-promise-native'); // requires installation of `request`
const cheerio = require('cheerio');

class FbScrape {
    constructor(options={}) {
        this.headers = options.headers || {
            'User-Agent': 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0' // you may have to update this at some point
        };
    }

    async getPosts(pageUrl, limit=20) {
        const staticPostsHtml = await rp.get({ url: pageUrl, headers: this.headers });
        if (limit <= 20) {
            return this._parsePostsHtml(staticPostsHtml);
        } else {
            let staticPosts = this._parsePostsHtml(staticPostsHtml);
            const nextResultsUrl = this._getNextPageAjaxUrl(staticPostsHtml);
            const ajaxPosts = await this._getAjaxPosts(nextResultsUrl, limit-20);
            return staticPosts.concat(ajaxPosts);
        }
    }

    _parsePostsHtml(postsHtml) {
        const $ = cheerio.load(postsHtml);
        const timeLinePostEls = $('.userContent').map((i,el)=>$(el)).get();
        const posts = timeLinePostEls.map(post => {
            return {
                message: post.html(),
                created_at: post.parents('.userContentWrapper').find('.timestampContent').html()
            }
        });
        return posts;
    }

    async _getAjaxPosts(resultsUrl, limit=8, posts=[]) {
        const responseBody = await rp.get({ url: resultsUrl, headers: this.headers });
        const extractedJson = JSON.parse(responseBody.substr(9));
        const postsHtml = extractedJson.domops[0][3].__html;
        const newPosts = this._parsePostsHtml(postsHtml);
        const allPosts = posts.concat(newPosts);
        const nextResultsUrl = this._getNextPageAjaxUrl(postsHtml);
        if (allPosts.length+1 >= limit)
            return allPosts;
        else
            return await this._getAjaxPosts(nextResultsUrl, limit, allPosts);
    }

    _getNextPageAjaxUrl(html) {
        return 'https://www.facebook.com' + /"(\/pages_reaction_units\/more[^"]+)"/g.exec(html)[1].replace(/&amp;/g, '&') + '&__a=1';
    }
}

const fbScrape = new FbScrape();
const minimum = 28; // minimum number of posts to request (gets rounded up to 20, 28, 36, 44, 52, 60, 68 etc... because of page sizes (page1=20; all_following_pages=8)
fbScrape.getPosts('https://www.facebook.com/pg/officialstackoverflow/posts/', minimum).then(posts => { // get at least the 28 latest posts
    // Log all posts
    for (const post of posts) {
        console.log(post.created_at, post.message);
    }
});
person Forivin    schedule 19.01.2019
comment
Почему он извлекает только 20 сообщений? Можно ли иметь больше? - person tleo; 24.11.2019
comment
Это решение не работает в США и Германии. Доступ блокируется. Попробуйте на reqbin.com - person Jamshaid K.; 18.09.2020
comment
Я только что проверил его, и он определенно все еще работает! reqbin не подходит для проверки этого. Просто используйте nodejs. - person Forivin; 18.09.2020