0x1、引言
事出必有因,最近发现,发在掘金的新文章,阅读量都大不如前了...
虽说不要太看重这个(假的),但写了,发出来了,肯定是希望有人看,有人讨论交流,这样才会有进步,不然放云笔记自己品不香么?
简单分析一下阅读量变少的原因,可能是下述三点:
之前写的文章都是 首发掘金,然后用自研耗子喂汁转换器 hzwz-markdown-wx
把md转换成带自定义样式的HTML,贴到公号上完事。
其他平台就懒得发了,粘贴复制太累了人,有想过写一个自动化脚本,后面因为各种原因搁置就忘记了。
最近突然想起这件事了,好文怎能埋没,其他平台也得发一发,照惯例,先问下有没有轮子先,有的话就不用自己造了,于是群里问了一波:
em...好像是没有的样子,没的话,就自己搞一个咯,也不算太复杂,恰好大敌产品经理最近请假,日常工作就是改改UI而已,摸鱼时间足够,搞起!!!
在不破解接口的情况下,搞定多个站点,效率最快、最简单的实现方式莫过于 → 浏览器模拟点点点
先罗列下想要发布的站点,有补充的欢迎在评论区留言~
和之前两节:
《Van ♂ Python | 某站点课程的简单爬取》 《Van♂Python | 某星球的简单爬取》
怕律师函警告,偷偷搞不敢发脚本不同,本节脚本开源哈,欢迎伸手党clone试用提建议~
可以发文的过程划分为三步:发布前 → 发布中 → 发布后,接着对每个阶段具体的流程进行细化:
简单解读下要点~
就是发布前的一些准备工作,先是 文章内容相关,分为两个部分,正文 + 附加信息,正文的话不同平台,支持的编译器略有差异,有备无患,所以准备下述三种:
然是是杂七杂八的附加信息,大概有这些:
标题、摘要、封面、标签、分类
然后 登录相关, 账号 + 密码,有其他要求的也可以加~
所有平台发文都要登录,所以发文前要进行 登陆状态判断,一般没登陆直接访问文章发布页,都会自动跳到登录页。当然也有例外,比如掘金还是在编辑页,但是发不了文章,所以需要自己触发登录相关的跳转。
然后是 自动登录,就是模拟人登录的流程,找到结点元素,点击、输入对应信息,然后执行登录。另外有写站点检测到登录异常没,还会触发各种验证码(滑块、点击、文字、旋转等),通知用户主动处理,然后轮询超时或休眠一段时间等待。
处理完登录后,接着就到 正文填充 了,支持直接输入的文本结点,直接塞,不支持的,可以:点击获取焦点 → 正文内容写入剪贴板 → 键盘Ctrl+A全选 → 键盘Ctrl+V粘贴。
紧接着到 附加信息 填充,找结点,点点点或者输入。
最后就是 文章发布 了,有些站点发布可能还有一些其他附加操作,没有的话就执行发布后的动作。
发布过程不一定顺风顺水,偶尔也会有异常,需要把异常信息写入文件中,用户手动发布或者引入重试机制重新发布~
分析得差不多了,接着就到代码设计了,先是实体,从上面看需要两个:文章信息 + 账号密码,后者一般跟网站绑定,没必要独立出来,先写出文章信息实体:
class Article:
def __init__(self, md_file=None, md_content=None, render_content=None, tags=None, avatar=None,
summary=None, category=None, column=None, title=None):
""" 初始化方法
Args:
md_file: md文件
md_content: md文本
render_content: 渲染后的文本
tags: 标签
avatar: 封面
summary: 摘要
category: 分类
column: 专栏
title: 标题
"""
self.md_file = md_file
self.md_content = md_content
self.render_content = render_content
self.tags = tags
self.avatar = avatar
self.summary = summary
self.category = category
self.column = column
self.title = title
复制代码
接着到发文,每个站点的行为都是类似的,抽取共性属性和方法,定义一个父类,子类按需实现即可:
class Publish:
def __init__(self, website_name=None, write_page_url=None, login_url=None,
account=None, password=None, is_publish=True, page=None, article=None):
""" 抽取发布文章的公有属性
Args:
website_name: 站点名
write_page_url: 发布页url
login_url: 登录页url
account: 账号
password: 密码
is_publish: 是否发布,默认为True
page: Pyppeteer 的 Page实例,代表浏览器的一个页面
"""
self.website_name = website_name
self.write_page_url = write_page_url
self.login_url = login_url
self.account = account
self.password = password
self.is_publish = is_publish
self.page = page
self.article = article
self.logger = logging.getLogger(self.website_name)
self.logger.setLevel(logging.INFO)
# 传入Page和Article
def set_page(self, page, article):
self.page = page
self.article = article
# 加载发布页
def load_write_page(self):
self.logger.info("加载写文章页:{}".format(self.write_page_url))
# 检查登录状态
def check_login_status(self):
self.logger.info("检查登录状态...")
# 自动登录
def auto_login(self):
self.logger.info("开始自动登录:{}".format(self.login_url))
# 内容填充
def fill_content(self):
self.logger.info("开始内容填充...")
# 其他填充
def fill_else(self):
self.logger.info("其他内容填充...")
# 发布
def publish_article(self):
self.logger.info("发布文章...")
# 结果处理
def deal_result(self):
self.logger.info("文章发布完毕...")
复制代码
接着以发布到掘金为例,演示下具体怎么玩~
根据实际情况,对父类中对应的方法进行重写,先是访问发布文章页:juejin.cn/editor/draf…
未登录,可以访问,不会自动跳转,所以需要我们自己判断,对比下登录前后的不同:
2333,不难发现,登录状态,右上角会有用户头像,检测下这个结点是否存在即可,看下结点信息:
不难写出这样的代码:
class JueJinPublish(Publish):
async def load_write_page(self):
super().load_write_page()
# 加载文章发布页,超时一分钟
await self.page.goto(self.write_page_url, options={'timeout': 60000})
await asyncio.sleep(1)
await self.check_login_status()
async def check_login_status(self):
super().check_login_status()
try:
await self.page.waitForXPath("//nav//div[@class='toggle-btn']", {'visible': 'visible', 'timeout': 3000})
self.logger.info("处于登录状态...")
await self.fill_content()
except errors.TimeoutError as e:
self.logger.warning(e)
self.logger.info("未登录,执行自动登录...")
await self.auto_login()
复制代码
流程:跳转首页 → 点击右上角登录按钮 → 其他登录方式 → 输入账号 → 输入密码 → 点击登录
然后贴心地出现了滑动验证:
等待用户验证,啥时候验证完?直接 等待登录按钮不可见 即可,超时1分钟,然后跳转文章编辑页~
async def auto_login(self):
super().auto_login()
try:
await self.page.goto(self.login_url, options={'timeout': 60000})
await asyncio.sleep(2)
login_bt = await self.page.Jx("//button[@class='login-button']")
await login_bt[0].click()
prompt_box = await self.page.Jx("//div[@class='prompt-box']/span")
await prompt_box[0].click()
account_input = await self.page.Jx("//input[@name='loginPhoneOrEmail']")
await account_input[0].type(self.account)
password = await self.page.Jx("//input[@name='loginPassword']")
await password[0].type(self.password)
login_btn = await self.page.Jx("//button[@class='btn']")
await login_btn[0].click()
self.logger.info("等待用户验证...")
# 接着超时等待登录按钮消失,提示用户可能要进行登录验证
await self.page.waitForXPath("//button[@class='login-button']", {'hidden': True, 'timeout': 60000})
self.logger.info("用户验证成功...")
await self.load_write_page()
except errors.TimeoutError:
self.logger.info("用户验证失败...")
self.logger.error("登录超时")
await self.page.close()
except Exception as e:
self.logger.error(e)
复制代码
跳转回文章发布页,然后是文章填充的流程:
填充标题 → 填充内容部分 → 选择Markdown主题 → 选择代码高亮样式
标题还好,拿到文本控件填充,内容部分不能直接塞,使用剪切板大法解决,然后是Markdon主题和代码高亮样式的选择,这个可不太好搞:
结点获取到焦点,然后才 动态弹出选项列表,Elements一跟结点选项列表就消失,拿不到结点信息,笔者的解决方法代码:
拿到焦点显示出列表时,打印网页源码,一步步定位
发布页那里好几个也是这样,判断下样式文本是否与预设的相同,是直接点击就好,不难写出这样的代码:
async def fill_content(self):
super().fill_content()
# 设置标题
title_input = await self.page.Jx("//input[@class='title-input title-input']")
await title_input[0].type(self.article.title)
# 内容部分不是纯文本输入,点击选中,然后复制粘贴一波~
content_input = await self.page.Jx("//div[@class='CodeMirror-scroll']")
await content_input[0].click()
cp_utils.set_copy_text(self.article.md_content)
await cp_utils.hot_key(self.page, "Control", "KeyA")
await cp_utils.hot_key(self.page, "Control", "KeyV")
# 掘金会进行图片压缩处理,要等一下下再进行后续处理
await asyncio.sleep(3)
# 选择Markdown主题和代码高亮样式
md_theme = await self.page.Jx("//div[@bytemd-tippy-path='16']")
await md_theme[0].hover()
# 选中喜欢的主题,比如:smartblue
md_theme_choose = await self.page.Jx(
"//div[@class='bytemd-dropdown-item-title' and text()='{}']".format('smartblue'))
await md_theme_choose[0].click()
# 同理选中喜欢的代码样式,比如:androidstudio
code_theme = await self.page.Jx("//div[@bytemd-tippy-path='17']")
await code_theme[0].hover()
code_theme_choose = await self.page.Jx(
"//div[@class='bytemd-dropdown-item-title' and text()='{}']".format('androidstudio'))
await code_theme_choose[0].click()
# 补充其他信息
await self.fill_else()
复制代码
附加信息填充的流程如下:
点击右上角发布按钮 → 选择分类 → 添加标签 → 上传文章封面 → 选择专栏(可选) → 输入摘要 → 点击确定并发布
如下图:
分类还好,比对下文本跟预设的是否一致,是选中,添加标签和上面的主题选择一样玩法,然后文章封面上传,找到 //input[@type='file']
的结点,调用下 uploadFile()
方法即可完成上传。
填充摘要,跟正文一样,使用剪切板粘贴大法接口,最后再点下确定并发布就好,同样不难写出下述代码:
async def fill_else(self):
super().fill_else()
# 点击发布按钮
publish_bt = await self.page.Jx("//button[@class='xitu-btn']")
await publish_bt[0].click()
# 选中分类
category_check = await self.page.Jx("//div[@class='item' and text()=' {} ']".format(self.article.category))
await category_check[0].click()
# 添加标签
for tag in self.article.tags:
tag_input = await self.page.Jx("//input[@class='byte-select__input']")
await tag_input[0].type(tag)
await asyncio.sleep(1)
# 默认选中第一个
tag_li = await self.page.Jx("//li[@class='byte-select-option byte-select-option--hover']")
await tag_li[0].click()
# 添加封面
upload_avatar = await self.page.Jx("//input[@type='file']")
await upload_avatar[0].uploadFile(self.article.avatar)
# 填充摘要
summary_textarea = await self.page.Jx("//textarea[@class='byte-input__textarea']")
await summary_textarea[0].click()
cp_utils.set_copy_text(self.article.summary)
await cp_utils.hot_key(self.page, "Control", "KeyA")
await cp_utils.hot_key(self.page, "Control", "KeyV")
await self.publish_article()
async def publish_article(self):
super().publish_article()
publish_btn = await self.page.Jx("//div[@class='btn-container']/button")
await publish_btn[1].click()
await asyncio.sleep(2)
await self.deal_result()
复制代码
发布完成后会跳转页面,然后展示成功与否相关的提示,这边直接查找是否有发布成功结点相关的信息即可:
其他什么结果写入的,后续可以慢慢改,接着运行看看发布文章的效果:
偷起懒来简直不要太爽!!!
基本的雏形就是这样,后续就是其他站点的脚本编写,加入配置文件,支持多站点同时发布,发布结果处理,还有一些逻辑的优化了~
先把仓库连接丢这:ChaoMdPublish,感兴趣的可以先Star下,下午抓紧摸鱼时间,把剩下的肝完,在补充亿点点细节,谢谢~
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。