CoderCastrov logo
CoderCastrov
Русский SEO

Парсинг Reddit в 2023 году

Парсинг Reddit в 2023 году
просмотров
5 мин чтение
#Русский SEO
Table Of Content

    Reddit недавно принял очень спорное решение сделать использование своего API платным. Несколько компаний вышли из бизнеса. Но есть несколько действительно изобретательных обходных путей, чтобы получить данные Reddit бесплатно, не используя PRAW.

    Я начал писать скрипт с использованием Selenium, и он работал довольно хорошо. Он работает естественным образом на Reddit, и нет заметной блокировки IP.

    subreddits = ['https://www.reddit.com/r/tech/top/?t=month']
    
    class ScrapeReddit():
        def __init__(self):
            # start headless if you want later on.
            options = Options()
            self.driver = webdriver.Safari(service=Service(executable_path='/usr/bin/safaridriver'), options=options)
           
            self.postids = []
        
        def lazy_scroll(self):
            current_height = self.driver.execute_script('window.scrollTo(0,document.body.scrollHeight);')
            while True:
                self.driver.execute_script('window.scrollTo(0,document.body.scrollHeight);')
                time.sleep(2)
                new_height = self.driver.execute_script('return document.body.scrollHeight')   
                if new_height == current_height:      # this means we have reached the end of the page!
                    html = self.driver.page_source
                    break
                current_height = new_height
            return html
    
        def get_posts(self):
            for link in subreddits:
                self.driver.get(link)
                self.driver.maximize_window()
                time.sleep(5)
                html = self.lazy_scroll()
                # html = self.driver.page_source
                parser = BeautifulSoup(html, 'html.parser')
                post_links = parser.find_all('a', {'slot': 'full-post-link'})
                print(len(post_links))
                count = 1
    
                for post_link in post_links:
                    # generate a unique id for each post
                    post_id = post_link['href'].split('/')[-3]
                    print(f"{count} - {post_id}")
                    count += 1
                    if post_id not in self.postids:
                        self.postids.append(post_id)
    
        def destroy(self):
            self.driver.close()
    

    Позвольте мне объяснить, что я здесь делаю. Я рассматриваю подраздел (r/tech в данном случае) и изменяю ссылку, чтобы получить лучшие посты за последний месяц. Я использую технику Selenium, чтобы обойти проблему ленивой прокрутки на Reddit. Я использую BeautifulSoup, чтобы получить все ссылки на посты, и для каждой из ссылок на посты я получаю идентификаторы постов (эти идентификаторы генерируются Reddit и являются уникальными). Вы также можете парсить другой контент, но это простое дополнение к скрипту.

    Теперь, имея эти идентификаторы постов, вы можете просто перейти по ссылке https://reddit.com/{post_id} и получить пост. Вам не нужно знать подраздел или название поста или что-либо еще. Теперь вы можете также парсить специфический контент поста, такой как комментарии, ответы и любые медиафайлы.

    Я делал это для своего проекта, и мне действительно было необходимо спарсить все ответы. Однако кнопка "показать больше ответов" после каждого комментария действительно раздражает, потому что нельзя предсказать, сколько из этих кнопок нужно нажать, чтобы получить все ответы. Я не смог найти рабочий обход этой проблемы. Любой метод всегда не работает для какого-то странного неожиданного случая.

    Интересное открытие

    Я искал способы получить ответы, которые были важны для моей задачи. И я нашел нечто действительно интересное.

    Оказывается, добавление .json к любому посту Reddit дает всю метаданные поста. Для меня это было странным открытием, потому что это не происходит на других социальных медиа. Забавно, что я попытался получить данные из нескольких постов Reddit, и примерно после 70-80 запросов я начал получать ошибку 429, что означает, что я делаю слишком много запросов. Поэтому я попытался использовать некоторые общие методы, чтобы избежать блокировки IP.

    Я попробовал добавить случайные задержки между запросами, и это действительно работает. Однако это занимает много времени для парсинга всего. Вы можете сделать это быстрее, если у вас есть доступ к распределенным серверам или прокси.

    Теперь, когда у нас есть готовые идентификаторы постов, мы можем добавить эти функции в уже определенный класс.

    def get_data(self, postid):
        base_url = "https://reddit.com/"
        url = base_url + postid + ".json"
        self.driver.get(url)
        self.driver.maximize_window()
        html = self.driver.page_source
        soup = BeautifulSoup(html, 'html.parser')
        text = soup.find('body').get_text()
        time.sleep(3)
        return text
    def get_post_details(self):
        jsons = []
        count = 1
        if not self.postids:
            print("No post ids found. Please run get_posts() first.")
            return
        for postid in self.postids:
            print(postid, count)
            text = self.get_data(postid)
            jsons.append(text)
            time.sleep(random.randint(1, 10))
            count += 1
        
        self.jsons = jsons
        return jsons

    Когда вы запускаете функцию get_post_details, она перебирает все ваши идентификаторы постов и получает всю метаданные поста сразу. Однако это занимает некоторое время из-за случайных задержек, необходимых для того, чтобы наш IP-адрес не попал в черный список.

    Теперь более важная задача - получить данные из этого json файла. Он довольно сложный и сильно вложенный. Мне потребовалось некоторое время, чтобы создать работающий скрипт. Я включу его здесь.

    def get_post_info(json_data):
        """       Получает текст поста, все комментарии и их ответы,        идентификаторы пользователей поста, комментариев и ответов,        и временные метки поста, комментариев и ответов        из JSON данных.    """
    
        post = json_data[0]['data']['children'][0]['data']
        post_body = post['title']
        post_user = post['author']
        post_time = post['created_utc']
        comments = json_data[1]['data']['children']
        comments_list = []
        for (comment, idx) in zip(comments, range(len(comments))):
            comment_body = comment['data']['body']
            comment_user = comment['data']['author']
            comment_time = comment['data']['created_utc']
            comments_list.append({'body': comment_body,
                                 'user': comment_user,
                                 'time': comment_time})
            comment_replies = []
    
            # добавить ответ к комментарию, к которому он относится
    
            if comment['data']['replies'] != '':
                replies = comment['data']['replies']['data']['children']
                for reply in replies:
                    reply_body = reply['data']['body']
                    reply_user = reply['data']['author']
                    reply_time = reply['data']['created_utc']
                    comment_replies.append({'body': reply_body,
                            'user': reply_user, 'time': reply_time})
            comments_list[idx]['replies'] = comment_replies
    
        return {
            'post_body': post_body,
            'post_user': post_user,
            'post_time': post_time,
            'comments': comments_list,
            }

    Обратите внимание, что вам нужно перебрать все сохраненные json документы и вызвать эту функцию каждый раз. Лучше сохранить его в файл.

    data = reddit.jsons
    
    res = []
    for i in range(len(data)):
      try:
        parsed_json = json.loads(data[i])
        res.append(get_post_info(parsed_json))
      except JSONDecodeError as e:
        print(e)
        continue
    import os
    from datetime import datetime
    
    def save_to_json(data, subreddit):
        """save it as /data/{subreddit}/{timestamp}.json"""
        timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
        filename = f'data/{subreddit}/{timestamp}.json'
        os.makedirs(os.path.dirname(filename), exist_ok=True)
        with open(filename, 'w') as f:
            json.dump(data, f)
    
    save_to_json(res, reddit.subreddit)

    Работает как ожидается.

    Вывод

    Метод 1: Selenium

    Преимущества

    • относительно быстро
    • проще
    • нет блокировки IP

    Недостатки

    • невозможно получить множество метаданных
    • проблема с ответами

    Метод 2: Сокращение json

    Преимущества

    • все данные в одном месте
    • мгновенно
    • не нужно действительно проходить через HTML-документ
    • быстрее в сочетании с распределенными серверами или прокси

    Недостатки

    • относительно медленно
    • возможная блокировка IP

    Есть и другие способы. Одним из таких онлайн-инструментов является socialgrep. Это довольно удобный инструмент для получения данных Reddit с помощью фильтров, которые даже Reddit больше не поддерживает. Захват заключается в том, что он не совсем бесплатный. Вы можете скачать всего 100 строк данных с бесплатной учетной записью. Я не верю в оплату за данные, но если вы действительно отчаянны и у вас есть деньги, это быстрый способ получить данные.

    Я не связан с socialgrep никаким образом.

    Вот и все. Спасибо за внимание.

    Ура.