Будем считать звезды

В последнее время я был, я терял сон
Мечтание о том, что мы можем быть Но, детка, я был, я много молился,
Сказал, больше не считая долларов
Мы будем считать звезды, да, мы будем считать звезды
( одна республика - подсчет звезд )

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

Я решил написать это в Python, потому что:

Сценарий выполняет ряд HTTP-запросов в списке помеченных сообщений , отслеживает числа в dict, а также сохраняет чистые данные HTML в файлы (чтобы было проще выполнять другие вычисления по данным в будущем, и у меня была возможность для изучения ввода-вывода файлов в Python).

Чтобы быть в безопасности, я включил небольшую задержку между некоторыми запросами (не хочу рисковать получать блокировку системой)

К сожалению, нет способа узнать, какой пользователь выделил большинство сообщений, но мы все знаем, кто это в любом случае .. .

Код:

from time import sleep
from bs4 import BeautifulSoup
from urllib import request
from collections import OrderedDict
import operator

room = 8595 # The 2nd Monitor (Code Review)
url = 'http://chat.stackexchange.com/rooms/info/{0}/the-2nd-monitor/?tab=stars&page={1}'
pages = 125

def write_files(filename, content):
    with open(filename, 'w', encoding = 'utf-8') as f:
        f.write(content)

def fetch_soup(room, page):
    resource = request.urlopen(url.format(room, page))
    content = resource.read().decode('utf-8')
    mysoup = BeautifulSoup(content)
    return mysoup

allstars = {}
def add_stars(message):
    message_soup = BeautifulSoup(str(message))
    stars = message_soup.select('.times').pop().string
    who = message_soup.select(".username a").pop().string

    # If there is only one star, the `.times` span item does not contain anything
    if stars == None:
        stars = 1

    if who in allstars:
        allstars[who] += int(stars)
    else:
        allstars[who] = int(stars)

for page in range(1, pages):
    print("Fetching page {0}".format(page))
    soup = fetch_soup(room, page)
    all_messages = soup.find_all(attrs={'class': 'monologue'})
    for message in all_messages:
        add_stars(message)
    write_files("{0}-page-{1}".format(room, page), soup.prettify())
    if page % 5 == 0:
        sleep(3)

# Create a sorted list from the dict with items sorted by value (number of stars)
sorted_stars = sorted(allstars.items(), key=lambda x:x[1])

for user in sorted_stars:
    print(user)

Результаты, спросите вы? Ну, вот они: (предупреждение о спойлере!) (я показываю только тех, у кого есть \ $> 50 \ $ звезд, чтобы сохранить список короче)

  

('apieceoffruit', 73)
 («ChrisW», 85)
 («Эдвард», 86)
 («Юуши», 93)
 («Марк-Андре», 98)
 ('nhgrif', 112)
 ('amon', 119)
 («Джеймс Хоури», 126)
 («Никто», 148)
 ('Джерри Коффин', 150)
 («БенВлоджи», 160)
 ('Donald.McLean', 174)
 ('konijn', 184)
 ('200_success', 209)
 ('Vogel612', 220)
 ('kleinfreund', 229)
 ('Корбин', 233)
 («Морвенн», 253)
 ('skiwi', 407)
 ('lol.upvote', 416)
 ('syb0rg', 475)
 («Малахи», 534)
 («retailcoder», 749)
 («Мат-кружка», 931)
 ('Simon Andrà © Forsberg', 1079)
 («Джамал», 1170)
 («Кружка со многими именами», 2096) (Мат-кружка, розничный кодер и lol.upvote - это один и тот же пользователь)
 ('rolfl', 2115)

Чувствуется странным делать .pop() для извлечения данных из супа выбора, существует ли другой подход? Однако, поскольку это первый раз, когда я использую Python, любые комментарии приветствуются.

41 голос | спросил Simon Forsberg 26 PMpSat, 26 Apr 2014 23:14:16 +040014Saturday 2014, 23:14:16

3 ответа


28

Это прекрасный повод для написания кода, и конечный продукт тоже неплох.

â~ ... Прежде всего, поздравляю вас с вашим Java-ridden mind для того, чтобы не заставлять классы на Python там, где они не нужны.

â~ ... Вы импортируете, но не используете OrderedDict и operator. Удаление неиспользуемых импортов.

â~ ... Обычно ваш код записывается таким образом, чтобы он мог использоваться как модуль, так и сценарий. Для этого используется трюк if __name__ == '__main__':

if __name__ == '__main__':
    # executed only if used as a script

â~ ... Вы объявляете несколько переменных, таких как room, url и pages спереди. Это затрудняет повторное использование кода:

  • Переменная room не должна быть объявлена ​​спереди как глобальная переменная, но в вашем основном разделе. Оттуда он может быть передан через все функции.

  • url специально, но без необходимости упоминает the-2nd-monitor. Это не является вредным, но ненужным, поскольку имеет значение только идентификатор. Кроме того, url - это очень короткое имя для такой большой области. Что-то вроде star_url_pattern было бы лучше - за исключением того, что глобальные «константы» должны быть названы all-uppercase:

    STAR_URL_PATTERN = 'http://chat.stackexchange.com/rooms/info/{0}/?tab=stars&page={1}'
    
  • Сохранять множественные имена для коллекций. pages скорее всего должны быть page_count. Но подождите - почему мы жестко кодируем это, а не извлекаем его из самой страницы? Просто следуйте ссылкам rel="next", пока не достигнете конца.

â~ ... Эта последняя идея может быть реализована с помощью функции генератора . Функция генератора Python похожа на простой итератор. Он может yield, или return при исчерпании. Мы могли бы построить такую ​​генераторную функцию, которая дает красивый суповый объект для каждой страницы и позаботится о следующем. В качестве эскиза:

