用Python实现公众号文章原汁原味剪藏
用Python实现公众号文章原汁原味剪藏
文章图片
每当我们看到不错的公众号文章是不是都忍不住想收藏起来,但是收藏起来有不少问题,有些文章会过期或是被作者删除,我们再想看的时候就找不到了。用过印象笔记的同学都知道,印象笔记的剪藏功能很强大,能原汁原味的保留样式,今天我们介绍用Python实现公众号图片和文字的下载,并且可以原汁原味的访问。
之前看了用 Python 抓取公号文章保存成 HTML受到启发,同时也进行了改进,比如加入了异步协程下载的处理,对样式和相对路径进行改进,随有这篇文章。异步协程的使用可以参考Python 异步爬虫 aiohttp 示例,案例能够跑通,我们下面进入正题。
需要解决的问题
- 网页源码内容的下载
我们需要使用Python正确的访问文章地址,这里需要请求框架模拟正常的用户请求,需要设置请求头Header,如果没有的话可能就会被反爬处理获取不到正确的内容
- 网页图片和样式资源的下载
对于图片资源下载来说,要考虑效率问题,如果使用同步方式一张一张下载,只有等待前一张下载完成才能下载下一张,图片很多的话是必要花费很多时间。我们可以采用异步Http请求来实现,把网络资源请求的多个任务做成异步的,多张图片同时请求,然后统一等待下载结果,效率会大大提升。
- 网页的图片资源本地化替换
网页中不仅涉及到图片资源,还涉及到一些样式和Javascript相关的前端逻辑处理,如果不做本地化处理的话,可能一些样式会无法访问,比如文章格式居中或是代码块样式等。如下图,就是下载下来的网页代码样式丢失的,就是js等资源问题本地访问失败的情况
文章图片
对于图片资源,由于网页中的图片是走的公众号平台的图床,往往有防盗链和访问限制,我们需要把网页源码中的图片资源链接替换成访问本地的地址,这里涉及到一些文本替换的逻辑。
这些需要提前使用pip提前安装好asyncio、aiohttp、BeautifulSoup4
import asyncio
import os
import time
import datetime
import reimport aiohttp
from aiohttp import TCPConnector
from bs4 import BeautifulSoup
网页源码下载
这里定义一个异步async的get_html方法,用来获取指定url的网页文章的源码内容,用来异步Http请求网页文章html源码,使用BeautifulSoup解析html文档为xml结构,然后获取文章作者和头图信息和网页图片img标签信息。
async def get_html(url, save_dir, temp_filename):
"""
获取url文章内容html,并返回原图片地址
:param url 地址
:param save_dir 保存目录
:param temp_filename 临时文件名
:return:图片链接地址
"""
async with aiohttp.ClientSession(
headers=headers, connector=TCPConnector(ssl=False)) as session:
async with session.get(url) as resp:
text = await resp.text()
# 使用beautiful解析网页内容
soup = BeautifulSoup(text, 'lxml')
# 解析微信文章网页meta信息获取文章作者、头图等信息
article_infos = parse_wechat_article_info(soup, url)
# 文件名windows环境安全处理
filename = article_infos.get("title", temp_filename)
filename = get_safe_file_name(filename)
# 保存网页内容到本地
save_html(save_dir, text, filename)
# 获取网页中所有img标签
imgs = soup.find_all("img")
img_links = []
for img in imgs:
if 'data-src' in str(img):
img_links.append(img['data-src'])
elif 'src=""' in str(img):
pass
elif "src" not in str(img):
pass
else:
img_links.append(img['src'])
return img_links, text, article_infos, filename
定义save_html函数,保存网页源码到本地
def save_html(save_path, html, name):
"""
保存html到本地
:param save_path: 保存地址
:param html: html文本内容
:param name: 保存名称
:return:, 'w'
"""
with open(os.path.join(save_path, name + '.html'), 'w', encoding='utf-8') as f:
f.write(html)
return html
图片异步下载
这里定义异步的
download_picfile
下载图片文件,保存图片为jpeg格式。首先对图片img标签中的data-src属性中的图片地址进行预处理,有些图片并不是以
http://
开头的绝对路径,而是相对路径和绝对路径的混合,为了能够正确请求,需要根据情况补充正确的图片地址。对于每次处理后的原链接和下载到本地的保存链接,以字典
{"old": pic_url, "new": image_file_abspath}
的形式存储到old_new_links_dicts
列表中,为后续替换为本地地址做准备。async def download_picfile(pic_url, save_path, new_pic_name, old_new_links_dicts, image_dirname='images'):
"""
下载图片文件,保存图片到本地,并更新html的图片路径
:param pic_url: 图片路径
:param save_path:保存基地地
:param new_pic_name:保存名称
:return:
"""
# print("image_url:" + pic_url)
request_pic_url = ""
# 跟据文章的图片格式进行处理
if pic_url.startswith('//'):
request_pic_url = 'https:' + pic_url
# print('image_type: 不带请求协议头的路径的图片 url:', pic_url)
elif pic_url.startswith('/') and pic_url.endswith('gif'):
request_pic_url = base_image_url + pic_url
# print('image_type: pic 不带基础路径的图片 url:', pic_url)
elif pic_url.endswith('png') or pic_url.endswith('jpg') or pic_url.endswith('gif') or pic_url.endswith('jpeg'):
request_pic_url = pic_url
# print('image_type: png,jpg,gif,jpeg url:', pic_url)
else:
# 全路径的
request_pic_url = pic_url
# print('image_type: 其他类型图片 url:', pic_url)
async with aiohttp.ClientSession(
headers=headers, connector=TCPConnector(ssl=False)) as session:
async with session.get(request_pic_url) as resp:
text = await resp.read()
# 创建指定目录
save_image_path = os.path.join(save_path, image_dirname)
if not os.path.exists(save_image_path):
os.makedirs(save_image_path)
image_type = "jpeg"
new_pic_save_name = new_pic_name + "." + image_type
image_file_abspath = os.path.join(save_image_path, new_pic_save_name)
print('save picture... name:', new_pic_save_name)
with open(image_file_abspath, 'wb') as f:
f.write(text)
# 替换更新html地址,记录到字典列表里面
if old_new_links_dicts is not None:
old_new_links_dicts.append({"old": pic_url, "new": image_file_abspath})
批量异步执行逻辑
主要的程序逻辑如下:
首先,获取loop执行第一个异步任务
get_html
获取links, html, article_info, article_filename这些信息然后,根据返回的图片地址列表links,批量追加download_picfile下载图片任务到loop的异步执行的任务列表中
调用
loop.run_until_complete()
等待所有异步下载图片任务执行完毕最后,调用
update_htmlfile_imgs
方法,把old_new_links_dicts
存放的原远程图片地址和图片对应本地相对地址,对本地的网页html源码进行更新# 异步循环处理
loop = asyncio.get_event_loop()
task = loop.create_task(get_html(link_url, base_path, base_picprefix))
# 获取文章task返回的信息,图片列表,网页的html文本,文章作者信息,处理后的文章标题
links, html, article_info, article_filename = loop.run_until_complete(task)
# 新的异步任务列,用来下载图片
tasks = []
old_new_links_dicts = []
# 构造图片存储文件名
index = 0
for link in links:
pic_name = base_picprefix + "_" + str(index)
tasks.append(download_picfile(link, base_path, pic_name, old_new_links_dicts))
index += 1
# 头图下载保存到html文件同路径,不记录到替换列表中
tasks.append(download_picfile(link, base_path, article_filename, None, ''))
# 批量执行并等待结果
loop.run_until_complete(asyncio.gather(*tasks))
# print("替换列表:", old_new_links_dicts)
# 执行完批量处理后,处理html的文本内容
update_htmlfile_imgs(base_path, article_filename, old_new_links_dicts)
end = time.time()
print(f'下载完成{index}个图片,用时:{end - start}秒')
网页图片路径替换
这里对html文件中进行读取,对每一行的内容,进行图片新旧地址的替换。替换过程先打开源html文件,再写入一个temp文件,对后删除源html并重命名temp文件为源html文件
图片相对路径替换:由于不同系统的文件路径是不一样的,windows有磁盘分区,而Mac、Linux等Unix类型的系统是根路径/开头的,使用文件绝对路径会产生问题,这里就需要预先对
old_new_links_dicts
中的new的本地路径替换成image/xxx.jpeg的相对路径,统一保存到html中的img标签的src属性里样式资源的替换:由于网页下载到了本地,资源链接很多都是写的相对地址,本地访问不到就会出现代码行和居中等样式问题,这里为了解决这个问题替换原来
"src=https://www.it610.com/"//res"
和"href=https://www.it610.com/"//res"
相对资源开头地址,换成网络的https://res
开头的地址,可以保证样式的处理# 修改 HTML 文件,将图片的路径改为本地的路径
def update_htmlfile_imgs(html_path, html_file_name, old_new_links_dicts):
"""
更新html文件中的图片链接,并更换为相对的图片地址
:param html_path: html文件目录
:param html_file_name: 文件名
:param old_new_links_dicts: 要替换图片的字典list
:return:
"""
# 预处理相对路径链接,遍历其中的内容
for link_dict in old_new_links_dicts:
# print("dict:",link_dict)
# 处理列表中的字典dict,只替换new链接的value值
for key in link_dict.keys():
if key == 'new':
local_abs_path = link_dict.get(key)
# 此时获取的路径是绝对路径,绝对路径转换相对路径
new_relative_link = get_imgfile_relative_path(local_abs_path)
link_dict[key] = new_relative_link# 打开两个文件,原始文件用来读,另一个文件将修改的内容写入
# 原始html
html_file_abs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), html_path, html_file_name + '.html')
# 临时html文件
html_file_temp_abs_path = os.path.join(os.path.abspath(__file__), html_path, html_file_name + '_bak.html')
with open(html_file_abs_path, encoding='utf-8') as f, open(html_file_temp_abs_path, 'w', encoding='utf-8') as fw:
# 遍历每行,用replace()方法替换路径
for line in f:
# 优先替换图片路径
for i in range(0, len(old_new_links_dicts)):
link_map = old_new_links_dicts[i]
old_link = link_map.get("old")
new_relative_link = link_map.get("new")
line = line.replace(old_link, new_relative_link)
# 图片的img标签属性处理下,图片默认是data-src的,估计微信有处理
line = line.replace("data-src", "src")
# 再替换其他的常规资源路径,如js等
line = line.replace("src=https://www.it610.com/"//res", "src=https://www.it610.com/"https://res")
line = line.replace("href=https://www.it610.com/"//res", "href=https://www.it610.com/"https://res")# 写入新文件
fw.write(line)
# 执行完,删除原始文件
os.remove(html_file_abs_path)
time.sleep(2)
# 修改新文件名为 html
os.rename(html_file_temp_abs_path, html_file_abs_path)
获取网页文章名保存
这里需要分析提取公众号文章的作者和标题信息,都放在meta标签里面,所以需要提取meta标签中的元素,如图:
文章图片
处理逻辑如下:
def parse_wechat_article_info(xml_bssoup, link_url):
"""
解析微信微信文章基本信息:作者,标题,摘要,文章头图
:param xml_bssoup: beautifulSoup解析的网页html的对象
:param link_url: 微信文章链接地址
:return:
"""
# 标题,作者,链接,头图,描述
title = ''
author = ''
link = link_url
head_image = ''
desc = ''
# 非公众号文章通用处理
if link_url.find("mp.weixin.qq.com") == -1:
print('非微信公众号文章 url:%s' % link_url)
if xml_bssoup.title_name is not None:
print(xml_bssoup.title_name.string)
title = xml_bssoup.title_name.string# 遍历meta标签获取,properties属性,meta标签有property和content两个属性,相当于key和value
meta_list = xml_bssoup.find_all('meta')
# print("meta_list:", meta_list)
for meta in meta_list:
if str(meta.attrs).find('property') != -1:
# 获取property属性值
prop_value = https://www.it610.com/article/meta['property']
# 标题
if prop_value =https://www.it610.com/article/='og:title':
title = meta['content']
# 作者
if prop_value =https://www.it610.com/article/='og:article:author':
author = meta['content']
# 文章头图
if prop_value =https://www.it610.com/article/='og:image':
head_image = meta['content']
# 描述
if prop_value =https://www.it610.com/article/='og:description':
desc = meta['content']# 文章描述
if str(meta.attrs).find('name=') != -1:
# 获取property属性值
name_value = https://www.it610.com/article/meta['name']
if name_value =https://www.it610.com/article/='description':
desc = meta['content']
if name_value =https://www.it610.com/article/='author':
author = meta['content']
# title, author, link, head_image, desc = ''
article_dict = {
"title": title,
"author": author,
"link": link,
"head_image": head_image,
"desc": desc}
# 遍历其中的内容
print("文章信息:", article_dict)
return article_dict
程序执行结果验证
好了,上面就是将文章页面和图片下载到本地的代码,接下来我们再Idea上运行一下 ,程序开始执行,打印日志如下:
可以看到,文章中的6张图片总下载时长是2.6秒,已经很快了
/opt/anaconda3/bin/python "/Users/glen/Nutstore Files/vscodeSpace/python/wechat_article_downloader.py"
文章信息: {'title': '用 Kindle + KindleMate + Anki + Economist 打造完美英语学习系统', 'author': '刘志新', 'link': 'https://mp.weixin.qq.com/s/HgwOO5RUT7BtZuNBuokwqw', 'head_image': 'http://mmbiz.qpic.cn/mmbiz_jpg/BBXrCicxMgHtPnR9ibVozdq1rUCl3gVhJ6HCDs6Pg2kmGe6FOicgSJbyya0boE4OH3icy5HUT4I3rH07ELibxwsZrvA/0?wx_fmt=jpeg', 'desc': '英语不是学的,而是用的'}
save picture... name: 20220417223340406126_4.jpeg
save picture... name: 用 Kindle + KindleMate + Anki + Economist 打造完美英语学习系统.jpeg
save picture... name: 20220417223340406126_2.jpeg
save picture... name: 20220417223340406126_5.jpeg
save picture... name: 20220417223340406126_0.jpeg
save picture... name: 20220417223340406126_1.jpeg
save picture... name: 20220417223340406126_3.jpeg
下载完成6个图片,用时:2.6417782306671143秒
我们去程序存放的目录,就能看到下载下来的文章和图片,图片文件统一放到
images
文件夹中,网页文件html
在 images
同级目录文章图片
这里把文章信息中的头图也按照文章名同名保存下来了
访问具体的html可以看文章信息,代码样式也正常展示了
文章图片
Mac上的html文件图片,也换成了本地的相对路径
文章图片
总结 本文为大家介绍了如何通过 Python 将公号文章原汁原味下载到本地,并保存为 HTML 和图片,这样就能实现文章的离线浏览了。当然如果你想将 HTML 转成 PDF 也很简单,直接用
pdfkit.from_file(xx.html,target.pdf)
方法直接将网页转成 PDF,而且这样转成的 PDF 也是带图片的。展望,后续可以基于此方案做扩展,比如加入GUI界面,实现多篇文章的批量下载,或者是封装成下载模块,给web的API接口使用,接入机器人如Hubot,使用IM聊天工具发送文章链接给bot机器人,机器人执行脚本或是服务进行文章的下载等。
这是我写的第一篇正式的技术文章,请多多大家指教,欢迎关注点赞分享
【代码*获取方式*】
章鱼的学习探索
公众号,回复:220417【用Python实现公众号文章原汁原味剪藏】
文章图片
推荐阅读
- 【xxl-job】分布式任务调度平台 使用总结
- 使用for-of遍历对象
- 模拟可读流,可写流实现
- 如何修复Windows 10亮度不起作用(解决办法教程)
- 如何修复iPhone屏幕录制不起作用(解决办法教程)
- Android 应用开机自启和无需权限开启悬浮框
- android app使用微信登录接口回调没有被执行的问题研究
- Android之Fragment的优点和作用
- android(Android中用文件初始化sqlite数据库)
- android高仿小视频应用锁3种存储库QQ小红点动画仿支付宝图表等源码