本文的主要内容是使用Python及Selenium自动发表文章到思否的实现思路及方式的介绍并为读者提供参考的源代码。
目录
实现思路
模拟登录目标网站,在此我们选择了使用QQ授权登录。
将文章内容写入网站的编辑器并发表。
Markdown文章的处理
目前来说,我的博客技术公馆使用Hugo进行构建。和许多其他的博客框架类似,Hugo中的文章主要由title ,categories 等配置字段及文章的正文。一般来说,配置字段都使用了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
由于目前的功能较为简单,我只选用了配置字段中的title ,tags ,categories 及draft 。title ,tags 及categories 会被应用于文章的发表过程中。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登录界面的分析我们可以发现,我们需要点击的头像位于id 为ptlogin_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')
文章标题的输入相对简单,只需找到id 为title 的输入框进行输入即可。代码如下所示:
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()
可以看到此部分就是一个简单的对标签的遍历。如果在思否中搜索不到该标签则会跳过。需要说明的是最后一行,由于添加完标签后标签界面并不会消失,此时如果直接进行后续操作则会报错。所以需要点击一下其他位置才可进行后续操作,在此我选择了点击标题。
对于正文的输入,思否的编辑器不像许多网站的编辑器只是简单的一个输入框。思否的编辑器中每行都是一个单独的元素,且使用的 不可进行输入。对此可以有着多种的解决方案,而我选取了一种迂回的方案进行解决。我首先模拟鼠标的点击使光标处在输入框中,接下来将正文复制进剪贴板并粘贴到编辑器中即可。
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/i_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('sureSubmitBtn'))
/confirm/i_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('--- ', 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/i_button = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('sureSubmitBtn'))
/confirm/i_button.click()