【编程】自定义下载简书文章和图片

欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】
【专题】简书下载器:Python-Tkinter项目编程入门


继续本专题前面的文章。

梳理Tkinter界面

梳理后运行结果如下:


对应的main.py代码如下:

from tkinter import *
from tkinter import ttk
import time
import random
import modules.reqs as reqs  # 导入reqs请求函数
import modules.options as opts
from tkinter.scrolledtext import ScrolledText

# 创建窗体
root = Tk()
root.title('简书文章下载器')
root.resizable(width=False, height=False)
root.config(background='#EEE')


def run():  # 启动获取动作
    reqs.getAll(opts, [0], [4])


def refreshInfo():  # 信息的自刷新函数
    text = reqs.genInfoStr()
    text += '\nState:'+reqs.state
    text += '\nCurVol:'+reqs.curVol
    text += '\nCurArt:'+reqs.curArt
    text += '\nCurImg:'+reqs.curImg
    info.config(text=text)
    info.after(500, refreshInfo)


# 创建界面
rown = 0  # 占位符
ttk.Frame(root, height=10).grid() 

rown += 1  # header输入框
iptHeader = ScrolledText(root, height=1, width=50)
iptHeader.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # header输入框说明
label1 = ttk.Label(root, text='粘贴header,不要包含:打头的部分,去除if-none-match行')
label1.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=20).grid(row=rown) 

rown += 1 # 文集序号输入框
iptVol = Text(root,height=1, width=50)
iptVol.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # vol输入框说明
label2 = ttk.Label(root, text='文集列表,请用逗号分隔,如0,1,2')
label2.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=20).grid(row=rown) 

rown += 1 # 文集序号输入框
iptArt = Text(root,height=1, width=50)
iptArt.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # vol输入框说明
label3 = ttk.Label(root, text='文章列表,请用逗号分隔,如0,1,2')
label3.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=30).grid(row=rown) 

# 运行按钮
rown += 1
bt = ttk.Button(root, text='开始下载', width=30,command=run)
bt.grid(row=rown, padx=10,ipady=10 ,pady=0, sticky='WE')

# 信息标签
rown += 1
info = ttk.Label(root, text='?/?')
info.grid(row=rown,  padx=10, ipady=10, ipadx=10, sticky=W)
info.after(500, refreshInfo)  # 自动循环更新

root.mainloop()

获取用户输入

在Tkinter中可以使用xxx.get('1.0',END)来获取输入内容,这里的1.0表示从头获取,END表示获取全部。

我们修改run函数:

def run():  # 启动获取动作
    hdrs = iptHeader.get("1.0", END)
    vols = iptVol.get("1.0", END)
    arts = iptArt.get("1.0", END)

    opts.headers = opts.str2obj(hdrs, '\n', ': ')

    volnarr = []
    vols=vols.replace('\n', '')
    volsarr = vols.split(',')
    volnarr = map(lambda x: int(x), volsarr)
    volnarr = list(set(volnarr))

    artnarr = []
    artsarr = arts.split(',')
    map(lambda x: int(x), artsarr)
    artnarr = list(set(artnarr))

    writeHeaders()  # 保存设置

    reqs.getAll(opts, volnarr, artnarr)

然后运行main.py,从浏览器复制粘贴你的headers,然后设定文集列表和文章列表,点击按钮就能开始下载文章了。

自动保存和读取headers

每次都手工复制headers比较麻烦,我们为软件增加自动保存和读取headers的功能。

每次打开的时候自动尝试读取config.txt文件,填充到iptHeaders,然后每次运行都自动将输入框的文字保存到config.txt文件。

在最底部添加代码:

def writeHeaders():  # 将header写入到临时文件
    hdrs = iptHeader.get("1.0", END)
    with open(os.getcwd()+'/config.txt', 'a') as f:
        f.write(hdrs)

def  readHeaders(): #读取设置文件并填充到界面
    fpath=os.getcwd()+'/config.txt'
    if os.path.exists(fpath):
        f=open(fpath,'r')
        hdrs=f.read()
        iptHeader.insert(INSERT,hdrs)

readHeaders()

root.mainloop()

注意root.mainloop()要放在最后。

最后汇总

如果要打包成独立运行的软件(不安装python也可以运行),那么请参考这个文章:

【编程】用Py2app打包Python-Tkinter项目

