Since my last post "Let's read a random Goodreads book in an optimal way," I have added new features to my program.
As far as this question is concerned: if you haven't already, it may help to read the last post for context.
Changes
Since the last post, I have made the following changes:
- In
to_int(rating_count)
, added if-else to catch if rating count is already in integer form - fixing error caused by.split()[0]
on an integer. - In
format_title(book_title)
, added code to return title with CSV formatting (double quotes around title, if title already had double quotes - "escaped" with""
), and added if-statement to remove occasional extra space in title (caused by removal of series identification). - In
get_book_rating_count(soup)
, added try-except to catch occasionalAttributeError
caused by missing rating count. - Added command line parsing, here's the "help" print out:
usage: Booky-v4.py [-h] [-v {0,1,2,3}] [-mr MIN_RATING] filename Gather random book data from Goodreads and append it to a file in CSV format, until the program is manually closed or until a connection issue. example command: Booky-v4.py -v3 -mr /path/to/file.csv positional arguments: filename output file, only supports FULL path (no tilde, etc.) optional arguments: -h, --help show this help message and exit -v {0,1,2,3}, --verbosity {0,1,2,3} specify verbosity level, default = 2 0 = no output 1 = continually print line count after appending to file 2 = after keyboard interrupt, print count of lines added since program start and program run time 3 = both verbosity options 1 and 2 -mr MIN_RATING, --min-rating MIN_RATING specify the minimum accepted rating, default = 30
Another example of command parsing:
$ python3 -OO Booky-v4.py -v2 --min-rating 400 /path/to/file.csv
- Added option to print program run time (in real time) and total count of lines added - upon keyboard interrupt (manual exit) - if
--verbosity
is 2 or 3. - Changed so program appends book data to a CSV file (in correct CSV format) instead of printing it in terminal.
- Added
FileNotFoundError
to main try-except block, along withos.path.isfile(FILE_PATH)
inside of the while loop to catch file deletion while the program is running. - Added option to continually print total line count after appendage (always prints on the same line thanks to
sys.stdout.write("\033[F")
) - if--verbosity
is 1 or 3.
Code
import argparse
from bs4 import BeautifulSoup as bs
import os.path
import requests
import re
import sys
import time
URL = "https://www.goodreads.com/book/random"
def to_int(rating_count):
""" Return rating count as a int """
if isinstance(rating_count, int):
return rating_count
else:
rating_count = rating_count.split()[0]
if ',' in rating_count:
return int(rating_count.replace(',', ''))
return int(rating_count)
def format_title(book_title):
"""
Return formatted book title:
"Actual Book Title! (Series identifier #3)" -> "Actual Book Title!"
"History of 4D Fish Slapping [Silly, Part 4]" -> "History of 4D Fish Slapping"
Along with CSV formatting
"""
book_title = ' '.join(book_title.split()).replace('&', '&')
book_title = re.sub(r'(\(|\[)(.*)(\)|\])','', book_title)
book_title = '"' + book_title.replace('"', '""') + '"'
if book_title[-2:-1] == ' ':
book_title = book_title[:-2] + '"'
return book_title
def get_html_source():
""" Return html source """
html_source = requests.get(URL).text
return bs(html_source, 'html.parser')
def get_book_rating_count(soup):
""" Return book rating count """
try:
book_rating_count = soup.find('span', attrs={'class', 'value-title'}).get_text()
return book_rating_count
except AttributeError:
""" Attribute error, the rating count is missing """
return -1
def get_book_title(soup):
""" Return book title"""
return soup.find('h1', attrs={'class': 'bookTitle'}).get_text()
def get_book_pages(soup):
""" Return book pages """
book_pages = soup.find('span', attrs={'itemprop': 'numberOfPages'}).get_text()
if " pages" in book_pages:
return book_pages.replace(' pages', '')
elif " page" in book_pages:
return book_pages.replace(' page', '')
return book_pages
def get_book_rating(soup):
""" Return book rating """
return soup.find('span', attrs={'itemprop': 'ratingValue'}).get_text()
def main():
""" Command line parsing, with specifiable verbosity, minimum rating, and required file name """
parser = argparse.ArgumentParser(description="Gather random book data from Goodreads and append it to a file in CSV format, until the program is manually closed or until a connection issue.\
\n\nexample command: Booky-v4.py -v3 -mr /path/to/file.csv",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("-v", "--verbosity", type=int, choices=[0,1,2,3],
help="specify verbosity level, default = 2\
\n0 = no output\
\n1 = continually print line count after appending to file\
\n2 = after keyboard interrupt, print count of lines added since program start and program run time\
\n3 = both verbosity options 1 and 2", default=2)
parser.add_argument("-mr", "--min-rating", type=int, default=30,
help="specify the minimum accepted rating, default = 30")
parser.add_argument("filename", help="output file, only supports FULL path (no tilde, etc.)")
args = parser.parse_args()
FILE_PATH = args.filename
MIN_RATING_COUNT = args.min_rating
try:
if args.verbosity in (2, 3):
start_time = time.time()
with open(FILE_PATH, 'r') as file:
init_filelength = sum(1 for i in file)
while True:
soup = get_html_source()
book_rating_count = get_book_rating_count(soup)
if to_int(book_rating_count) >= MIN_RATING_COUNT:
try:
book_pages = get_book_pages(soup)
book_title = format_title(get_book_title(soup))
book_rating = get_book_rating(soup)
""" Re-check if file exists to catch deletion while program is running """
if book_pages != "0" and os.path.isfile(FILE_PATH):
with open(FILE_PATH, 'a') as file:
file.write(book_title + ',' + book_pages + ',' + book_rating + '\n')
if args.verbosity in (1, 3):
sys.stdout.write("\033[F")
with open(FILE_PATH, 'r') as file:
print("\nLine count:", sum(1 for i in file), end='')
elif not os.path.isfile(FILE_PATH):
raise FileNotFoundError
except AttributeError:
continue
except FileNotFoundError:
print("File '", FILE_PATH, "' does not exist, or has been deleted.", sep='')
except KeyboardInterrupt:
if args.verbosity in (2, 3):
with open(FILE_PATH, 'r') as file:
lines_added = sum(1 for i in file) - init_filelength
print("\nLines added: ", lines_added, " (since program start)", sep='')
print("Run time: ", round(time.time() - start_time, 3), " seconds (real)", sep='')
if __name__ == '__main__':
try:
main()
except ConnectionResetError:
print("\nConnection reset by peer, exiting program.")
pass
The code versions are on GitHub - version 2 (from last post) and version 4 (current code) - if that helps with anything.
I am new to this whole argparse
stuff - let me know if I can improve anything in that department. I also attempted to document my code effectively, but I could always do better.
How can I optimize my code, both by improving the code's logic/structure or by improving performance?