"""Parse left-nav tree (ul.dep2/dep3/dep4) anchors with ancestor tracking. Build menuCd -> target_blank map restricted to the navigation tree only.""" import urllib.request, ssl, re, io, sys, json from html.parser import HTMLParser sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE def fetch(url): req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) return urllib.request.urlopen(req, timeout=25, context=ctx).read().decode('utf-8', 'replace') class NavParser(HTMLParser): def __init__(self): super().__init__() self.ul_stack = [] # stack of ul class strings self.in_nav_depth = 0 # how many depN uls we are inside self.cur_a = None # (menuCd, blank, href) while inside an within nav self.text = '' self.results = [] # list of dict def handle_starttag(self, tag, attrs): d = dict(attrs) if tag == 'ul': cls = d.get('class', '') or '' isnav = bool(re.match(r'dep[2-5]', cls.strip())) self.ul_stack.append(isnav) if isnav: self.in_nav_depth += 1 elif tag == 'a' and self.in_nav_depth > 0: href = d.get('href', '') or '' mc = re.search(r'menuCd=(DOM_\d+)', href) blank = (d.get('target', '') == '_blank') self.cur_a = {'code': mc.group(1) if mc else None, 'href': href, 'blank': blank} self.text = '' def handle_data(self, data): if self.cur_a is not None: self.text += data def handle_endtag(self, tag): if tag == 'a' and self.cur_a is not None: self.cur_a['label'] = self.text.strip() self.results.append(self.cur_a) self.cur_a = None elif tag == 'ul' and self.ul_stack: isnav = self.ul_stack.pop() if isnav: self.in_nav_depth -= 1 def build_map(htmls): amap = {} # code -> set of blank values labels = {} for h in htmls: p = NavParser(); p.feed(h) for a in p.results: if a['code']: amap.setdefault(a['code'], set()).add(a['blank']) labels[a['code']] = a['label'] return amap, labels if __name__ == '__main__': # fetch one representative page per top category to cover full nav tree seeds = [ 'DOM_000000101001001000', # 시민마당 'DOM_000000102001002000', # 종합민원 'DOM_000000103013019000', # 분야별정보 'DOM_000000104007011000', # 소통/정보공개 'DOM_000000105001001000', # 정읍은 'DOM_000000104001001000', # 정보공개 ] htmls = [] for s in seeds: try: htmls.append(fetch('https://www.jeongeup.go.kr/index.jeongeup?menuCd=' + s)) except Exception as e: print('seed fail', s, e) amap, labels = build_map(htmls) print('distinct nav menuCd:', len(amap)) data = json.load(io.open('_dump.json', encoding='utf-8')) miss = [] blank_rows = [] for row in data: k = row.get('K', '') mc = re.search(r'menuCd=(DOM_\d+)', k) if not mc: continue code = mc.group(1) if code not in amap: miss.append((row['r'], row.get('F') or row.get('E') or row.get('G'))) continue vals = amap[code] if True in vals: blank_rows.append((row['r'], row.get('L'), True in vals and False in vals, row.get('F') or row.get('E') or row.get('G'))) print('rows not found in nav:', len(miss)) for r, f in miss: print(' MISS', r, f) print('rows whose nav anchor is target=_blank:', len(blank_rows)) for r, l, conflict, f in blank_rows: print(' BLANK', r, 'L=' + str(l), 'conflict' if conflict else '', f)