1. 程式人生 > >公眾號開發之wx-tools+springboot應用實戰-音樂爬蟲推送[JAVA]

公眾號開發之wx-tools+springboot應用實戰-音樂爬蟲推送[JAVA]

天啦嚕!微信公眾號開發如此簡單!竟然是因為……

當然是因為wx-tools啦!

springboot+wx-tools實踐!音樂爬蟲推送公眾號DEMO

先理一下大概的開發步驟:
1. 建立一個Web工程(可以是Servlet/Spring Web/Spring boot)
2. pom引入wx-tools / 手動引入wx-tools.jar包(可以去github下載自己build)
3. 編寫wx.properties配置檔案
4. 接入微信公眾平臺,驗證伺服器地址的有效性
5. 實現自己的業務邏輯

簡單嗎?接下來一起寫一個簡單的Demo吧

1. 建立web專案

注意!本demo使用的是SpringBoot,如果你使用原生servlet,原理是一樣的。這裡就不再演示。

使用maven建立,或者在eclipse/IDEA建立web專案。

如何建立web專案,相信大家都會的了。就不詳細介紹了。如果不會,自行度娘。

基於SpringBoot爬蟲專案

這裡我我基於springBoot建立了一個專案名為:music_collector

music_collector是一個爬蟲專案,爬取各大音樂網的排行榜,並且可以支援設定關鍵字,來查詢微信圖文推送並推送給使用者。(原理是通過搜狗搜尋)

具體建立SpringBoot,這裡就不贅述了。百度一下就知道了。

  • 如果使用maven建立專案,指令如下:
mvn archetype:generate -DgroupId=wxtools.demo -DartifactId=demo -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=local
  • 注意:此指令建立的web工程版本是2.3的,比較低。可以修改web.xml,變成3.0
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>demo</display-name> </web-app>

引入wx-tools依賴

  • maven地址(最新版本可以去中央庫查詢wx-tools)
<dependency>
  <groupId>com.soecode.wx-tools</groupId>
  <artifactId>wx-tools</artifactId>
  <version>2.1.4-RELEASE</version>
</dependency>

注意:如果需要看原始碼的務必手動修改編碼。匯入後記得修改jar包的編碼。window下預設讀GBK,而框架本身是UTF-8。
* 修改方式:在eclipse的工程下,對著jar包右鍵 –> Properties –> Encoding –> UTF-8

建立wx.properties配置檔案

  • 搭好專案基本框架後,在src/main/resources下新建wx.properties檔案
#配置如下
wx.appId=你的appId
wx.appSecret=你的appSecret
wx.token=你設定的token
wx.aesKey=如果選擇安全模式,需要填入。如果是明文模式,填空就好了
wx.mchId=商戶ID

最終專案目錄結構如下:

專案結構

不要填錯了哦!注意大小寫。

2. 驗證伺服器地址的有效性

2.3.1 啟動web服務

可以使用SpringBoot啟動或者tomcat/jBoss都可以。

內網對映

微信開發需要把本地127.0.0.1對映到公網上,微信伺服器才可以把訊息推送給你的程式。

對映工具有很多,例如:花生殼、Ngrok等。這裡使用了免費版的Ngrok。
ngrok客戶端windows+64版

  • 使用方法很簡單:下載解壓,雙擊開啟,輸入二級域名即可把本地127.0.0.1映射出去。

效果圖

驗證一下伺服器的有效性

這時候,wx-tools下的所有api都可以呼叫了。

我們驗證一下伺服器的有效性。

驗證介面官方文件 建議先看官方文件,理解好開發步驟,在繼續下去。

  • 編寫WxController對接微信伺服器

@RestController
@RequestMapping("/wx")
public class WxController {

    private IService iService = new WxService();