整个项目代码已经同步到我的github中,你也可以从dist文件夹下找到mac版本的打包软件直接下载使用。地址是:

zhyuzh的简书下载器JianshuDownloader

整个项目暂时告一段落,全部文章已整理在:

【专题】Python-Tkinter项目编程入门

最终代码

最新修订请参照 Github项目:JianshuDownloader

main.py

from tkinter import *
from tkinter import ttk
import time
import random
import modules.reqs as reqs  # 导入reqs请求函数
import modules.options as opts
from tkinter.scrolledtext import ScrolledText
import os

# 创建窗体
root = Tk()
root.title('简书文章下载器')
root.resizable(width=False, height=False)
root.config(background='#EEE')


def run():  # 启动获取动作
    hdrs = iptHeader.get("1.0", END)
    vols = iptVol.get("1.0", END)
    arts = iptArt.get("1.0", END)

    opts.headers = opts.str2obj(hdrs, '\n', ': ')

    volnarr = []
    vols=vols.replace('\n', '')
    volsarr = vols.split(',')
    volnarr = map(lambda x: int(x), volsarr)
    volnarr = list(set(volnarr))

    artnarr = []
    artsarr = arts.split(',')
    map(lambda x: int(x), artsarr)
    artnarr = list(set(artnarr))

    writeHeaders()  # 保存设置

    reqs.getAll(opts, volnarr, artnarr)


def refreshInfo():  # 信息的自刷新函数
    text = reqs.genInfoStr()
    text += '\nState:'+reqs.state
    text += '\nCurVol:'+reqs.curVol
    text += '\nCurArt:'+reqs.curArt
    text += '\nCurImg:'+reqs.curImg
    info.config(text=text)
    info.after(500, refreshInfo)


# 创建界面
rown = 0  # 占位符
ttk.Frame(root, height=10).grid()

rown += 1  # header输入框
iptHeader = ScrolledText(root, height=1, width=50)
iptHeader.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # header输入框说明
text1 = '''
请从浏览器右击检查打开控制台
切换到Network部分
从XHR类型中找到notebooks请求
复制它的Request Headers部分
注意不要包含:打头的部分,并去除if-none-match行'
'''
label1 = ttk.Label(root, text=text1)
label1.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=20).grid(row=rown)

rown += 1  # 文集序号输入框
iptVol = Text(root, height=1, width=50)
iptVol.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # vol输入框说明
label2 = ttk.Label(root, text='文集列表,请用英文逗号分隔,如0,1,2')
label2.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=20).grid(row=rown)

rown += 1  # 文集序号输入框
iptArt = Text(root, height=1, width=50)
iptArt.grid(row=rown, padx=10, pady=0, sticky=W)

rown += 1  # vol输入框说明
label3 = ttk.Label(root, text='文章列表,请用英文逗号分隔,如0,1,2')
label3.grid(row=rown, pady=0, padx=10, sticky=W)

rown += 1  # 占位符
ttk.Frame(root, height=30).grid(row=rown)

# 运行按钮
rown += 1
bt = ttk.Button(root, text='开始下载', width=30, command=run)
bt.grid(row=rown, padx=10, ipady=10, pady=0, sticky='WE')

# 信息标签
rown += 1
info = ttk.Label(root, text='?/?')
info.grid(row=rown,  padx=10, ipady=10, ipadx=10, sticky=W)
info.after(500, refreshInfo)  # 自动循环更新


def writeHeaders():  # 将header写入到临时文件
    hdrs = iptHeader.get("1.0", END)
    with open(os.getcwd()+'/config.txt', 'a') as f:
        f.write(hdrs)


def readHeaders():  # 读取设置文件并填充到界面
    fpath = os.getcwd()+'/config.txt'
    if os.path.exists(fpath):
        f = open(fpath, 'r')
        hdrs = f.read()
        iptHeader.insert(INSERT, hdrs)


readHeaders()

root.mainloop()

modules/reqs.py


import time
from threading import Thread
import requests
import json
import os
import re
import math


atotal = 1  # 总文章数量
afini = 0  # 已读取的文章数量
cookiestr = ''  # cookie全局变量
curVol = '...'  # 正在获取的文集名
curArt = '...'  # 正在获取的文章名
curImg = '...'  # 正在获取的图片名
state = '等待中'  # 当前状态,等待中,获取中,已完成。


