Van♂Python | 焯!🤡随手写个文章多平台发布脚本

·  阅读 4944
Van♂Python | 焯!🤡随手写个文章多平台发布脚本

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

0x1、引言

事出必有因,最近发现,发在掘金的新文章,阅读量都大不如前了...

虽说不要太看重这个(假的),但写了,发出来了,肯定是希望有人看,有人讨论交流,这样才会有进步,不然放云笔记自己品不香么?

简单分析一下阅读量变少的原因,可能是下述三点:

  • 奖励丰厚的征文活动,吸引了一大波新的创作者,也产生了海量的文章,僧多粥少,流量自然也少了;
  • 个性化推荐算法,旧文过时而当道,新文首发即雪藏,具体官方解读可见:关于个性化推荐画风反馈的说明
  • 自己写的文章质量下降了,怎么可能(甩锅)?该有的灵魂表情包都有啊,而且都是上手实操。

之前写的文章都是 首发掘金,然后用自研耗子喂汁转换器 hzwz-markdown-wx 把md转换成带自定义样式的HTML,贴到公号上完事。

其他平台就懒得发了,粘贴复制太累了人,有想过写一个自动化脚本,后面因为各种原因搁置就忘记了。

最近突然想起这件事了,好文怎能埋没,其他平台也得发一发,照惯例,先问下有没有轮子先,有的话就不用自己造了,于是群里问了一波:

em...好像是没有的样子,没的话,就自己搞一个咯,也不算太复杂,恰好大敌产品经理最近请假,日常工作就是改改UI而已,摸鱼时间足够,搞起!!!

在不破解接口的情况下,搞定多个站点,效率最快、最简单的实现方式莫过于 → 浏览器模拟点点点

先罗列下想要发布的站点,有补充的欢迎在评论区留言~

和之前两节:

《Van ♂ Python | 某站点课程的简单爬取》 《Van♂Python | 某星球的简单爬取》

怕律师函警告,偷偷搞不敢发脚本不同,本节脚本开源哈,欢迎伸手党clone试用提建议~

0x2、战术分析

可以发文的过程划分为三步:发布前发布中发布后,接着对每个阶段具体的流程进行细化:

简单解读下要点~

发布前

就是发布前的一些准备工作,先是 文章内容相关,分为两个部分,正文 + 附加信息,正文的话不同平台,支持的编译器略有差异,有备无患,所以准备下述三种:

  • MD文本 → 绝大部分平台都支持这个~
  • MD文件 → 部分平台不支持MD文本,但支持导入MD文件,比如知乎;
  • 渲染后的文本 → 部分平台不支持MD,可能需要解析渲染后复制,如51CTO博客旧版的WuKong编辑器;

然是是杂七杂八的附加信息,大概有这些:

标题摘要封面标签分类

然后 登录相关账号 + 密码,有其他要求的也可以加~

发布中

所有平台发文都要登录,所以发文前要进行 登陆状态判断,一般没登陆直接访问文章发布页,都会自动跳到登录页。当然也有例外,比如掘金还是在编辑页,但是发不了文章,所以需要自己触发登录相关的跳转。

然后是 自动登录,就是模拟人登录的流程,找到结点元素,点击、输入对应信息,然后执行登录。另外有写站点检测到登录异常没,还会触发各种验证码(滑块、点击、文字、旋转等),通知用户主动处理,然后轮询超时或休眠一段时间等待。

处理完登录后,接着就到 正文填充 了,支持直接输入的文本结点,直接塞,不支持的,可以:点击获取焦点正文内容写入剪贴板键盘Ctrl+A全选键盘Ctrl+V粘贴

紧接着到 附加信息 填充,找结点,点点点或者输入。

最后就是 文章发布 了,有些站点发布可能还有一些其他附加操作,没有的话就执行发布后的动作。

发布后

发布过程不一定顺风顺水,偶尔也会有异常,需要把异常信息写入文件中,用户手动发布或者引入重试机制重新发布~

0x3、详细设计

分析得差不多了,接着就到代码设计了,先是实体,从上面看需要两个:文章信息 + 账号密码,后者一般跟网站绑定,没必要独立出来,先写出文章信息实体:

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("文章发布完毕...")
复制代码

接着以发布到掘金为例,演示下具体怎么玩~

0x4、实例讲解——掘金发文流程

① 登录状态检测

根据实际情况,对父类中对应的方法进行重写,先是访问发布文章页: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()
复制代码

⑤ 发布结果处理

发布完成后会跳转页面,然后展示成功与否相关的提示,这边直接查找是否有发布成功结点相关的信息即可:

其他什么结果写入的,后续可以慢慢改,接着运行看看发布文章的效果:

偷起懒来简直不要太爽!!!

0x5、小结

基本雏形已经写完,目前支持:掘金、CSDN博客、51CTO博客、简书、知乎站点的一文多发,后续有闲暇时间会慢慢优化补全,最终期望是做成一个带图形化页面的工具或者像 OpenWrite 这样的插件,使其更好用及更易用~

仓库地址:ChaoMdPublish,欢迎试用,Star、Fork、提issues,谢谢~

收藏成功!
已添加到「」, 点击更改