diff --git a/requirements.txt b/requirements.txt index 4da040c..e952693 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,19 @@ altgraph==0.17.4 backports.tarfile==1.2.0 certifi==2023.7.22 +cffi==1.16.0 charset-normalizer==2.0.12 colorama==0.4.6 +cryptography==43.0.0 +decorator==5.1.1 docutils==0.20.1 idna==3.4 importlib-metadata==7.0.1 importlib_resources==6.4.0 jaraco.classes==3.4.0 jaraco.context==5.3.0 -jaraco.functools==4.0.2 +jaraco.functools==4.0.1 +jeepney==0.8.0 keyring==25.2.1 lxml==4.9.2 markdown-it-py==3.0.0 @@ -20,17 +24,20 @@ nh3==0.2.18 packaging==23.2 pefile==2023.2.7 pkginfo==1.10.0 +py==1.11.0 +pycparser==2.22 pycryptodomex==3.19.0 Pygments==2.18.0 pyinstaller==6.4.0 pyinstaller-hooks-contrib==2024.1 -PySocks==1.7.1 pywin32-ctypes==0.2.2 readme_renderer==43.0 requests==2.27.1 requests-toolbelt==1.0.0 +retry==0.9.2 rfc3986==2.0.0 rich==13.7.1 +SecretStorage==3.3.3 twine==5.1.1 typing_extensions==4.12.2 urllib3==1.26.19 diff --git a/test.py b/test.py index 48af8dc..02ccbe0 100644 --- a/test.py +++ b/test.py @@ -1 +1,103 @@ -# edit test py 1 \ No newline at end of file +# -*- coding: utf-8 -*- +from tookit.fofa_client import Client + +def get_base_results(key,search_key,size=10000,page=1): + client = Client(key) + data = client.search(search_key, size=size, page=page,fields="link,port,protocol,country,region,city,as_number,as_organization,host,domain,os," + "server,product") + return data["results"] + + +def add_city_to_region(result_pool,current_country, current_region, current_city): + """ + 添加城市信息 包含 country region city + @param result_pool: + @param current_country: + @param current_region: + @param current_city: + """ + # 检查 current_country 是否存在于 result_pool["country"] + if current_country not in result_pool["country"]: + # 如果不存在,则创建它,并初始化为一个空字典 + result_pool["country"][current_country] = {} + + # 检查 current_region 是否存在于 current_country 对应的字典中 + if current_region not in result_pool["country"][current_country]: + # 如果不存在,则创建它,并初始化为一个空集合 + result_pool["country"][current_country][current_region] = set() + + # 现在可以安全地向 current_region 的集合中添加 current_city + result_pool["country"][current_country][current_region].add(current_city) + + +def init_result_pool(results): + result_pool = { + "country":{}, + "port":set(), + "product":set(), + } + for result in results: + current_country = result[3] + current_region = result[4] + current_city = result[5] + add_city_to_region(result_pool,current_country,current_region,current_city) + result_pool["port"].add(result[1]) + products = result[12].split(",") + for product in products: + result_pool["product"].add(product) + + # result_pool["country"][current_country][current_region].add(current_city) + print(result_pool) + return result_pool + +def get_new_search_key_list(source_key,result_pool): + search_key_result = [] + + # 真 + + ## 城市 + country_result = [] + for country_code, country_data in result_pool['country'].items(): + + for region_code, region_data in country_data.items(): + for city in region_data: + country_result.append(f'({source_key}) && country="{country_code}" && region="{region_code}" && city="{city}"') + + ## 产品 + product_result = [] + for search_key in country_result: + for product in result_pool['product']: + product_result.append(f'{search_key} && product="{product}"') + + + ## 端口 + port_result = [] + for search_key in product_result: + for port in result_pool["port"]: + port_result.append(f'{search_key} && port="{port}"') + + + # false_port_key = "" + # for port in result_pool["port"]: + # false_port_key += f" && port != {port}" + # + # for search_key in country_result: + # port_result.append(f"{search_key} {false_port_key}") + + for result in port_result: + print(result) + + + return + +if __name__ == "__main__": + key = '' # 输入key + source_key = 'header="thinkphp" || header="think_template"' + result = get_base_results(key,source_key,100) + print(result) + result_pool = init_result_pool(result) + get_new_search_key_list(source_key,result_pool) + # print(result) + # print(len(result)) + + diff --git a/tookit/fofa_client/__init__.py b/tookit/fofa_client/__init__.py new file mode 100644 index 0000000..72f487e --- /dev/null +++ b/tookit/fofa_client/__init__.py @@ -0,0 +1,2 @@ +from .client import Client +from .exception import FofaError diff --git a/tookit/fofa_client/__main__.py b/tookit/fofa_client/__main__.py new file mode 100644 index 0000000..1375045 --- /dev/null +++ b/tookit/fofa_client/__main__.py @@ -0,0 +1,376 @@ + + +import os +import click +import fofa +import json +import logging +from tqdm import tqdm + +from .helper import XLSWriter, CSVWriter + +COLORIZE_FIELDS = { + 'ip': 'green', + 'port': 'yellow', + 'domain': 'magenta', + 'as_organization': 'cyan', +} + +def escape_data(args): + # Make sure the string is unicode so the terminal can properly display it + # We do it using format() so it works across Python 2 and 3 + args = u'{}'.format(args) + return args.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t') + + +# Define the main entry point for all of our commands +@click.group(context_settings={'help_option_names': ['-h', '--help']}) +def main(): + pass + +def get_user_key(): + return { + 'key': os.environ.get('FOFA_KEY', ''), + } + +def print_data(data): + click.echo(json.dumps(data, ensure_ascii=False, sort_keys=True, indent=4)) + +@main.command() +def info(): + """Shows general information about your account""" + para = get_user_key() + api = fofa.Client(**para) + try: + r = api.get_userinfo() + except fofa.FofaError as e: + raise click.ClickException(e.message) + print_data(r) + + +@main.command() +@click.option('--detail/--no-detail', '-D', help='show host detail info', default=False, flag_value=True) +@click.argument('host', metavar='', nargs=-1) +def host(detail, host): + """Aggregated information for the specified host. """ + para = get_user_key() + api = fofa.Client(**para) + + try: + r = api.search_host(host, detail=detail) + except fofa.FofaError as e: + raise click.ClickException(e.message) + + print_data(r) + + +def fofa_count(client, query): + """Returns the number of results for a fofa query. + """ + try: + r = client.search(query, size=1, fields='ip') + except fofa.FofaError as e: + raise click.ClickException(e.message) + + click.echo(r['size']) + + +def fofa_stats(client, query, fields='ip,port,protocol', size=5): + """Returns the number of results for a fofa query. + """ + try: + r = client.search_stats(query, size=size, fields=fields) + except fofa.FofaError as e: + raise click.ClickException(e.message) + + print_data(r) + +def fofa_search_all(client, query, fields, num): + size = 10000 + page = 1 + result = { + 'size': 0, + 'results': [], + 'consumed_fpoint': 0, + } + total = 0 + while True: + try: + remain_num = num - total + if remain_num < size: + size = remain_num + + r = client.search(query, fields=fields, page=page, size=size) + data = r['results'] + total += len(data) + + result['results'] += data + result['size'] += r['size'] + result['consumed_fpoint'] += r['consumed_fpoint'] + result['query'] = r['query'] + + if len(data) < size or total >= num: + break + + page+=1 + except fofa.FofaError as e: + raise click.ClickException(u'search page {}, error: {}'.format(page, e.message)) + return result + +def fofa_paged_search_save(writer, client, query, fields, num): + """ Perform paged search using the search API and save the results to a writer. + + Args: + writer: Writer object (e.g., CSVWriter or XLSWriter) for saving the results. + client: FOFA API client. + query: FOFA query string. + fields: Comma-separated string of fields to include in the search results. + num: Number of results to save. + """ + size = 10000 + page = 1 + result = { + 'size': 0, + 'writed': 0, + 'consumed_fpoint': 0, + } + total = 0 + progress_bar = tqdm(total=num, desc='Downloading Fofa Data', leave=True, unit='item', unit_scale=True) + try: + while True: + remain_num = num - total + if remain_num < size: + size = remain_num + + r = client.search(query, fields=fields, page=page, size=size) + data = r['results'] + total += len(data) + + for d1 in data: + progress_bar.update(1) + writer.write_data(d1) + + if num > r['size']: + progress_bar.total = r['size'] + + progress_bar.refresh() + + result['size'] = r['size'] + result['consumed_fpoint'] += r['consumed_fpoint'] + result['query'] = r['query'] + result['writed'] = total + + if len(data) < size or total >= num: + break + + page+=1 + progress_bar.set_postfix({'completed': True}) + except fofa.FofaError as e: + raise click.ClickException(u'search page {}, error: {}'.format(page, e.message)) + except Exception as e: + raise click.ClickException(u'search page {}, error: {}'.format(next, e)) + + return result + +def fofa_next_search_save(writer, client, query, fields, num): + """ Perform next search using the search next API and save the results to a writer. + + Args: + writer: Writer object (e.g., CSVWriter or XLSWriter) for saving the results. + client: FOFA API client. + query: FOFA query string. + fields: Comma-separated string of fields to include in the search results. + num: Number of results to save. + """ + size = 10000 + page = 1 + result = { + 'size': 0, + 'writed': 0, + 'consumed_fpoint': 0, + } + total = 0 + next = '' + progress_bar = tqdm(total=num, desc='Downloading Fofa Data', leave=True, unit='item', unit_scale=True) + try: + while True: + remain_num = num - total + if remain_num < size: + size = remain_num + + r = client.search_next(query, fields=fields, next=next, size=size) + data = r['results'] + total += len(data) + + for d1 in data: + progress_bar.update(1) + writer.write_data(d1) + + if num > r['size']: + progress_bar.total = r['size'] + + progress_bar.refresh() + + next = r['next'] + result['size'] = r['size'] + result['consumed_fpoint'] += r['consumed_fpoint'] + result['query'] = r['query'] + result['writed'] = total + + if len(data) < size or total >= num: + break + + page+=1 + progress_bar.set_postfix({'completed': True}) + except fofa.FofaError as e: + raise click.ClickException(u'search next {}, error: {}'.format(next, e.message)) + except Exception as e: + raise click.ClickException(u'search next {}, error: {}'.format(next, e)) + + return result + + +def fofa_download(client, query, fields, num, save_file, filetype='xls'): + header = fields.split(',') + + if filetype == 'xls': + writer = XLSWriter(save_file) + else: + writer = CSVWriter(save_file) + + writer.write_data(header) + result = None + try: + if client.can_use_next(): + result = fofa_next_search_save(writer, client, query, fields, num) + else: + result = fofa_paged_search_save(writer, client, query, fields, num) + finally: + writer.close_writer() + if result: + click.echo("Query: '{}', saved to file: '{}', total: {:,}, written: {:,}, consumed fpoints: {:,}\n".format( + result['query'], + save_file, + result['size'], + result['writed'], + result['consumed_fpoint'] + )) + else: + raise click.ClickException('No result') + +from tabulate import tabulate + +@main.command() +@click.option('--count', '-c', default=False, flag_value=True, help='Count the number of results.') +@click.option('--stats', default=False, flag_value=True, help='Query statistics information.') +@click.option('--save', metavar='', help='Save the results to a file, supports csv and xls formats.') +@click.option('--color/--no-color', default=True, help='Enable/disable colorized output. Default: True') +@click.option('--fields', '-f', help='List of properties to show in the search results.', default='ip,port,protocol,domain') +@click.option('--size', help='The number of search results that should be returned. Default: 100', default=100, type=int) +@click.option('-v', '--verbose', count=True, help='Increase verbosity level. Use -v for INFO level, -vv for DEBUG level.') +@click.option('--retry', help='The number of times to retry the HTTP request in case of failure. Default: 10', default=10, type=int) +@click.argument('query', metavar='') +def search(count, stats, save, color, fields, size, verbose, retry, query): + """ Returns the results for a fofa query. + + If the query contains special characters like && or ||, please enclose it in single quotes (''). + + Example: + + # Show results in the console + + fofa search 'title="fofa" && cert.is_match=true' + + # Count the number of results + + fofa search --count 'title="fofa" && cert.is_match=true' + + # Query statistics information + + fofa search --stats 'title="fofa" && cert.is_match=true' + + # Save the results to a csv file + + fofa search --save results.csv 'title="fofa" && cert.is_match=true' + + # Save the results to an Excel file + + fofa search --save results.xlsx 'title="fofa" && cert.is_match=true' + """ + if query == '': + raise click.ClickException('Empty fofa query') + + fields = fields.strip() + + default_log_level = logging.WARN + # 根据 -v 参数增加 verbosity level + if verbose == 1: + default_log_level = logging.INFO + elif verbose >= 2: + default_log_level = logging.DEBUG + logging.basicConfig(level=default_log_level) + + para = get_user_key() + api = fofa.Client(**para) + api.tries = retry + + # count mode + if count: + fofa_count(api, query) + return + + # stat mode + if stats: + fofa_stats(api, query, fields, size) + return + + # download mode + if save: + filetype = '' + if save.endswith('.csv'): + filetype = 'csv' + elif save.endswith('.xls') or save.endswith('.xlsx'): + filetype = 'xls' + else: + raise click.ClickException('save only support .csv or .xls file') + fofa_download(api, query, fields, size, save, filetype) + return + + # search mode + r = fofa_search_all(api, query, fields, size) + + if r['size'] == 0: + raise click.ClickException('No result') + + flds = fields.split(',') + + # stats line + stats = "#stat query:'{}' total:{:,} size:{:,} consumed fpoints:{:,}".format( + r['query'], + r['size'], + len(r['results']), + r['consumed_fpoint'] + ) + click.echo(stats) + + datas = [] + for line in r['results']: + row = [] + + for index, field in enumerate(flds): + tmp = u'' + value = line[index] + if value: + tmp = escape_data(value) + if color: + tmp = click.style(tmp, fg=COLORIZE_FIELDS.get(field, 'white')) + row.append(tmp) + if len(row) != len(flds): + logging.error("row mismatch: %s", row) + datas.append(row) + + table = tabulate(datas, headers=flds, tablefmt="simple", colalign=("left",)) + click.echo(table) + +if __name__ == "__main__": + main() diff --git a/tookit/fofa_client/client.py b/tookit/fofa_client/client.py new file mode 100644 index 0000000..96540d3 --- /dev/null +++ b/tookit/fofa_client/client.py @@ -0,0 +1,373 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# Author: Erdog, Loveforkeeps, Alluka +import base64 +import json +import logging +import requests +import os +import sys + +from retry.api import retry_call + +if __name__ == '__main__': + # 当作为脚本直接执行时的处理方式 + from exception import FofaError + from helper import get_language, encode_query +else: + # 当作为包/模块导入时的处理方式 + from .exception import FofaError + from .helper import get_language, encode_query + +class Client: + """ + A class representing the FOFA client. + + :param key: The Fofa api key. If not specified, it will be read from the FOFA_KEY environment variable. + :type key: str + :param base_url: The base URL of the FOFA API. Defaults to 'https://fofa.info'. + :type base_url: str + :param proxies: A proxies array for the requests library, e.g. {'https': 'your proxy'} + :type proxies: dict + + """ + + def __init__(self, key='', base_url='', proxies=None): + """ Initialize the FOFA client. + """ + if key == '': + key = os.environ.get('FOFA_KEY', '') + + if base_url == '': + base_url = os.environ.get('FOFA_BASE_URL', 'https://fofa.info') + + self.key = key + + self.base_url = base_url.rstrip('/') + + self.lang = 'en' + sys_lang = get_language() + if sys_lang != None and sys_lang.startswith('zh'): + self.lang = 'zh-CN' + + self._session = requests.Session() + if proxies: + self._session.proxies.update(proxies) + self._session.trust_env = False + + # retry config, in seconds + self.tries = 5 # Number of retry attempts + self.delay = 1 # Initial delay between retries + self.max_delay = 60 # Maximum delay between retries + self.backoff = 2 # Backoff factor for exponential backoff + + def get_userinfo(self): + """ + Get user info for current user. + + :return: User information in JSON format. + :rtype: dict + :raises FofaException: If an error occurs during the API request. + + :Example: + + The returned JSON result will be in the following format: + + .. code-block:: json + + { + "username": "sample", + "fofacli_ver": "4.0.3", + "fcoin": 0, + "error": false, + "fofa_server": true, + "avatar": "https://nosec.org/missing.jpg", + "vip_level": 0, + "is_verified": false, + "message": "", + "isvip": false, + "email": "username@sample.net" + } + + """ + return self.__do_req( "/api/v1/info/my") + + def search(self, query_str, page=1, size=100, fields="", opts={}): + """ + Search data in FOFA. + + :param query_str: The search query string. + + Example 1: + 'ip=127.0.0.1' + + Example 2: + 'header="thinkphp" || header="think_template"' + + :type query_str: str + :param page: Page number. Default is 1. + :type page: int + :param size: Number of results to be returned in one page. Default is 100. + :type size: int + :param fields: Comma-separated list of fields to be included in the query result. + Example: + 'ip,city' + :type fields: str + :param opts: Additional options for the query. This should be a dictionary of key-value pairs. + :type opts: dict + :return: Query result in JSON format. + :rtype: dict + + .. code-block:: json + + { + "results": [ + [ + "111.**.241.**:8111", + "111.**.241.**", + "8111" + ], + [ + "210.**.181.**", + "210.**.181.**", + "80" + ] + ], + "mode": "extended", + "error": false, + "query": "app=\\"网宿科技-公司产品\\"", + "page": 1, + "size": 2 + } + + """ + param = opts + param['qbase64'] = encode_query(query_str) + param['page'] = page + param['fields'] = fields + param['size'] = size + logging.debug("search '%s' page:%d size:%d", query_str, page, size) + return self.__do_req('/api/v1/search/all', param) + + def can_use_next(self): + """ + Check if the "search_next" API can be used. + + :return: True if the "search_next" API can be used, False otherwise. + :rtype: bool + """ + try: + self.search_next('bad=query', size=1) + except FofaError as e: + if e.code == 820000: + return True + return False + + def search_next(self, query_str, fields='', size=100, next='', full=False, opts={}): + """ + Query the next page of search results. + + :param query_str: The search query string. + + Example 1: + 'ip=127.0.0.1' + + Example 2: + 'header="thinkphp" || header="think_template"' + + :param fields: The fields to be included in the response. + Default: 'host,ip,port' + :type fields: str + + :param size: The number of results to be returned per page. + Default: 100 + Maximum: 10,000 + :type size: int + + :param next: The ID for pagination. + The next value is returned in the response of previous search query. + If not provided, the first page of results will be returned. + :type next: str + + :param full: Specify if all data should be searched. + Default: False (search within the past year) + Set to True to search all data. + :type full: bool + + :param opts: Additional options for the search. + :type opts: dict + + :return: The query result in JSON format. + :rtype: dict + """ + param = opts + param['qbase64'] = encode_query(query_str) + param['fields'] = fields + param['size'] = size + param['full'] = full + if next and next != '': + param['next'] = next + + logging.debug("search next for '%s' size:%d, next:%s", query_str, size, next) + return self.__do_req('/api/v1/search/next', param) + + def search_stats(self, query_str, size=5, fields='', opts={}): + """ + Query the statistics of the search results. + + :param query_str: The search query string. + + Example 1: + 'ip=127.0.0.1' + + Example 2: + 'header="thinkphp" || header="think_template"' + + :type query_str: str + + :param size: The number of results to be aggregated for each item. + Default: 5 + :type size: int + + :param fields: The fields to be included in the aggregation. + Example: 'ip,city' + :type fields: str + + :param opts: Additional options for the search. + :type opts: dict + + :return: query result in json format + + + .. code-block:: json + + { + "distinct": { + "ip": 1717, + "title": 411 + }, + "lastupdatetime": "2022-06-17 13:00:00", + "aggs": { + "title": [ + { + "count": 35, + "name": "百度一下,你就知道" + }, + { + "count": 25, + "name": "百度网盘-免费云盘丨文件共享软件丨超大容量丨存储安全" + }, + { + "count": 16, + "name": "百度智能云-登录" + }, + { + "count": 2, + "name": "百度翻译开放平台" + } + ], + "countries": [] + }, + "error": false + } + + """ + param = opts + param['qbase64'] = encode_query(query_str) + param['fields'] = fields + param['size'] = size + return self.__do_req('/api/v1/search/stats', param) + + def search_host(self, host, detail=False, opts={}): + """ + Search for host information based on the specified IP address or domain. + + :param host: The IP address or domain of the host to search for. + :type host: str + :param detail: Optional. Specifies whether to show detailed information. Default is False. + :type detail: bool + :param opts: Optional. Additional options for the search. Default is an empty dictionary. + :type opts: dict + :return: The query result in JSON format. + :rtype: dict + + .. code-block:: json + + { + "error": false, + "host": "78.48.50.249", + "ip": "78.48.50.249", + "asn": 6805, + "org": "Telefonica Germany", + "country_name": "Germany", + "country_code": "DE", + "protocol": [ + "http", + "https" + ], + "port": [ + 80, + 443 + ], + "category": [ + "CMS" + ], + "product": [ + "Synology-WebStation" + ], + "update_time": "2022-06-11 08:00:00" + } + + """ + param = opts + param['detail'] = detail + + u = '/api/v1/host/%s' % host + return self.__do_req(u, param) + + def __do_req(self, path, params=None, method='get'): + u = self.base_url + path + data = None + req_param = {} + + if not self.key or self.key == '': + raise FofaError("Empty fofa api key") + + if params == None: + req_param = { + "key": self.key, + "lang": self.lang, + } + else: + req_param = params + req_param['key'] = self.key + req_param['lang'] = self.lang + + if method == 'post': + data = params + params = None + + def make_request(): + headers = {"Accept-Encoding": "gzip"} + response = self._session.request(url=u, method=method, data=data, params=req_param, headers=headers) + if response.status_code != 200: + raise Exception("Request failed with status code: {}".format(response.status_code)) + return response + res = retry_call(make_request, + tries = self.tries, + delay = self.delay, + max_delay = self.max_delay, + backoff=self.backoff) + data = res.json() + if 'error' in data and data['error']: + raise FofaError(data['errmsg']) + return data + +if __name__ == "__main__": + client = Client() + logging.basicConfig(level=logging.DEBUG) + print(client.can_use_next()) + print(json.dumps(client.get_userinfo(), ensure_ascii=False)) + print(json.dumps(client.search('app="网宿科技-公司产品"', page=1), ensure_ascii=False)) + print(json.dumps(client.search_host('78.48.50.249', detail=True), ensure_ascii=False)) + print(json.dumps(client.search_stats('domain="baidu.com"', fields='title'), ensure_ascii=False)) diff --git a/tookit/fofa_client/exception.py b/tookit/fofa_client/exception.py new file mode 100644 index 0000000..525b3e7 --- /dev/null +++ b/tookit/fofa_client/exception.py @@ -0,0 +1,27 @@ + +import re + +def extract_error_code(error_message): + """Extracts the error code from an error message. + + Args: + error_message (str): The error message. + + Returns: + str: The extracted error code, or None if no error code is found. + """ + error_code_pattern = r'\[(-?\d+)\]' + match = re.search(error_code_pattern, error_message) + if match: + return int(match.group(1)) + else: + return None + +class FofaError(Exception): + """This exception gets raised whenever an error returned by the Fofa API.""" + def __init__(self, message): + self.message = message + self.code = extract_error_code(message) + + def __str__(self): + return self.message diff --git a/tookit/fofa_client/helper.py b/tookit/fofa_client/helper.py new file mode 100644 index 0000000..f8bb0c0 --- /dev/null +++ b/tookit/fofa_client/helper.py @@ -0,0 +1,55 @@ +import locale +import sys +import base64 + +def get_language(): + """ get shell language """ + if hasattr(locale, 'getdefaultlocale'): + shell_lang, _ = locale.getdefaultlocale() + else: + shell_lang = locale.getdefaultlocale()[0] + return shell_lang + + +if sys.version_info[0] > 2: + # Python 3 + def encode_query(query_str): + encoded_query = query_str.encode() + encoded_query = base64.b64encode(encoded_query) + return encoded_query.decode() +else: + # Python 2 + def encode_query(query_str): + encoded_query = base64.b64encode(query_str) + return encoded_query + + +# import csv +# import xlsxwriter +# +# class CSVWriter: +# def __init__(self, file_path): +# self.file_path = file_path +# self.file = open(file_path, 'w', newline='', encoding='utf-8') +# self.writer = csv.writer(self.file) +# +# def write_data(self, data): +# self.writer.writerow(data) +# +# def close_writer(self): +# self.file.close() +# +# +# class XLSWriter: +# def __init__(self, file_path): +# self.file_path = file_path +# self.workbook = xlsxwriter.Workbook(file_path) +# self.worksheet = self.workbook.add_worksheet() +# self.current_row = 0 +# +# def write_data(self, data): +# self.worksheet.write_row(self.current_row, 0, data) +# self.current_row += 1 +# +# def close_writer(self): +# self.workbook.close()