这一部分介绍了对doc/docx、xls/xlsx、pdf三种类型文档的爬虫
由于这三种文档的下载方式比较类似,故在一篇文章中介绍
下载完成后均存储成pdf格式,对于绝大多数用户,可以应对绝大多数情况了
下载部分文档可能会存在少数格式错误,请谅解
写在最前
文章较长,如果需要代码话请直接移步最后(别忘了需要的依赖🧐)
或者前往github获取全部代码(好用的话不妨给个star):BaiduWenkuSpider
觉得好用的话也可以收藏,点赞,关注博主啊
简介
本项目是基于python实现对百度文库可预览文档的下载,实现了对以下文档格式的下载:
- doc/docx
- ppt/pptx
- xls/xlsx
- txt
⚠️本项目下载的文档均为pdf格式(除txt外)
⚠️项目是本人原创,转载请注明出处
⚠️项目是本人课程设计的作品,请勿用于商业用途
系列文章
具体实现
Step 1——问题分析
由于doc
/xls
与docx
/xlsx
下载方式相同,下面的介绍里都使用doc
与xls
在百度文库搜索一篇doc文档,出于一般性考虑,这里寻找了一篇包含图片的文档,如下图:
通过检查元素不难发现,文章中的图片和文字是分离的
暂且不考虑格式的问题,我们先来找到百度文库将文档中的图片和文字都存放在哪里了
根据前面两篇的经验,这些数据很大概率都是动态加载的(其实根据平时的浏览也可以发现,页面是随着滚动实时加载的),应该是由js动态加载的。虽然博主并不大懂js
(确实不大懂,临时学了一丢丢),但是数据应该是以json
的形势加载的,寻找到这些数据应该就可以找到文字和图片的链接了
Step 2——开始寻找json数据(脱发)
在Chrome抓包工具中的Network中搜索json,果然有所发现
随意打开其中的一个,发现其内容确实是json
数据,将其中的内容拷贝下来分析一下,
(使用的是sublime,将json
文件格式化,并将unicode
转为中文),
由于内容过长,这里略去一部分
wenku_1({
"outline": null,
"outlineMiss": null,
"font": {
"c04cb50d14791711cc79177c0010001": "宋体",
"c04cb50d14791711cc79177c0020001": "Times New Roman"
},
"style": [{
"t": "style",
"c": [1, 2, 3, 4, 0],
"s": {
"color": "#000000",
"font-size": "21.06"
}
},
...
...
{
"t": "style",
"c": [4],
"s": {
"letter-spacing": "0.031"
}
}],
"body": [{
"c": "第二章",
"p": {
"h": 21.06,
"w": 63.179,
"x": 135.036,
"y": 120.899,
"z": 2
},
"ps": null,
"t": "word",
"r": [1]
},{
"c": {
"ix": 0,
"iy": 1268,
"iw": 220,
"ih": 97
},
"p": {
"h": 97,
"opacity": 1,
"rotate": 0,
"w": 198,
"x": 135,
"x0": 135,
"x1": 135,
"x2": 333,
"x3": 333,
"y": 298.274,
"y0": 385.574,
"y1": 298.274,
"y2": 298.274,
"y3": 385.574,
"z": 13
},
"ps": null,
"s": {
"pic_file": "\/home\/iknow\/conv\/\/data\/\/bdef\/\/454516425\/\/454516425_1_13.png"
},
"t": "pic"
},
...
...
{
"c": " ",
"p": {
"h": 22.66,
"w": 5.264,
"x": 250.59,
"y": 846.374,
"z": 24
},
"ps": { "_enter": 1 },
"t": "word",
"r": [2]
}],
"page": {
"ph": 1262.879,
"pw": 892.979,
"iw": 893,
"ih": 2595,
"v": 6,
"t": "1",
"pptlike": false,
"cx": 0,
"cy": 0,
"cw": 892.979,
"ch": 1262.879
}
})
整个json数据大致可以分为4个部分,其中较为重要的是:
style
中的属性body
中的属性- 文字内容,主要为
c
属性 - 图片内容
- 文字内容,主要为
page
中的属性
找到了数据来源下一步就是要想办法获取所有的json
数据了
但是注意到json
数据中的图片链接显然是无法直接使用的,同样需要找到真正的图片链接
"pic_file":"\/home\/iknow\/conv\/\/data\/\/bdef\/\/454516425\/\/454516425_1_13.png"
Step 3——全部json数据的获取与图片的获取
首先来分析一下之前json文件的网页url
https://wkbjcloudbos.bdimg.com/v1/docconvert3504/wk/b57419a439104753b1478adb0d66b47e/0.json?responseContentType=application%2Fjavascript&responseCacheControl=max-age%3D3888000&responseExpires=Wed%2C%2020%20May%202020%2023%3A47%3A59%20%2B0800&authorization=bce-auth-v1%2Ffa1126e91489401fa7cc85045ce7179e%2F2020-04-05T15%3A47%3A59Z%2F3600%2Fhost%2F6b779f1dc2f2671429696d6f087d438b75c8a549cc8876665e39c6b74fcb9760&x-bce-range=0-4096&token=eyJ0eXAiOiJKSVQiLCJ2ZXIiOiIxLjAiLCJhbGciOiJIUzI1NiIsImV4cCI6MTU4NjEwNTI3OSwidXJpIjp0cnVlLCJwYXJhbXMiOlsicmVzcG9uc2VDb250ZW50VHlwZSIsInJlc3BvbnNlQ2FjaGVDb250cm9sIiwicmVzcG9uc2VFeHBpcmVzIiwieC1iY2UtcmFuZ2UiXX0%3D.CJEaHFuEqdzDjLX4eQVQDaM%2BvOgTCAy5w6SltafmQAI%3D.1586105279
看上去有点乱对不对,但是没关系,Chrome可以帮我们解析
responseContentType: application%2Fjavascript
responseCacheControl: max-age%3D3888000
responseExpires: Wed%2C%2020%20May%202020%2023%3A47%3A59%20%2B0800
authorization: bce-auth-v1%2Ffa1126e91489401fa7cc85045ce7179e%2F2020-04-05T15%3A47%3A59Z%2F3600%2Fhost%2F6b779f1dc2f2671429696d6f087d438b75c8a549cc8876665e39c6b74fcb9760
x-bce-range: 0-4096
token: eyJ0eXAiOiJKSVQiLCJ2ZXIiOiIxLjAiLCJhbGciOiJIUzI1NiIsImV4cCI6MTU4NjEwNTI3OSwidXJpIjp0cnVlLCJwYXJhbXMiOlsicmVzcG9uc2VDb250ZW50VHlwZSIsInJlc3BvbnNlQ2FjaGVDb250cm9sIiwicmVzcG9uc2VFeHBpcmVzIiwieC1iY2UtcmFuZ2UiXX0%3D.CJEaHFuEqdzDjLX4eQVQDaM%2BvOgTCAy5w6SltafmQAI%3D.1586105279
虽说解析出来了,但是直接构造这个url
显然不太现实,自然而然想到是否可以像前两篇一样,在某个地方(角落)找到全部的url
呢?
然而,在抓包工具里找了一圈,也没有发现任何蛛丝马迹(可能是我没找到)
但是,抱着试一试的心态在网站源代码上搜索了一下responseContentType
(上述属性的一个)
❗️❗️❗️令人舒适的事情发生了
眼花对不对,但是!!!虽然格式不完全匹配,但是再仔细观察之后可以发现所有的json
文件的url
乃至所有图片的url
都在网站的源代码中就能找到
- url中包含json
\x22https:\\\/\\\/wkbjcloudbos.bdimg.com\\\/v1\\\/docconvert3504\\\/wk\\\/b57419a439104753b1478adb0d66b47e\\\/0.json?responseContentType=application%2Fjavascript&responseCacheControl=max-age%3D3888000&responseExpires=Wed%2C%2020%20May%202020%2023%3A47%3A59%20%2B0800&authorization=bce-auth-v1%2Ffa1126e91489401fa7cc85045ce7179e%2F2020-04-05T15%3A47%3A59Z%2F3600%2Fhost%2F6b779f1dc2f2671429696d6f087d438b75c8a549cc8876665e39c6b74fcb9760&x-bce-range=0-4096&token=eyJ0eXAiOiJKSVQiLCJ2ZXIiOiIxLjAiLCJhbGciOiJIUzI1NiIsImV4cCI6MTU4NjEwNTI3OSwidXJpIjp0cnVlLCJwYXJhbXMiOlsicmVzcG9uc2VDb250ZW50VHlwZSIsInJlc3BvbnNlQ2FjaGVDb250cm9sIiwicmVzcG9uc2VFeHBpcmVzIiwieC1iY2UtcmFuZ2UiXX0%3D.CJEaHFuEqdzDjLX4eQVQDaM%2BvOgTCAy5w6SltafmQAI%3D.1586105279\x22
- url中包含png
\x22https:\\\/\\\/wkbjcloudbos.bdimg.com\\\/v1\\\/docconvert3504\\\/wk\\\/b57419a439104753b1478adb0d66b47e\\\/0.png?responseContentType=image%2Fpng&responseCacheControl=max-age%3D3888000&responseExpires=Wed%2C%2020%20May%202020%2023%3A47%3A59%20%2B0800&authorization=bce-auth-v1%2Ffa1126e91489401fa7cc85045ce7179e%2F2020-04-05T15%3A47%3A59Z%2F3600%2Fhost%2F0336326b05ce36fe2f927d6c23a3e5e21c98e6deb442298790a4b6de4949f189&x-bce-range=0-214236&token=eyJ0eXAiOiJKSVQiLCJ2ZXIiOiIxLjAiLCJhbGciOiJIUzI1NiIsImV4cCI6MTU4NjEwNTI3OSwidXJpIjp0cnVlLCJwYXJhbXMiOlsicmVzcG9uc2VDb250ZW50VHlwZSIsInJlc3BvbnNlQ2FjaGVDb250cm9sIiwicmVzcG9uc2VFeHBpcmVzIiwieC1iY2UtcmFuZ2UiXX0%3D.O16kBbGCue3q1HL3h%2BDKF%2F%2B6K9yRT6hRYxsao%2F4hPzs%3D.1586105279\x22
知道了如何获取这些信息,接下来就是爬取网页的源代码,利用正则表达式将这些url提取出来,当然,注意到在源代码中的url是无法直接使用的,需要简单的处理一下
具体处理主要分两步:
- 去除多余
\
- 去除开头末尾的
\x22
第一步很好理解,至于第二步,我也困扰了一会
之后查到原来这东西就是双引号,因为爬虫获取的内容本就是字符串形式,把这些内容直接替换掉即可
名称 | 字符 | ASCII | 16进制 | URL编码 | HTML编码 |
---|---|---|---|---|---|
双引号 |
" |
34 |
\x22 |
22% |
"或" |
虽然说得到了所有数据,但是由于各类文档中文字与图片都是有排版的,谁都不愿意看到自己的文档是乱作一团,文字与图片不分离的吧
下面的部分将介绍我鼓捣的一种方法(不知道之前有没有人这样干过,盲猜没有),最大程度上实现了对原格式文档的生成
Step 4——构造本地html文件,并将html文件转换生成pdf
其实最开始采用的不是这个办法,而是将上述获取的数据按照自己的理解进行排列(因为json
数据中包含 x
, y
,h
,w
这些很明显的位置数据)然后采用PIL绘图的方式将数据绘制在图片上,再生成pdf但是自己的理解毕竟太过肤浅(原网站太🐶),最终放弃了这个思路
于是想到了按照构造网页的方法,现生成html,再看看能不能将html文件转换成pdf
于是搜索了一下,果然pyhton
是有这个第三方库的,叫做pdfkit
但是这个第三方库需要依赖一个叫做wkhtmltopdf
的工具,故使用前需要先按下载一下这个第三方库
可以参考一下这篇 Python快速将HTML转PDF ,妈妈再也不会担心我不会转PDF了(类似的教程挺多的)
或者这篇Convert HTML to PDF with Python
Step 4.1——接下来就是要分析一下json数据是被怎么处理的
要想知道这个问题,最简单的办法就是去找js代码,然而并不大懂js
(这就尴尬了😳😳)
那么只能采用最笨的办法了,回到抓包工具去找js
代码
虽说没有直接的目标,但是还是可以用一些小技巧的,先看一下下面这张图(没错,和前面是同一张,懒得截)
注意到不论是文字还是图片都是在html
标签中的而这个标签中又包含了一个class
属性,通过搜索这个class属性,就可以大致确定某段js代码是不是参与了文字和图片的处理(当然,这是我的推测,但不妨一试)
在诸多js中搜索了一下,发现了这个名字中包含htmlReader
的js
将其打开,搜索reader-word-layer
,果然有所发现
那么接下来就将所有内容拷贝至sublime
进行分析
在其中找到了疑似js(内容过长,就不展示了)
define("wkcommon:widget/ui/reader/view/doc/render.js", function(t, e, r){}
Step 4.2——使用python复现这一段js代码
说实话,这是一段惨痛的历史,由于js中的函数大多长下面👇这样
w = function(t, e, r, i, o, a) {
var n, p, s, h, u = [],
l = 2,
c = e.v,
f = a.picUrl,
d = a.fontMapping,
x = {
word: m,
pic: F.other
},
y = {
pic: '<div class="reader-pic-layer" style="z-index:__NUM__"><div class="ie-fix">',
word: '<div class="reader-txt-layer" style="z-index:__NUM__"><div class="ie-fix">'
},
g = "</div></div>",
w = 0,
v = 0;
t.sort(function(t, e) {
return t.p.z - e.p.z
});
for (var _ = -1, b = t.length; ++_ < b;) n = t[_], "pic" === n.t && (w = Math.max(w, n.c.ih + n.c.iy + 5), v = Math.max(v, n.c.iw));
for (var _ = -1, b = t.length; ++_ < b;) n = t[_], s = n.t, !p && (p = h = s), p === s ? u.push(x[n.t](n, i, r, d, e, c, f, o, w, v)) : (u.push(g), u.push(y[s].replace(/__NUM__/, l++)), u.push(x[n.t](n, i, r, d, e, c, f, o, w, v)), p = s);
return y[h].replace(/__NUM__/, 1) + u.join("") + g
}
不仅对变量名毫无提示,而且存在大量的嵌套,而没有学过js的我,只能一脸懵逼,但是没办法,日子还是要过,代码还是要写,功能还是要复现(我不会说这都是课设逼出来的)
于是乎只能边查js
的语法,边推测变量名以及函数的功能,这一段分析的过程就不阐述了,在代码中有注释相应的参数,并且修改了部分函数名
Step 4.3——获取CSS
虽然通过复现js,利用json数据生成了html,但是利用这个html生成的pdf显然格式不正确,且图片与文字完全分离了,见下图(左图为百度文库截图,右图为生成的pdf截图)
显然文本没有得到正确的处理,那么问题出在哪里?
选中文本后观察Styles
栏,在加载时需要用到CSS
那么CSS
又从哪里获取呢?
事实上上图中标记出的css同样在网站源代码中即可获取,在网页源代码的head
标签内找到以下内容
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/pkg/pkg_wkcommon_base_885f681.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/widget/ui/css_core/ui/core_v3/core_v3_582da0f.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/pkg/viewcommon_90cf82e.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/pkg/pkg_wkview_npm_ce517f8.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/pkg/toctoolbar_63e2fff.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/widget/doc_claim/doc_claim_55aa90b.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/widget/common_toc_reader/reader_xreader/index_62d7ae8.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/pkg/pkg_wkcommon_htmlReader_d0cec71.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/pkg/pkg_wkcommon_pay_f7322bb.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/widget/common_toc/common/style/main_82a8131.css" />
而在上图中发现的css
在这里均可以找到,经过测试,经需要其中的四个css
文件
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/pkg/pkg_wkcommon_base_885f681.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkcommon/widget/ui/css_core/ui/core_v3/core_v3_582da0f.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/widget/common_toc_reader/reader_xreader/index_62d7ae8.css" />
<link rel="stylesheet" type="text/css" href="//wkstatic.bdimg.com/static/wkview/widget/common_toc/common/style/main_82a8131.css" />
为了转换成pdf,需要提前将这一部分css
文件下载,并写入html
文件的head
标签中
这一部分内容相对比较简单,仅需要利用正则表达式提取出相应内容即可
下面看一看添加CSS后转换生成的pdf(是不是像那么回事了😄)
Step 4.4——获取超过50页的内容
其实到了这里,基本上已经可以完成对相应文档的下载了,但是在搜索的时候突然发现了一个问题,百度文库对于长度超过50的文章会放在不同的url中,也就是说,仅用初始的url最多只可以下载前五十页的内容,这显然不是我们想要的(都到这份上了,50页怎么能满足🤬)
首先来看看问题
那么点击浏览后50页看看会发生什么,很容易注意到网页的url发生了变化,在最后多处了pn=51
的标签
根据经验判断,这个pn=51
应该对应页数,而按照百度的分页策略,应该每50页作为一个网页
那么问题就可以转换为根据初始url构造多个包含新新页数的url,进行爬取即可
当然这里面还有一些细节上的东西,比如要修改转换时的参数等等,这部分也留下了一个问题:
使用pdfkit将html转换生成pdf时,页面比例存在一些问题,在html文件中格式正常的文档,经过转换后部分内容会出现偏差,因此这里就先把html文件保存了,如果需要删除的话请读者自己删除
关于这一点我也没找到解决的办法,如果有较好的方法,忘不吝赐教
注:在这里留下wkhtmltopdf的相关参数设置 wkhtmltopdf参数
⚠️⚠️⚠️需要下载的第三方库(博主的版本)
库名 | 版本 |
---|---|
requests |
2.19.1 |
chardet |
3.0.4 |
bs4 |
4.6.3 |
PIL |
5.2.0 |
pdfkit |
0.6.1 |
⚠️⚠️⚠️使用pdfkit需要安装wkhtmltopdf,再次放上下载的教程
可以参考一下这篇 Python快速将HTML转PDF ,妈妈再也不会担心我不会转PDF了(类似的教程挺多的)
或者这篇Convert HTML to PDF with Python
至于其他库的安装使用pip命令即可
⚠️⚠️⚠️另外,由于部分文档挂着doc,xls,pdf的样子,实际上却是以ppt的格式存放的,故使用时需要百度文库爬虫(二)PPT下载中介绍的Ppt下载方式
完整代码
- 出于方便的考虑,将下载ppt的代码也放在这里了:
from requests import get
from PIL import Image
from os import removedirs,remove,mkdir,getcwd
from os.path import join, exists
from requests.exceptions import ReadTimeout
from chardet import detect
from bs4 import BeautifulSoup
from re import findall
from json import loads
from time import time
class GetPpt:
def __init__(self, url, savepath):
self.url = url
self.savepath = savepath if savepath != '' else getcwd()
self.tempdirpath = self.makeDirForImageSave()
self.pptsavepath = self.makeDirForPptSave()
self.html = ''
self.wkinfo ={} # 存储文档基本信息:title、docType、docID
self.ppturls = [] # 顺序存储包含ppt图片的url
self.getHtml()
self.getWkInfo()
# 获取网站源代码
def getHtml(self):
try:
header = {'User-Agent': 'Mozilla/5.0 '
'(Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/78.0.3904.108 Safari/537.36'}
response = get(self.url, headers = header)
self.transfromEncoding(response)
self.html = BeautifulSoup(response.text, 'html.parser') #格式化
except ReadTimeout as e:
print(e)
return None
# 转换网页源代码为对应编码格式
def transfromEncoding(self,html):
html.encoding = detect(html.content).get("encoding") #检测并修改html内容的编码方式
# 获取文档基本信息:名字,类型,文档ID
def getWkInfo(self):
items = ["'title'","'docType'","'docId'","'totalPageNum"]
for item in items:
ls = findall(item+".*'", str(self.html))
if len(ls) != 0:
message = ls[0].split(':')
self.wkinfo[eval(message[0])] = eval(message[1])
# 获取json字符串
def getJson(self, url):
"""
:param url: json文件所在页面的url
:return: json格式字符串
"""
response = get(url)
jsonstr = response.text[response.text.find('(')+1: response.text.rfind(')')] # 获取json格式数据
return jsonstr
# 获取json字符串对应的字典
def convertJsonToDict(self, jsonstr):
"""
:param jsonstr: json格式字符串
:return: json字符串所对应的python字典
"""
textdict = loads(jsonstr) # 将json字符串转换为python的字典对象
return textdict
# 创建临时文件夹保存图片
def makeDirForImageSave(self):
if not exists(join(self.savepath,'tempimages')):
mkdir(join(self.savepath,'tempimages'))
return join(self.savepath,'tempimages')
# 创建临时文件夹保存ppt
def makeDirForPptSave(self):
if not exists(join(self.savepath,'pptfiles')):
mkdir(join(self.savepath,'pptfiles'))
return join(self.savepath,'pptfiles')
# 从json文件中提取ppt图片的url
def getImageUrlForPPT(self):
timestamp = round(time()*1000) # 获取时间戳
desturl = "https://wenku.baidu.com/browse/getbcsurl?doc_id="+\
self.wkinfo.get("docId")+\
"&pn=1&rn=99999&type=ppt&callback=jQuery1101000870141751143283_"+\
str(timestamp) + "&_=" + str(timestamp+1)
textdict = self.convertJsonToDict(self.getJson(desturl))
self.ppturls = [x.get('zoom') for x in textdict.get('list')]
# 通过给定的图像url及名称保存图像至临时文件夹
def getImage(self, imagename, imageurl):
imagename = join(self.tempdirpath, imagename)
with open(imagename,'wb') as ig:
ig.write(get(imageurl).content) #content属性为byte
# 将获取的图片合成pdf文件
def mergeImageToPDF(self, pages):
if pages == 0:
raise IOError
namelist = [join(self.tempdirpath, str(x)+'.png') for x in range(pages)]
firstimg = Image.open(namelist[0])
imglist = []
for imgname in namelist[1:]:
img = Image.open(imgname)
img.load()
if img.mode == 'RGBA': # png图片的转为RGB mode,否则保存时会引发异常
img.mode = 'RGB'
imglist.append(img)
savepath = join(self.pptsavepath, self.wkinfo.get('title')+'.pdf')
firstimg.save(savepath, "PDF", resolution=100.0,
save_all=True, append_images=imglist)
# 清除下载的图片
def removeImage(self,pages):
namelist = [join(self.tempdirpath, str(x)+'.png') for x in range(pages)]
for name in namelist:
if exists(name):
remove(name)
if exists(join(self.savepath,'tempimages')):
removedirs(join(self.savepath,'tempimages'))
def getPPT(self):
self.getImageUrlForPPT()
for page, url in enumerate(self.ppturls):
self.getImage(str(page)+'.png', url)
self.mergeImageToPDF(len(self.ppturls))
self.removeImage(len(self.ppturls))
if __name__ == '__main__':
GetPpt('https://wenku.baidu.com/view/a5fc216dc9d376eeaeaad1f34693daef5ff7130b.html?from=search', '存储路径').getPPT()
- 下载doc/xls/pdf的代码
import requests
import os
from requests.exceptions import ReadTimeout
import chardet
from bs4 import BeautifulSoup
import re
import json
import math
from PIL import Image
import pdfkit
from GetPpt import GetPpt
import time
class GetAll:
def __init__(self, url, savepath):
"""
:param url: 待爬取文档所在页面的url
:param savepath: 生成文档保存路径
"""
self.url = url
self.savepath = savepath if savepath != '' else os.getcwd()
self.startpage = 1
self.url = self.url + "&pn=1"
self.html = ''
self.wkinfo ={} # 存储文档基本信息:title、docType、docID、totalPageNum
self.jsonurls = []
self.pdfurls = []
self.getHtml()
self.getWkInfo()
self.htmlsdirpath = self.makeDirForHtmlSave()
self.pdfsdirpath = self.makeDirForPdfSave()
self.htmlfile = self.wkinfo.get('title')+".html"
# 创建临时文件夹保存html文件
def makeDirForHtmlSave(self):
if not os.path.exists(os.path.join(self.savepath,'htmlfiles')):
os.mkdir(os.path.join(self.savepath,'htmlfiles'))
return os.path.join(self.savepath, 'htmlfiles')
def makeDirForPdfSave(self):
if not os.path.exists(os.path.join(self.savepath, 'pdffiles')):
os.mkdir(os.path.join(self.savepath,'pdffiles'))
return os.path.join(self.savepath,'pdffiles')
# 创建html文档,用于组织爬取的文件
def creatHtml(self):
with open(os.path.join(self.htmlsdirpath, str(self.startpage) + self.htmlfile), "w") as f:
# 生成文档头
message = """
<!DOCTYPE html>
<html class="expanded screen-max">
<head>
<meta charset="utf-8">
<title>文库</title>"""
f.write(message)
def addMessageToHtml(self,message):
""":param message:向html文档中添加内容 """
with open(os.path.join(self.htmlsdirpath, str(self.startpage) + self.htmlfile), "a", encoding='utf-8') as a:
a.write(message)
# 获取网站源代码
def getHtml(self):
try:
header = {'User-Agent': 'Mozilla/5.0 '
'(Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/78.0.3904.108 Safari/537.36'}
response = requests.get(self.url, headers = header)
self.transfromEncoding(response)
self.html = BeautifulSoup(response.text, 'html.parser') # 格式化html
except ReadTimeout as e:
print(e)
# 转换网页源代码为对应编码格式
def transfromEncoding(self, html):
html.encoding = chardet.detect(html.content).get("encoding") #检测并修改html内容的编码方式
# 获取文档基本信息:名字,类型,文档ID
def getWkInfo(self):
items = ["'title'","'docType'","'docId'","'totalPageNum"]
for item in items:
ls = re.findall(item+".*'", str(self.html))
if len(ls) != 0:
message = ls[0].split(':')
self.wkinfo[eval(message[0])] = eval(message[1])
# 获取存储信息的json文件的url
def getJsonUrl(self):
urlinfo = re.findall("WkInfo.htmlUrls = '.*?;", str(self.html))
urls = re.findall("https:.*?}", urlinfo[0])
urls = [str(url).replace("\\", "").replace('x22}','') for url in urls ]
self.jsonurls = urls
# 获取json字符串
def getJson(self, url):
"""
:param url: json文件所在页面的url
:return: json格式字符串
"""
response = requests.get(url)
jsonstr = response.text[response.text.find('(')+1: response.text.rfind(')')] # 获取json格式数据
return jsonstr
# 获取json字符串对应的字典
def convertJsonToDict(self, jsonstr):
"""
:param jsonstr: json格式字符串
:return: json字符串所对应的python字典
"""
textdict = json.loads(jsonstr) # 将json字符串转换为python的字典对象
return textdict
# 判断文档是否为ppt格式
def isPptStyle(self):
iswholepic = False
ispptlike = False
for url in self.jsonurls:
if "0.json" in url:
textdict = self.convertJsonToDict(self.getJson(url))
# 若json文件中的style属性为空字符串且font属性为None,则说明pdf全由图片组成
if textdict.get("style") == "" and textdict.get("font") is None:
iswholepic = True
break
elif textdict.get('page').get('pptlike'):
ispptlike = True
break
break
return iswholepic and ispptlike
# 从html中匹配出与控制格式相关的css文件的url
def getCssUrl(self):
pattern = re.compile('<link href="//.*?\.css')
allmessage = pattern.findall(str(self.html))
allcss = [x.replace('<link href="', "https:") for x in allmessage]
return allcss
def getPageTag(self):
""":return:返回id属性包含 data-page-no 的所有标签,即所有页面的首标签"""
def attributeFilter(tag):
return tag.has_attr('data-page-no')
return self.html.find_all(attributeFilter)
def getDocIdUpdate(self):
""":return:doc_id_update字符串"""
pattern = re.compile('doc_id_update:".*?"')
for i in pattern.findall(str(self.html)):
return i.split('"')[1]
def getAllReaderRenderStyle(self):
""":return: style <id = "reader - render - style">全部内容"""
page = 1
style = '<style id='+'"reader-render-style">\n'
for url in self.jsonurls:
if "json" in url:
textdict = self.convertJsonToDict(self.getJson(url))
style += self.getReaderRenderStyle(textdict.get('style'), textdict.get('font'), textdict.get('font'), page)
page += 1
else:
break
style += "</style>\n"
return style
def getReaderRenderStyle(self, allstyle, font, r, page):
"""
:param allstyle: json数据的style内容
:param font: json数据的font内容
:param r: TODO:解析作用未知,先取值与e相同
:param page: 当前页面
:return: style <id = "reader - render - style">
"""
p, stylecontent = "", []
for index in range(len(allstyle)):
style = allstyle[index]
if style.get('s'):
p = self.getPartReaderRenderStyle(style.get('s'), font, r).strip(" ")
l = "reader-word-s" + str(page) + "-"
p and stylecontent.append("." + l + (",." + l).join([str(x) for x in style.get('c')]) + "{ " + p + "}")
if style.get('s').get("font-family"):
pass
stylecontent.append("#pageNo-" + str(page) + " .reader-parent{visibility:visible;}")
return "".join(stylecontent)
def getPartReaderRenderStyle(self, s, font, r):
"""
:param s: json style下的s属性
:param font: json font属性
:param r: fontMapping TODO:先取为与e相同
:return: style <id = "reader - render - style">中的部分字符串
"""
content = []
n, p = 10, 1349.19 / 1262.85 # n为倍数, p为比例系数, 通过页面宽度比得出
def fontsize(f):
content.append("font-size:" + str(math.floor(eval(f) * n * p)) + "px;")
def letterspacing(l):
content.append("letter-spacing:" + str(eval(l) * n) + "px;")
def bold(b):
"false" == b or content.append("font-weight:600;")
def fontfamily(o):
n = font.get(o) or o if font else o
content.append("font-family:'" + n + "','" + o + "','" + (r.get(n) and r[n] or n) + "';")
for attribute in s:
if attribute == "font-size":
fontsize(s[attribute])
elif attribute == "letter-spacing":
letterspacing(s[attribute])
elif attribute == "bold":
bold(s[attribute])
elif attribute == "font-family":
fontfamily(s[attribute])
else:
content.append(attribute + ":" + s[attribute] + ";")
return "".join(content)
# 向html中添加css
def AddCss(self):
urls = self.getCssUrl()
urls = [url for url in urls if "htmlReader" in url or "core" in url or "main" in url or "base" in url]
for url in urls:
message = '<style type="text/css">'+requests.get(url).text+"</style>>"
self.addMessageToHtml(message)
content = self.getAllReaderRenderStyle() # 获取文本控制属性css
self.addMessageToHtml(content)
def addMainContent(self):
"""
:param startpage: 开始生成的页面数
:return:
"""
self.addMessageToHtml("\n\n\n<body>\n")
docidupdate = self.getDocIdUpdate()
# 分别获取json和png所在的url
jsonurl = [x for x in self.jsonurls if "json" in x]
pngurl = [x for x in self.jsonurls if "png" in x]
tags = self.getPageTag()
for page, tag in enumerate(tags):
if page > 50:
break
tag['style'] = "height: 1349.19px;"
tag['id'] = "pageNo-" + str(page+1)
self.addMessageToHtml(str(tag).replace('</div>', ''))
diu = self.getDocIdUpdate()
n = "-webkit-transform:scale(1.00);-webkit-transform-origin:left top;"
textdict = self.convertJsonToDict(self.getJson(jsonurl[page]))
# 判断是否出现图片url少于json文件url情况
if page < len(pngurl):
maincontent = self.creatMainContent(textdict.get('body'), textdict.get('page'), textdict.get('font'), page + 1, docidupdate,
pngurl[page])
else:
maincontent = self.creatMainContent(textdict.get('body'), textdict.get('page'), textdict.get('font'), page + 1, docidupdate, "")
content = "".join([
'<div class="reader-parent-' + diu + " reader-parent " + '" style="position:relative;top:0;left:0;' + n + '">',
'<div class="reader-wrap' + diu + '" style="position:absolute;top:0;left:0;width:100%;height:100%;">',
'<div class="reader-main-' + diu + '" style="position:relative;top:0;left:0;width:100%;height:100%;">', maincontent,
"</div>", "</div>", "</div>", "</div>"])
self.addMessageToHtml(content)
print("已完成%s页的写入,当前写入进度为%f" % (str(page+self.startpage), 100*(page+self.startpage)/int(self.wkinfo.get('totalPageNum'))) + '%')
self.addMessageToHtml("\n\n\n</body>\n</html>")
def isNumber(self, obj):
"""
:param obj:任意对象
:return: obj是否为数字
"""
return isinstance(obj, int) or isinstance(obj, float)
def creatMainContent(self, body, page, font, currentpage, o, pngurl):
"""
:param body: body属性
:param page: page属性
:param font: font属性
:param currentpage: 当前页面数
:param o:doc_id_update
:param pngurl: 图片所在url
:return:文本及图片的html内容字符串
"""
content, p, s, h = 0, 0, 0, 0
main = []
l = 2
c = page.get('v')
d = font # d原本为fongmapping
y = {
"pic": '<div class="reader-pic-layer" style="z-index:__NUM__"><div class="ie-fix">',
"word": '<div class="reader-txt-layer" style="z-index:__NUM__"><div class="ie-fix">'
}
g = "</div></div>"
MAX1 , MAX2 = 0, 0
body = sorted(body, key=lambda k: k.get('p').get('z'))
for index in range(len(body)):
content = body[index]
if "pic" == content.get('t'):
MAX1 = max(MAX1, content.get('c').get('ih') + content.get('c').get('iy') + 5)
MAX2 = max(MAX2, content.get('c').get('iw'))
for index in range(len(body)):
content = body[index]
s = content.get('t')
if not p:
p = h = s
if p == s:
if content.get('t') == "word":
# m函数需要接受可变参数
main.append(self.creatTagOfWord(content, currentpage, font, d, c))
elif content.get('t') == 'pic':
main.append(self.creatTagOfImage(content, pngurl, MAX1, MAX2))
else:
main.append(g)
main.append(y.get(s).replace('__NUM__', str(l)))
l += 1
if content.get('t') == "word":
# m函数需要接受可变参数
main.append(self.creatTagOfWord(content, currentpage, font, d, c))
elif content.get('t') == 'pic':
main.append(self.creatTagOfImage(content, pngurl, MAX1, MAX2))
p = s
return y.get(h).replace('__NUM__', "1") + "".join(main) + g
def creatTagOfWord(self, t, currentpage, font, o, version, *args):
"""
:param t: body中的每个属性
:param currentpage: page
:param font: font属性
:param o:font属性
:param version: page中的version属性
:param args:
:return:<p>标签--文本内容
"""
p = t.get('p')
ps = t.get('ps')
s = t.get('s')
z = ['<b style="font-family:simsun;"> </b>', "\n"]
k, N = 10, 1349.19 / 1262.85
# T = self.j
U = self.O(ps)
w, h, y, x, D= p.get('w'), p.get('h'), p.get('y'), p.get('x'), p.get('z')
pattern=re.compile("[\s\t\0xa0]| [\0xa0\s\t]$")
final = []
if U and ps and ((ps.get('_opacity') and ps.get('_opacity') == 1) or (ps.get('_alpha') and ps.get('_alpha') == 0)):
return ""
else:
width = math.floor(w * k * N)
height = math.floor(h * k * N)
final.append("<p "+'class="'+"reader-word-layer" + self.processStyleOfR(t.get('r'), currentpage) + '" ' + 'style="' + "width:" +str(width) + "px;" + "height:" + str(height) + "px;" + "line-height:" + str(height) + "px;")
final.append("top:"+str(math.floor(y * k * N))+"px;"+"left:"+str(math.floor(x * k * N))+"px;"+"z-index:"+str(D)+";")
final.append(self.processStyleOfS(s, font, o, version))
final.append(self.processStyleOf_rotate(ps.get('_rotate'), w, h, x, y, k, N) if U and ps and self.isNumber(ps.get('_rotate')) else "")
final.append(self.processStyleOfOpacity(ps.get('_opacity')) if U and ps and ps.get('_opacity') else "")
final.append(self.processStyleOf_scaleX(ps.get('_scaleX'), width, height) if U and ps and ps.get('_scaleX') else "")
final.append(str(isinstance(t.get('c'), str) and len(t.get('c')) == 1 and pattern.match(t.get('c')) and "font-family:simsun;") if isinstance(t.get('c'), str) and len(t.get('c')) == 1 and pattern.match(t.get('c')) else "")
final.append('">')
final.append(t.get('c') if t.get('c') else "")
final.append(U and ps and str(self.isNumber(ps.get('_enter'))) and z[ps.get('_enter') if ps.get('_enter') else 1] or "")
final.append("</p>")
return "".join(final)
def processStyleOfS(self, t, font, r, version):
"""
:param t: 文本的s属性
:param font: font属性
:param r:font属性
:param version:
:return:处理好的S属性字符串
"""
infoOfS = []
n = {"font-size": 1}
p , u = 10, 1349.19 / 1262.85
def fontfamily(o):
n = font.get(o) or o if font else o
if abs(version) > 5:
infoOfS.append("font-family:'"+ n + "','" + o + "','" + (r.get('n') and r[n] or n) + "';")
else:
infoOfS.append("font-family:'" + o + "','" + n + "','" + (r.get(n) and r[n] or n) + "';")
def bold(e):
"false" == e or infoOfS.append("font-weight:600;")
def letter(e):
infoOfS.append("letter-spacing:" + str(eval(e) * p) + "px;")
if t is not None:
for attribute in t:
if attribute == "font-family":
fontfamily(t[attribute])
elif attribute == "bold":
bold(t[attribute])
elif attribute == "letter-spacing":
letter(t[attribute])
else:
infoOfS.append(attribute + ":" + (str(math.floor(((t[attribute] if self.isNumber(t[attribute]) else eval(t[attribute])) * p * u))) + "px" if n.get(attribute) else t[attribute]) + ";")
return "".join(infoOfS)
def processStyleOfR(self, r, page):
"""
:param r: 文本的r属性
:param page: 当前页面
:return:
"""
l = " " + "reader-word-s" + str(page) + "-"
return "".join([l + str(x) for x in r]) if isinstance(r, list) and len(r) != 0 else ""
def processStyleOf_rotate(self, t, w, h, x, y, k, N):
"""
:param t: _rotate属性
:param w: body中p.w
:param h: body中p.h
:param x: body中p.x
:param y: body中p.y
:param k: 倍数10
:param N: 比例系数
:return: 处理好的_rotate属性字符串
"""
p = []
s = k * N
if t == 90:
p.append("left:" + str(math.floor(x + (w - h) / 2) * s) + "px;" + "top:" + str(math.floor(y - (h - w) / 2) * s) + "px;" + "text-align: right;" + "height:" + str(math.floor(h + 7) * s) + "px;")
elif t == 180:
p.append("left:" + str(math.floor(x - w) * s) + "px;" + "top:" + str(math.floor(y - h) * s) + "px;")
elif t == 270:
p.append("left:" + str(math.floor(x + (h - w) / 2) * s) + "px;" + "top:" + str(math.floor(y - (w - h) / 2) * s) + "px;")
return "-webkit-"+"transform:rotate("+str(t)+"deg);"+"".join(p)
def processStyleOf_scaleX(self, t, width, height):
"""
:param t: _scaleX属性
:param width: 计算好的页面width
:param height:计算好的页面height
:return: 处理好的_scaleX属性字符串
"""
return "-webkit-" + "transform: scaleX(" + str(t) + ");" + "-webkit-" + "transform-origin:left top;width:" + str(width + math.floor(width / 2)) + "px;height:" + str(height + math.floor(height / 2)) + "px;"
def processStyleOfOpacity(self,t):
"""
:param t: opacity属性
:return:处理好的opacity属性字符串
"""
t = (t or 0),
return "opacity:" + str(t) + ";"
def creatTagOfImage(self,t,url, *args):
"""
:param t: 图片的字典
:param url:图片链接
:param args:
:return:图像标签
"""
u, l = t.get('p'), t.get('c')
if u.get("opacity") and u.get('opacity') == 0:
return ""
else:
if u.get("x1") or (u.get('rotate') != 0 and u.get('opacity') != 1):
message = '<div class="reader-pic-item" style="' + "background-image: url(" + url + ");" + "background-position:" + str(-l.get('ix')) + "px " + str(-l.get('iy')) + "px;" \
+ "width:" + str(l.get('iw')) + "px;" + "height:" + str(l.get('ih')) + "px;" + self.getStyleOfImage(u, l) + 'position:absolute;overflow:hidden;"></div>'
else:
[s, h] = [str(x) for x in args]
message = '<p class="reader-pic-item" style="' + "width:" + str(l.get('iw')) + "px;" + "height:" + str(l.get('ih')) + "px;" + self.getStyleOfImage(u, l) + 'position:absolute;overflow:hidden;"><img width="' + str(h) + '" height="' + str(s) + '" style="position:absolute;top:-' + str(l.get('iy')) + "px;left:-" + str(l.get('ix')) + "px;clip:rect(" + str(l.get('iy')) + "px," + str(int(h) - l.get('ix')) + "px, " + str(s) + "px, " + str(l.get('ix')) + 'px);" src="' + url + '" alt=""></p>'
return message
def getStyleOfImage(self, t, e):
"""
:param t: 图片p属性
:param e: 图片c属性
:return:
"""
def parseFloat(string):
"""
:param string:待处理的字符串
:return: 返回字符串中的首个有效float值,若字符首位为非数字,则返回nan
"""
if string is None:
return math.nan
elif isinstance(string, float):
return string
elif isinstance(string, int):
return float(string)
elif string[0] != ' ' and not str.isdigit(string[0]):
return math.nan
else:
p = re.compile("\d+\.?\d*")
all = p.findall(string)
return float(all[0]) if len(all) != 0 else math.nan
if t is None:
return ""
else:
r, o, a, n = 0, 0, "", 0
iw = e.get('iw')
ih = e.get('ih')
u = 1349.19 / 1262.85
l = str(t.get('x') * u) + "px"
c = str(t.get('y') * u) + "px"
d = ""
x = {}
w = {"opacity": 1, "rotate": 1, "z": 1}
for n in t:
x[n] = t[n] * u if (self.isNumber(t[n]) and not w.get(n)) else t[n]
if x.get('w') != iw or x.get('h') != ih:
if x.get('x1'):
a = self.P(x.get('x0'), x.get('y0'), x.get('x1'), x.get('y1'), x.get('x2'), x.get('y2'))
r = parseFloat(parseFloat(a[0])/iw if len(a) else x.get('w') / iw)
o = parseFloat(parseFloat(a[1])/ih if len(a) else x.get('h') / ih)
m, v = iw * (r-1), ih * (o-1)
c = str((x.get('y1') + x.get('y3')) / 2 - parseFloat(ih) / 2)+"px" if x.get('x1') else str(x.get('y') + v / 2) + "px"
l = str((x.get('x1') + x.get('x3')) / 2 - parseFloat(iw) / 2)+"px" if x.get('x1') else str(x.get('x') + m / 2) + "px"
d = "-webkit-" + "transform:scale(" + str(r) + "," + str(o) + ")"
message = "z-index:" + str(x.get('z')) + ";" + "left:" + l + ";" + "top:" + c + ";" + "opacity:" + str(x.get('opacity') or 1) + ";"
if x.get('x1'):
message += self.O(x.get('rotate')) if x.get('rotate') > 0.01 else self.O(0, x.get('x1'), x.get('x2'), x.get('y0'), x.get('y1'), d)
else:
message += d + ";"
return message
def P(self,t, e, r, i, o, a):
p = round(math.sqrt(math.pow(abs(t - r), 2) + math.pow(abs(e - i), 2)), 4)
s = round(math.sqrt(math.pow(abs(r - o), 2) + math.pow(abs(i - a), 2)), 4)
return [s, p]
def O(self, t, *args):
[e, r, i, o, a] = [0, 0, 0, 0, ""] if len(args) == 0 else [x for x in args]
n = o > i
p = e > r
if n and p:
a += " Matrix(1,0,0,-1,0,0)"
elif n:
a += " Matrix(1,0,0,-1,0,0)"
elif p:
a += " Matrix(-1,0,0,1,0,0)"
elif t:
a += " rotate(" + str(t) + "deg)"
return a + ";"
def convertHtmlToPdf(self):
savepath = os.path.join(self.pdfsdirpath, str(self.startpage)+self.wkinfo.get('title') + '.pdf')
# 每个url的最大页数为50
exactpages = int(self.wkinfo.get('totalPageNum'))
if exactpages > 50:
exactpages = 50
options = {'disable-smart-shrinking':'',
'lowquality': '',
'image-quality': 60,
'page-height': str(1349.19*0.26458333),
'page-width': '291',
'margin-bottom': '0',
'margin-top': '0',
}
pdfkit.from_file(os.path.join(self.htmlsdirpath, str(self.startpage) + self.htmlfile), savepath, options=options)
def Run(self):
self.getJsonUrl()
# 判断是否文档是否为ppt格式
if self.isPptStyle():
GetPpt(self.url, self.savepath).getPPT()
else:
for epoch in range(int(int(self.wkinfo.get('totalPageNum'))/50)+1):
self.startpage = epoch * 50 + 1
if epoch == 0:
self.creatHtml()
start = time.time()
print('-------------Start Add Css--------------')
self.AddCss()
print('-------------Css Add Finissed-----------')
end = time.time()
print("Add Css Cost: %ss" % str(end - start))
start = time.time()
print('-------------Start Add Content----------')
self.addMainContent()
print('-------------Content Add Finished-------')
end = time.time()
print("Add MainContent Cost: %ss" % str(end - start))
start = time.time()
print('-------------Start Convert--------------')
self.convertHtmlToPdf()
print('-------------Convert Finished-----------')
end = time.time()
print("Convert Cost: %ss" % str(end - start))
else:
self.url = self.url[:self.url.find('&pn=')] + "&pn=" + str(self.startpage)
print(self.url)
self.getHtml()
self.getJsonUrl()
self.creatHtml()
start = time.time()
print('-------------Start Add Css--------------')
self.AddCss()
print('-------------Css Add Finissed-----------')
end = time.time()
print("Add Css Cost: %ss" % str(end - start))
start = time.time()
print('-------------Start Add Content----------')
self.addMainContent()
print('-------------Content Add Finished-------')
end = time.time()
print("Add MainContent Cost: %ss" % str(end - start))
start = time.time()
print('-------------Start Convert--------------')
self.convertHtmlToPdf()
print('-------------Convert Finished-----------')
end = time.time()
print("Convert Cost: %ss" % str(end - start))
if __name__ == '__main__':
# 若存储路径为空,则在当前文件夹生成
GetAll('https://wenku.baidu.com/view/fb92d7d3b8d528ea81c758f5f61fb7360a4c2b61.html?from=search',"存储路径").Run()
测试
这里仅使用前面提到的文档进行测试
写在最后
虽然这里只演示了doc的下载,但是由于使用的方法对pdf与xls都是适用的,所以没有再做展示
虽然项目还存在一些问题,但是对于日常使用来说已经足够了
当然,如果使用过程中存在什么问题,也请联系博主,会尽量予以解答
但是由于个人的原因,可能不会及时回复,请谅解
这个项目由于使用了第三方库完成转换,在时间上可能不尽如人意,对于页数过长的文档下载时间可能相对较长,可能以后会有所修改(也可能没时间修改了)
转载:https://blog.csdn.net/qq_43444349/article/details/105352567