1. 前置说明

1,微信公众号的对话框界面是 Native 的,比如:

2,微信公众号的文章界面是 WebView 的,比如:

3,新版微信使用 xweb 内核,开启调试的方式是在手机微信内点击或扫码打开 http://debugxweb.qq.com/?inspector=true,打开后将跳转到微信首页,跳转后表示已经开启调试。

4,微信公众号有两种链接:

5,获取微信公众号文章链接的其他方式包括:

本文讲述如何通过安卓微信获取公众号文章链接

6,如果想使用 Appium 调试混合安卓应用,那么构建应用时,必须在 android.webkit.WebView 元素上,将 setWebContentsDebuggingEnabled 属性设置为 true。对于微信而言,执行第 3 步即可。如果想了解更多详情,请阅读 https://appium.github.io/appium.io/docs/en/writing-running-appium/web/hybrid/

7,在获取公众号文章链接之前,需要先关注公众号。


2. 方式 1 - 不通过 WebView 直接获取永久链接(代码在第 4 节)

依次执行如下操作,以获取文章链接:

  1. 点击界面右上角的三个点
  2. 在弹出的窗口中,点击复制链接
  3. 从剪贴板获取链接

3. 方式 2 - 通过 WebView 获取临时链接和文章(代码在第 4 节)

在将 Driver 的上下文切换到 WebView 时,可以通过 Appium 日志查看应用使用的 WebView 版本(与系统 WebView 实现的版本不一定相同),比如:

[13f965c5][Chromedriver@f092] Got response with status 200: {"value":{"capabilities":{"acceptInsecureCerts":false,"browserName":"chrome","browserVersion":"126.0.6478.188","chrome":{"chromedriverVersion":"126.0.6478.182 (5b5d8292ddf182f8b2096fa665b473b6317906d5-refs/branch-heads/6478@{#1776})"},"fedcm:accounts":true,"goog:chromeOptions":{"debuggerAddress":"localhost:60658"},"pageLoadStrategy":"normal","platformName":"android","proxy":{},"setWindowRect":false,"strictFileInteractability":false,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000},"unhandledPromptBehavior":"dismiss and notify","webauthn:extension:credBlob":false,"webauthn:extension:largeBlob":false,"webauthn:extension:minPinLength":false,"webauthn:extension:prf":false,"webauthn:virtualAuthenticators":false},"sessionId":"5a9581a653cfeb1e67d1de673d54cf84"}}

需要下载与之匹配的 chromedriver(本文是 126.0.6478.X):

  1. 查阅 https://developer.chrome.com/docs/chromedriver/downloads/version-selection?hl=zh-cn,了解如何下载相应版本的 chromedriver
  2. 进入 https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints,搜索 known-good-versions-with-downloads.json,查找相应版本 chromedriver 的下载地址

不要忘记执行第 1 节的第 3 步。


4. Python 实现

说明:

  1. 本示例代码只获取第一篇文章
import random
import time

from selenium.webdriver.remote.webelement import WebElement
from typing_extensions import Self, Optional, Any
import logging

from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver.extensions.android.nativekey import AndroidKey
from appium.options.android import UiAutomator2Options
from selenium.webdriver.common.by import By

logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] [%(filename)s:%(lineno)d] %(levelname)s - %(message)s',
    datefmt="%Y-%m-%d %H:%M:%S",
)

LOGGER: logging.Logger = logging.getLogger(__name__)


