docstr: read change from url
  The parameter url is from the _get_change_notices_url method.
  Determine the stock add_date/remove_date based on the title.
  The response contains three cases:
      1.Only excel_url(extract data from excel_url)
      2.Both the excel_url and the body text(try to extract data from excel_url first, and then try to extract data from body text)
      3.Only body text(extract data from body text)
  
  Parameters
  ----------
  url : str
      change url
  
  Returns
  -------
      pd.DataFrame:
          symbol      date        type
          SH600000  2019-11-11    add
          SH600000  2020-11-10    remove
      dtypes:
          symbol: str
          date: pd.Timestamp
          type: str, value from ["add", "remove"]
docstr_tok: ["read","change","from","url","The","parameter","url","is","from","the","_get_change_notices_url","method",".","Determine","the","stock","add_date\/remove_date","based","on","the","title",".","The","response","contains","three","cases",":","1.Only","excel_url","(","extract","data","from","excel_url",")","2.Both","the","excel_url","and","the","body","text","(","try","to","extract","data","from","excel_url","first",",","and","then","try","to","extract","data","from","body","text",")","3.Only","body","text","(","extract","data","from","body","text",")","Parameters","--","--","--","--","--","url",":","str","change","url","Returns","--","--","--","-","pd.DataFrame",":","symbol","date","type","SH600000","2019-11-11","add","SH600000","2020-11-10","remove","dtypes",":","symbol",":","str","date",":","pd.Timestamp","type",":","str",",","value","from","[","``","add","''",",","``","remove","''","]"]
code: def _read_change_from_url(self, url: str) -> pd.DataFrame:
      """read change from url
      The parameter url is from the _get_change_notices_url method.
      Determine the stock add_date/remove_date based on the title.
      The response contains three cases:
          1.Only excel_url(extract data from excel_url)
          2.Both the excel_url and the body text(try to extract data from excel_url first, and then try to extract data from body text)
          3.Only body text(extract data from body text)
  
      Parameters
      ----------
      url : str
          change url
  
      Returns
      -------
          pd.DataFrame:
              symbol      date        type
              SH600000  2019-11-11    add
              SH600000  2020-11-10    remove
          dtypes:
              symbol: str
              date: pd.Timestamp
              type: str, value from ["add", "remove"]
      """
      resp = retry_request(url).json()["data"]
      title = resp["title"]
      if not title.startswith("关于"):
          return pd.DataFrame()
      if "沪深300" not in title:
          return pd.DataFrame()
  
      logger.info(
          f"load index data from https://www.csindex.com.cn/#/about/newsDetail?id={url.split('id=')[-1]}"
      )
      _text = resp["content"]
      date_list = re.findall(
          r"(\d{4}).*?年.*?(\d+).*?月.*?(\d+).*?日", _text
      )
      if len(date_list) >= 2:
          add_date = pd.Timestamp("-".join(date_list[0]))
      else:
          _date = pd.Timestamp(
              "-".join(re.findall(r"(\d{4}).*?年.*?(\d+).*?月", _text)[0])
          )
          add_date = get_trading_date_by_shift(
              self.calendar_list, _date, shift=0
          )
      if "盘后" in _text or "市后" in _text:
          add_date = get_trading_date_by_shift(
              self.calendar_list, add_date, shift=1
          )
      remove_date = get_trading_date_by_shift(
          self.calendar_list, add_date, shift=-1
      )
  
      excel_url = None
      if resp.get("enclosureList", []):
          excel_url = resp["enclosureList"][0]["fileUrl"]
      else:
          excel_url_list = re.findall('.*href="(.*?xls.*?)".*', _text)
          if excel_url_list:
              excel_url = excel_url_list[0]
              if not excel_url.startswith("http"):
                  excel_url = (
                      excel_url
                      if excel_url.startswith("/")
                      else "/" + excel_url
                  )
                  excel_url = f"http://www.csindex.com.cn{excel_url}"
      if excel_url:
          try:
              logger.info(
                  f"get {add_date} changes from the excel, title={title}, excel_url={excel_url}"
              )
              df = self._parse_excel(excel_url, add_date, remove_date)
          except ValueError:
              logger.info(
                  f"get {add_date} changes from the web page, title={title}, url=https://www.csindex.com.cn/#/about/newsDetail?id={url.split('id=')[-1]}"
              )
              df = self._parse_table(_text, add_date, remove_date)
      else:
          logger.info(
              f"get {add_date} changes from the web page, title={title}, url=https://www.csindex.com.cn/#/about/newsDetail?id={url.split('id=')[-1]}"
          )
          df = self._parse_table(_text, add_date, remove_date)
      return df
  
imports: import re
  import abc
  import sys
  from io import BytesIO
  from typing import List, Iterable
  from pathlib import Path
  import fire
  import requests
  import pandas
  import baostock
  from tqdm import tqdm
  from loguru import logger
  from data_collector.index import IndexBase
  from data_collector.utils import get_calendar_list, get_trading_date_by_shift, deco_retry
  from data_collector.utils import get_instruments
label_desc: Use image node_id 12 for calling the CSIIndex obj's underlying member method code with example usage: obj._read_change_from_url(url) and returns: df, pd, pd