目录
爬取当当图书排行榜
一个课堂作业,使用re,BeautifulSoup等模板爬取当当图书排行榜(榜单自选),本篇重点在输出格式对齐那里,完整代码在结尾。
查看当当图书排行榜,分析其网址各部分代表的意义,选取特定分类和时间区间进行爬取
进入多个页面,仔细分析网址变化,可以发现网址最后的‘01.03.52.00.00.00’代表着书的类别,这个分类是影视小说排行榜;‘-year-’或者‘-month-’代表按照年份或者月份的排行榜;‘2020-0-1’是起始时间;最后的数字‘-10’代表当前页面,每页有20本图书。
我们用kind代表书的分类,time代表时间区间,depth代表共读取的页数,则” ‘http://bang.dangdang.com/books/bestsellers/’ + kind + time + ‘-’ + str(i+1) ”即是网址,循环爬取可以得到所有页面的数据。
在这里插入代码片
def main():
kind = '01.03.52.00.00.00'#排行榜书的类别,这个种类是影视小说
time = '-year-2020-0-1'#2020年整年的排行榜
depth = 10#页数
start_url = 'http://bang.dangdang.com/books/bestsellers/' + kind + time + '-'
infoList = []
for i in range(depth):
try:
url = start_url + str(i+1)
html = getHTMLText(url)
parsePage(infoList, html)
except:
continue
printGoodsList(infoList)
解析网页内容,查看所需信息(排名,书名,作者,出版社,价格)的位置;
爬取网页,使用 BeautifulSoup 进行解析,将代码写入到同目录下 “ceshi.txt”文件中
def getHTMLText(url):
try:
r = requests.get(url, timeout=100)
r.raise_for_status()
r.encoding = 'GBK'
#r.encoding = 'UTF-8'
return r.text
except:
return ""
def parsePage(ilt, html):
try:
soup = BeautifulSoup(html, 'html.parser')
'''
f = open("ceshi.txt", 'w', encoding="UTF-8")
f.write(str(soup))
f.close()
'''
查看网页代码,可以看出书名在
作者和出版社在
分别爬取书名,作者,当当价和定价。由于书名中可能含有很多介绍性话语,如书名“希腊人左巴(三毛、村上春树生命中的过客,奥斯卡金像奖影片原著。“如果叫我在世界上选择一位导师的话,我肯定选择左巴,他教给了我热爱”我们使用split进行剔除,选择‘,’,‘空格’,‘(’依次进行分割,再选择第一段,我们就得到了书名。最后还要将‘——’替换为‘–’,因为python的print在打印中文破折号时距离不正确,会导致无法对其,因此我们将其更换为英文破折号。对齐问题后面会详细提到。
看代码可以看到我设置了许多过滤条件,这实在是因为爬取到的书名后缀实在太多了,比如后来时间都与你有关(肖战李现同款书,韩寒 何炅 谢娜 吴昕 戚薇 阚清子等明星推荐),再比如请和我谈一场这样的恋爱吧!(一本甜到少女心炸裂、撩到大脑充血的恋爱书!随书附赠一套心灵糖分补充卡+恋爱大作战挑战卡),一个书名你搞那么长介绍干嘛啦,不过我还真有点想看的冲动(doge。
处理作者时,类似的先进行分割,去除介绍性话语,再将网页上错误显示的‘?’更改为’·’;
价格可以直接选择,然后将其加入到列表ilt中。
names = soup.find_all('div', attrs = {'class': 'name'})
writers = soup.find_all('div', attrs={'class': 'publisher_info'})
price_ns = soup.find_all('span', 'price_n')
price_rs = soup.find_all('span', 'price_r')
for i in range(len(names)):
publisher = writers[2*i].text.split('(')[0].split(' ')[0].split(',')[0].split(',')[0].replace('?','·')
title = names[i].text.split('(')[0].split('(')[0].split(' ')[0].split('(')[0].replace('——','--')
price_n = price_ns[i].string
price_r = price_rs[i].string
#print(price_n, price_r)
ilt.append([publisher, title, price_n, price_r])
筛选所需数据,输出并保存,尝试输出对齐
对列表中保存的数据进行打印,并保存到文本文档中。这个时候遇到了本次实验最大问题:输出无法对齐。
如下图,在使用format尝试对齐时,发现中英文字符混杂的书名和作者会导致无法对齐。
(如果只包含中文,输出还是可以对齐的,把空格改为全角空格即可)
修改为全角空格填充,依然没用。
既然自动填充空格无效,我们尝试编写函数手动填充空格。查阅资料可知,中文字符在UTF8编码下占据三个字节,英文字符在UTF8编码下占据一个字节。因此我们在打印一个字符串之前,可以通过遍历字符串,计算其中中文字符和英文字符各自的数目。
(gbk编码下中文字符占据两个字节)
在文本编辑器中,中文字符的宽度正好是英文字符的两倍,因此我们通过公式
“width * 2 - sigle - double * 2”就可以计算出应该填充空格的数目。(width是为字符串设定的占用长度)
格式函数代码如下图。再次进行输出,依然无法对齐。
仔细查看输出结果,发觉在python的打印中,中文字符的宽度并不等于英文字符的两倍!
而是8:5的关系!!
(笔者有种想杀人的冲动!!!)
于是把原公式中的2改成1.6,但显然当计算得到的空格数目不是整数的,输出距离还是不正确。
我又想到可以用看不到的中文字符(灵感来源于王者荣耀重复名)对原字符串进行填充,使其中的中文字符数是5的倍数。在查阅资料后,找到了全角空格(全角空格占据的宽度等于中文字符宽度)。
因此my_format函数可以改为:
def my_format(str, width, align):#定义函数接受三个参数:要输出的字符串(str)、总占用宽度(int)、对齐方式(str:l、r对应左右)
sigle = 0
double = 0
longkong = 0
sep=' '#定义占位符
long_sep = ' '
for i in str:#统计单字宽和1.6字宽的数目
if len(i.encode('UTF-8')) == 1 or len(i.encode('UTF-8')) == 2:
sigle += 1
elif len(i.encode('UTF-8')) == 3:
double += 1
if double % 5 != 0:
longkong = (5 - double % 5)
double += longkong
if align == 'l':
return str + long_sep * longkong + int(width * 2 - sigle - double * 1.6) * sep
elif align == 'r':
return long_sep * longkong + int(width * 2 - sigle - double * 1.6) * sep + str
另外字符‘·’的encode编码是2个字符,但其字符宽度是一个英文字符,因此在遍历时单独列出。
最后输出结果,完美对齐:
另外,由于在一半的文本编辑器如记事本中,中文字符的宽度依然是英文字符的2倍,所以如果输出到文本文件中时,需要修改my_format中的参数。
完整代码
import requests
from bs4 import BeautifulSoup
import re
def getHTMLText(url):
try:
r = requests.get(url, timeout=100)
r.raise_for_status()
r.encoding = 'GBK'
#r.encoding = 'UTF-8'
return r.text
except:
return ""
def parsePage(ilt, html):
try:
soup = BeautifulSoup(html, 'html.parser')
'''
f = open("ceshi.txt", 'w', encoding="UTF-8")
f.write(str(soup))
f.close()
'''
names = soup.find_all('div', attrs = {'class': 'name'})
writers = soup.find_all('div', attrs={'class': 'publisher_info'})
price_ns = soup.find_all('span', 'price_n')
price_rs = soup.find_all('span', 'price_r')
for i in range(len(names)):
publisher = writers[2*i].text.split('(')[0].split(' ')[0].split(',')[0].split(',')[0].replace('?','·')
title = names[i].text.split('(')[0].split('(')[0].split(' ')[0].split('(')[0].replace('——','--')
price_n = price_ns[i].string
price_r = price_rs[i].string
#print(price_n, price_r)
ilt.append([publisher, title, price_n, price_r])
except:
print("")
def printGoodsList(ilt):
print(my_format('序号', 8, 'l') + my_format('书名', 25, 'l') +\
my_format('作者', 15, 'l') + my_format('当前价格', 8, 'l') + my_format('定价', 8, 'l'))
count = 0
for g in ilt:
count = count + 1
print(my_format(str(count), 8, 'l') + my_format(g[1], 25, 'l') + \
my_format(g[0], 15, 'l') + my_format(g[2], 8, 'l') + my_format(g[3], 8, 'l'))
def my_format(str, width, align):#定义函数接受三个参数:要输出的字符串(str)、总占用宽度(int)、对齐方式(str:l、r对应左右)
sigle = 0
double = 0
longkong = 0
sep=' '#定义占位符
long_sep = ' '
for i in str:#统计单字宽和1.6字宽的数目
if len(i.encode('UTF-8')) == 1 or len(i.encode('UTF-8')) == 2:
sigle += 1
elif len(i.encode('UTF-8')) == 3:
double += 1
if double % 5 != 0:
longkong = (5 - double % 5)
double += longkong
if align == 'l':
return str + long_sep * longkong + int(width * 2 - sigle - double * 1.6) * sep
elif align == 'r':
return long_sep * longkong + int(width * 2 - sigle - double * 1.6) * sep + str
def main():
kind = '01.03.52.00.00.00'#排行榜书的类别,这个种类是影视小说
time = '-year-2020-0-1'#2020年整年的排行榜
depth = 10#页数
start_url = 'http://bang.dangdang.com/books/bestsellers/' + kind + time + '-'
infoList = []
for i in range(depth):
try:
url = start_url + str(i+1)
html = getHTMLText(url)
parsePage(infoList, html)
except:
continue
printGoodsList(infoList)
main()