class Weixin:

    def __init__(
            self: Self,
            *,
            platform_version: str,
            device_name: str,
            server_url: str = "http://localhost:4723",
            implicitly_wait: float = 8,
            explicitly_wait: float = 4,
            new_command_timeout: float = 3600 * 8,
            udid: Optional[str] = None,
    ) -> None:
        self._desired_caps: dict[str, Any] = {
            "platformName": "Android",
            "automationName": "UiAutomator2",
            # 使用 Unicode 编码发送字符串
            "unicodeKeyboard": True,
            # 隐藏键盘
            "resetKeyboard": True,
            # 不重置 App
            "noReset": True,
            # 加速动态页面的速度
            "settings[waitForIdleTimeout]": 0,

            # 微信包名
            "appPackage": "com.tencent.mm",
            "appActivity": ".ui.LauncherUI",

            # 安卓版本
            "platformVersion": platform_version,
            # 设备名称,最好唯一
            "deviceName": device_name,
            # 两条 Appium 命令间的最长时间间隔,单位是秒
            "newCommandTimeout": new_command_timeout,
        }
        # deviceName 只能用于区分苹果设备;
        # udid 用于区分安卓设备,可以通过 adb devices 获取
        if udid:
            self._desired_caps["udid"] = udid
        # Appium 服务地址
        self._server_url: str = server_url
        self._implicitly_wait: float = implicitly_wait
        self._explicitly_wait: float = explicitly_wait

    def _enter_page(self: Self, search_word: str) -> None:
        """
        进入公众号页面。
        对于微信公众号,必须先使用一个 Driver 将文章页面放到最前面,然后再启动新 Driver,才能点击三个点或调试 WebView。

        :param search_word: 公众号名称
        """
        driver: webdriver.Remote = webdriver.Remote(
            self._server_url,
            options=UiAutomator2Options().load_capabilities(self._desired_caps),
        )
        driver.implicitly_wait(self._implicitly_wait)
        # 激活 App 使其处于前台
        driver.activate_app(self._desired_caps["appPackage"])

        LOGGER.info("clicking 通讯录")
        driver.find_element(By.XPATH, '(//android.widget.TextView[@text="通讯录"])[1]').click()
        LOGGER.info("clicked 通讯录")
        LOGGER.info("clicking 公众号")
        driver.find_element(By.XPATH, '(//android.widget.TextView[@text="公众号"])[1]').click()
        LOGGER.info("clicked 公众号")
        LOGGER.info("clicking search logo")
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, '搜索').click()
        LOGGER.info("clicked search logo")
        LOGGER.info("inputting search word")
        driver.find_element(AppiumBy.CLASS_NAME, 'android.widget.EditText').send_keys(search_word)
        driver.press_keycode(AndroidKey.ENTER)
        LOGGER.info("inputted search word")
        LOGGER.info("clicking search word")
        driver.find_element(By.XPATH, f'(//android.widget.TextView[@text="{search_word}"])[1]').click()
        LOGGER.info("clicked search word")
        LOGGER.info("clicking settings")
        driver.find_element(By.XPATH, '//android.widget.ImageView[@content-desc="设置"]').click()
        LOGGER.info("clicked settings")

        # 点击第一篇文章的图片
        LOGGER.info("clicking a blogpost")
        driver.find_element(
            By.XPATH,
            '(//androidx.recyclerview.widget.RecyclerView)[1]//android.widget.ImageView[1]'
        ).click()
        LOGGER.info("clicked the blogpost")

        driver.quit()

    def path_1(self: Self, search_word: str) -> str:
        """
        方式 1
        """
        self._enter_page(search_word)
        driver: webdriver.Remote = webdriver.Remote(
            self._server_url,
            options=UiAutomator2Options().load_capabilities(self._desired_caps),
        )
        driver.implicitly_wait(self._implicitly_wait)
        LOGGER.info("clicking 更多信息")
        driver.find_element(By.XPATH, '//android.widget.ImageView[@content-desc="更多信息"]').click()
        LOGGER.info("clicked 更多信息")
        LOGGER.info("clicking 复制链接")
        driver.find_element(By.XPATH, '//android.widget.TextView[@text="复制链接"]').click()
        LOGGER.info("clicked 复制链接")
        url: str = driver.get_clipboard_text()
        driver.quit()
        return url

    def path_2(self: Self, search_word: str) -> str:
        """
        方式 2
        """
        # 向 Capabilities 中添加 chromedriver
        self._desired_caps["chromedriverExecutable"] = \
            "C:\\Users\\Lenovo\\Downloads\\chromedriver-win64\\chromedriver-win64\\chromedriver.exe"
        self._desired_caps["showChromedriverLog"] = True

        # 进入文章界面
        self._enter_page(search_word)

        driver: webdriver.Remote = webdriver.Remote(
            self._server_url,
            options=UiAutomator2Options().load_capabilities(self._desired_caps),
        )
        driver.implicitly_wait(self._implicitly_wait)
        # 将上下文从 Native 切换到 WebView
        switched_to: str = "WEBVIEW_com.tencent.mm"
        for context in driver.contexts:
            LOGGER.info("context - %s", context)
        LOGGER.info("switching context to %s", switched_to)
        driver.switch_to.context(switched_to)
        LOGGER.info("switched context to %s", switched_to)
        # 等待页面加载
        time.sleep(random.randint(1, 4))
        url: str = driver.current_url
        # 获取、打印标题
        title: WebElement = driver.find_element(By.XPATH, '//*[@id="activity-name"]')
        LOGGER.info("title - %s", title.text)
        driver.quit()
        return url

    def terminate(self: Self) -> None:
        """
        终止 App
        """
        driver: webdriver.Remote = webdriver.Remote(
            self._server_url,
            options=UiAutomator2Options().load_capabilities(self._desired_caps),
        )
        driver.implicitly_wait(self._implicitly_wait)
        LOGGER.info("terminating %s app", self._desired_caps["appPackage"])
        driver.terminate_app(self._desired_caps["appPackage"])
        driver.quit()