def genInfoStr():  # 拼接信息字符串
    global atotal
    global afini
    infoStr = '正在获取('+str(afini)+'/'+str(atotal)+'):'
    per = atotal/15
    fi = math.ceil(afini/per)
    for _ in range(fi):
        infoStr += '■'
    for _ in range(15-fi):
        infoStr += '□'
    return infoStr

# volnarr文集列表,artnarr文章列表,例如volnarr=[0,3,1]表示只获取三个文集,默认为全部


def getAll(opt, volnarr, artnarr):  # 启动线程
    t = Thread(target=getVolums, args=(opt, volnarr, artnarr))  # 多线程,避免锁死界面
    t.start()
    global state
    state = '获取中'
    print(state)
    return 'RUNNING!'


def getArticle(art, vol, opt):  # 获取文章内容
    artId = art['id']
    urlArt = 'https://www.jianshu.com/author/notes/'+str(artId)+'/content'
    res = requests.get(urlArt, headers=opt.headers)
    resdata = json.loads(res.text)
    cont = resdata['content']

   # 文件路径
    dir = os.getcwd()+'/data/' + vol['name'] + '/'
    fname = dir+art['title']
    if not os.path.exists(dir):
        os.makedirs(dir)
        os.makedirs(dir+'/imgs/')

    # 获取图片地址
    imglist = re.findall(r"[^`]\!\[[^\]]*\]\((.+?)\)", cont, re.S)
    for iu in imglist:
        imgUrl = iu.split('?')[0]
        global curImg
        curImg = os.path.basename(imgUrl)
        print('GETTING IMAGE:', curImg)
        if not os.path.exists(imgUrl):
            res = requests.get(imgUrl)
            img = res.content
            with open(dir+'imgs/'+os.path.basename(imgUrl), 'wb') as f:
                f.write(img)
            time.sleep(1)

    # 保存md文件
    if art['note_type'] == 2:
        fname += '.md'
    else:
        fname += '.html'
    if os.path.exists(fname):
        os.remove(fname)

    with open(fname, 'a') as f:
        newcont = cont.replace(
            'https://upload-images.jianshu.io/upload_images', 'imgs')
        f.write(newcont)
        f.close()

    return 'getArticles OK!'


def getArticlesList(vol, opt, artnarr):  # 获取文章列表
    volId = vol['id']
    urlVol = 'https://www.jianshu.com/author/notebooks/'+str(volId)+'/notes'
    res = requests.get(urlVol, headers=opt.headers)
    resdata = json.loads(res.text)
    getlist = []  # 指定获取列表
    if len(artnarr) == 0:
        getlist = resdata
    else:
        for i in artnarr:
            if i < len(resdata):
                getlist.append(resdata[i])

    # 每个文集计算文章数量
    global atotal
    global afini
    atotal = len(getlist)
    afini = 0
    print("ATOTAL:", atotal, afini)

    n = 0
    for d in getlist:
        global curArt
        curArt = str(n)+':' + d['title']
        n += 1
        print('GETTING ARTICLE:', curArt)
        time.sleep(1)
        getArticle(d, vol, opt)
        afini += 1  # 计数加1
    return 'getArticlesList OK!'


def getVolums(opt, volnarr, artnarr):  # 获取文集列表
    res = requests.get(opt.urlVolumns, headers=opt.headers)
    resdata = json.loads(res.text)
    getlist = []  # 指定获取列表
    if len(volnarr) == 0:
        getlist = resdata
    else:
        for i in volnarr:
            if i < len(resdata):
                getlist.append(resdata[i])
    n = 0  # 序号
    for d in getlist:
        global curVol
        curVol = str(n)+':'+d['name']
        n += 1
        print('GETTING VOLUMN:', curVol)
        time.sleep(1)
        getArticlesList(d, opt, artnarr)

    print("DOWNLOAD FINI!", atotal, afini)
    global state
    state = '已完成'
    print(state)
    return 'getVolums OK!'

modules/options.py

savePath='../data'
urlVolumns = 'https://www.jianshu.com/author/notebooks'
params = {'order_by': 'shared_at', 'page': '1'}
headers = '''
请复制粘贴您浏览器的headers信息
'''
def str2obj(s, s1=';', s2='='):
    li = s.split(s1)
    res = {}
    for kv in li:
        li2 = kv.split(s2)
        if len(li2) > 1:
            res[li2[0]] = li2[1]
    return res

headers = str2obj(headers, '\n', ': ')

欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】


每个人的智能新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352