    @GetMapping
    public String check(String signature, String timestamp, String nonce, String echostr) {
        if (iService.checkSignature(signature, timestamp, nonce, echostr)) {
            return echostr;
        }
        return null;
    }

}
  • 開啟服務,然後去微信公眾平臺後臺或者測試號後臺填寫資料驗證即可。( 確保wx.properties與微信後臺配置一致即可。)

  • 開啟微信配置後臺:(這裡我是用測試號的,還沒有測試號的可以點選這裡申請

微信配置後臺

如果發現可以連線,就說明已經與微信伺服器對接成功(驗證成功)。接下來任何對你的公眾號操作,微信伺服器都會轉發訊息給你的伺服器(你的程式)。

3. 接收微信伺服器發來的訊息

當你驗證伺服器有訊息成功後,微信伺服器就會把你的公眾號任何事件和訊息,以post請求推送到你驗證的那個url地址上。所以我們現在需要做的就是寫一個Post接收方法,來接收發來的訊息~

完善WxController,新增Post接收方法

@RestController
@RequestMapping("/wx")
public class WxController {

    private IService iService = new WxService();

    @GetMapping
    public String check(String signature, String timestamp, String nonce, String echostr) {
        if (iService.checkSignature(signature, timestamp, nonce, echostr)) {
            return echostr;
        }
        return null;
    }

    @PostMapping
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();

        try {
            // 微信伺服器推送過來的是XML格式。
            WxXmlMessage wx = XStreamTransformer.fromXml(WxXmlMessage.class, request.getInputStream());
            System.out.println("訊息:\n " + wx.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            out.close();
        }

    }
}

可以看到,我把微信伺服器發來的訊息(request)的輸入流扔給了XStreamTransformer來幫忙解析XML,並返回WxXmlMessage類。
- WxXmlMessage類是統一訊息的載體。所有訊息包括事件推送都包含在這個類當中。

嘗試與公眾號互動,觀察控制檯

這時候,我們可以嘗試向公眾號傳送一句:“Hello”。我們看一下結果。

Message

wx-tools自動把發來的訊息封裝成一個類,方便處理。

我們再試試接收並解析事件(Event)推送,例如:新使用者關注事件推送。

我用另一個微訊號關注了一波我的測試號,觀察控制檯:

關注事件

好了,至此。你已經成功一大半了。為什麼呢!因為剩下的就是拿著這個訊息(Message)去各種處理,最後返回給使用者想要的東西即可~

下篇會講一些路由器的使用方式,請移步下篇!嘻嘻

4. WxMessageRouter的使用例子

建立選單欄

假設一下,我現在想把公眾號的選單欄設定成這樣的:

Menu

我們可以寫一個Main方法,來更改公眾號的選單欄,程式碼如下:

public class Menu {

    public static void main(String[] args) {
        IService iService = new WxService();
        WxMenu menu = new WxMenu();
        List<WxMenu.WxMenuButton> btnList = new ArrayList<>();

        //飆升功能
        WxMenu.WxMenuButton btn1 = new WxMenu.WxMenuButton();
        btn1.setName("分類");
        List<WxMenu.WxMenuButton> subList = new ArrayList<>();
        WxMenu.WxMenuButton btn1_1 = new WxMenu.WxMenuButton();
        btn1_1.setType(WxConsts.MENU_BUTTON_CLICK);
        btn1_1.setKey(MenuKey.HOT_SONG);
        btn1_1.setName("飆升榜");
        WxMenu.WxMenuButton btn1_2 = new WxMenu.WxMenuButton();
        btn1_2.setType(WxConsts.MENU_BUTTON_CLICK);
        btn1_2.setKey(MenuKey.TOP_500);
        btn1_2.setName("TOP500");
        WxMenu.WxMenuButton btn1_3 = new WxMenu.WxMenuButton();
        btn1_3.setType(WxConsts.MENU_BUTTON_CLICK);
        btn1_3.setKey(MenuKey.NET_HOT_SONG);
        btn1_3.setName("網路紅歌");
        WxMenu.WxMenuButton btn1_4 = new WxMenu.WxMenuButton();
        btn1_4.setType(WxConsts.MENU_BUTTON_CLICK);
        btn1_4.setKey(MenuKey.HUAYU_SONG);
        btn1_4.setName("華語新歌");
        WxMenu.WxMenuButton btn1_5 = new WxMenu.WxMenuButton();
        btn1_5.setType(WxConsts.MENU_BUTTON_CLICK);
        btn1_5.setKey(MenuKey.XINAO_SONG);
        btn1_5.setName("洗腦神曲");

        WxMenu.WxMenuButton btn2 = new WxMenu.WxMenuButton();
        btn2.setType(WxConsts.MENU_BUTTON_CLICK);
        btn2.setKey(MenuKey.CHANGE_NEWS);
        btn2.setName("換一組");

        WxMenu.WxMenuButton btn3 = new WxMenu.WxMenuButton();
        btn3.setType(WxConsts.MENU_BUTTON_CLICK);
        btn3.setKey(MenuKey.HELP);
        btn3.setName("幫助");

        subList.addAll(Arrays.asList(btn1_1, btn1_2, btn1_3, btn1_4, btn1_5));
        btn1.setSub_button(subList);

        //將三個按鈕設定進btnList
        btnList.add(btn1);
        btnList.add(btn2);
        btnList.add(btn3);
        //設定進選單類
        menu.setButton(btnList);
        //呼叫API即可
        try {
            //引數1--menu  ,引數2--是否是個性化定製。如果是個性化選單欄,需要設定MenuRule
            iService.createMenu(menu, false);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
    }
}
  • 接下來直接執行就行了,你就會看到你的公眾號選單欄的變化。

選單欄的引數設定,請參考官方文件 - 選單欄的建立, 選單欄的按鈕有分很多種型別(type),例如click,view等。詳情建議先看官方文件。

建立訊息路由並設定第一個規則(Rule)

假設需求如下: 我只接收選單欄型別為:Click,且Key為MenuKey.HELP的訊息,其他一律放棄不接收。

正常來說,常規寫法應該是加一個if判斷,例如:

if("CLICK".equals(message.event) &&  MenuKey.HELP.equals(message.eventKey)){
    doSomething();
}

這樣的寫法會隨著需求增多,而越來越龐大臃腫。

而wx-tools使用訊息路由的方式去過濾處理訊息,寫法如下:

  • 補充WxController中的Post處理方法:
    @PostMapping
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();

        // 建立一個路由器
        WxMessageRouter router = new WxMessageRouter(iService);
        try {
            // 微信伺服器推送過來的是XML格式。
            WxXmlMessage wx = XStreamTransformer.fromXml(WxXmlMessage.class, request.getInputStream());
            System.out.println("訊息:\n " + wx.toString());
            router.rule().event(WxConsts.EVT_CLICK).eventKey(MenuKey.HELP).handler(HelpDocHandler.getInstance()).end();
            // 把訊息傳遞給路由器進行處理
            WxXmlOutMessage xmlOutMsg = router.route(wx);
            if (xmlOutMsg != null)
                // 因為是明文,所以不用加密,直接返回給使用者
                out.print(xmlOutMsg.toXml());。

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            out.close();
        }

    }
  • HelpDocHandler.java程式碼如下:

Handler程式碼採用單例模式,為了解決訊息重試的問題。但由於網路問題導致返回結果慢了。會自動重試,返回多條資訊。當然,這種做法不建議你們使用,因為這樣寫,同一時間僅僅允許一個執行緒進來處理,並不適合多執行緒環境。

因為這個音樂爬蟲專案是做給我女票使用的,使用者僅僅只有一個。所以我可以這麼做,而你不們作為專案,不可以!!!好了繼續~

官方關於訊息重試的機制點選這裡檢視

public class HelpDocHandler implements WxMessageHandler {

    private static HelpDocHandler instance = null;

    private boolean isRun = false;

    private HelpDocHandler(){}

    public static synchronized HelpDocHandler getInstance(){
        if (instance == null) {
            instance = new HelpDocHandler();
        }
        return instance;
    }

    private synchronized  boolean getIsRun() {
        return isRun;
    }

    private synchronized void setRun(boolean run) {
        isRun = run;
    }


    @Override
    public WxXmlOutMessage handle(WxXmlMessage wxMessage, Map<String, Object> context, IService iService) throws WxErrorException {
        WxXmlOutMessage response = null;
        if (!getIsRun()) {
            setRun(true);
            response = execute(wxMessage);
            setRun(false);
        }
        return response;
    }

    private WxXmlOutMessage execute(WxXmlMessage wxMessage) {
        return WxXmlOutMessage.TEXT().content(ResponseConst.HELP).toUser(wxMessage.getFromUserName()).fromUser(wxMessage.getToUserName()).build();
    }
}
  • 接下來測試一下,點選公眾號的幫助按鈕,正常返回幫助說明。而點選其他按鈕,均無反應。

這樣我們就成功利用訊息路由和建立第一個規則(Rule)去過濾訊息,並處理返回了。

是不是很簡單~2333

現在我們嘗試使用wx-tools其他特性,新增更多的功能進去吧~gogogo

完成選單欄剩餘功能

按鈕[分類-飆升榜]

需求:當用戶點選此按鈕,後臺爬蟲幫我爬當天酷狗飆升榜的列表下來,並返回。 酷狗飆升榜在這。

  • 爬蟲技術不是本文件的主要內容,所以在此就不細講如何爬下資料。有興趣的同學可以閱讀下原始碼。(原理很簡單,僅僅使用HttpClient+Jsoup)

新增RankHandler.java

此handler專門處理爬蟲分類Event事件。

RankHandler.java:(負責處理訊息部分)

public class RankHandler implements WxMessageHandler {
    private static RankHandler instance = null;
    private boolean isRun = false;
    private RankHandler() {}

    public static synchronized RankHandler getInstance() {
        if (instance == null) {
            instance = new RankHandler();
        }
        return instance;
    }

    @Override
    public WxXmlOutMessage handle(WxXmlMessage wxMessage, Map<String, Object> context, IService iService)
            throws WxErrorException {
        StringBuilder result = new StringBuilder();
        if (!getIsRun()) {
            setRun(true);
            try {
                result = execute(wxMessage);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                setRun(false);
            }
        } else {
            result.append(ResponseConst.DUPLICATE_REQUEST);
        }
        return WxXmlOutMessage.TEXT().content(result.toString()).toUser(wxMessage.getFromUserName()).fromUser(wxMessage.getToUserName()).build();
    }

    private StringBuilder execute(WxXmlMessage wxMessage) throws Exception{
        StringBuilder stringBuilder = new StringBuilder();
        try {
            switch (wxMessage.getEventKey()) {
                case MenuKey.HOT_SONG:
                    collectSongRank(stringBuilder, UrlConst.HOT_RANK_URL);
                    break;
                default:
                    stringBuilder.append("暫時無此分類噢!");
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder;
    }

    private void collectSongRank(StringBuilder stringBuilder, String url) throws IOException {
        RankCollector collector = new RankCollector();
        Rank rank = collector.collect(url);
        stringBuilder.append("\uD83D\uDD25" + rank.getScope() + "[" + rank.getUpdateTime() + "]\n\n");
        for (HotSinger hotSinger : rank.getHotSingerList()) {
            stringBuilder.append(hotSinger.getName() + "-" + hotSinger.getHotSong() + "\n");
        }
    }

    private synchronized boolean getIsRun() {
        return isRun;
    }

    private synchronized void setRun(boolean run) {
        isRun = run;
    }

}

RankCollector.java: (爬蟲部分)

public class RankCollector {

    public Rank collect(String url) throws IOException {
        return getRank(url);
    }

    private Rank getRank(String rankUrl) throws IOException {
        Rank rank = new Rank();
        String body = HttpClientUtil.get(rankUrl);
        Document doc = Jsoup.parse(body);
        rank.setScope(doc.select("a[class=current]").attr("title"));
        rank.setUpdateTime(doc.select("span[class=rank_update]").text());
        List<Element> aElements = doc.select("a[data-active=playDwn]");
        for(int i = 0; i < aElements.size(); i++){
            String[] splitArray = aElements.get(i).text().split("-");
            String name = splitArray[0].toString().trim();
            String song = splitArray[1].toString().trim();
            rank.getHotSingerList().add(new HotSinger(doc.select("a[class=current]").attr("title"), name, song));
        }
        return rank;
    }
}

新增新的規則(Rule)

修改新增WxController的路由規則:

router.rule().event(WxConsts.EVT_CLICK).eventKey(MenuKey.HELP).handler(HelpDocHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.HOT_SONG).handler(RankHandler.getInstance()).end();
  • 這裡解釋一下:next()和end()的意思。
  • next() 表示訊息經過第一個規則(Rule)之後,允許繼續匹配下面的規則,代表著同一個訊息有可能被多個Handler處理。
  • end() 表示規則的結束。當訊息滿足某條規則時遇到end(),不會再往下匹配規則,就此結束。

重新執行,並測試

HotSong

當天的飆升榜就這樣返回回來了~很激動有木有!

  • 好了同一原理實現其他4個排行榜按鈕的功能,這裡就不展示了。詳情可以檢視demo原始碼。

5. WxMessageMatcher介面實現例子

WxMessageMatcher(訊息匹配器)介面用於一些簡單的匹配,可以自定義匹配邏輯,如格式驗證。匹配成功則繼續往下執行,否則不允許通過。

需求:我想當用戶傳送文字訊息:“我是誰”時,後臺獲取該使用者的微信使用者資訊(暱稱)並返回。

建立WhoAmIMatcher.java

用於匹配符合”我是誰“的訊息。

public class WhoAmIMatcher implements WxMessageMatcher{

    @Override
    public boolean match(WxXmlMessage message) {
        if(StringUtils.isNotEmpty(message.getContent())){
            if(message.getContent().equals("我是誰")){
                return true;
            }
        }
        return false;
    }

}

建立WhoAmIHandler.java

用於處理當匹配到“我是誰”的訊息。

  • 修改路由規則:(這時候已經把所有的爬蟲都寫完啦!所以路由規則這麼多!
router.rule().msgType(WxConsts.XML_MSG_TEXT).matcher(new WhoAmIMatcher()).handler(new WhoAmIHandler()).end()
                    .rule().event(WxConsts.EVT_CLICK).eventKey(MenuKey.HELP).handler(HelpDocHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.HOT_SONG).handler(RankHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.TOP_500).handler(RankHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.NET_HOT_SONG).handler(RankHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.HUAYU_SONG).handler(RankHandler.getInstance()).next()
                    .rule().eventKey(MenuKey.XINAO_SONG).handler(RankHandler.getInstance()).end();

執行,並測試

如圖所示,輸入”我是誰“的時候,返回了我微信的暱稱。

WhoAmI

小小總結一下

至此,我們已經嘗試使用了路由器Router,規則Rule,匹配器Matcher,處理器Handler。

還有使用了IServer統一介面呼叫去獲取使用者資訊。

IServer 介面是整合所有wx-tools已經實現的微信介面,統一呼叫入口。它的實現是WxService.java。想看實現原始碼的可以戳:這裡

6.WxMessageInterceptor介面實現例子

WxMessageInterceptor(攔截器)功能與Matcher相同,用於過濾攔截,但是與Matcher最大的不同就是,它支援更復雜的業務處理,因為它攜帶了IService和上下文context,可以利用這兩個引數進行業務處理。

  • IService : 統一介面呼叫入口。
  • context :上下文,可以向handler傳遞引數。

WxMessageInterceptor攔截器介面,可以處理更加複雜的驗證。例如身份驗證,時效校驗等等。

由於我的音樂爬蟲專案並沒有涉及到這麼深的處理,所以這裡只給出一個DemoInterceptor看看。

假設需求:只有使用者關注公眾號時長大於3天才能參與活動。

  • 建立DemoInterceptor.java 實現 WxMessageInterceptor介面
/**
* Demo 攔截器,可以通過WxService做更加複雜的攔截,例如身份驗證,許可權驗證等操作。
* @author antgan
*
*/
public class DemoInterceptor implements WxMessageInterceptor{

    public boolean intercept(WxXmlMessage wxMessage, Map context, IService wxService) throws WxErrorException {
        //可以使用wxService的微信API方法
        //可以在Handler和Interceptor傳遞訊息,使用context上下文
        //可以實現自己的業務邏輯
        //這裡就不編寫驗證關注三天以上的使用者了
        if(/*使用者關注時長大於3天*/){
            return true;
        }
        return false;
    }
}
  • 接下來你們都知道的,修改路由器規則,新增interceptor。

例如:

router.rule().matcher(new DemoMatcher()).interceptor(new DemoInterceptor()).handler(new DemoMessageHandler()).end();

搞定。接下來交給wx-tools去做吧~
簡單吧!

7. 關於WxMessageRouter及Rule的詳解

WxMessageRouter訊息路由器,不知道你們理解了多少,接下來還是詳細講解一下需要注意的細節。

提到這個路由器,就要說說另一個東西:WxMessageRouterRule。簡稱規則(Rule)。

定義規則,用於對來自微信伺服器的訊息進行過濾和篩選,只針對有效訊息進行處理,提高伺服器處理效率。

通過鏈式配置路由規則(Rule),根據規則把來自微信的訊息交給handler處理。

注意:

  1. 配置路由規則時儘量按照從細到粗的原則,否則可能訊息可能會被提前處理
  2. 預設情況下訊息只會被處理一次,除非使用 {WxMessageRouterRule的next()方法}
  3. 規則的結束必須用WxMessageRouterRule的end()方法或者WxMessageRouterRule的next()方法,否則不會生效。

使用方法:

//初始化一個路由器,把wxService傳入。
WxMessageRouter router = new WxMessageRouter(wxService);
//新建路由規則,通過rule()方法建立新的規則,然後鏈式填寫過濾條件。MSG_TYPE等引數填入WxConst中的常量,這裡不作展示,可以檢視WxConst程式碼或官方文件,有註釋。
router.rule().msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT").matcher(matcher).interceptor(interceptor, ...).handler(handler, ...).end()
.rule().msgType("MSG_TYPE")...//另外一個匹配規則.end();
// 將WxXmlMessage交給訊息路由器,處理後得到結果。
WxXmlOutMessage xmlOutMsg = router.route(wxXmlMessage);

關於路由規則條件

  1. 對於一條訊息(WxXMLMessage)允許多個規則(Rule)去進行過濾和處理。用next()方法去連線兩個規則。但是最後必須是以end()方法結束。
  2. 每條規則可以允許多個攔截器(Interceptor),多個處理器(Handler)處理。返回最後一個Handler處理的結果。
  3. 路由規則還提供正則表示式過濾,對於簡單的過濾需求,如只接受數字訊息。不想繁瑣的建立Matcher匹配器。可以如下寫法。
//正則表示式:^[0-9]*$只接受數字訊息,其他訊息過濾。
router.rule().rContent("^[0-9]*$").handler(new DemoHandler()).end();

去除多餘訊息,高效處理爭對性訊息,真是好用又簡單。

小小總結一下

至此,如果你跟著做,並且都成功了話。你已經入門了wx-tools了。以後無非就是根據使用者需求新增路由規則,並新增處理器去處理,入門教程就到此結束了。

其實DEMO裡還有關鍵字搜尋並推送圖文訊息給使用者的功能. 文件裡沒演示出來.有興趣的同學可以查閱原始碼.

最終效果圖~可以是這樣

推送