def test() -> None:
    # 需要先关注公众号
    search_word: str = "Java基基"
    wx: Weixin = Weixin(
        device_name="redminote13",
        platform_version="14",
    )
    try:
        print("permanent url - ", wx.path_1(search_word))
    finally:
        wx.terminate()
    try:
        print("temp url - ", wx.path_2(search_word))
    finally:
        wx.terminate()


if __name__ == "__main__":
    test()

输出示例:

permanent url -  https://mp.weixin.qq.com/s/C4Bz5vgvb70V0syKld5wUw
temp url -  https://mp.weixin.qq.com/s?__biz=MzUxOTc4NjEyMw==&mid=2247557678&idx=1&sn=f7f3aca74f402b8354eff50a19ed1454&chksm=f9f7f9cace8070dc228c190df6167f356a87647750baa615660a62e
d62559f2021b64eddeab0&scene=231&subscene=0&clicktime=1727124988&enterid=1727124988&sessionid=0&ascene=3&fasttmpl_type=0&fasttmpl_fullversion=7396431-zh_CN-zip&fasttmpl_flag=0&realr
eporttime=1727124988811&devicetype=android-34&version=28003334&nettype=WIFI&abtest_cookie=AAACAA%3D%3D&lang=zh_CN&session_us=gh_4985c77f7fde&countrycode=US&exportkey=n_ChQIAhIQQEkm
VZBXX3g3W2QzKLJEZRLoAQIE97dBBAEAAAAAAN2BJEbo%2BBIAAAAOpnltbLcz9gKNyK89dVj09wBwO%2FYhwUF3PcDjQg2DdD%2B0dk8MpneT%2BoB%2BwlPdbRqQisQFGqh7akAUUFFwJrPIi7V17dHLQfzwf4%2FaUOhtJLM8wHE0zHCQ
Y0spLdCD6B48lzTtMDgolr1LUxU7iI6Xa8Z1mhJ5wx2%2BtESq46XH36U2HOcnNskUVDFv14BlccQyiV0i3xpPansdjF4JbbmAvVuPibzu%2Fb238S6iqpgxSz8si5rASC%2BeWmqExopFKH%2BEFEHPaEi7je4vVnXAjT79sL8%3D&pass_ticket=4NpFFXxMKqi95fAFcRIhy%2FOFVjx0P0Qk4rUfByuSPPh0PhibTQm2024H9URLldSP&wx_header=3