from urllib.parse import urljoin

def walk_pages(start_url):
    current_page = start_url
    while True:
        content = ... # fetch the current_page
        soup = BeautifulSoup(content)
        yield soup
        # find the next page
        next_link = soup.find('a', {'rel': 'next'})
        if next_link is None:
            return
        # urljoin takes care of resolving the relative URL
        current_page = urljoin(current_page, next_link.['href'])

â~ ... Пожалуйста, не используйте urllib.request. Эта библиотека имеет ужасный интерфейс и более или менее разрушена дизайном. Вы можете заметить, что метод .read() возвращает необработанные байты, а не использует кодировку из заголовка Content-Type для автоматического декодирования содержимого. Это полезно при обработке двоичных данных, но HTML-страница text . Вместо hardcoding кодировка utf-8 (которая даже не является кодировкой по умолчанию для HTML), мы могли бы использовать лучшую библиотеку, например requests . Тогда:

import requests
response = requests.get(current_page)
response.raise_for_status()  # throw an error (only for 4xx or 5xx responses)
content = response.text  # transparently decodes the content

â~ ... Ваша переменная allstars должна не только называться чем-то вроде all_stars (обратите внимание на разделение слов с помощью подчеркивания), но также не будет глобальной переменной. Рассмотрите возможность передачи его в качестве параметра add_stars или обертывания этого словаря в объекте, где add_stars будет методом.

â~ ... Я не совсем понимаю, почему вы пишете каждую страницу в файл. Я подозреваю, что это было предназначено как помощь для отладки, но это не добавляет никакого значения пользователю этого скрипта. Вместо того, чтобы загромождать текущий рабочий каталог, сделайте это поведение необязательным.

â~ ... Не сравнивайте с None с помощью оператора == - это для общего сравнения. Чтобы проверить подлинность, используйте оператор is: if stars is None. Иногда предпочтительнее полагаться на булевскую перегрузку объекта. Например, массивы считаются ложными, если они пусты.


Говорить легко,
кодирование сложно.
Это рефакторинг
одинаково плохо?

import time
from bs4 import BeautifulSoup
import requests
from urllib.parse import urljoin

STAR_URL_TEMPLATE = 'http://chat.{0}/rooms/info/{1}/?tab=stars'

def star_pages(start_url):
    current_page = start_url
    while True:
        print("GET {}".format(current_page))
        response = requests.get(current_page)
        response.raise_for_status()
        soup = BeautifulSoup(response.text)
        yield soup
        # find the next page
        next_link = soup.find('a', {'rel': 'next'})
        if next_link is None:
            return
        # urljoin takes care of resolving the relative URL
        current_page = urljoin(current_page, next_link['href'])

def star_count(room_id, site='stackexchange.com'):
    stars = {}
    for page in star_pages(STAR_URL_TEMPLATE.format(site, room_id)):
        for message in page.find_all(attrs={'class': 'monologue'}):
            author = message.find(attrs={'class': 'username'}).string

            star_count = message.find(attrs={'class': 'times'}).string
            if star_count is None:
                star_count = 1

            if author not in stars:
                stars[author] = 0
            stars[author] += int(star_count)

        # be nice to the server, and wait after each page
        time.sleep(1)
    return stars

if __name__ == '__main__':
    the_2nd_monitor_id = 8595
    stars = star_count(the_2nd_monitor_id)
    # print out the stars in descending order
    for author, count in sorted(stars.items(), key=lambda pair: pair[1], reverse=True):
        print("{}: {}".format(author, count))
ответил amon 6 Maypm14 2014, 18:04:37
8

Я не вижу причин, по которым вам нужно будет вытеснять элементы из массивов, возвращенных из .select() - вы могли бы просто сделать что-то вроде

 message_soup.select('.times')[0].string

Любой подход будет генерировать исключение, если сообщение не содержит класс .times, поэтому вы можете добавить обработку исключений:

try:
    stars = message_soup.select('.times')[0].string
except IndexError:
    stars = None

Сказав это, я не думаю, что особенно важно wrong использовать .pop() - это зависит от других факторов, которые могут прийти к суждению. Сотрудник, которого я уважаю, кажется, думает, что использование pop() в python немного непитотическое. Этому нравится другой коллега, которому больше нравится Lisp. Лично я думаю, что я оставил бы его, , если не было что-то убедительное в отношении изменения структуры данных.

ответил LiavK 27 AMpSun, 27 Apr 2014 00:16:37 +040016Sunday 2014, 00:16:37
2

Есть также некоторые возможные улучшения BeautifulSoup:

  • Настоятельно рекомендуется указать базовый парсер, который будет использоваться BeautifulSoup под капотом:

    soup = BeautifulSoup(response.text, "html.parser")
    # soup = BeautifulSoup(response.text, "lxml")
    # soup = BeautifulSoup(response.text, "html5lib")
    

    Если вы не укажете парсер, BeautifulSoup автоматически выбирает его из того, что доступно в текущей среде Python, и может работать по-разному на разных компьютерах и в других средах, что приводит к неожиданным последствиям. См. Также раздел "Установка парсера .

  • Вместо выполнения .select() и .pop() вы можете вызвать метод .select_one(), который будет возвращать один элемент или None (если не найдено элементов)
  • soup.find_all(attrs={'class': 'monologue'}) может быть заменен более сжатым вызовом селектора CSS: soup.select('.monologue')
ответил alecxe 13 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowWed, 13 Sep 2017 16:15:41 +0300 2017, 16:15:41

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132