(Java篇)爬取微信公衆號文章並保存爲 PDF 格式

前言

背景: 某一天,拿着本身的手機看着技術文章,然而手機看技術文章,有時候確實蛋疼,由於一旦代碼多起來,小屏幕看的仍是眼花;又或者某一天以爲這一篇文章,以爲寫的很棒棒哦,因而先收藏,打算過幾天看,而後等我幾天再次打開收藏的文章,臥X,竟然被做者刪了···;或者想對某個博主的文章進行分類···javascript

因而就萌生了能不能爬下「微信公衆號」文章,保存到電腦的想法html

現在鋪天蓋地的安利 Python ,雖然有着「人生苦短,我用 Python」一說,但我仍是想在「爬蟲」這方面支持一下我大 Java(好吧,其實本身折騰一番,仍是寫着 Java 舒服,平時寫 python 仍是少)java

1、抓包

關於手機抓包(這裏指 Android 手機),推薦使用 Fiddler 工具來抓包,Fiddler 自行去下載。python

劃重點:請確保電腦和手機鏈接在同一局域網的同一個 WiFi,別又說怎麼抓不到包android

1.查詢電腦當前 IP

Win + R (快捷鍵),打開【運行】窗口,而後輸入 cmd 回車,彈窗命令窗口,緊接着輸入:ipconfigweb

IP

記着 ip,一會配置手機 WiFi,不會配置的能夠看 Fiddler 官網這篇文章:docs.telerik.com/fiddler/Con…sql

打開手機 WiFi 管理,顯示 WiFi 的高級選項,設置代理服務器爲手工,代理主機名爲剛剛電腦 IPv4 地址:192.168.0.XXX ,代理服務器端口默認設置爲:8888json

WiFi設置

2.手機安裝 Fiddler 證書

由於微信的網絡請求爲 HTTPS ,安全性高,因此 Fiddler 須要在手機端安裝它的信任證書,才能抓到微信的請求(比喻:Fiddler 充當代理人、中間商,在創建 https 的過程搞事情,瞞天過海,以獲取信任)。瀏覽器

http://ipv4.fiddler:8888/

操做以下:
  1. 手機瀏覽器打開:ipv4.fiddler:8888/
  2. 下載證書 FiddlerRoot Certificate
  3. 手機安裝這個證書,安裝過程可能須要設置屏幕密碼
  4. 打開 【Fiddler】-【Tools】-【HTTPS】,勾選 Capture HTTPS traffic

Capture HTTPS traffic

3.HttpCanary

除了 Fiddler 以外,這邊推薦安卓另外一個抓包工具:HttpCanary,安裝 apk 到手機,也可實現實時抓包。安全

傳送門:手機抓包+注入黑科技HttpCanary——最強大的Android抓包注入工具

HttpCanary

2、爬蟲

配置好抓包工具以後,打開某公衆號,切換歷史文章消息,而後點擊更多消息,此時觀察 Fiddler 抓包狀況。

每次抓包前,建議先清空歷史抓包數據,而後在執行操做,這樣方便定位連接。

歷史文章消息

因而咱們能夠很輕易的拿到微信公衆號獲取文章接口地址:

mp.weixin.qq.com/mp/profile_…

接口地址信息

切換到 WebForms 選項卡,能夠看到 Get 請求下的參數信息,後面咱們模擬請求,照着寫就 ok 了(Get請求參數,能夠寫在連接裏面)

接口參數

在上面這個圖,我框了幾個重要參數的,這幾個參數涉及到微信服務端的校驗相關操做,因此在複製的時候,記得不要搞錯了,不然會提示 session 錯誤。

我我的試錯發現,每次爬一個新的公衆號,只需對應修改這 4 個參數便可:__biz、appmsg_token、pass_ticket、wap_sid2

如何爬取全部文章呢? 作過手機客戶端的童鞋,應該知道咱們用 RecyclerviewListView下拉刷新或者上拉加載更多的時候,接口通常須要配置 nextpage 的參數吧,對應微信文章接口就是:offset 參數(理解爲偏移量),count參數(理解爲每次加載的數目)。

舉例:我設置 offset 爲0,count 爲 10,那麼第一頁數據就加載10條,那第二頁的起始點就應該是 offset = 10count 不做修改依舊爲 10。但願你們能理解個人例子,相信這個不難

1.構建請求,遞歸調用

請求這東西,固然是 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("沒法獲取文章,參數錯誤");
                }
            }
複製代碼

2.保存文章信息

好像代碼也不是特別多,哈哈,而後把爬取的數據保存到一個 txt 文本文件裏面,我這邊用的格式是: 時間-@@-標題-@@-連接後面方便使用「-@@-」分割字符串),固然你也能夠鏈接 Mysql,來存儲信息,我就偷懶了,沒搞了。

txt文件

3.基於Java的爬蟲框架——WebMagic (補充)

某網友提醒,WebMagic 爲 Java 現成的爬蟲框架,這裏貼出來,僅供網友參考使用,大概看了下官網,感受不錯哦~

傳送門: webmagic.io

3、Html 轉 Pdf

既然拿到每一篇文章的 Url 了,那保存成 Html 不是很 easy 的事情嗎,可是如何將 html convert Pdf 呢?

1.wkhtmltopdf 工具

1.1下載 wkhtmltopdf 並安裝

傳送門: wkhtmltopdf.org/,注意:系統版本的選擇,我這邊是 Windows 系統

1.2配置環境變量

若是你沒有配置系統環境變量的話,就須要到 wkhtmltopdf 的安裝目錄下的 bin 文件夾下面,去執行命令

配置環境變量

1.3如何使用

例如:你想把 Google 網頁轉成 pdf

wkhtmltopdf http://google.com google.pdf
複製代碼

2.解決 wkhtmltopdf 保存圖片丟失問題

經過 wkhtmltopdf 保存 pdf 的時候,存在網絡圖片丟失的問題,也就是不顯示圖片,那如何解決這個問題呢?經過替換 html 中,img 標籤的 data-src 和 src 的屬性值,由 http 連接改成本地路徑便可

思路:請求文章 url,獲取 html 信息,經過 jsoup 解析 html,而後經過選擇器選擇 img 標籤,接着獲取 imgdata-src 的屬性值(圖片地址),而後遍歷下載圖片到本地,下載圖片成功以後,經過 jsoup 提供的方法,修改該 imgdata-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();
}
複製代碼

3.轉換成 PDF

/**
 * 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 黨別噴代碼量哈,求放過~)

pdf集合

小結

感謝您的閱讀,若有不對的地方,還請指出修正!文中不理解的地方,可加 qq 交流:1029226002。

相關文章
相關標籤/搜索