背景: 某一天,拿着本身的手機看着技術文章,然而手機看技術文章,有時候確實蛋疼,由於一旦代碼多起來,小屏幕看的仍是眼花;又或者某一天以爲這一篇文章,以爲寫的很棒棒哦,因而先收藏,打算過幾天看,而後等我幾天再次打開收藏的文章,臥X,竟然被做者刪了···;或者想對某個博主的文章進行分類···javascript
因而就萌生了能不能爬下「微信公衆號」文章,保存到電腦的想法html
現在鋪天蓋地的安利 Python ,雖然有着「人生苦短,我用 Python」一說,但我仍是想在「爬蟲」這方面支持一下我大 Java(好吧,其實本身折騰一番,仍是寫着 Java 舒服,平時寫 python 仍是少)java
關於手機抓包(這裏指 Android 手機),推薦使用 Fiddler 工具來抓包,Fiddler 自行去下載。python
劃重點:請確保電腦和手機鏈接在同一局域網的同一個 WiFi,別又說怎麼抓不到包android
Win + R (快捷鍵),打開【運行】窗口,而後輸入 cmd 回車,彈窗命令窗口,緊接着輸入:ipconfigweb
記着 ip,一會配置手機 WiFi,不會配置的能夠看 Fiddler 官網這篇文章:docs.telerik.com/fiddler/Con…sql
打開手機 WiFi 管理,顯示 WiFi 的高級選項,設置代理服務器爲手工,代理主機名爲剛剛電腦 IPv4 地址:192.168.0.XXX ,代理服務器端口默認設置爲:8888json
由於微信的網絡請求爲 HTTPS ,安全性高,因此 Fiddler 須要在手機端安裝它的信任證書,才能抓到微信的請求(比喻:Fiddler 充當代理人、中間商,在創建 https 的過程搞事情,瞞天過海,以獲取信任)。瀏覽器
FiddlerRoot Certificate
Capture HTTPS traffic
除了 Fiddler 以外,這邊推薦安卓另外一個抓包工具:HttpCanary,安裝 apk 到手機,也可實現實時抓包。安全
傳送門:手機抓包+注入黑科技HttpCanary——最強大的Android抓包注入工具
配置好抓包工具以後,打開某公衆號,切換歷史文章消息,而後點擊更多消息,此時觀察 Fiddler 抓包狀況。
每次抓包前,建議先清空歷史抓包數據,而後在執行操做,這樣方便定位連接。
因而咱們能夠很輕易的拿到微信公衆號獲取文章接口地址:
切換到 WebForms
選項卡,能夠看到 Get
請求下的參數信息,後面咱們模擬請求,照着寫就 ok 了(Get
請求參數,能夠寫在連接裏面)
在上面這個圖,我框了幾個重要參數的,這幾個參數涉及到微信服務端的校驗相關操做,因此在複製的時候,記得不要搞錯了,不然會提示 session 錯誤。
我我的試錯發現,每次爬一個新的公衆號,只需對應修改這 4 個參數便可:__biz、appmsg_token、pass_ticket、wap_sid2
如何爬取全部文章呢? 作過手機客戶端的童鞋,應該知道咱們用 Recyclerview
或 ListView
作下拉刷新或者上拉加載更多的時候,接口通常須要配置 nextpage
的參數吧,對應微信文章接口就是:offset
參數(理解爲偏移量),count
參數(理解爲每次加載的數目)。
舉例:我設置 offset
爲0,count
爲 10,那麼第一頁數據就加載10條,那第二頁的起始點就應該是 offset = 10
,count
不做修改依舊爲 10。但願你們能理解個人例子,相信這個不難
請求這東西,固然是 okhttp 來啦,引用相關 jar 包或者 gradle 依賴。說明一下:User-Agent 使用 Fiddler 抓取的值,以模擬手機客戶端的請求。核心代碼以下:
String url = "https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=%s&f=json&offset=%d&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=%s&wxtoken=&appmsg_token=%s&f=json ";
url = String.format(url, MyClass.__biz, startIndex, MyClass.pass_ticket, MyClass.appmsg_token);
// System.out.println(url);
String cookie = "rewardsn=; wxtokenkey=777; wxuin=777750088; devicetype=android-26; version=2700033c; lang=zh_CN; pass_ticket=%s; wap_sid2=%s";
cookie = String.format(cookie, MyClass.pass_ticket, MyClass.wap_sid2);
Request request = new Request.Builder()
.url(url)
.get()
.addHeader("Host", "mp.weixin.qq.com")
.addHeader("Connection", "keep-alive")
.addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G9500 Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/6.2 TBS/044704 Mobile Safari/537.36 MMWEBID/8994 MicroMessenger/7.0.3.1400(0x2700033C) Process/toolsmp NetType/WIFI Language/zh_CN")
.addHeader("Accept-Language", "zh-CN,zh-CN;q=0.9,en-US;q=0.8")
.addHeader("X-Requested-With", "XMLHttpRequest")
.addHeader("Cookie", cookie)
.addHeader("Accept", "*/*")
.build();
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
String body = response.body().string();
JSONObject jo = new JSONObject(body);
if (jo.getInt("ret") == 0) {
currentTimes++;
System.out.println("當前是第" + currentTimes + "次");
String general_msg_list = jo.getString("general_msg_list");
general_msg_list = general_msg_list.replace("\\/", "/");
// json 解析
JSONObject jo2 = new JSONObject(general_msg_list);
JSONArray msgList = jo2.getJSONArray("list");
for (int i = 0; i < msgList.length(); i++) {
JSONObject j = msgList.getJSONObject(i);
JSONObject msgInfo = j.getJSONObject("comm_msg_info");
long datetime = msgInfo.getLong("datetime");
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String date = sdf.format(new Date(datetime * 1000));
if (j.has("app_msg_ext_info")) {
JSONObject app_msg_ext_info = j.getJSONObject("app_msg_ext_info");
JSONArray multi_app_msg_item_list = app_msg_ext_info.getJSONArray("multi_app_msg_item_list");
if (multi_app_msg_item_list.length() > 0) {
//多圖文 do nothing
} else {
String content_url = app_msg_ext_info.getString("content_url");
String title = app_msg_ext_info.getString("title");
int copyright_stat = app_msg_ext_info.getInt("copyright_stat");
String record = date + "-@@-" + title + "-@@-" + content_url;
System.out.println(record);
datas.add(record);
}
} else {
System.out.println("非圖文推送");
}
}
// can_msg_continue 來判斷是否還有下一頁數據
if (jo.getInt("can_msg_continue") == 1) {
Thread.sleep(1000);
startIndex = jo.getInt("next_offset");
execute();
} else {
System.out.println("爬取完成!");
// 完成以後,保存結果
saveToFile();
}
} else {
System.out.println("沒法獲取文章,參數錯誤");
}
}
複製代碼
好像代碼也不是特別多,哈哈,而後把爬取的數據保存到一個 txt 文本文件裏面,我這邊用的格式是: 時間-@@-標題-@@-連接 (後面方便使用「-@@-」分割字符串),固然你也能夠鏈接 Mysql,來存儲信息,我就偷懶了,沒搞了。
某網友提醒,WebMagic 爲 Java 現成的爬蟲框架,這裏貼出來,僅供網友參考使用,大概看了下官網,感受不錯哦~
傳送門: webmagic.io
既然拿到每一篇文章的 Url 了,那保存成 Html 不是很 easy 的事情嗎,可是如何將 html convert Pdf
呢?
傳送門: wkhtmltopdf.org/,注意:系統版本的選擇,我這邊是 Windows 系統
若是你沒有配置系統環境變量的話,就須要到 wkhtmltopdf 的安裝目錄下的 bin 文件夾下面,去執行命令
例如:你想把 Google 網頁轉成 pdf
wkhtmltopdf http://google.com google.pdf
複製代碼
經過 wkhtmltopdf 保存 pdf 的時候,存在網絡圖片丟失的問題,也就是不顯示圖片,那如何解決這個問題呢?經過替換 html 中,img 標籤的 data-src 和 src 的屬性值,由 http 連接改成本地路徑便可。
思路:請求文章 url
,獲取 html
信息,經過 jsoup
解析 html,而後經過選擇器選擇 img 標籤,接着獲取 img
的 data-src
的屬性值(圖片地址),而後遍歷下載圖片到本地,下載圖片成功以後,經過 jsoup
提供的方法,修改該 img
的 data-src
的屬性值,替換原先的 html
信息。核心代碼以下:
Jsoup介紹:html解析神器
Request request = new Request.Builder().url(url).get().build();
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
String html = response.body().string();
// System.out.println(html);
Document doc = Jsoup.parse(html);
//找到圖片標籤
Elements img = doc.select("img");
for (int i = 0; i < img.size(); i++) {
// 圖片地址
String imgUrl = img.get(i).attr("data-src");
if (imgUrl != null && !imgUrl.equals("")) {
Request request2 = new Request.Builder()
.url(imgUrl)
.get()
.build();
Response execute = okHttpClient.newCall(request2).execute();
if (execute.isSuccessful()) {
String imgPath = imgDir + MD5Utils.MD5Encode(imgUrl, "") + ".png";
File imgFile = new File(imgPath);
if (!imgFile.exists()) {
// 下載圖片
InputStream in = execute.body().byteStream();
FileOutputStream ot = new FileOutputStream(new File(imgPath));
BufferedOutputStream bos = new BufferedOutputStream(ot);
byte[] buf = new byte[8 * 1024];
int b;
while ((b = in.read(buf, 0, buf.length)) != -1) {
bos.write(buf, 0, b);
bos.flush();
}
bos.close();
ot.close();
in.close();
}
//從新賦值爲本地路徑
img.get(i).attr("data-src", imgPath);
img.get(i).attr("src", imgPath);
//導出 html
html = doc.outerHtml();
}
execute.close();
}
}
String htmlPath = dirPath + fileName + ".html";
final File f = new File(htmlPath);
if (!f.exists()) {
Writer writer = new FileWriter(f);
BufferedWriter bw = new BufferedWriter(writer);
bw.write(html);
bw.close();
writer.close();
}
// 轉換
HtmlToPdf.convert(htmlPath, destPath);
// 刪除html文件
if (f.exists()) {
f.delete();
}
response.close();
}
複製代碼
/**
* html轉pdf
*/
public static boolean convert(String srcPath, String destPath) {
StringBuilder cmd = new StringBuilder();
cmd.append("wkhtmltopdf");
cmd.append(" ");
cmd.append("--enable-plugins");
cmd.append(" ");
cmd.append("--enable-forms");
cmd.append(" ");
cmd.append("--disable-javascript"); // 禁用 js,提升轉換效率
cmd.append(" ");
cmd.append(" \"");
cmd.append(srcPath);
cmd.append("\" ");
cmd.append(" ");
cmd.append(destPath);
System.out.println(cmd.toString());
boolean result = true;
try {
Process proc = Runtime.getRuntime().exec(cmd.toString());
HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
error.start();
output.start();
proc.waitFor();
} catch (Exception e) {
result = false;
e.printStackTrace();
}
return result;
}
複製代碼
獲取終端輸入輸出信息,上面代碼的 HtmlToPdfInterceptor
public class HtmlToPdfInterceptor extends Thread {
private InputStream is;
public HtmlToPdfInterceptor(InputStream is) {
this.is = is;
}
@Override
public void run() {
try {
InputStreamReader isr = new InputStreamReader(is, "utf-8");
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line.toString()); //輸出內容
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
wkhtmltopdf 的轉換過程速度比較慢,建議開多個線程搞,我是 5 個線程去轉換,最後看一下成果圖(python 黨別噴代碼量哈,求放過~)
感謝您的閱讀,若有不對的地方,還請指出修正!文中不理解的地方,可加 qq 交流:1029226002。