使用Python及Selenium自动发表文章 篇二:思否

2020-05-04
6分钟阅读时长

前文中我们已经探讨过使用Python及Selenium自动发表文章的实现思路。本文是使用Python及Selenium自动发表文章系列文章的第二篇。在本文中我们将实现将文章自动发表到思否上。

本文的主要内容是使用Python及Selenium自动发表文章到思否的实现思路及方式的介绍并为读者提供参考的源代码。

实现思路

在自动发表文章时,我们的实现思路大致如下:

  1. 对Markdown文章进行处理,生成相应的配置及内容。
  2. 模拟登录目标网站,在此我们选择了使用QQ授权登录。
  3. 将文章内容写入网站的编辑器并发表。

Markdown文章的处理

目前来说,我的博客技术公馆使用Hugo进行构建。和许多其他的博客框架类似,Hugo中的文章主要由titlecategories等配置字段及文章的正文。一般来说,配置字段都使用了yaml语法或toml语法。由于我使用的是yaml语法,因此此文中的示例为处理yaml语法的配置字段。

对于文章的处理较为简单,只需按配置字段对应的语法进行处理并将其与正文内容保存即可。

为了将文章更规整地保存,我将配置字段及正文保存在Post实体类中,如下所示:

class Post(object):

    def __init__(self, title, tags, categories, content, draft):
        self.title = title
        self.tags = tags
        self.categories = categories
        self.content = content
        self.draft = draft

由于目前的功能较为简单,我只选用了配置字段中的titletagscategoriesdrafttitletagscategories会被应用于文章的发表过程中。draft则决定文章是否为草稿。

由于Hugo文章中使用---来划分yaml语法的配置字段,我们在读取文章时,只需依据---将文章分为配置字段及正文即可。为了处理yaml语法的配置字段,需要import yaml来对其进行操作。

模拟登录目标网站

由于许多平台在使用账号密码登录时都会进行验证,且绝大多数国内平台都支持QQ授权登录。为了避免对验证码的识别且登录模块的复用,我选择了使用QQ进行登录。

我们首先会读取本地cookies,如果不存在本地cookies则使用QQ登录。读取cookies的代码如下所示:

try:
    with open('segmentfault_cookies.json', 'r', encoding='utf-8') as f:
        cookies = json.loads(f.read())
except FileNotFoundError:
    cookies = json.loads(segmentfault_login.qq(driver, timeout))

使用QQ登录时,只需模拟点击QQ登录的按钮,就可以进入登录界面。如果有对Selenium基本操作还不熟悉的读者可以阅读我之前的所写的文章使用Selenium自动化你的浏览器。此文中包含了所有后续会用到的Selenium操作。登录部分每个网站的实现均一致。

通过对QQ登录界面的分析我们可以发现,我们需要点击的头像位于idptlogin_iframe的iframe中,因此我们需要进入此iframe中进行后续操作。

iframe = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe'))
driver.switch_to_frame(iframe)

在iframe中我们只需点击头像的位置即可进行登录。需要注意的是,在进行授权登录时,相关的操作需要使用等待的方式来实现。因为登录界面部分内容需要较长的时间进行加载。

login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_class_name('face'))
login.click()

可以看到,我们只需在iframe中找到头像的元素并点击即可成功登录。

在编辑器中写入文章

登录成功后我们即可访问写博客的界面进行书写。在思否中写文章的界面有一定记录会被跳转到写作指导界面,此时需要进行如下处理:

write_link = 'https://segmentfault.com/write'
driver.get(write_link)
url = driver.current_url
if 'howtowrite' in driver.current_url:
    driver.get('https://segmentfault.com/write?freshman=1')

文章标题的输入相对简单,只需找到idtitle的输入框进行输入即可。代码如下所示:

title = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('title'))
pyperclip.copy(post.title)
title.clear()
title.send_keys(Keys.CONTROL, 'v')

由于思否发表文章时必须输入标签,因此我们直接使用文章配置字段tags中的内容。具体实现代码如下所示:

for tag in post.tags:
    pyperclip.copy(tag)
    search_tag.send_keys(Keys.CONTROL, 'v')
    search_result = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('tagSearchResult').find_element_by_tag_name('a'))
    if search_result.text == '找不到相关标签':
        pass
    else:
        search_result.click()
ActionChains(driver).move_to_element(title).click().perform()

可以看到此部分就是一个简单的对标签的遍历。如果在思否中搜索不到该标签则会跳过。需要说明的是最后一行,由于添加完标签后标签界面并不会消失,此时如果直接进行后续操作则会报错。所以需要点击一下其他位置才可进行后续操作,在此我选择了点击标题。

对于正文的输入,思否的编辑器不像许多网站的编辑器只是简单的一个输入框。思否的编辑器中每行都是一个单独的元素,且使用的<span>不可进行输入。对此可以有着多种的解决方案,而我选取了一种迂回的方案进行解决。我首先模拟鼠标的点击使光标处在输入框中,接下来将正文复制进剪贴板并粘贴到编辑器中即可。

contentclick = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id="sfEditor"]/div/div[2]/div/div[1]/div[1]/div/div[6]/div[1]/div/div/div/div[5]/pre'))
contentclick.click()
pyperclip.copy(post.content)
ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').perform()

为了实现对剪贴板的操作,我在其中使用了pyperclip库进行剪贴板的处理。

此处的另一种实现思路是通过send_keys()方法模拟键盘输入。然而在使用的过程中我发现send_keys()方法无法输入反括号(`)。针对此问题网络上并没有适合的有关解释。由于send_keys()方法的实现方式是模拟键盘输入,因此我猜测可能是由于语言的有关问题导致的。如果读者也遇到过此类问题欢迎在评论中指出解决方法。

在输入完正文内容后,只需点击发表按钮即可将文章发表了。点击发表按钮的有关代码如下所示:

submit_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id="submitDiv"]/button'))
submit_button.click()
confirm_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('sureSubmitBtn'))
confirm_button.click()

可以看到,思否发布文章时需要首先点击发表之后再点击确认即可。

实现方式

在前文中,我们已经了解了将文章发表在思否上的思路。接下来则是实现具体的代码部分。

主程序

segmentfault.py为在思否上发表文章的主程序。

import json

from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait

import post_reader
import segmentfault_login
import segmentfault_writer


base_url = 'https://segmentfault.com/'
driver = webdriver.Chrome()
driver.get(base_url)
driver.delete_all_cookies()

timeout = 5

try:
    with open('segmentfault_cookies.json', 'r', encoding='utf-8') as f:
        cookies = json.loads(f.read())
except FileNotFoundError:
    cookies = json.loads(segmentfault_login.qq(driver, timeout))

for cookie in cookies:
    driver.add_cookie({
        'name': cookie['name'],
        'value': cookie['value'],
        'path': cookie['path'],
        'domain': cookie['domain'],
        'secure': cookie['secure']
    })
driver.get('https://segmentfault.com/')

mypost = post_reader.read_file('your_post.md')
segmentfault_writer.write(mypost, driver, timeout)

Markdown文章的处理

segmentfault.py中我们分别调用了各模块进行文章读取,登录及文章发表等操作。在post_reader.py中的则是对文章进行读取的相关操作。

import yaml

from post import Post


def read_file(file_path):
    with open(file_path, 'r', encoding= 'UTF-8') as f:
        whole = f.read().split('---\n', 2)
        content = whole[2]
        
        try:
            config = yaml.safe_load(whole[1])
            title = config['title']
            tags = config['tags']
            categories = config['categories']
            draft = config['draft']
            return Post(title, categories, content, draft)
        except yaml.YAMLError as exc:
            print(exc)

使用QQ登录目标网站

在读取文章内容后则是使用QQ进行登录,这一部分在authorization.py中实现。

from selenium.webdriver.support.wait import WebDriverWait
 

def qq(driver, timeout):
    window_handles = driver.window_handles
    driver.switch_to_window(window_handles[-1])
 
    iframe = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe'))
    driver.switch_to_frame(iframe)
 
    login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_class_name('face'))
    login.click()

在编辑器中写入文章

最终我们只需在思否的文本编辑器中完成文章的发表即可,这一部分在segmentfault_writer.py中实现。

import time

import pyperclip
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains

from post import Post

def write(post, driver, timeout):
    write_link = 'https://segmentfault.com/write'
    driver.get(write_link)
    url = driver.current_url
    if 'howtowrite' in driver.current_url:
        driver.get('https://segmentfault.com/write?freshman=1')

    # 添加标题
    title = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('title'))
    pyperclip.copy(post.title)
    title.clear()
    title.send_keys(Keys.CONTROL, 'v')
    time.sleep(3)

    # 添加标签
    add_tag_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('add-tag-btn'))
    add_tag_button.click()
    search_tag = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('searchTag'))

    for tag in post.tags:
        pyperclip.copy(tag)
        search_tag.send_keys(Keys.CONTROL, 'v')
        search_result = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('tagSearchResult').find_element_by_tag_name('a'))
        if search_result.text == '找不到相关标签':
            pass
        else:
            search_result.click()
    ActionChains(driver).move_to_element(title).click().perform()
    time.sleep(3)

    # 添加正文
    content_click = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id="sfEditor"]/div/div[2]/div/div[1]/div[1]/div/div[6]/div[1]/div/div/div/div[5]/pre'))
    content_click.click()
    pyperclip.copy(post.content)
    ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').perform()

    # 发表文章
    submit_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id="submitDiv"]/button'))
    submit_button.click()
    confirm_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('sureSubmitBtn'))
    confirm_button.click()

由于思否对文章是自动保存的,因此如果读者只需保存为草稿的话,注释掉该部分即可。

以上为部分核心代码的简单分析,全部源代码读者可在后文处下载。

后记

本文中实现了将文章自动发表到思否的相关操作。由于思否编辑器的限制,在输入正文内容时我们采取了一些迂回的方式进行处理。

因为本文的目的是简洁明了地给读者展示发表文章的思路及过程,所以对异常处理等方面并没有进行深入地处理。在之后的文章中我们将专门针对此问题进行改进。

源代码

读者可点击此处获取本文源代码。如果本文对你有帮助的话,欢迎star我的项目并关注我!

本文首发于我的个人博客wangchucheng.com
原文链接:https://wangchucheng.com/zh/posts/python-selenium-post-2/
本博客文章除特别声明外均为原创,采用CC BY-NC-SA 4.0 许可协议进行许可。超出CC BY-NC-SA 4.0 许可协议的使用请联系作者获得授权。

Avatar

WANG Chucheng

说学逗唱样样不精的地道天津人。